From ae6d6689eec02b0328eb5e7b1f1f48c523268a8f Mon Sep 17 00:00:00 2001 From: pacan-bot Date: Sat, 11 Jul 2026 02:46:59 +0000 Subject: машина: физика reVC (выбор дрон/машина) + фикс руля, скорости и фары MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - руль: влево/вправо больше не перепутаны (инвертирован знак A/D) - скорость: БРОВЕНОСЕЦ 99->120 км/ч и вдвое живее разгон (engineAccel 16->34, maxVel 200->270); ЖОПЕРРАРИ 40->63 км/ч (engineAccel 18->32, maxVel 210->300, traction 1.15->1.30, меньше буксует на грунте) - фары: две area-light (RectAreaLight/LTC, как у роторов дрона) + светящиеся линзы, светят вперёд и чуть вниз на дорогу - в репозиторий добавлены исходники физики: serve/car_phys.js, serve/_carmod.js --- serve/_carmod.js | 128 ++++++++ serve/car_phys.js | 805 ++++++++++++++++++++++++++++++++++++++++++++++ serve/index.html | 944 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 1875 insertions(+), 2 deletions(-) create mode 100644 serve/_carmod.js create mode 100644 serve/car_phys.js (limited to 'serve') diff --git a/serve/_carmod.js b/serve/_carmod.js new file mode 100644 index 0000000..da70fd3 --- /dev/null +++ b/serve/_carmod.js @@ -0,0 +1,128 @@ +/* ===== АВТО-РЕЖИМ: выбор транспорта + езда на ported reVC-физике (client) ===== */ +var carMode = false, carObj = null, carMesh = null, carGround = null, carAcc = 0, carPreset = 'brovenosec'; +var carMesh_wheels = []; +var carHeadlights = []; +var CAR_SPAWN = { x: 70, z: -40 }; + +function buildCarMesh() { + var h = carObj.h; + var g = new THREE.Group(); + var L = h.Dimension.y, Wd = h.Dimension.x, Ht = h.Dimension.z; + var body = new THREE.Mesh(new THREE.BoxGeometry(Wd, Ht * 0.6, L), + new THREE.MeshStandardMaterial({ color: 0xc81e3a, roughness: 0.45, metalness: 0.3 })); + body.position.y = Ht * 0.15; g.add(body); + var cab = new THREE.Mesh(new THREE.BoxGeometry(Wd * 0.86, Ht * 0.5, L * 0.5), + new THREE.MeshStandardMaterial({ color: 0x20242c, roughness: 0.3, metalness: 0.4 })); + cab.position.set(0, Ht * 0.55, -L * 0.05); g.add(cab); + var nose = new THREE.Mesh(new THREE.BoxGeometry(Wd * 0.5, Ht * 0.2, 0.3), + new THREE.MeshStandardMaterial({ color: 0xffd23a, emissive: 0x442200 })); + nose.position.set(0, Ht * 0.1, L * 0.5); g.add(nose); // жёлтый нос = вперёд (+Z) + // фары — area lights (LTC, как у дрона): светят вперёд (+Z) и чуть вниз на дорогу + var hlX = Wd * 0.34, hlY = Ht * 0.16, hlZ = L * 0.5 + 0.04; + var lensMat = new THREE.MeshStandardMaterial({ color: 0xfff6d8, emissive: 0xfff0b0, emissiveIntensity: 2.6, roughness: 0.25, metalness: 0.0 }); + carHeadlights = []; + for (var hi = 0; hi < 2; hi++) { + var sx = hi === 0 ? -1 : 1; + var lens = new THREE.Mesh(new THREE.BoxGeometry(Wd * 0.26, Ht * 0.18, 0.10), lensMat); + lens.position.set(sx * hlX, hlY, hlZ); g.add(lens); + var hl = new THREE.RectAreaLight(0xfff2cc, 11.0, Wd * 0.55, Ht * 0.42); + hl.position.set(sx * hlX, hlY, hlZ + 0.06); + hl.rotation.set(Math.PI + 0.16, 0, 0); // локальный -Z -> +Z (вперёд), лёгкий наклон вниз + g.add(hl); carHeadlights.push(hl); + } + var wr = carObj.wheelRadius; + var wheelGeo = new THREE.CylinderGeometry(wr, wr, 0.3, 16); + var wheelMat = new THREE.MeshStandardMaterial({ color: 0x111214, roughness: 0.85 }); + carMesh_wheels = []; + for (var i = 0; i < 4; i++) { + var w = new THREE.Mesh(wheelGeo, wheelMat); + var pb = carObj.m_wheelPosBody[i]; + w.position.set(pb.x, pb.y, pb.z); + w.rotation.set(0, 0, Math.PI / 2); // ось колеса вдоль X + g.add(w); carMesh_wheels.push(w); + } + return g; +} + +function spawnCar() { + var CP = window.PZCarPhys; + var hd = CP.makeHandling(CP.PRESETS[carPreset]); + carGround = CP.makeGround(groundH, WATER_Y, { patch: 1.5 }); + var gy = groundH(CAR_SPAWN.x, CAR_SPAWN.z); + carObj = new CP.Car(hd, { x: CAR_SPAWN.x, y: gy + 2.0, z: CAR_SPAWN.z, heading: 0 }); + carMesh = buildCarMesh(); + scene.add(carMesh); + try { window.PZCAR = carObj; } catch(e){} +} + +function readCarInput() { + var th = 0, st = 0; + if (keys['KeyW'] || keys['ArrowUp']) th += 1; + if (keys['KeyS'] || keys['ArrowDown']) th -= 1; + if (keys['KeyA'] || keys['ArrowLeft']) st += 1; // руль влево (исправлено: влево/вправо было перепутано) + if (keys['KeyD'] || keys['ArrowRight']) st -= 1; // руль вправо + return { throttle: th, steer: st, handbrake: !!keys['Space'], brakeHeld: (th < 0) }; +} + +var _cbR = new THREE.Vector3(), _cbU = new THREE.Vector3(), _cbF = new THREE.Vector3(), _cbM = new THREE.Matrix4(); +function carRespawn() { + var CP = window.PZCarPhys, gy = groundH(CAR_SPAWN.x, CAR_SPAWN.z); + carObj.pos = CP._v(CAR_SPAWN.x, gy + 2.0, CAR_SPAWN.z); + carObj.moveSpeed = CP._v(0, 0, 0); carObj.turnSpeed = CP._v(0, 0, 0); + carObj.setHeading(0); +} +function carStep(dt) { + if (!carObj) return; + if (keys['KeyR']) carRespawn(); + var inp = readCarInput(); carAcc += dt; var n = 0; + while (carAcc >= 0.02 && n < 6) { carObj.step(inp, carGround); carAcc -= 0.02; n++; } // фиксированные 50 Гц + carMesh.position.set(carObj.pos.x, carObj.pos.y, carObj.pos.z); + _cbR.set(carObj.right.x, carObj.right.y, carObj.right.z); + _cbU.set(carObj.up.x, carObj.up.y, carObj.up.z); + _cbF.set(carObj.fwd.x, carObj.fwd.y, carObj.fwd.z); + _cbM.makeBasis(_cbR, _cbU, _cbF); carMesh.quaternion.setFromRotationMatrix(_cbM); + for (var i = 0; i < 4; i++) { + var w = carMesh_wheels[i]; if (!w) continue; + var pb = carObj.m_wheelPosBody[i]; w.position.set(pb.x, pb.y, pb.z); + var steer = (i < 2) ? carObj.m_fSteerAngle : 0; // FL,FR поворачиваются + w.rotation.set(0, -steer, Math.PI / 2); + } +} + +var _carGoal = new THREE.Vector3(), _carLook = new THREE.Vector3(); +function carCamera() { + controls.enabled = false; + _cbF.set(carObj.fwd.x, 0, carObj.fwd.z); if (_cbF.lengthSq() < 1e-4) _cbF.set(0, 0, 1); _cbF.normalize(); + _carGoal.set(carObj.pos.x, carObj.pos.y, carObj.pos.z).addScaledVector(_cbF, -12).addScaledVector(WUP, 5.5); + camera.position.lerp(_carGoal, 0.12); + _carLook.set(carObj.pos.x, carObj.pos.y + 1.3, carObj.pos.z); + _camLook.lerp(_carLook, 0.3); camera.lookAt(_camLook); +} + +/* --- стартовый выбор транспорта --- */ +(function vehicleSelect() { + var ov = document.createElement('div'); + ov.id = 'vsOverlay'; + ov.style.cssText = 'position:fixed;inset:0;z-index:10050;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(4,6,12,.88);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);font-family:system-ui,Segoe UI,Roboto,sans-serif;color:#eef3ff'; + ov.innerHTML = + '
ПЛАНЕТА ЖОПА
' + + '
На чём поедешь?
' + + '
' + + '' + + '' + + '
' + + '
сменить транспорт — перезагрузка страницы
'; + document.body.appendChild(ov); + ['pointerdown', 'touchstart', 'mousedown', 'keydown'].forEach(function (ev) { ov.addEventListener(ev, function (e) { e.stopPropagation(); }); }); + document.getElementById('vsDrone').onclick = function () { carMode = false; ov.remove(); }; + document.getElementById('vsCar').onclick = function () { + carMode = true; + try { spawnCar(); } catch (e) { console.error('spawnCar', e); } + try { if (typeof drone !== 'undefined' && drone) drone.visible = false; } catch (e) {} + try { sd.style.display = 'none'; } catch (e) {} + try { dp.style.display = 'none'; } catch (e) {} + var hp = document.getElementById('help'); + if (hp) hp.innerHTML = '🚗 МАШИНА (физика reVC): W газ · S тормоз/задний · A/D руль · Space ручник · R сброс'; + ov.remove(); + }; +})(); diff --git a/serve/car_phys.js b/serve/car_phys.js new file mode 100644 index 0000000..087e7bc --- /dev/null +++ b/serve/car_phys.js @@ -0,0 +1,805 @@ +/* ===================================================================== + Планета Жопа — АВТОФИЗИКА + Faithful JS port of the GTA:VC (reVC) car handling model. + Ported 1:1 from kirillsurkov/racing_game (reVC "miami"): + - cTransmission::InitGearRatios / CalculateDriveAcceleration (Transmission.cpp) + - cHandlingDataMgr::ConvertDataToGameUnits (HandlingMgr.cpp) + - CVehicle::ProcessWheel (tyre traction/skid model) (Vehicle.cpp) + - CAutomobile::ProcessControl driving core (susp+drive+brake+steer) + - CPhysical rigid body: ApplyMoveForce/ApplyTurnForce/GetMass, + ApplySpringCollisionAlt, ApplySpringDampening, ApplyAirResistance, + ApplyGravity, ApplyMoveSpeed, ApplyTurnSpeed (Physical.cpp) + Units: 1 unit = 1 m, 1 step = 1/50 s (GetTimeStep()==1.0). Runs at fixed 50 Hz. + Coordinate frame adapted to the game's world: Y up (VC uses Z up). + Works in node (plain {x,y,z}) and in-browser (same code). + ===================================================================== */ +(function (root) { + 'use strict'; + + // ---- constants (from reVC) ---- + var GRAVITY = 0.008; // Physical.h #define GRAVITY (0.008f) + var WHEEL_FRICTION = 0.9; // cHandlingDataMgr::Initialise fWheelFriction + var STEP = 1.0; // CTimer::GetTimeStep() nominal (50 Hz) + // wheel indices (CARWHEEL_*) + var FL = 0, FR = 1, RL = 2, RR = 3; + // tWheelState + var WHEEL_STATE_NORMAL = 0, WHEEL_STATE_SPINNING = 1, WHEEL_STATE_SKIDDING = 2, WHEEL_STATE_FIXED = 3; + + // ---- tiny vec3 on {x,y,z} ---- + function v(x, y, z) { return { x: x || 0, y: y || 0, z: z || 0 }; } + function vset(a, x, y, z) { a.x = x; a.y = y; a.z = z; return a; } + function vcopy(a) { return { x: a.x, y: a.y, z: a.z }; } + function vadd(a, b) { return v(a.x + b.x, a.y + b.y, a.z + b.z); } + function vsub(a, b) { return v(a.x - b.x, a.y - b.y, a.z - b.z); } + function vscale(a, s) { return v(a.x * s, a.y * s, a.z * s); } + function vaddi(a, b) { a.x += b.x; a.y += b.y; a.z += b.z; return a; } + function vaddscaled(a, b, s) { a.x += b.x * s; a.y += b.y * s; a.z += b.z * s; return a; } + function dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; } + function cross(a, b) { + return v(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); + } + function magsq(a) { return a.x * a.x + a.y * a.y + a.z * a.z; } + function mag(a) { return Math.sqrt(magsq(a)); } + function norm(a) { var m = mag(a); if (m > 1e-9) { return v(a.x / m, a.y / m, a.z / m); } return v(0, 0, 0); } + function sq(x) { return x * x; } + function DEGTORAD(d) { return d * Math.PI / 180; } + + // ===================================================================== + // Handling: raw cfg fields -> game units (ConvertDataToGameUnits 1:1) + // ===================================================================== + // raw fields mirror HANDLING.CFG columns (post the *0.4 that LoadHandlingData + // applies to engineAccel at field 14). + function makeHandling(raw) { + var h = { + name: raw.name, + fMass: raw.mass, + Dimension: v(raw.dimX, raw.dimY, raw.dimZ), + CentreOfMass: v(raw.comX, raw.comY, raw.comZ), + nPercentSubmerged: raw.submerged, + fTractionMultiplier: raw.tractionMult, + fTractionLoss: raw.tractionLoss, + fTractionBias: raw.tractionBias, + fBrakeDeceleration: raw.brakeDecel, + fBrakeBias: raw.brakeBias, + bABS: raw.abs, + fSteeringLock: raw.steerLock, + fSuspensionForceLevel: raw.susForce, + fSuspensionDampingLevel: raw.susDamp, + fSuspensionUpperLimit: raw.susUpper, + fSuspensionLowerLimit: raw.susLower, + fSuspensionBias: raw.susBias, + fSuspensionAntidiveMultiplier: raw.susAntidive || 0, + fCollisionDamageMultiplier: raw.collDmg || 1, + Flags: raw.flags || 0, + Transmission: { + nNumberOfGears: raw.nGears, + nDriveType: raw.driveType, // 'F' | 'R' | '4' + nEngineType: raw.engineType || 'P', + Flags: raw.flags || 0, + fEngineAcceleration: raw.engineAccel * 0.4, // LoadHandlingData field 14: *0.4 + fMaxVelocity: raw.maxVel, + Gears: [ {}, {}, {}, {}, {}, {} ] + } + }; + convertToGameUnits(h); + initGearRatios(h.Transmission); + // moment of inertia already set by convert + return h; + } + + function convertToGameUnits(h) { + var T = h.Transmission; + T.fEngineAcceleration *= 1.0 / (50.0 * 50.0); + T.fMaxVelocity *= 1000.0 / (60.0 * 60.0 * 50.0); + h.fBrakeDeceleration *= 1.0 / (50.0 * 50.0); + h.fTurnMass = (sq(h.Dimension.x) + sq(h.Dimension.y)) * h.fMass / 12.0; + if (h.fTurnMass < 10.0) h.fTurnMass *= 5.0; + h.fInvMass = 1.0 / h.fMass; + h.fBuoyancy = 100.0 / h.nPercentSubmerged * GRAVITY * h.fMass; + + // drag-limited real max velocity (ConvertDataToGameUnits 1:1) + var a = 0.0, b = 100.0, velocity = T.fMaxVelocity; + while (a < b && velocity > 0.0) { + velocity -= 0.01; + a = T.fEngineAcceleration / 6.0; + var a_drag = 0.5 * sq(velocity) * h.Dimension.x * h.Dimension.z / h.fMass; + b = -velocity * (1.0 / (a_drag + 1.0) - 1.0); + } + T.fMaxCruiseVelocity = velocity; + T.fMaxVelocity = velocity * 1.2; + T.fMaxReverseVelocity = -0.2; + + if (T.nDriveType === '4') T.fEngineAcceleration /= 4.0; + else T.fEngineAcceleration /= 2.0; + } + + function initGearRatios(T) { + var G = T.Gears; + var i; + for (i = 0; i < 6; i++) G[i] = { fMaxVelocity: 0, fShiftUpVelocity: 0, fShiftDownVelocity: 0 }; + for (i = 1; i <= T.nNumberOfGears; i++) { + var g0 = G[i - 1], g1 = G[i]; + g1.fMaxVelocity = i / T.nNumberOfGears * T.fMaxVelocity; + var velocityDiff = g1.fMaxVelocity - g0.fMaxVelocity; + if (i >= T.nNumberOfGears) { + g1.fShiftUpVelocity = T.fMaxVelocity; + } else { + G[i + 1].fShiftDownVelocity = velocityDiff * 0.42 + g0.fMaxVelocity; + g1.fShiftUpVelocity = velocityDiff * 0.6667 + g0.fMaxVelocity; + } + } + G[0].fMaxVelocity = T.fMaxReverseVelocity; + G[0].fShiftUpVelocity = -0.01; + G[0].fShiftDownVelocity = T.fMaxReverseVelocity; + G[1].fShiftDownVelocity = -0.01; + } + + var HANDLING_2G_BOOST = 2, HANDLING_1G_BOOST = 1; + + // cTransmission::CalculateDriveAcceleration (recursive gear selection) 1:1 + function calcDriveAccel(T, gasPedal, gearRef, velocity, cheat) { + var fVelocity = velocity; + if (fVelocity < T.fMaxReverseVelocity) { return 0.0; } + if (fVelocity > T.fMaxVelocity) { return 0.0; } + + var gear = gearRef.gear; + var pGearRatio = T.Gears[gear]; + if (fVelocity > pGearRatio.fShiftUpVelocity) { + if (gear !== 0 || gasPedal > 0.0) { + gearRef.gear = gear + 1; + return calcDriveAccel(T, gasPedal, gearRef, fVelocity, false); + } + } else if (fVelocity < pGearRatio.fShiftDownVelocity && gear !== 0) { + if (gear !== 1 || gasPedal < 0.0) { + gearRef.gear = gear - 1; + return calcDriveAccel(T, gasPedal, gearRef, fVelocity, false); + } + } + + var speedMul, accelMul; + var Flags = T.Flags; + if (gear < 1) { + accelMul = (Flags & HANDLING_2G_BOOST) ? 2.0 : 1.0; + speedMul = -1.0; + } else if (T.nNumberOfGears === 1) { + accelMul = 1.0; speedMul = 1.0; + } else { + var f = 1.0 - (gear - 1) / (T.nNumberOfGears - 1); + speedMul = 3.0 * sq(f) + 1.0; + if (Flags & HANDLING_2G_BOOST) { + if (gear === 1) accelMul = (Flags & HANDLING_1G_BOOST) ? 2.0 : 1.6; + else if (gear === 2) accelMul = 1.3; + else accelMul = 1.0; + } else if ((Flags & HANDLING_1G_BOOST) && gear === 1) { + accelMul = 2.0; + } else accelMul = 1.0; + } + + var fCheat = cheat ? 1.2 : 1.0; + var targetVelocity = T.Gears[gear].fMaxVelocity * speedMul * fCheat; + var accel = (targetVelocity - fVelocity) * (T.fEngineAcceleration * accelMul) / Math.abs(targetVelocity); + var fAcceleration; + if (Math.abs(fVelocity) < Math.abs(T.Gears[gear].fMaxVelocity * fCheat)) + fAcceleration = gasPedal * accel * STEP; + else + fAcceleration = 0.0; + return fAcceleration; + } + + // ===================================================================== + // Car rigid body + // ===================================================================== + function Car(handling, opts) { + opts = opts || {}; + this.h = handling; + this.m_fMass = handling.fMass; + this.m_fTurnMass = handling.fTurnMass; + this.m_vecCentreOfMass = vcopy(handling.CentreOfMass); + this.m_fAirResistance = handling.Dimension.x * handling.Dimension.z / handling.fMass; // CAutomobile ctor + + // pose: position + orthonormal basis (right,up,fwd) + this.pos = v(opts.x || 0, opts.y || 0, opts.z || 0); + this.right = v(1, 0, 0); + this.up = v(0, 1, 0); + this.fwd = v(0, 0, 1); + if (opts.heading != null) this.setHeading(opts.heading); + + // velocities + this.moveSpeed = v(0, 0, 0); // m_vecMoveSpeed (units/step) + this.turnSpeed = v(0, 0, 0); // m_vecTurnSpeed (rad/step, world) + + // controls + this.m_fGasPedal = 0; + this.m_fBrakePedal = 0; + this.m_fSteerAngle = 0; + this.m_fSteerInput = 0; + this.bIsHandbrakeOn = false; + this.m_doingBurnout = 0; + + // transmission state + this.gearRef = { gear: 1 }; + this.m_fTireTemperature = 1.0; + + // per-wheel state + var i; + this.m_aSuspensionSpringRatio = [1, 1, 1, 1]; + this.m_aSuspensionSpringRatioPrev = [1, 1, 1, 1]; + this.m_aWheelTimer = [0, 0, 0, 0]; + this.m_aWheelSpeed = [0, 0, 0, 0]; // wheel angular speed (visual) + this.m_aWheelRotation = [0, 0, 0, 0]; // accumulated wheel angle (visual) + this.m_aWheelState = [0, 0, 0, 0]; + this.m_wheelContactPoint = [v(), v(), v(), v()]; // world, relative to pos + this.m_wheelContactNormal = [v(0, 1, 0), v(0, 1, 0), v(0, 1, 0), v(0, 1, 0)]; + this.m_wheelOnGround = [false, false, false, false]; + + this.m_nWheelsOnGround = 0; + this.m_nDriveWheelsOnGround = 0; + + // --- suspension geometry (per wheel, body space) --- + // Wheel mount X (side): +right = right side. Y(height): mount above hub. Z(fwd): fore/aft. + var d = handling.Dimension; + var halfW = d.x * 0.5; // track half-width + var halfL = d.y * 0.42; // wheelbase half-length + this.wheelRadius = opts.wheelRadius || Math.max(0.32, d.z * 0.22); + var Ls = handling.fSuspensionUpperLimit - handling.fSuspensionLowerLimit; // spring travel + if (Ls < 0.05) Ls = 0.30; + this.m_suspSpringLength = Ls; + // total ray length = spring travel + wheel radius (VC: lineLength = springLength + radius) + this.m_suspLineLength = Ls + this.wheelRadius; + // mount height above body origin so hub sits ~ at origin + var mountY = handling.fSuspensionUpperLimit; + // wheel body positions: [FL, FR, RL, RR]; +Z is forward, +X is right + this.m_wheelPosBody = [ + v(-halfW, mountY, halfL), // FRONT_LEFT + v( halfW, mountY, halfL), // FRONT_RIGHT + v(-halfW, mountY, -halfL), // REAR_LEFT + v( halfW, mountY, -halfL) // REAR_RIGHT + ]; + // spring direction in body space = straight down (-up) + this.m_springDirBody = v(0, -1, 0); + // height above road (approx, VC m_fHeightAboveRoad) + this.m_fHeightAboveRoad = Ls * (1.0 - 1.0 / (4.0 * handling.fSuspensionForceLevel)) - handling.fSuspensionLowerLimit; + } + + Car.prototype.setHeading = function (rad) { + // rotate basis about world-Y so fwd points to heading (0 => +Z) + var c = Math.cos(rad), s = Math.sin(rad); + this.fwd = v(s, 0, c); + this.up = v(0, 1, 0); + this.right = norm(cross(this.up, this.fwd)); + }; + + Car.prototype.heading = function () { return Math.atan2(this.fwd.x, this.fwd.z); }; + + // world position of a body-space point + Car.prototype.toWorldDir = function (b) { + return v( + this.right.x * b.x + this.up.x * b.y + this.fwd.x * b.z, + this.right.y * b.x + this.up.y * b.y + this.fwd.y * b.z, + this.right.z * b.x + this.up.z * b.y + this.fwd.z * b.z + ); + }; + + // CPhysical::GetSpeed(r): velocity of a point r (relative to pos) + Car.prototype.getSpeed = function (r) { + return vadd(this.moveSpeed, cross(this.turnSpeed, r)); + }; + + // CPhysical::ApplyMoveForce + Car.prototype.applyMoveForce = function (f) { + vaddi(this.moveSpeed, vscale(f, 1.0 / this.m_fMass)); + }; + // CPhysical::ApplyTurnForce (impulse j at point p, relative to pos) + Car.prototype.applyTurnForce = function (j, p) { + var com = this.toWorldDir(this.m_vecCentreOfMass); + var turnimpulse = cross(vsub(p, com), j); + vaddi(this.turnSpeed, vscale(turnimpulse, 1.0 / this.m_fTurnMass)); + }; + // CPhysical::GetMass(pos,dir) + Car.prototype.getMass = function (pos, dir) { + return 1.0 / (magsq(cross(pos, dir)) / this.m_fTurnMass + 1.0 / this.m_fMass); + }; + + Car.prototype.applyGravity = function () { + this.moveSpeed.y -= GRAVITY * STEP; + }; + + // CPhysical::ApplyAirResistance + Car.prototype.applyAirResistance = function () { + if (this.m_fAirResistance > 0.1) { + var f = Math.pow(this.m_fAirResistance, STEP); + this.moveSpeed = vscale(this.moveSpeed, f); + this.turnSpeed = vscale(this.turnSpeed, f); + } else { + var f2 = Math.pow(1.0 / Math.abs(1.0 + this.m_fAirResistance * 0.5 * magsq(this.moveSpeed)), STEP); + this.moveSpeed = vscale(this.moveSpeed, f2); + this.turnSpeed = vscale(this.turnSpeed, 0.99); + } + }; + + // CPhysical::ApplySpringCollisionAlt + Car.prototype.applySpringCollisionAlt = function (springConst, springDir, point, springRatio, bias, forceDir) { + var compression = 1.0 - springRatio; + if (compression > 0.0) { + var fd = vcopy(forceDir); + if (dot(springDir, fd) > 0.0) fd = vscale(fd, -1.0); + var step = Math.min(STEP, 3.0); + var impulse = GRAVITY * this.m_fMass * step * springConst * compression * bias * 2.0; + this.applyMoveForce(vscale(fd, impulse)); + this.applyTurnForce(vscale(fd, impulse), point); + return fd; // return possibly-flipped force dir (VC mutates forceDir; used as springDir below) + } + return forceDir; + }; + + // CPhysical::ApplySpringDampening + Car.prototype.applySpringDampening = function (damping, springDir, point, speed) { + var speedA = dot(speed, springDir); + var gs = this.getSpeed(point); + var speedB = dot(gs, springDir); + if (speedB === 0.0) return; + var step = Math.min(STEP, 3.0); + var impulse = -damping * (speedA + speedB) / 2.0 * this.m_fMass * step * 0.53; + var a = this.m_fTurnMass / ((magsq(point) + 1.0) * 2.0 * this.m_fMass); + a = Math.min(a, 1.0); + var b = Math.abs(impulse / (speedB * this.m_fMass)); + if (a < b) impulse *= a / b; + this.applyMoveForce(vscale(springDir, impulse)); + this.applyTurnForce(vscale(springDir, impulse), point); + }; + + // CVehicle::ProcessWheel (tyre traction/skid) 1:1 + Car.prototype.processWheel = function (wheelFwd, wheelRight, wheelContactSpeed, wheelContactPoint, + wheelsOnGround, thrust, brake, adhesion, wheelId, wsRef) { + var bAlreadySkidding = false; + var fwd = 0.0, right = 0.0; + + var bBraking = brake !== 0.0; + if (bBraking) thrust = 0.0; + var bDriving = thrust !== 0.0; + + var contactSpeedFwd = dot(wheelContactSpeed, wheelFwd); + var contactSpeedRight = dot(wheelContactSpeed, wheelRight); + + if (wsRef.state !== WHEEL_STATE_NORMAL) bAlreadySkidding = true; + wsRef.state = WHEEL_STATE_NORMAL; + + adhesion *= STEP; + if (bAlreadySkidding) adhesion *= this.h.fTractionLoss; + + if (contactSpeedRight !== 0.0) { + right = -contactSpeedRight / wheelsOnGround; + } + + if (bDriving) { + fwd = thrust; + if (right > 0.0) { if (right > adhesion) right = adhesion; } + else { if (right < -adhesion) right = -adhesion; } + } else if (contactSpeedFwd !== 0.0) { + fwd = -contactSpeedFwd / wheelsOnGround; + if (!bBraking) { + if (this.m_fGasPedal < 0.01) { + if (this.m_fMass < 500.0) + brake = 0.2 * WHEEL_FRICTION / this.m_fMass; + else + brake = WHEEL_FRICTION / this.m_fMass; + } + } + if (brake > adhesion) { + if (Math.abs(contactSpeedFwd) > 0.005) wsRef.state = WHEEL_STATE_FIXED; + } else { + if (fwd > 0.0) { if (fwd > brake) fwd = brake; } + else { if (fwd < -brake) fwd = -brake; } + } + } + + var speedSq = sq(right) + sq(fwd); + if (sq(adhesion) < speedSq) { + if (wsRef.state !== WHEEL_STATE_FIXED) { + if (bDriving && contactSpeedFwd < 0.2) wsRef.state = WHEEL_STATE_SPINNING; + else wsRef.state = WHEEL_STATE_SKIDDING; + } + var l = Math.sqrt(speedSq); + var tractionLoss = bAlreadySkidding ? 1.0 : this.h.fTractionLoss; + right *= adhesion * tractionLoss / l; + fwd *= adhesion * tractionLoss / l; + } + + if (fwd !== 0.0 || right !== 0.0) { + var totalSpeed = vadd(vscale(wheelFwd, fwd), vscale(wheelRight, right)); + + var turnDirection = vcopy(totalSpeed); + var separateTurnForce = false; + var antidive = this.h.fSuspensionAntidiveMultiplier; + if (antidive > 0.0) { + if (bBraking) { + separateTurnForce = true; + turnDirection = vsub(totalSpeed, vscale(wheelFwd, antidive * fwd)); + } else if (bDriving) { + separateTurnForce = true; + turnDirection = vsub(totalSpeed, vscale(wheelFwd, 0.5 * antidive * fwd)); + } + } + + var direction = vcopy(totalSpeed); + var speed = mag(totalSpeed); + var turnSpeed = separateTurnForce ? mag(turnDirection) : speed; + direction = norm(direction); + if (separateTurnForce) turnDirection = norm(turnDirection); + else turnDirection = direction; + + var impulse = speed * this.m_fMass; + var turnImpulse = turnSpeed * this.getMass(wheelContactPoint, turnDirection); + + this.applyMoveForce(vscale(direction, impulse)); + this.applyTurnForce(vscale(turnDirection, turnImpulse), wheelContactPoint); + } + }; + + // CVehicle::ProcessWheelRotation (visual) + function processWheelRotation(state, fwd, speed, radius) { + var angularVelocity; + if (state === WHEEL_STATE_SPINNING) angularVelocity = -1.1; + else if (state === WHEEL_STATE_FIXED) angularVelocity = 0.0; + else angularVelocity = -dot(fwd, speed) / radius; + return angularVelocity * STEP; + } + + // ===================================================================== + // Suspension raycast against the heightfield world + // ===================================================================== + // groundInfo(x,z) -> {h, nx,ny,nz}. Provided by caller (built from groundH). + Car.prototype.raycastWheels = function (ground) { + for (var i = 0; i < 4; i++) { + this.m_aSuspensionSpringRatioPrev[i] = this.m_aSuspensionSpringRatio[i]; + var mountW = vadd(this.pos, this.toWorldDir(this.m_wheelPosBody[i])); // world mount + var springDir = this.toWorldDir(this.m_springDirBody); // world down-ish + springDir = norm(springDir); + // ray: from mount along springDir, length lineLength. Find ground crossing. + // Sample ground under the ray at the mount's (x,z) projected downward. + // Because terrain is a heightfield, march the ray and find where it passes below ground. + var L = this.m_suspLineLength; + var found = false, distHit = L, gh = 0, gnorm = v(0, 1, 0); + // coarse+fine march + var N = 8, prevAbove = null, prevT = 0; + for (var k = 0; k <= N; k++) { + var t = L * k / N; + var px = mountW.x + springDir.x * t; + var py = mountW.y + springDir.y * t; + var pz = mountW.z + springDir.z * t; + var g = ground(px, pz); + var above = py - g.h; // >0 means ray point above ground + if (above <= 0 && prevAbove !== null && prevAbove > 0) { + // crossing between prevT and t -> refine + var t0 = prevT, t1 = t, a0 = prevAbove, a1 = above; + for (var r = 0; r < 6; r++) { + var tm = 0.5 * (t0 + t1); + var mx = mountW.x + springDir.x * tm, mz = mountW.z + springDir.z * tm, my = mountW.y + springDir.y * tm; + var gg = ground(mx, mz); + var am = my - gg.h; + if (am <= 0) { t1 = tm; a1 = am; } else { t0 = tm; a0 = am; } + } + distHit = 0.5 * (t0 + t1); + var hx = mountW.x + springDir.x * distHit, hz = mountW.z + springDir.z * distHit; + var gh2 = ground(hx, hz); + gnorm = v(gh2.nx, gh2.ny, gh2.nz); + found = true; + break; + } + prevAbove = above; prevT = t; + } + + if (found) { + // raw ratio along the line, then rescale by wheel radius (VC) + var wheelRadiusNorm = 1.0 - this.m_suspSpringLength / this.m_suspLineLength; + var rawRatio = distHit / this.m_suspLineLength; + var ratio = (rawRatio - wheelRadiusNorm) / (1.0 - wheelRadiusNorm); + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + this.m_aSuspensionSpringRatio[i] = ratio; + // contact point (relative to pos) + var cpW = v(mountW.x + springDir.x * distHit, mountW.y + springDir.y * distHit, mountW.z + springDir.z * distHit); + this.m_wheelContactPoint[i] = vsub(cpW, this.pos); + this.m_wheelContactNormal[i] = norm(gnorm); + this.m_wheelOnGround[i] = ratio < 1.0; + } else { + this.m_aSuspensionSpringRatio[i] = 1.0; + this.m_wheelContactPoint[i] = this.toWorldDir(v(this.m_wheelPosBody[i].x, this.m_wheelPosBody[i].y - this.m_suspLineLength, this.m_wheelPosBody[i].z)); + this.m_wheelOnGround[i] = false; + } + } + }; + + // ===================================================================== + // Main per-step control (CAutomobile::ProcessControl driving core) 1:1 + // ===================================================================== + Car.prototype.processControl = function (ground) { + var i; + var h = this.h, T = h.Transmission; + + // --- CPhysical::ProcessControl: gravity + air resistance (order as VC) --- + this.applyGravity(); + this.applyAirResistance(); + + // --- suspension raycast (our world-collision analog) --- + this.raycastWheels(ground); + + var fwdWorld = this.fwd; + var fwdSpeed = Math.abs(dot(this.moveSpeed, fwdWorld)); + + var contactPoints = [null, null, null, null]; + var contactSpeeds = [null, null, null, null]; + var springDirections = [null, null, null, null]; + + // gather compressed springs + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 1.0) { + contactPoints[i] = this.m_wheelContactPoint[i]; + springDirections[i] = norm(this.toWorldDir(this.m_springDirBody)); + } else { + contactPoints[i] = this.m_wheelContactPoint[i]; + } + } + + // springs push up + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 1.0) { + var bias = h.fSuspensionBias; + if (i === RL || i === RR) bias = 1.0 - bias; + var fd = this.applySpringCollisionAlt(h.fSuspensionForceLevel, springDirections[i], + contactPoints[i], this.m_aSuspensionSpringRatio[i], bias, this.m_wheelContactNormal[i]); + springDirections[i] = fd; // VC then uses this as spring dir for dampening below + } + } + + // recompute contact speeds; if normal.z>0.35 use -normal as spring dir (VC: normal.y here) + for (i = 0; i < 4; i++) { + contactSpeeds[i] = this.getSpeed(contactPoints[i]); + if (this.m_aSuspensionSpringRatio[i] < 1.0 && this.m_wheelContactNormal[i].y > 0.35) + springDirections[i] = vscale(this.m_wheelContactNormal[i], -1.0); + } + + // dampen springs + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 0.99999 && springDirections[i]) + this.applySpringDampening(h.fSuspensionDampingLevel, springDirections[i], contactPoints[i], contactSpeeds[i]); + } + + // recompute contact speeds + for (i = 0; i < 4; i++) contactSpeeds[i] = this.getSpeed(contactPoints[i]); + + // --- engine acceleration --- + fwdSpeed = dot(this.moveSpeed, fwdWorld); + var acceleration = calcDriveAccel(T, this.m_fGasPedal, this.gearRef, fwdSpeed, false); + + var brake = this.m_fBrakePedal * h.fBrakeDeceleration * STEP; + var brakeBiasFront = 2.0 * h.fBrakeBias; + var brakeBiasRear = 2.0 - h.fBrakeBias; + var tractionBiasFront = 2.0 * h.fTractionBias; + var tractionBiasRear = 2.0 - tractionBiasFront; + + // count wheels on ground + this.m_nWheelsOnGround = 0; + this.m_nDriveWheelsOnGround = 0; + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 1.0) this.m_aWheelTimer[i] = 4.0; + else this.m_aWheelTimer[i] = Math.max(this.m_aWheelTimer[i] - STEP, 0.0); + if (this.m_aWheelTimer[i] > 0.0) { + this.m_nWheelsOnGround++; + if (T.nDriveType === '4') this.m_nDriveWheelsOnGround++; + else if (T.nDriveType === 'F') { if (i === FL || i === FR) this.m_nDriveWheelsOnGround++; } + else if (T.nDriveType === 'R') { if (i === RL || i === RR) this.m_nDriveWheelsOnGround++; } + } + } + + // traction (STATUS_PLAYER path) + var traction = 0.004; + traction *= h.fTractionMultiplier / 4.0; + + var hasFront = (T.nDriveType !== 'R'); + var hasRear = (T.nDriveType !== 'F'); + + var wheelsOnGround = Math.max(this.m_nWheelsOnGround, 1); + + // ---- FRONT wheels ---- + if (this.m_aWheelTimer[FL] > 0.0 || this.m_aWheelTimer[FR] > 0.0) { + var s = Math.sin(this.m_fSteerAngle), c = Math.cos(this.m_fSteerAngle); + for (var fi = 0; fi < 2; fi++) { + var wi = fi === 0 ? FL : FR; + if (this.m_aWheelTimer[wi] <= 0.0) continue; + var fThrust = hasFront ? acceleration : 0.0; + var n = this.m_wheelContactNormal[wi]; + var wFwd = vsub(fwdWorld, vscale(n, dot(fwdWorld, n))); + wFwd = norm(wFwd); + var wRight = norm(cross(wFwd, n)); + var tmp = vsub(vscale(wFwd, c), vscale(wRight, s)); + wRight = vadd(vscale(wFwd, s), vscale(wRight, c)); + wFwd = tmp; + var adhesion = this.adhesiveLimit(this.m_wheelContactNormal[wi]) * traction; + var wsRef = { state: this.m_aWheelState[wi] }; + this.processWheel(wFwd, wRight, contactSpeeds[wi], contactPoints[wi], + wheelsOnGround, fThrust, brake * brakeBiasFront, adhesion * tractionBiasFront, wi, wsRef); + this.m_aWheelState[wi] = wsRef.state; + this.m_aWheelSpeed[wi] = processWheelRotation(wsRef.state, wFwd, contactSpeeds[wi], this.wheelRadius); + this.m_aWheelRotation[wi] += this.m_aWheelSpeed[wi]; + } + } else { + // front wheels off ground (visual spin decay) + for (var fo = 0; fo < 2; fo++) { + var wo = fo === 0 ? FL : FR; + if (hasFront && acceleration !== 0.0) { + if (acceleration > 0.0) { if (this.m_aWheelSpeed[wo] < 2.0) this.m_aWheelSpeed[wo] -= 0.2; } + else { if (this.m_aWheelSpeed[wo] > -2.0) this.m_aWheelSpeed[wo] += 0.1; } + } else this.m_aWheelSpeed[wo] *= 0.95; + this.m_aWheelRotation[wo] += this.m_aWheelSpeed[wo]; + } + } + + // ---- REAR wheels ---- + if (this.m_aWheelTimer[RL] > 0.0 || this.m_aWheelTimer[RR] > 0.0) { + var rearBrake = brake; + var rearTraction = traction; + if (this.bIsHandbrakeOn) { + rearBrake = 20000.0; + } else if (this.m_doingBurnout && hasRear) { + rearBrake = 0.0; rearTraction = 0.0; + // VC: ApplyTurnForce(contactPoints[REAR_LEFT], -0.001*turnMass*steer*GetRight()) (force j, point p) + this.applyTurnForce(contactPoints[RL], vscale(this.right, -0.001 * this.m_fTurnMass * this.m_fSteerAngle)); + } else if (this.m_fTireTemperature > 1.0) { + rearTraction *= this.m_fTireTemperature; + } + for (var ri = 0; ri < 2; ri++) { + var rwi = ri === 0 ? RL : RR; + if (this.m_aWheelTimer[rwi] <= 0.0) continue; + var rThrust = hasRear ? acceleration : 0.0; + var rn = this.m_wheelContactNormal[rwi]; + var rFwd = vsub(fwdWorld, vscale(rn, dot(fwdWorld, rn))); + rFwd = norm(rFwd); + var rRight = norm(cross(rFwd, rn)); + var rAdh = this.adhesiveLimit(rn) * rearTraction; + var rwsRef = { state: this.m_aWheelState[rwi] }; + this.processWheel(rFwd, rRight, contactSpeeds[rwi], contactPoints[rwi], + wheelsOnGround, rThrust, rearBrake * brakeBiasRear, rAdh * tractionBiasRear, rwi, rwsRef); + this.m_aWheelState[rwi] = rwsRef.state; + this.m_aWheelSpeed[rwi] = processWheelRotation(rwsRef.state, rFwd, contactSpeeds[rwi], this.wheelRadius); + this.m_aWheelRotation[rwi] += this.m_aWheelSpeed[rwi]; + } + } else { + for (var rro = 0; rro < 2; rro++) { + var rwo = rro === 0 ? RL : RR; + if (hasRear && acceleration !== 0.0) { + if (acceleration > 0.0) { if (this.m_aWheelSpeed[rwo] < 2.0) this.m_aWheelSpeed[rwo] -= 0.2; } + else { if (this.m_aWheelSpeed[rwo] > -2.0) this.m_aWheelSpeed[rwo] += 0.1; } + } else this.m_aWheelSpeed[rwo] *= 0.95; + this.m_aWheelRotation[rwo] += this.m_aWheelSpeed[rwo]; + } + } + + if (this.m_doingBurnout && !this.bIsHandbrakeOn) { /* keep */ } else this.m_doingBurnout = 0; + }; + + // adhesive limit for our terrain (ADHESIVE_LOOSE dirt vs ADHESIVE_RUBBER wheel). + // Firm dirt/grass ~1.0; steep/loose a bit less. Representative of VC surface.dat. + Car.prototype.adhesiveLimit = function (normal) { + // steeper ground -> a touch less grip + var flat = Math.max(0, normal.y); + return 0.92 + 0.08 * flat; + }; + + // integrate: ApplyMoveSpeed + ApplyTurnSpeed + reorthonormalize + Car.prototype.integrate = function () { + // position + vaddscaled(this.pos, this.moveSpeed, STEP); + // orientation: rotate axes by turnSpeed (denormalizes), then reorthonormalize + var tv = vscale(this.turnSpeed, STEP); + this.right = vadd(this.right, cross(tv, this.right)); + this.up = vadd(this.up, cross(tv, this.up)); + this.fwd = vadd(this.fwd, cross(tv, this.fwd)); + // Gram-Schmidt (Y-up, Z-fwd, X-right) + this.up = norm(this.up); + this.fwd = norm(vsub(this.fwd, vscale(this.up, dot(this.fwd, this.up)))); + this.right = norm(cross(this.up, this.fwd)); + // rebuild fwd exactly orthogonal + this.fwd = norm(cross(this.right, this.up)); + }; + + // --- input -> pedals & steering (CAutomobile::ProcessControl player path) 1:1 --- + // inp: {throttle:-1..1 (W - S), steer:-1..1 (left neg?), handbrake:bool} + Car.prototype.setInput = function (inp) { + var speed = dot(this.moveSpeed, this.fwd); + this.bIsHandbrakeOn = !!inp.handbrake; + + // steering low-pass then squared curve + var targetSteer = inp.steer; // -1..1 + this.m_fSteerInput += (targetSteer - this.m_fSteerInput) * 0.2 * STEP; + if (this.m_fSteerInput > 1) this.m_fSteerInput = 1; + if (this.m_fSteerInput < -1) this.m_fSteerInput = -1; + var fValue = this.m_fSteerInput < 0 ? -sq(this.m_fSteerInput) : sq(this.m_fSteerInput); + this.m_fSteerAngle = DEGTORAD(this.h.fSteeringLock) * fValue; + + // accelerate / brake + var acceleration = inp.throttle; // (accel - brake), -1..1 + if (Math.abs(speed) < 0.01) { + if (inp.throttle > 0.58 && inp.brakeHeld) { // both -> burnout + this.m_fGasPedal = inp.throttle; this.m_fBrakePedal = 1.0; this.m_doingBurnout = 1; + } else { + this.m_fGasPedal = acceleration; this.m_fBrakePedal = 0.0; + } + } else { + if (speed * acceleration < 0.0) { this.m_fGasPedal = 0.0; this.m_fBrakePedal = Math.abs(acceleration); } + else { this.m_fGasPedal = acceleration; this.m_fBrakePedal = 0.0; } + } + if (this.bIsHandbrakeOn) { this.m_fBrakePedal = 0.0; } + }; + + // one fixed 50 Hz step + Car.prototype.step = function (inp, ground) { + this.setInput(inp); + this.processControl(ground); + this.integrate(); + }; + + // speed helpers + Car.prototype.speedKmh = function () { return mag(this.moveSpeed) * 50 * 3.6; }; + Car.prototype.fwdSpeedMs = function () { return dot(this.moveSpeed, this.fwd) * 50; }; + + // ===================================================================== + // Default handling presets (VC-range values; model is the 1:1 part) + // ===================================================================== + // Presets: the *model* above is the 1:1 reVC port. These per-car numbers use + // VC-plausible ranges tuned to stay planted on Планета Жопа's fbm terrain + // (the original HANDLING.CFG isn't shipped in the repo — it's game data). + var PRESETS = { + // БРОВЕНОСЕЦ 4x4 — reliable all-rounder: fast (≈100 km/h), climbs the butt-hills, + // grippy 4WD, tall soft suspension. The friendly default. + brovenosec: { + name: 'БРОВЕНОСЕЦ 4x4', mass: 1650, dimX: 2.1, dimY: 4.9, dimZ: 1.5, + comX: 0, comY: 0.0, comZ: -0.15, submerged: 80, + tractionMult: 1.1, tractionLoss: 0.9, tractionBias: 0.5, + nGears: 5, maxVel: 270, engineAccel: 34, driveType: '4', engineType: 'P', + brakeDecel: 9, brakeBias: 0.5, abs: 0, steerLock: 38, + susForce: 1.2, susDamp: 0.18, susUpper: 0.32, susLower: -0.30, susBias: 0.5, + susAntidive: 0.3, collDmg: 0.3, flags: 0 + }, + // ЖОПЕРРАРИ — rear-drive sport: quick and tail-happy on flats, slides in corners, + // low & wide. Bogs a little launching up steep hills (proper RWD character). + zhoperrari: { + name: 'ЖОПЕРРАРИ', mass: 1500, dimX: 2.0, dimY: 4.9, dimZ: 1.35, + comX: 0, comY: 0.0, comZ: -0.25, submerged: 82, + tractionMult: 1.30, tractionLoss: 0.82, tractionBias: 0.52, + nGears: 5, maxVel: 300, engineAccel: 32, driveType: 'R', engineType: 'P', + brakeDecel: 10, brakeBias: 0.52, abs: 0, steerLock: 38, + susForce: 1.15, susDamp: 0.16, susUpper: 0.28, susLower: -0.28, susBias: 0.5, + susAntidive: 0.35, collDmg: 0.6, flags: 0 + } + }; + + var api = { + Car: Car, makeHandling: makeHandling, PRESETS: PRESETS, + // build a ground(x,z) sampler from a groundH function (adds normals). + // opts.patch = tire contact-patch radius: the tire bridges bumps smaller than + // itself, so we sample height as a small patch-average and take the normal over + // the same width. This is a heightfield adaptation (VC reads a real collision + // mesh), NOT a change to the handling model — it just stops a hard suspension + // from chattering on sub-tire noise. Larger patch = calmer ride. + makeGround: function (groundH, water_y, opts) { + opts = opts || {}; + var e = opts.patch || 1.5; // normal/patch half-width (m) + return function (x, z) { + // 5-tap patch-averaged height (center + 4 around at radius e) + var hC = groundH(x, z); + var hX = groundH(x + e, z), hXm = groundH(x - e, z); + var hZ = groundH(x, z + e), hZm = groundH(x, z - e); + var h = (hC * 2 + hX + hXm + hZ + hZm) / 6; + // normal from the patch gradient + var nx = (hXm - hX) / (2 * e), nz = (hZm - hZ) / (2 * e), ny = 1.0; + var m = Math.sqrt(nx * nx + ny * ny + nz * nz); + return { h: h, nx: nx / m, ny: ny / m, nz: nz / m }; + }; + }, + _v: v, _dot: dot, _cross: cross, _mag: mag + }; + + if (typeof module !== 'undefined' && module.exports) module.exports = api; + root.PZCarPhys = api; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/serve/index.html b/serve/index.html index 8041504..a335c21 100644 --- a/serve/index.html +++ b/serve/index.html @@ -2191,6 +2191,7 @@ const _fwd = new THREE.Vector3(), _rgt = new THREE.Vector3(), _up = new THREE.Ve const _err = new THREE.Vector3(), _eb = new THREE.Vector3(), _qi = new THREE.Quaternion(), _q1 = new THREE.Quaternion(), _q2 = new THREE.Quaternion(), _dq = new THREE.Quaternion(), _qe = new THREE.Quaternion(), _acc = new THREE.Vector3(); const _camF = new THREE.Vector3(), _camR = new THREE.Vector3(), _camGoal = new THREE.Vector3(), _camLook = new THREE.Vector3(70, 22, -60), _tmp = new THREE.Vector3(); function step(dt) { + if (carMode) { carStep(dt); return; } dt = Math.min(dt, 0.03); if (!isFinite(S.s)) S.s = 0; const direct = S.mode === 'direct'; @@ -2373,7 +2374,7 @@ function tick(now) { arrowSet.position.copy(S.p); arrowSet.setDirection(S.dbgSet); } // camera: chase (always behind the drone) or free orbit - if (camMode === 'chase') { + if (carMode) { carCamera(); } else if (camMode === 'chase') { controls.enabled = false; _camF.set(0, 0, 1).applyQuaternion(S.q); _camF.y = 0; if (_camF.lengthSq() < 1e-3) _camF.set(0, 0, 1); _camF.normalize(); _camGoal.copy(S.p).addScaledVector(_camF, -18).addScaledVector(WUP, 7); @@ -2392,6 +2393,7 @@ function tick(now) { (_dispE.setFromQuaternion(S.q, 'YXZ'), ' крен: ' + (_dispE.z * 57.3).toFixed(0) + '° тангаж: ' + (_dispE.x * 57.3).toFixed(0)) + '° · онлайн: ' + (1 + Object.keys(peers).length) + (mpStatus ? ' ' + mpStatus : '') + (freezePos ? ' · ⏸ ПОЗИЦИЯ ЗАФИКС.' : '') + (dbgVec ? ' · ↑вектора' : ''); + if (carMode && carObj) { hud.textContent = '\uD83D\uDE97 ' + carObj.h.name + ' \u0441\u043A\u043E\u0440: ' + carObj.speedKmh().toFixed(0) + ' \u043A\u043C/\u0447 \u043F\u0435\u0440\u0435\u0434\u0430\u0447\u0430: ' + carObj.gearRef.gear + ' \u00B7 \u043E\u043D\u043B\u0430\u0439\u043D: ' + (1 + Object.keys(peers).length) + (mpStatus ? ' ' + mpStatus : ''); } rcTick(); // ЖОПО-СВО: рубежи + гоночный HUD dayNight.updateSky(now); // цикл день/ночь: солнце/луна/звёзды/небо/свет/туман skyGroup.position.copy(camera.position); // sky/planets ride with the camera (at infinity, no clip) @@ -2467,7 +2469,7 @@ function connectMP() { } connectMP(); setInterval(() => { - if (wsConn && wsConn.readyState === 1) wsConn.send(JSON.stringify({ + if (!carMode && wsConn && wsConn.readyState === 1) wsConn.send(JSON.stringify({ t: 'state', n: myName, s: { x: +S.p.x.toFixed(2), y: +S.p.y.toFixed(2), z: +S.p.z.toFixed(2), q: [+S.q.x.toFixed(3), +S.q.y.toFixed(3), +S.q.z.toFixed(3), +S.q.w.toFixed(3)], @@ -2513,6 +2515,944 @@ if (('ontouchstart' in window) || navigator.maxTouchPoints > 0) { } addEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); rtScene.setSize(Math.floor(innerWidth * renderer.getPixelRatio()), Math.floor(innerHeight * renderer.getPixelRatio())); }); try { window.PZ = { THREE, scene, camera, S, groundH, WATER_Y, herd, herdMeshes, MAP, MAPS: PZ_MAPS }; } catch(e) {} +/* ==== PZ_CAR_INTEGRATION (inlined car_phys.js + _carmod.js) ==== */ +/* ===================================================================== + Планета Жопа — АВТОФИЗИКА + Faithful JS port of the GTA:VC (reVC) car handling model. + Ported 1:1 from kirillsurkov/racing_game (reVC "miami"): + - cTransmission::InitGearRatios / CalculateDriveAcceleration (Transmission.cpp) + - cHandlingDataMgr::ConvertDataToGameUnits (HandlingMgr.cpp) + - CVehicle::ProcessWheel (tyre traction/skid model) (Vehicle.cpp) + - CAutomobile::ProcessControl driving core (susp+drive+brake+steer) + - CPhysical rigid body: ApplyMoveForce/ApplyTurnForce/GetMass, + ApplySpringCollisionAlt, ApplySpringDampening, ApplyAirResistance, + ApplyGravity, ApplyMoveSpeed, ApplyTurnSpeed (Physical.cpp) + Units: 1 unit = 1 m, 1 step = 1/50 s (GetTimeStep()==1.0). Runs at fixed 50 Hz. + Coordinate frame adapted to the game's world: Y up (VC uses Z up). + Works in node (plain {x,y,z}) and in-browser (same code). + ===================================================================== */ +(function (root) { + 'use strict'; + + // ---- constants (from reVC) ---- + var GRAVITY = 0.008; // Physical.h #define GRAVITY (0.008f) + var WHEEL_FRICTION = 0.9; // cHandlingDataMgr::Initialise fWheelFriction + var STEP = 1.0; // CTimer::GetTimeStep() nominal (50 Hz) + // wheel indices (CARWHEEL_*) + var FL = 0, FR = 1, RL = 2, RR = 3; + // tWheelState + var WHEEL_STATE_NORMAL = 0, WHEEL_STATE_SPINNING = 1, WHEEL_STATE_SKIDDING = 2, WHEEL_STATE_FIXED = 3; + + // ---- tiny vec3 on {x,y,z} ---- + function v(x, y, z) { return { x: x || 0, y: y || 0, z: z || 0 }; } + function vset(a, x, y, z) { a.x = x; a.y = y; a.z = z; return a; } + function vcopy(a) { return { x: a.x, y: a.y, z: a.z }; } + function vadd(a, b) { return v(a.x + b.x, a.y + b.y, a.z + b.z); } + function vsub(a, b) { return v(a.x - b.x, a.y - b.y, a.z - b.z); } + function vscale(a, s) { return v(a.x * s, a.y * s, a.z * s); } + function vaddi(a, b) { a.x += b.x; a.y += b.y; a.z += b.z; return a; } + function vaddscaled(a, b, s) { a.x += b.x * s; a.y += b.y * s; a.z += b.z * s; return a; } + function dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; } + function cross(a, b) { + return v(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); + } + function magsq(a) { return a.x * a.x + a.y * a.y + a.z * a.z; } + function mag(a) { return Math.sqrt(magsq(a)); } + function norm(a) { var m = mag(a); if (m > 1e-9) { return v(a.x / m, a.y / m, a.z / m); } return v(0, 0, 0); } + function sq(x) { return x * x; } + function DEGTORAD(d) { return d * Math.PI / 180; } + + // ===================================================================== + // Handling: raw cfg fields -> game units (ConvertDataToGameUnits 1:1) + // ===================================================================== + // raw fields mirror HANDLING.CFG columns (post the *0.4 that LoadHandlingData + // applies to engineAccel at field 14). + function makeHandling(raw) { + var h = { + name: raw.name, + fMass: raw.mass, + Dimension: v(raw.dimX, raw.dimY, raw.dimZ), + CentreOfMass: v(raw.comX, raw.comY, raw.comZ), + nPercentSubmerged: raw.submerged, + fTractionMultiplier: raw.tractionMult, + fTractionLoss: raw.tractionLoss, + fTractionBias: raw.tractionBias, + fBrakeDeceleration: raw.brakeDecel, + fBrakeBias: raw.brakeBias, + bABS: raw.abs, + fSteeringLock: raw.steerLock, + fSuspensionForceLevel: raw.susForce, + fSuspensionDampingLevel: raw.susDamp, + fSuspensionUpperLimit: raw.susUpper, + fSuspensionLowerLimit: raw.susLower, + fSuspensionBias: raw.susBias, + fSuspensionAntidiveMultiplier: raw.susAntidive || 0, + fCollisionDamageMultiplier: raw.collDmg || 1, + Flags: raw.flags || 0, + Transmission: { + nNumberOfGears: raw.nGears, + nDriveType: raw.driveType, // 'F' | 'R' | '4' + nEngineType: raw.engineType || 'P', + Flags: raw.flags || 0, + fEngineAcceleration: raw.engineAccel * 0.4, // LoadHandlingData field 14: *0.4 + fMaxVelocity: raw.maxVel, + Gears: [ {}, {}, {}, {}, {}, {} ] + } + }; + convertToGameUnits(h); + initGearRatios(h.Transmission); + // moment of inertia already set by convert + return h; + } + + function convertToGameUnits(h) { + var T = h.Transmission; + T.fEngineAcceleration *= 1.0 / (50.0 * 50.0); + T.fMaxVelocity *= 1000.0 / (60.0 * 60.0 * 50.0); + h.fBrakeDeceleration *= 1.0 / (50.0 * 50.0); + h.fTurnMass = (sq(h.Dimension.x) + sq(h.Dimension.y)) * h.fMass / 12.0; + if (h.fTurnMass < 10.0) h.fTurnMass *= 5.0; + h.fInvMass = 1.0 / h.fMass; + h.fBuoyancy = 100.0 / h.nPercentSubmerged * GRAVITY * h.fMass; + + // drag-limited real max velocity (ConvertDataToGameUnits 1:1) + var a = 0.0, b = 100.0, velocity = T.fMaxVelocity; + while (a < b && velocity > 0.0) { + velocity -= 0.01; + a = T.fEngineAcceleration / 6.0; + var a_drag = 0.5 * sq(velocity) * h.Dimension.x * h.Dimension.z / h.fMass; + b = -velocity * (1.0 / (a_drag + 1.0) - 1.0); + } + T.fMaxCruiseVelocity = velocity; + T.fMaxVelocity = velocity * 1.2; + T.fMaxReverseVelocity = -0.2; + + if (T.nDriveType === '4') T.fEngineAcceleration /= 4.0; + else T.fEngineAcceleration /= 2.0; + } + + function initGearRatios(T) { + var G = T.Gears; + var i; + for (i = 0; i < 6; i++) G[i] = { fMaxVelocity: 0, fShiftUpVelocity: 0, fShiftDownVelocity: 0 }; + for (i = 1; i <= T.nNumberOfGears; i++) { + var g0 = G[i - 1], g1 = G[i]; + g1.fMaxVelocity = i / T.nNumberOfGears * T.fMaxVelocity; + var velocityDiff = g1.fMaxVelocity - g0.fMaxVelocity; + if (i >= T.nNumberOfGears) { + g1.fShiftUpVelocity = T.fMaxVelocity; + } else { + G[i + 1].fShiftDownVelocity = velocityDiff * 0.42 + g0.fMaxVelocity; + g1.fShiftUpVelocity = velocityDiff * 0.6667 + g0.fMaxVelocity; + } + } + G[0].fMaxVelocity = T.fMaxReverseVelocity; + G[0].fShiftUpVelocity = -0.01; + G[0].fShiftDownVelocity = T.fMaxReverseVelocity; + G[1].fShiftDownVelocity = -0.01; + } + + var HANDLING_2G_BOOST = 2, HANDLING_1G_BOOST = 1; + + // cTransmission::CalculateDriveAcceleration (recursive gear selection) 1:1 + function calcDriveAccel(T, gasPedal, gearRef, velocity, cheat) { + var fVelocity = velocity; + if (fVelocity < T.fMaxReverseVelocity) { return 0.0; } + if (fVelocity > T.fMaxVelocity) { return 0.0; } + + var gear = gearRef.gear; + var pGearRatio = T.Gears[gear]; + if (fVelocity > pGearRatio.fShiftUpVelocity) { + if (gear !== 0 || gasPedal > 0.0) { + gearRef.gear = gear + 1; + return calcDriveAccel(T, gasPedal, gearRef, fVelocity, false); + } + } else if (fVelocity < pGearRatio.fShiftDownVelocity && gear !== 0) { + if (gear !== 1 || gasPedal < 0.0) { + gearRef.gear = gear - 1; + return calcDriveAccel(T, gasPedal, gearRef, fVelocity, false); + } + } + + var speedMul, accelMul; + var Flags = T.Flags; + if (gear < 1) { + accelMul = (Flags & HANDLING_2G_BOOST) ? 2.0 : 1.0; + speedMul = -1.0; + } else if (T.nNumberOfGears === 1) { + accelMul = 1.0; speedMul = 1.0; + } else { + var f = 1.0 - (gear - 1) / (T.nNumberOfGears - 1); + speedMul = 3.0 * sq(f) + 1.0; + if (Flags & HANDLING_2G_BOOST) { + if (gear === 1) accelMul = (Flags & HANDLING_1G_BOOST) ? 2.0 : 1.6; + else if (gear === 2) accelMul = 1.3; + else accelMul = 1.0; + } else if ((Flags & HANDLING_1G_BOOST) && gear === 1) { + accelMul = 2.0; + } else accelMul = 1.0; + } + + var fCheat = cheat ? 1.2 : 1.0; + var targetVelocity = T.Gears[gear].fMaxVelocity * speedMul * fCheat; + var accel = (targetVelocity - fVelocity) * (T.fEngineAcceleration * accelMul) / Math.abs(targetVelocity); + var fAcceleration; + if (Math.abs(fVelocity) < Math.abs(T.Gears[gear].fMaxVelocity * fCheat)) + fAcceleration = gasPedal * accel * STEP; + else + fAcceleration = 0.0; + return fAcceleration; + } + + // ===================================================================== + // Car rigid body + // ===================================================================== + function Car(handling, opts) { + opts = opts || {}; + this.h = handling; + this.m_fMass = handling.fMass; + this.m_fTurnMass = handling.fTurnMass; + this.m_vecCentreOfMass = vcopy(handling.CentreOfMass); + this.m_fAirResistance = handling.Dimension.x * handling.Dimension.z / handling.fMass; // CAutomobile ctor + + // pose: position + orthonormal basis (right,up,fwd) + this.pos = v(opts.x || 0, opts.y || 0, opts.z || 0); + this.right = v(1, 0, 0); + this.up = v(0, 1, 0); + this.fwd = v(0, 0, 1); + if (opts.heading != null) this.setHeading(opts.heading); + + // velocities + this.moveSpeed = v(0, 0, 0); // m_vecMoveSpeed (units/step) + this.turnSpeed = v(0, 0, 0); // m_vecTurnSpeed (rad/step, world) + + // controls + this.m_fGasPedal = 0; + this.m_fBrakePedal = 0; + this.m_fSteerAngle = 0; + this.m_fSteerInput = 0; + this.bIsHandbrakeOn = false; + this.m_doingBurnout = 0; + + // transmission state + this.gearRef = { gear: 1 }; + this.m_fTireTemperature = 1.0; + + // per-wheel state + var i; + this.m_aSuspensionSpringRatio = [1, 1, 1, 1]; + this.m_aSuspensionSpringRatioPrev = [1, 1, 1, 1]; + this.m_aWheelTimer = [0, 0, 0, 0]; + this.m_aWheelSpeed = [0, 0, 0, 0]; // wheel angular speed (visual) + this.m_aWheelRotation = [0, 0, 0, 0]; // accumulated wheel angle (visual) + this.m_aWheelState = [0, 0, 0, 0]; + this.m_wheelContactPoint = [v(), v(), v(), v()]; // world, relative to pos + this.m_wheelContactNormal = [v(0, 1, 0), v(0, 1, 0), v(0, 1, 0), v(0, 1, 0)]; + this.m_wheelOnGround = [false, false, false, false]; + + this.m_nWheelsOnGround = 0; + this.m_nDriveWheelsOnGround = 0; + + // --- suspension geometry (per wheel, body space) --- + // Wheel mount X (side): +right = right side. Y(height): mount above hub. Z(fwd): fore/aft. + var d = handling.Dimension; + var halfW = d.x * 0.5; // track half-width + var halfL = d.y * 0.42; // wheelbase half-length + this.wheelRadius = opts.wheelRadius || Math.max(0.32, d.z * 0.22); + var Ls = handling.fSuspensionUpperLimit - handling.fSuspensionLowerLimit; // spring travel + if (Ls < 0.05) Ls = 0.30; + this.m_suspSpringLength = Ls; + // total ray length = spring travel + wheel radius (VC: lineLength = springLength + radius) + this.m_suspLineLength = Ls + this.wheelRadius; + // mount height above body origin so hub sits ~ at origin + var mountY = handling.fSuspensionUpperLimit; + // wheel body positions: [FL, FR, RL, RR]; +Z is forward, +X is right + this.m_wheelPosBody = [ + v(-halfW, mountY, halfL), // FRONT_LEFT + v( halfW, mountY, halfL), // FRONT_RIGHT + v(-halfW, mountY, -halfL), // REAR_LEFT + v( halfW, mountY, -halfL) // REAR_RIGHT + ]; + // spring direction in body space = straight down (-up) + this.m_springDirBody = v(0, -1, 0); + // height above road (approx, VC m_fHeightAboveRoad) + this.m_fHeightAboveRoad = Ls * (1.0 - 1.0 / (4.0 * handling.fSuspensionForceLevel)) - handling.fSuspensionLowerLimit; + } + + Car.prototype.setHeading = function (rad) { + // rotate basis about world-Y so fwd points to heading (0 => +Z) + var c = Math.cos(rad), s = Math.sin(rad); + this.fwd = v(s, 0, c); + this.up = v(0, 1, 0); + this.right = norm(cross(this.up, this.fwd)); + }; + + Car.prototype.heading = function () { return Math.atan2(this.fwd.x, this.fwd.z); }; + + // world position of a body-space point + Car.prototype.toWorldDir = function (b) { + return v( + this.right.x * b.x + this.up.x * b.y + this.fwd.x * b.z, + this.right.y * b.x + this.up.y * b.y + this.fwd.y * b.z, + this.right.z * b.x + this.up.z * b.y + this.fwd.z * b.z + ); + }; + + // CPhysical::GetSpeed(r): velocity of a point r (relative to pos) + Car.prototype.getSpeed = function (r) { + return vadd(this.moveSpeed, cross(this.turnSpeed, r)); + }; + + // CPhysical::ApplyMoveForce + Car.prototype.applyMoveForce = function (f) { + vaddi(this.moveSpeed, vscale(f, 1.0 / this.m_fMass)); + }; + // CPhysical::ApplyTurnForce (impulse j at point p, relative to pos) + Car.prototype.applyTurnForce = function (j, p) { + var com = this.toWorldDir(this.m_vecCentreOfMass); + var turnimpulse = cross(vsub(p, com), j); + vaddi(this.turnSpeed, vscale(turnimpulse, 1.0 / this.m_fTurnMass)); + }; + // CPhysical::GetMass(pos,dir) + Car.prototype.getMass = function (pos, dir) { + return 1.0 / (magsq(cross(pos, dir)) / this.m_fTurnMass + 1.0 / this.m_fMass); + }; + + Car.prototype.applyGravity = function () { + this.moveSpeed.y -= GRAVITY * STEP; + }; + + // CPhysical::ApplyAirResistance + Car.prototype.applyAirResistance = function () { + if (this.m_fAirResistance > 0.1) { + var f = Math.pow(this.m_fAirResistance, STEP); + this.moveSpeed = vscale(this.moveSpeed, f); + this.turnSpeed = vscale(this.turnSpeed, f); + } else { + var f2 = Math.pow(1.0 / Math.abs(1.0 + this.m_fAirResistance * 0.5 * magsq(this.moveSpeed)), STEP); + this.moveSpeed = vscale(this.moveSpeed, f2); + this.turnSpeed = vscale(this.turnSpeed, 0.99); + } + }; + + // CPhysical::ApplySpringCollisionAlt + Car.prototype.applySpringCollisionAlt = function (springConst, springDir, point, springRatio, bias, forceDir) { + var compression = 1.0 - springRatio; + if (compression > 0.0) { + var fd = vcopy(forceDir); + if (dot(springDir, fd) > 0.0) fd = vscale(fd, -1.0); + var step = Math.min(STEP, 3.0); + var impulse = GRAVITY * this.m_fMass * step * springConst * compression * bias * 2.0; + this.applyMoveForce(vscale(fd, impulse)); + this.applyTurnForce(vscale(fd, impulse), point); + return fd; // return possibly-flipped force dir (VC mutates forceDir; used as springDir below) + } + return forceDir; + }; + + // CPhysical::ApplySpringDampening + Car.prototype.applySpringDampening = function (damping, springDir, point, speed) { + var speedA = dot(speed, springDir); + var gs = this.getSpeed(point); + var speedB = dot(gs, springDir); + if (speedB === 0.0) return; + var step = Math.min(STEP, 3.0); + var impulse = -damping * (speedA + speedB) / 2.0 * this.m_fMass * step * 0.53; + var a = this.m_fTurnMass / ((magsq(point) + 1.0) * 2.0 * this.m_fMass); + a = Math.min(a, 1.0); + var b = Math.abs(impulse / (speedB * this.m_fMass)); + if (a < b) impulse *= a / b; + this.applyMoveForce(vscale(springDir, impulse)); + this.applyTurnForce(vscale(springDir, impulse), point); + }; + + // CVehicle::ProcessWheel (tyre traction/skid) 1:1 + Car.prototype.processWheel = function (wheelFwd, wheelRight, wheelContactSpeed, wheelContactPoint, + wheelsOnGround, thrust, brake, adhesion, wheelId, wsRef) { + var bAlreadySkidding = false; + var fwd = 0.0, right = 0.0; + + var bBraking = brake !== 0.0; + if (bBraking) thrust = 0.0; + var bDriving = thrust !== 0.0; + + var contactSpeedFwd = dot(wheelContactSpeed, wheelFwd); + var contactSpeedRight = dot(wheelContactSpeed, wheelRight); + + if (wsRef.state !== WHEEL_STATE_NORMAL) bAlreadySkidding = true; + wsRef.state = WHEEL_STATE_NORMAL; + + adhesion *= STEP; + if (bAlreadySkidding) adhesion *= this.h.fTractionLoss; + + if (contactSpeedRight !== 0.0) { + right = -contactSpeedRight / wheelsOnGround; + } + + if (bDriving) { + fwd = thrust; + if (right > 0.0) { if (right > adhesion) right = adhesion; } + else { if (right < -adhesion) right = -adhesion; } + } else if (contactSpeedFwd !== 0.0) { + fwd = -contactSpeedFwd / wheelsOnGround; + if (!bBraking) { + if (this.m_fGasPedal < 0.01) { + if (this.m_fMass < 500.0) + brake = 0.2 * WHEEL_FRICTION / this.m_fMass; + else + brake = WHEEL_FRICTION / this.m_fMass; + } + } + if (brake > adhesion) { + if (Math.abs(contactSpeedFwd) > 0.005) wsRef.state = WHEEL_STATE_FIXED; + } else { + if (fwd > 0.0) { if (fwd > brake) fwd = brake; } + else { if (fwd < -brake) fwd = -brake; } + } + } + + var speedSq = sq(right) + sq(fwd); + if (sq(adhesion) < speedSq) { + if (wsRef.state !== WHEEL_STATE_FIXED) { + if (bDriving && contactSpeedFwd < 0.2) wsRef.state = WHEEL_STATE_SPINNING; + else wsRef.state = WHEEL_STATE_SKIDDING; + } + var l = Math.sqrt(speedSq); + var tractionLoss = bAlreadySkidding ? 1.0 : this.h.fTractionLoss; + right *= adhesion * tractionLoss / l; + fwd *= adhesion * tractionLoss / l; + } + + if (fwd !== 0.0 || right !== 0.0) { + var totalSpeed = vadd(vscale(wheelFwd, fwd), vscale(wheelRight, right)); + + var turnDirection = vcopy(totalSpeed); + var separateTurnForce = false; + var antidive = this.h.fSuspensionAntidiveMultiplier; + if (antidive > 0.0) { + if (bBraking) { + separateTurnForce = true; + turnDirection = vsub(totalSpeed, vscale(wheelFwd, antidive * fwd)); + } else if (bDriving) { + separateTurnForce = true; + turnDirection = vsub(totalSpeed, vscale(wheelFwd, 0.5 * antidive * fwd)); + } + } + + var direction = vcopy(totalSpeed); + var speed = mag(totalSpeed); + var turnSpeed = separateTurnForce ? mag(turnDirection) : speed; + direction = norm(direction); + if (separateTurnForce) turnDirection = norm(turnDirection); + else turnDirection = direction; + + var impulse = speed * this.m_fMass; + var turnImpulse = turnSpeed * this.getMass(wheelContactPoint, turnDirection); + + this.applyMoveForce(vscale(direction, impulse)); + this.applyTurnForce(vscale(turnDirection, turnImpulse), wheelContactPoint); + } + }; + + // CVehicle::ProcessWheelRotation (visual) + function processWheelRotation(state, fwd, speed, radius) { + var angularVelocity; + if (state === WHEEL_STATE_SPINNING) angularVelocity = -1.1; + else if (state === WHEEL_STATE_FIXED) angularVelocity = 0.0; + else angularVelocity = -dot(fwd, speed) / radius; + return angularVelocity * STEP; + } + + // ===================================================================== + // Suspension raycast against the heightfield world + // ===================================================================== + // groundInfo(x,z) -> {h, nx,ny,nz}. Provided by caller (built from groundH). + Car.prototype.raycastWheels = function (ground) { + for (var i = 0; i < 4; i++) { + this.m_aSuspensionSpringRatioPrev[i] = this.m_aSuspensionSpringRatio[i]; + var mountW = vadd(this.pos, this.toWorldDir(this.m_wheelPosBody[i])); // world mount + var springDir = this.toWorldDir(this.m_springDirBody); // world down-ish + springDir = norm(springDir); + // ray: from mount along springDir, length lineLength. Find ground crossing. + // Sample ground under the ray at the mount's (x,z) projected downward. + // Because terrain is a heightfield, march the ray and find where it passes below ground. + var L = this.m_suspLineLength; + var found = false, distHit = L, gh = 0, gnorm = v(0, 1, 0); + // coarse+fine march + var N = 8, prevAbove = null, prevT = 0; + for (var k = 0; k <= N; k++) { + var t = L * k / N; + var px = mountW.x + springDir.x * t; + var py = mountW.y + springDir.y * t; + var pz = mountW.z + springDir.z * t; + var g = ground(px, pz); + var above = py - g.h; // >0 means ray point above ground + if (above <= 0 && prevAbove !== null && prevAbove > 0) { + // crossing between prevT and t -> refine + var t0 = prevT, t1 = t, a0 = prevAbove, a1 = above; + for (var r = 0; r < 6; r++) { + var tm = 0.5 * (t0 + t1); + var mx = mountW.x + springDir.x * tm, mz = mountW.z + springDir.z * tm, my = mountW.y + springDir.y * tm; + var gg = ground(mx, mz); + var am = my - gg.h; + if (am <= 0) { t1 = tm; a1 = am; } else { t0 = tm; a0 = am; } + } + distHit = 0.5 * (t0 + t1); + var hx = mountW.x + springDir.x * distHit, hz = mountW.z + springDir.z * distHit; + var gh2 = ground(hx, hz); + gnorm = v(gh2.nx, gh2.ny, gh2.nz); + found = true; + break; + } + prevAbove = above; prevT = t; + } + + if (found) { + // raw ratio along the line, then rescale by wheel radius (VC) + var wheelRadiusNorm = 1.0 - this.m_suspSpringLength / this.m_suspLineLength; + var rawRatio = distHit / this.m_suspLineLength; + var ratio = (rawRatio - wheelRadiusNorm) / (1.0 - wheelRadiusNorm); + if (ratio < 0) ratio = 0; + if (ratio > 1) ratio = 1; + this.m_aSuspensionSpringRatio[i] = ratio; + // contact point (relative to pos) + var cpW = v(mountW.x + springDir.x * distHit, mountW.y + springDir.y * distHit, mountW.z + springDir.z * distHit); + this.m_wheelContactPoint[i] = vsub(cpW, this.pos); + this.m_wheelContactNormal[i] = norm(gnorm); + this.m_wheelOnGround[i] = ratio < 1.0; + } else { + this.m_aSuspensionSpringRatio[i] = 1.0; + this.m_wheelContactPoint[i] = this.toWorldDir(v(this.m_wheelPosBody[i].x, this.m_wheelPosBody[i].y - this.m_suspLineLength, this.m_wheelPosBody[i].z)); + this.m_wheelOnGround[i] = false; + } + } + }; + + // ===================================================================== + // Main per-step control (CAutomobile::ProcessControl driving core) 1:1 + // ===================================================================== + Car.prototype.processControl = function (ground) { + var i; + var h = this.h, T = h.Transmission; + + // --- CPhysical::ProcessControl: gravity + air resistance (order as VC) --- + this.applyGravity(); + this.applyAirResistance(); + + // --- suspension raycast (our world-collision analog) --- + this.raycastWheels(ground); + + var fwdWorld = this.fwd; + var fwdSpeed = Math.abs(dot(this.moveSpeed, fwdWorld)); + + var contactPoints = [null, null, null, null]; + var contactSpeeds = [null, null, null, null]; + var springDirections = [null, null, null, null]; + + // gather compressed springs + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 1.0) { + contactPoints[i] = this.m_wheelContactPoint[i]; + springDirections[i] = norm(this.toWorldDir(this.m_springDirBody)); + } else { + contactPoints[i] = this.m_wheelContactPoint[i]; + } + } + + // springs push up + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 1.0) { + var bias = h.fSuspensionBias; + if (i === RL || i === RR) bias = 1.0 - bias; + var fd = this.applySpringCollisionAlt(h.fSuspensionForceLevel, springDirections[i], + contactPoints[i], this.m_aSuspensionSpringRatio[i], bias, this.m_wheelContactNormal[i]); + springDirections[i] = fd; // VC then uses this as spring dir for dampening below + } + } + + // recompute contact speeds; if normal.z>0.35 use -normal as spring dir (VC: normal.y here) + for (i = 0; i < 4; i++) { + contactSpeeds[i] = this.getSpeed(contactPoints[i]); + if (this.m_aSuspensionSpringRatio[i] < 1.0 && this.m_wheelContactNormal[i].y > 0.35) + springDirections[i] = vscale(this.m_wheelContactNormal[i], -1.0); + } + + // dampen springs + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 0.99999 && springDirections[i]) + this.applySpringDampening(h.fSuspensionDampingLevel, springDirections[i], contactPoints[i], contactSpeeds[i]); + } + + // recompute contact speeds + for (i = 0; i < 4; i++) contactSpeeds[i] = this.getSpeed(contactPoints[i]); + + // --- engine acceleration --- + fwdSpeed = dot(this.moveSpeed, fwdWorld); + var acceleration = calcDriveAccel(T, this.m_fGasPedal, this.gearRef, fwdSpeed, false); + + var brake = this.m_fBrakePedal * h.fBrakeDeceleration * STEP; + var brakeBiasFront = 2.0 * h.fBrakeBias; + var brakeBiasRear = 2.0 - h.fBrakeBias; + var tractionBiasFront = 2.0 * h.fTractionBias; + var tractionBiasRear = 2.0 - tractionBiasFront; + + // count wheels on ground + this.m_nWheelsOnGround = 0; + this.m_nDriveWheelsOnGround = 0; + for (i = 0; i < 4; i++) { + if (this.m_aSuspensionSpringRatio[i] < 1.0) this.m_aWheelTimer[i] = 4.0; + else this.m_aWheelTimer[i] = Math.max(this.m_aWheelTimer[i] - STEP, 0.0); + if (this.m_aWheelTimer[i] > 0.0) { + this.m_nWheelsOnGround++; + if (T.nDriveType === '4') this.m_nDriveWheelsOnGround++; + else if (T.nDriveType === 'F') { if (i === FL || i === FR) this.m_nDriveWheelsOnGround++; } + else if (T.nDriveType === 'R') { if (i === RL || i === RR) this.m_nDriveWheelsOnGround++; } + } + } + + // traction (STATUS_PLAYER path) + var traction = 0.004; + traction *= h.fTractionMultiplier / 4.0; + + var hasFront = (T.nDriveType !== 'R'); + var hasRear = (T.nDriveType !== 'F'); + + var wheelsOnGround = Math.max(this.m_nWheelsOnGround, 1); + + // ---- FRONT wheels ---- + if (this.m_aWheelTimer[FL] > 0.0 || this.m_aWheelTimer[FR] > 0.0) { + var s = Math.sin(this.m_fSteerAngle), c = Math.cos(this.m_fSteerAngle); + for (var fi = 0; fi < 2; fi++) { + var wi = fi === 0 ? FL : FR; + if (this.m_aWheelTimer[wi] <= 0.0) continue; + var fThrust = hasFront ? acceleration : 0.0; + var n = this.m_wheelContactNormal[wi]; + var wFwd = vsub(fwdWorld, vscale(n, dot(fwdWorld, n))); + wFwd = norm(wFwd); + var wRight = norm(cross(wFwd, n)); + var tmp = vsub(vscale(wFwd, c), vscale(wRight, s)); + wRight = vadd(vscale(wFwd, s), vscale(wRight, c)); + wFwd = tmp; + var adhesion = this.adhesiveLimit(this.m_wheelContactNormal[wi]) * traction; + var wsRef = { state: this.m_aWheelState[wi] }; + this.processWheel(wFwd, wRight, contactSpeeds[wi], contactPoints[wi], + wheelsOnGround, fThrust, brake * brakeBiasFront, adhesion * tractionBiasFront, wi, wsRef); + this.m_aWheelState[wi] = wsRef.state; + this.m_aWheelSpeed[wi] = processWheelRotation(wsRef.state, wFwd, contactSpeeds[wi], this.wheelRadius); + this.m_aWheelRotation[wi] += this.m_aWheelSpeed[wi]; + } + } else { + // front wheels off ground (visual spin decay) + for (var fo = 0; fo < 2; fo++) { + var wo = fo === 0 ? FL : FR; + if (hasFront && acceleration !== 0.0) { + if (acceleration > 0.0) { if (this.m_aWheelSpeed[wo] < 2.0) this.m_aWheelSpeed[wo] -= 0.2; } + else { if (this.m_aWheelSpeed[wo] > -2.0) this.m_aWheelSpeed[wo] += 0.1; } + } else this.m_aWheelSpeed[wo] *= 0.95; + this.m_aWheelRotation[wo] += this.m_aWheelSpeed[wo]; + } + } + + // ---- REAR wheels ---- + if (this.m_aWheelTimer[RL] > 0.0 || this.m_aWheelTimer[RR] > 0.0) { + var rearBrake = brake; + var rearTraction = traction; + if (this.bIsHandbrakeOn) { + rearBrake = 20000.0; + } else if (this.m_doingBurnout && hasRear) { + rearBrake = 0.0; rearTraction = 0.0; + // VC: ApplyTurnForce(contactPoints[REAR_LEFT], -0.001*turnMass*steer*GetRight()) (force j, point p) + this.applyTurnForce(contactPoints[RL], vscale(this.right, -0.001 * this.m_fTurnMass * this.m_fSteerAngle)); + } else if (this.m_fTireTemperature > 1.0) { + rearTraction *= this.m_fTireTemperature; + } + for (var ri = 0; ri < 2; ri++) { + var rwi = ri === 0 ? RL : RR; + if (this.m_aWheelTimer[rwi] <= 0.0) continue; + var rThrust = hasRear ? acceleration : 0.0; + var rn = this.m_wheelContactNormal[rwi]; + var rFwd = vsub(fwdWorld, vscale(rn, dot(fwdWorld, rn))); + rFwd = norm(rFwd); + var rRight = norm(cross(rFwd, rn)); + var rAdh = this.adhesiveLimit(rn) * rearTraction; + var rwsRef = { state: this.m_aWheelState[rwi] }; + this.processWheel(rFwd, rRight, contactSpeeds[rwi], contactPoints[rwi], + wheelsOnGround, rThrust, rearBrake * brakeBiasRear, rAdh * tractionBiasRear, rwi, rwsRef); + this.m_aWheelState[rwi] = rwsRef.state; + this.m_aWheelSpeed[rwi] = processWheelRotation(rwsRef.state, rFwd, contactSpeeds[rwi], this.wheelRadius); + this.m_aWheelRotation[rwi] += this.m_aWheelSpeed[rwi]; + } + } else { + for (var rro = 0; rro < 2; rro++) { + var rwo = rro === 0 ? RL : RR; + if (hasRear && acceleration !== 0.0) { + if (acceleration > 0.0) { if (this.m_aWheelSpeed[rwo] < 2.0) this.m_aWheelSpeed[rwo] -= 0.2; } + else { if (this.m_aWheelSpeed[rwo] > -2.0) this.m_aWheelSpeed[rwo] += 0.1; } + } else this.m_aWheelSpeed[rwo] *= 0.95; + this.m_aWheelRotation[rwo] += this.m_aWheelSpeed[rwo]; + } + } + + if (this.m_doingBurnout && !this.bIsHandbrakeOn) { /* keep */ } else this.m_doingBurnout = 0; + }; + + // adhesive limit for our terrain (ADHESIVE_LOOSE dirt vs ADHESIVE_RUBBER wheel). + // Firm dirt/grass ~1.0; steep/loose a bit less. Representative of VC surface.dat. + Car.prototype.adhesiveLimit = function (normal) { + // steeper ground -> a touch less grip + var flat = Math.max(0, normal.y); + return 0.92 + 0.08 * flat; + }; + + // integrate: ApplyMoveSpeed + ApplyTurnSpeed + reorthonormalize + Car.prototype.integrate = function () { + // position + vaddscaled(this.pos, this.moveSpeed, STEP); + // orientation: rotate axes by turnSpeed (denormalizes), then reorthonormalize + var tv = vscale(this.turnSpeed, STEP); + this.right = vadd(this.right, cross(tv, this.right)); + this.up = vadd(this.up, cross(tv, this.up)); + this.fwd = vadd(this.fwd, cross(tv, this.fwd)); + // Gram-Schmidt (Y-up, Z-fwd, X-right) + this.up = norm(this.up); + this.fwd = norm(vsub(this.fwd, vscale(this.up, dot(this.fwd, this.up)))); + this.right = norm(cross(this.up, this.fwd)); + // rebuild fwd exactly orthogonal + this.fwd = norm(cross(this.right, this.up)); + }; + + // --- input -> pedals & steering (CAutomobile::ProcessControl player path) 1:1 --- + // inp: {throttle:-1..1 (W - S), steer:-1..1 (left neg?), handbrake:bool} + Car.prototype.setInput = function (inp) { + var speed = dot(this.moveSpeed, this.fwd); + this.bIsHandbrakeOn = !!inp.handbrake; + + // steering low-pass then squared curve + var targetSteer = inp.steer; // -1..1 + this.m_fSteerInput += (targetSteer - this.m_fSteerInput) * 0.2 * STEP; + if (this.m_fSteerInput > 1) this.m_fSteerInput = 1; + if (this.m_fSteerInput < -1) this.m_fSteerInput = -1; + var fValue = this.m_fSteerInput < 0 ? -sq(this.m_fSteerInput) : sq(this.m_fSteerInput); + this.m_fSteerAngle = DEGTORAD(this.h.fSteeringLock) * fValue; + + // accelerate / brake + var acceleration = inp.throttle; // (accel - brake), -1..1 + if (Math.abs(speed) < 0.01) { + if (inp.throttle > 0.58 && inp.brakeHeld) { // both -> burnout + this.m_fGasPedal = inp.throttle; this.m_fBrakePedal = 1.0; this.m_doingBurnout = 1; + } else { + this.m_fGasPedal = acceleration; this.m_fBrakePedal = 0.0; + } + } else { + if (speed * acceleration < 0.0) { this.m_fGasPedal = 0.0; this.m_fBrakePedal = Math.abs(acceleration); } + else { this.m_fGasPedal = acceleration; this.m_fBrakePedal = 0.0; } + } + if (this.bIsHandbrakeOn) { this.m_fBrakePedal = 0.0; } + }; + + // one fixed 50 Hz step + Car.prototype.step = function (inp, ground) { + this.setInput(inp); + this.processControl(ground); + this.integrate(); + }; + + // speed helpers + Car.prototype.speedKmh = function () { return mag(this.moveSpeed) * 50 * 3.6; }; + Car.prototype.fwdSpeedMs = function () { return dot(this.moveSpeed, this.fwd) * 50; }; + + // ===================================================================== + // Default handling presets (VC-range values; model is the 1:1 part) + // ===================================================================== + // Presets: the *model* above is the 1:1 reVC port. These per-car numbers use + // VC-plausible ranges tuned to stay planted on Планета Жопа's fbm terrain + // (the original HANDLING.CFG isn't shipped in the repo — it's game data). + var PRESETS = { + // БРОВЕНОСЕЦ 4x4 — reliable all-rounder: fast (≈100 km/h), climbs the butt-hills, + // grippy 4WD, tall soft suspension. The friendly default. + brovenosec: { + name: 'БРОВЕНОСЕЦ 4x4', mass: 1650, dimX: 2.1, dimY: 4.9, dimZ: 1.5, + comX: 0, comY: 0.0, comZ: -0.15, submerged: 80, + tractionMult: 1.1, tractionLoss: 0.9, tractionBias: 0.5, + nGears: 5, maxVel: 270, engineAccel: 34, driveType: '4', engineType: 'P', + brakeDecel: 9, brakeBias: 0.5, abs: 0, steerLock: 38, + susForce: 1.2, susDamp: 0.18, susUpper: 0.32, susLower: -0.30, susBias: 0.5, + susAntidive: 0.3, collDmg: 0.3, flags: 0 + }, + // ЖОПЕРРАРИ — rear-drive sport: quick and tail-happy on flats, slides in corners, + // low & wide. Bogs a little launching up steep hills (proper RWD character). + zhoperrari: { + name: 'ЖОПЕРРАРИ', mass: 1500, dimX: 2.0, dimY: 4.9, dimZ: 1.35, + comX: 0, comY: 0.0, comZ: -0.25, submerged: 82, + tractionMult: 1.30, tractionLoss: 0.82, tractionBias: 0.52, + nGears: 5, maxVel: 300, engineAccel: 32, driveType: 'R', engineType: 'P', + brakeDecel: 10, brakeBias: 0.52, abs: 0, steerLock: 38, + susForce: 1.15, susDamp: 0.16, susUpper: 0.28, susLower: -0.28, susBias: 0.5, + susAntidive: 0.35, collDmg: 0.6, flags: 0 + } + }; + + var api = { + Car: Car, makeHandling: makeHandling, PRESETS: PRESETS, + // build a ground(x,z) sampler from a groundH function (adds normals). + // opts.patch = tire contact-patch radius: the tire bridges bumps smaller than + // itself, so we sample height as a small patch-average and take the normal over + // the same width. This is a heightfield adaptation (VC reads a real collision + // mesh), NOT a change to the handling model — it just stops a hard suspension + // from chattering on sub-tire noise. Larger patch = calmer ride. + makeGround: function (groundH, water_y, opts) { + opts = opts || {}; + var e = opts.patch || 1.5; // normal/patch half-width (m) + return function (x, z) { + // 5-tap patch-averaged height (center + 4 around at radius e) + var hC = groundH(x, z); + var hX = groundH(x + e, z), hXm = groundH(x - e, z); + var hZ = groundH(x, z + e), hZm = groundH(x, z - e); + var h = (hC * 2 + hX + hXm + hZ + hZm) / 6; + // normal from the patch gradient + var nx = (hXm - hX) / (2 * e), nz = (hZm - hZ) / (2 * e), ny = 1.0; + var m = Math.sqrt(nx * nx + ny * ny + nz * nz); + return { h: h, nx: nx / m, ny: ny / m, nz: nz / m }; + }; + }, + _v: v, _dot: dot, _cross: cross, _mag: mag + }; + + if (typeof module !== 'undefined' && module.exports) module.exports = api; + root.PZCarPhys = api; +})(typeof window !== 'undefined' ? window : globalThis); + +/* ===== АВТО-РЕЖИМ: выбор транспорта + езда на ported reVC-физике (client) ===== */ +var carMode = false, carObj = null, carMesh = null, carGround = null, carAcc = 0, carPreset = 'brovenosec'; +var carHeadlights = []; +var carMesh_wheels = []; +var CAR_SPAWN = { x: 70, z: -40 }; + +function buildCarMesh() { + var h = carObj.h; + var g = new THREE.Group(); + var L = h.Dimension.y, Wd = h.Dimension.x, Ht = h.Dimension.z; + var body = new THREE.Mesh(new THREE.BoxGeometry(Wd, Ht * 0.6, L), + new THREE.MeshStandardMaterial({ color: 0xc81e3a, roughness: 0.45, metalness: 0.3 })); + body.position.y = Ht * 0.15; g.add(body); + var cab = new THREE.Mesh(new THREE.BoxGeometry(Wd * 0.86, Ht * 0.5, L * 0.5), + new THREE.MeshStandardMaterial({ color: 0x20242c, roughness: 0.3, metalness: 0.4 })); + cab.position.set(0, Ht * 0.55, -L * 0.05); g.add(cab); + var nose = new THREE.Mesh(new THREE.BoxGeometry(Wd * 0.5, Ht * 0.2, 0.3), + new THREE.MeshStandardMaterial({ color: 0xffd23a, emissive: 0x442200 })); + nose.position.set(0, Ht * 0.1, L * 0.5); g.add(nose); // жёлтый нос = вперёд (+Z) + // фары — area lights (LTC, как у дрона): светят вперёд (+Z) и чуть вниз на дорогу + var hlX = Wd * 0.34, hlY = Ht * 0.16, hlZ = L * 0.5 + 0.04; + var lensMat = new THREE.MeshStandardMaterial({ color: 0xfff6d8, emissive: 0xfff0b0, emissiveIntensity: 2.6, roughness: 0.25, metalness: 0.0 }); + carHeadlights = []; + for (var hi = 0; hi < 2; hi++) { + var sx = hi === 0 ? -1 : 1; + var lens = new THREE.Mesh(new THREE.BoxGeometry(Wd * 0.26, Ht * 0.18, 0.10), lensMat); + lens.position.set(sx * hlX, hlY, hlZ); g.add(lens); + var hl = new THREE.RectAreaLight(0xfff2cc, 11.0, Wd * 0.55, Ht * 0.42); + hl.position.set(sx * hlX, hlY, hlZ + 0.06); + hl.rotation.set(Math.PI + 0.16, 0, 0); // локальный -Z -> +Z (вперёд), лёгкий наклон вниз + g.add(hl); carHeadlights.push(hl); + } + var wr = carObj.wheelRadius; + var wheelGeo = new THREE.CylinderGeometry(wr, wr, 0.3, 16); + var wheelMat = new THREE.MeshStandardMaterial({ color: 0x111214, roughness: 0.85 }); + carMesh_wheels = []; + for (var i = 0; i < 4; i++) { + var w = new THREE.Mesh(wheelGeo, wheelMat); + var pb = carObj.m_wheelPosBody[i]; + w.position.set(pb.x, pb.y, pb.z); + w.rotation.set(0, 0, Math.PI / 2); // ось колеса вдоль X + g.add(w); carMesh_wheels.push(w); + } + return g; +} + +function spawnCar() { + var CP = window.PZCarPhys; + var hd = CP.makeHandling(CP.PRESETS[carPreset]); + carGround = CP.makeGround(groundH, WATER_Y, { patch: 1.5 }); + var gy = groundH(CAR_SPAWN.x, CAR_SPAWN.z); + carObj = new CP.Car(hd, { x: CAR_SPAWN.x, y: gy + 2.0, z: CAR_SPAWN.z, heading: 0 }); + carMesh = buildCarMesh(); + scene.add(carMesh); + try { window.PZCAR = carObj; } catch(e){} +} + +function readCarInput() { + var th = 0, st = 0; + if (keys['KeyW'] || keys['ArrowUp']) th += 1; + if (keys['KeyS'] || keys['ArrowDown']) th -= 1; + if (keys['KeyA'] || keys['ArrowLeft']) st += 1; // руль влево (исправлено: влево/вправо было перепутано) + if (keys['KeyD'] || keys['ArrowRight']) st -= 1; // руль вправо + return { throttle: th, steer: st, handbrake: !!keys['Space'], brakeHeld: (th < 0) }; +} + +var _cbR = new THREE.Vector3(), _cbU = new THREE.Vector3(), _cbF = new THREE.Vector3(), _cbM = new THREE.Matrix4(); +function carRespawn() { + var CP = window.PZCarPhys, gy = groundH(CAR_SPAWN.x, CAR_SPAWN.z); + carObj.pos = CP._v(CAR_SPAWN.x, gy + 2.0, CAR_SPAWN.z); + carObj.moveSpeed = CP._v(0, 0, 0); carObj.turnSpeed = CP._v(0, 0, 0); + carObj.setHeading(0); +} +function carStep(dt) { + if (!carObj) return; + if (keys['KeyR']) carRespawn(); + var inp = readCarInput(); carAcc += dt; var n = 0; + while (carAcc >= 0.02 && n < 6) { carObj.step(inp, carGround); carAcc -= 0.02; n++; } // фиксированные 50 Гц + carMesh.position.set(carObj.pos.x, carObj.pos.y, carObj.pos.z); + _cbR.set(carObj.right.x, carObj.right.y, carObj.right.z); + _cbU.set(carObj.up.x, carObj.up.y, carObj.up.z); + _cbF.set(carObj.fwd.x, carObj.fwd.y, carObj.fwd.z); + _cbM.makeBasis(_cbR, _cbU, _cbF); carMesh.quaternion.setFromRotationMatrix(_cbM); + for (var i = 0; i < 4; i++) { + var w = carMesh_wheels[i]; if (!w) continue; + var pb = carObj.m_wheelPosBody[i]; w.position.set(pb.x, pb.y, pb.z); + var steer = (i < 2) ? carObj.m_fSteerAngle : 0; // FL,FR поворачиваются + w.rotation.set(0, -steer, Math.PI / 2); + } +} + +var _carGoal = new THREE.Vector3(), _carLook = new THREE.Vector3(); +function carCamera() { + controls.enabled = false; + _cbF.set(carObj.fwd.x, 0, carObj.fwd.z); if (_cbF.lengthSq() < 1e-4) _cbF.set(0, 0, 1); _cbF.normalize(); + _carGoal.set(carObj.pos.x, carObj.pos.y, carObj.pos.z).addScaledVector(_cbF, -12).addScaledVector(WUP, 5.5); + camera.position.lerp(_carGoal, 0.12); + _carLook.set(carObj.pos.x, carObj.pos.y + 1.3, carObj.pos.z); + _camLook.lerp(_carLook, 0.3); camera.lookAt(_camLook); +} + +/* --- стартовый выбор транспорта --- */ +(function vehicleSelect() { + var ov = document.createElement('div'); + ov.id = 'vsOverlay'; + ov.style.cssText = 'position:fixed;inset:0;z-index:10050;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(4,6,12,.88);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);font-family:system-ui,Segoe UI,Roboto,sans-serif;color:#eef3ff'; + ov.innerHTML = + '
ПЛАНЕТА ЖОПА
' + + '
На чём поедешь?
' + + '
' + + '' + + '' + + '
' + + '
сменить транспорт — перезагрузка страницы
'; + document.body.appendChild(ov); + ['pointerdown', 'touchstart', 'mousedown', 'keydown'].forEach(function (ev) { ov.addEventListener(ev, function (e) { e.stopPropagation(); }); }); + document.getElementById('vsDrone').onclick = function () { carMode = false; ov.remove(); }; + document.getElementById('vsCar').onclick = function () { + carMode = true; + try { spawnCar(); } catch (e) { console.error('spawnCar', e); } + try { if (typeof drone !== 'undefined' && drone) drone.visible = false; } catch (e) {} + try { sd.style.display = 'none'; } catch (e) {} + try { dp.style.display = 'none'; } catch (e) {} + var hp = document.getElementById('help'); + if (hp) hp.innerHTML = '🚗 МАШИНА (физика reVC): W газ · S тормоз/задний · A/D руль · Space ручник · R сброс'; + ov.remove(); + }; +})(); + +/* ==== /PZ_CAR_INTEGRATION ==== */ + requestAnimationFrame(tick); })(); -- cgit v1.2.3