Phase 2 of turning the game into the company landing page. - Delete the Apache motorcycle model, its BIKE handling spec, the motorcycle row in the factory yard, rider and lean animation, and its prompt and dialogue strings. - The garage now holds cars only; the oriented-box collision, traffic lane checks and other vehicles are unchanged. - Rework the vehicle tests to cover gaps with a car instead of a bike, and add a check that no motorcycle code or text remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
227 lines
8.0 KiB
JavaScript
227 lines
8.0 KiB
JavaScript
import * as THREE from 'three';
|
|
import data from '../../data/mellat.json';
|
|
import { resolveCircle } from '../world/collision.js';
|
|
import { stepCar } from '../systems/vehicle.js';
|
|
import { EXIT_MAX_SPEED } from '../../constants/vehicles.js';
|
|
import { collideDriving, nearestRide, takeRide, walkAroundVehicles } from './garage.js';
|
|
import { strings } from '../../i18n/fa.js';
|
|
/**
|
|
* Runs the look operation for this subsystem.
|
|
* @param {*} context - context.
|
|
* @param {*} dx - dx.
|
|
* @param {*} dy - dy.
|
|
* @param {*} k - k.
|
|
* @returns {*} Operation result, when applicable.
|
|
*/
|
|
export function look(context, dx, dy, k) {
|
|
// In AR view the phone's orientation aims the camera; dragging only helps when there is no sensor.
|
|
if (context.real.on) {
|
|
if (context.arView.hasOrientation) return;
|
|
context.cam.yaw -= dx * k;
|
|
context.cam.arPitch = THREE.MathUtils.clamp(
|
|
(context.cam.arPitch ?? 0) - dy * k * 0.8,
|
|
-1.2,
|
|
1.2,
|
|
);
|
|
context.cam.lastMouse = context.elapsed;
|
|
return;
|
|
}
|
|
context.cam.yaw -= dx * k;
|
|
context.cam.pitch = THREE.MathUtils.clamp(context.cam.pitch + dy * k * 0.8, -0.15, 1.15);
|
|
context.cam.lastMouse = context.elapsed;
|
|
}
|
|
|
|
// Touch joystick: x steers / strafes, y walks / throttles.
|
|
/**
|
|
* Gets the player into the nearest vehicle, or out of the current one.
|
|
* @param {object} context - Game context.
|
|
*/
|
|
export // ---------- Car ----------
|
|
function toggleCar(context) {
|
|
if (context.payment.phase !== 'idle' || context.state.busy) return;
|
|
if (context.real.on) {
|
|
context.say('father', strings.real.noVirtualCar);
|
|
return;
|
|
}
|
|
if (context.mode === 'walk') getIn(context);
|
|
else getOut(context);
|
|
context.refreshHUD();
|
|
}
|
|
|
|
/**
|
|
* Gets into the closest vehicle within reach, if there is one.
|
|
* @param {object} context - Game context.
|
|
*/
|
|
function getIn(context) {
|
|
const ride = nearestRide(context);
|
|
if (!ride) return;
|
|
takeRide(context, ride);
|
|
context.mode = 'drive';
|
|
context.cam.distance = Math.max(context.cam.distance, 8);
|
|
context.tone(320, 0.2, 0.05, 'square');
|
|
context.say('father', strings.dialogue.carOn);
|
|
}
|
|
|
|
/**
|
|
* Steps out of the current vehicle on its left side, or wherever is free.
|
|
* @param {object} context - Game context.
|
|
*/
|
|
function getOut(context) {
|
|
const { state, spec } = context.vehicle;
|
|
if (Math.abs(state.speed) > EXIT_MAX_SPEED) {
|
|
context.say('father', strings.dialogue.stopFirst);
|
|
return;
|
|
}
|
|
const left = { x: -Math.sin(state.heading), z: -Math.cos(state.heading) };
|
|
const step = spec.halfWidth + 1.8;
|
|
const spot = resolveCircle(
|
|
context.collision,
|
|
state.x + left.x * step,
|
|
state.z + left.z * step,
|
|
1,
|
|
);
|
|
Object.assign(context.player, { x: spot.x, z: spot.z, heading: state.heading });
|
|
state.speed = 0;
|
|
context.mode = 'walk';
|
|
context.cam.distance = 5.5;
|
|
context.tone(260, 0.2, 0.05, 'square');
|
|
}
|
|
|
|
// ---------- Machines, shop and payment ----------
|
|
// In real-world mode "at the machine" is a GPS radius, because the fix itself is off by metres.
|
|
/**
|
|
* Updates walk from the active simulation.
|
|
* @param {*} context - context.
|
|
* @param {*} dt - dt.
|
|
* @returns {*} Operation result, when applicable.
|
|
*/
|
|
export function updateWalk(context, dt) {
|
|
const forward = {
|
|
x: -Math.sin(context.cam.yaw),
|
|
z: -Math.cos(context.cam.yaw),
|
|
},
|
|
right = {
|
|
x: Math.cos(context.cam.yaw),
|
|
z: -Math.sin(context.cam.yaw),
|
|
};
|
|
const iy = context.axis(['w', 'arrowup'], ['s', 'arrowdown']) + context.stick.y,
|
|
ix = context.axis(['d', 'arrowright'], ['a', 'arrowleft']) + context.stick.x;
|
|
const frozen = context.state.busy || context.payment.phase !== 'idle';
|
|
let dx = forward.x * iy + right.x * ix,
|
|
dz = forward.z * iy + right.z * ix;
|
|
const len = Math.hypot(dx, dz);
|
|
const run = context.keys.has('shift');
|
|
const target = frozen || len < 0.1 ? 0 : (run ? 6.5 : 3.4) * Math.min(1, len);
|
|
context.player.speed += (target - context.player.speed) * Math.min(1, dt * 8);
|
|
if (len > 0.1 && !frozen) {
|
|
dx /= len;
|
|
dz /= len;
|
|
const want = Math.atan2(-dz, dx);
|
|
context.player.heading +=
|
|
Math.atan2(Math.sin(want - context.player.heading), Math.cos(want - context.player.heading)) *
|
|
Math.min(1, dt * 9);
|
|
}
|
|
// Face the machine while buying.
|
|
const station = context.payment.station ?? context.payment.delivering?.station;
|
|
if (frozen && station) {
|
|
const want = Math.atan2(-(station.z - context.player.z), station.x - context.player.x);
|
|
context.player.heading +=
|
|
Math.atan2(Math.sin(want - context.player.heading), Math.cos(want - context.player.heading)) *
|
|
Math.min(1, dt * 6);
|
|
}
|
|
const mx = Math.cos(context.player.heading) * context.player.speed * dt,
|
|
mz = -Math.sin(context.player.heading) * context.player.speed * dt;
|
|
const p = walkAroundVehicles(
|
|
context,
|
|
resolveCircle(context.collision, context.player.x + mx, context.player.z + mz, 1),
|
|
);
|
|
const { minX, minZ, maxX, maxZ } = data.bounds;
|
|
context.player.x = THREE.MathUtils.clamp(p.x, minX + 15, maxX - 15);
|
|
context.player.z = THREE.MathUtils.clamp(p.z, minZ + 15, maxZ - 15);
|
|
context.player.gait += dt * context.player.speed * 1.9;
|
|
}
|
|
|
|
/**
|
|
* Pushes the driven vehicle out of buildings using the circles along its body.
|
|
* @param {object} context - Game context.
|
|
*/
|
|
function keepOutOfBuildings(context) {
|
|
const car = context.car;
|
|
for (const { along, radius } of context.vehicle.spec.probes) {
|
|
const cx = car.x + Math.cos(car.heading) * along,
|
|
cz = car.z - Math.sin(car.heading) * along;
|
|
const p = resolveCircle(context.collision, cx, cz, radius);
|
|
if (!p.hit) continue;
|
|
context.debugHits.push({
|
|
kind: 'building',
|
|
x: Math.round(cx),
|
|
z: Math.round(cz),
|
|
v: +car.speed.toFixed(1),
|
|
});
|
|
car.x += p.x - cx;
|
|
car.z += p.z - cz;
|
|
if (Math.abs(car.speed) > 4) context.tone(90, 0.2, 0.08, 'square');
|
|
car.speed *= 0.35;
|
|
}
|
|
}
|
|
|
|
// Collision log for ?debug runs; trimmed so it never grows without bound.
|
|
/**
|
|
* Updates drive from the active simulation.
|
|
* @param {*} context - context.
|
|
* @param {*} dt - dt.
|
|
* @returns {*} Operation result, when applicable.
|
|
*/
|
|
export function updateDrive(context, dt) {
|
|
if (context.debugHits.length > 200) context.debugHits.splice(0, 100);
|
|
const throttle = THREE.MathUtils.clamp(
|
|
context.axis(['w', 'arrowup'], ['s', 'arrowdown']) + context.stick.y,
|
|
-1,
|
|
1,
|
|
);
|
|
const steer = THREE.MathUtils.clamp(
|
|
context.axis(['d', 'arrowright'], ['a', 'arrowleft']) + context.stick.x,
|
|
-1,
|
|
1,
|
|
);
|
|
// At top speed the car covers almost 2 m per step, so move it in short slices that can't
|
|
// tunnel through a thin wall, a fence or another vehicle.
|
|
const slices = Math.max(1, Math.ceil((Math.abs(context.car.speed) * dt) / 0.6));
|
|
for (let i = 0; i < slices; i++) {
|
|
stepCar(
|
|
context.car,
|
|
{ throttle, steer, handbrake: context.keys.has(' ') },
|
|
dt / slices,
|
|
context.vehicle.spec,
|
|
);
|
|
keepOutOfBuildings(context);
|
|
collideDriving(context, dt / slices);
|
|
}
|
|
const { minX, minZ, maxX, maxZ } = data.bounds;
|
|
context.car.x = THREE.MathUtils.clamp(context.car.x, minX + 15, maxX - 15);
|
|
context.car.z = THREE.MathUtils.clamp(context.car.z, minZ + 15, maxZ - 15);
|
|
Object.assign(context.player, {
|
|
x: context.car.x,
|
|
z: context.car.z,
|
|
heading: context.car.heading,
|
|
});
|
|
// Chase camera swings back behind the car when the mouse is idle.
|
|
if (context.elapsed - context.cam.lastMouse > 1.4 && Math.abs(context.car.speed) > 1) {
|
|
const want =
|
|
Math.atan2(-Math.cos(context.car.heading), Math.sin(context.car.heading)) +
|
|
(context.car.speed < 0 ? Math.PI : 0);
|
|
context.cam.yaw +=
|
|
Math.atan2(Math.sin(want - context.cam.yaw), Math.cos(want - context.cam.yaw)) *
|
|
Math.min(1, dt * 2.2);
|
|
context.cam.pitch += (0.28 - context.cam.pitch) * Math.min(1, dt * 1.5);
|
|
}
|
|
if (context.engine && context.soundOn) {
|
|
context.engine.o.frequency.setTargetAtTime(
|
|
45 + Math.abs(context.car.speed) * 5 + Math.max(0, throttle) * 15,
|
|
context.audio.currentTime,
|
|
0.1,
|
|
);
|
|
context.engine.g.gain.setTargetAtTime(0.025, context.audio.currentTime, 0.2);
|
|
}
|
|
}
|