refactor: remove the motorcycle
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>
This commit is contained in:
@@ -2,7 +2,7 @@ import * as THREE from 'three';
|
||||
import { createVendingMachine } from './vending.js';
|
||||
import { createTruck } from './carModels.js';
|
||||
import { createLuxuryCar } from './luxuryCars.js';
|
||||
import { MANAGER_CAR_BRAND, MOTORCYCLE_COUNT, VEHICLE_BLACK } from '../../constants/vehicles.js';
|
||||
import { MANAGER_CAR_BRAND, VEHICLE_BLACK } from '../../constants/vehicles.js';
|
||||
import { createPerson, createCrew, nameTag } from './staff.js';
|
||||
import { FACTORY } from '../../constants/locations.js';
|
||||
import { textTexture } from './textures.js';
|
||||
@@ -1581,15 +1581,10 @@ function factoryBuildGeometry12(build) {
|
||||
* @returns {*} Operation result, when applicable.
|
||||
*/
|
||||
function factoryBuildGeometry13(build) {
|
||||
// Bay centres for the staff cars and the motorcycle row; the garage puts drivable vehicles there.
|
||||
// Bay centres for the staff cars; the garage parks drivable cars there.
|
||||
const heading = build.worldHeading(0, -1);
|
||||
build.parking = {
|
||||
cars: [58, 62, 66].map((u) => ({ ...build.toWorld(u, -19), heading })),
|
||||
// Motorcycles barely reverse, so they stand facing out towards the driveway.
|
||||
bikes: Array.from({ length: MOTORCYCLE_COUNT }, (_, i) => ({
|
||||
...build.toWorld(36.6 + i * 1.1, -19.5),
|
||||
heading: build.worldHeading(0, 1),
|
||||
})),
|
||||
};
|
||||
|
||||
// ---------- The manager arrives in his Bugatti ----------
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import * as THREE from 'three';
|
||||
import { VEHICLE_BLACK } from '../../constants/vehicles.js';
|
||||
import { extrudeProfile, mat, measureFootprint } from './carModels.js';
|
||||
import { createPerson } from './staff.js';
|
||||
|
||||
// Naked street bike in the style of an Apache RTR: sculpted tank, split seat, angular tail and a
|
||||
// short underbelly exhaust. 2 m long, faces local +X. `wheels` is [rear, front]; `body` is the
|
||||
// group that leans into corners; `rider` sits on it and is shown only while the bike is ridden.
|
||||
|
||||
const WHEEL_RADIUS = 0.31;
|
||||
const WHEEL_X = [-0.67, 0.67];
|
||||
|
||||
/**
|
||||
* Builds one spoked wheel that spins about local z.
|
||||
* @param {THREE.Material} rubber - Tyre material.
|
||||
* @param {THREE.Material} rim - Rim material.
|
||||
* @returns {THREE.Group} The wheel.
|
||||
*/
|
||||
function createWheel(rubber, rim) {
|
||||
const wheel = new THREE.Group();
|
||||
const tyre = new THREE.Mesh(new THREE.TorusGeometry(WHEEL_RADIUS - 0.05, 0.05, 8, 24), rubber);
|
||||
tyre.castShadow = true;
|
||||
wheel.add(tyre);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const spoke = new THREE.Mesh(new THREE.BoxGeometry(0.48, 0.035, 0.03), rim);
|
||||
spoke.rotation.z = (i * Math.PI) / 3;
|
||||
wheel.add(spoke);
|
||||
}
|
||||
return wheel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a box to a group.
|
||||
* @param {THREE.Group} group - Parent group.
|
||||
* @param {THREE.Material} material - Material.
|
||||
* @param {{size: number[], at: number[], tilt?: number}} placement - Size, centre and z rotation.
|
||||
*/
|
||||
function box(group, material, { size, at, tilt = 0 }) {
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(...size), material);
|
||||
mesh.position.set(...at);
|
||||
mesh.rotation.z = tilt;
|
||||
mesh.castShadow = true;
|
||||
group.add(mesh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the tank, seat, tail, engine and exhaust.
|
||||
* @param {THREE.Group} body - Leaning body group.
|
||||
* @param {object} kit - Materials.
|
||||
*/
|
||||
function addBodywork(body, kit) {
|
||||
const tank = [
|
||||
[0.55, 0.82],
|
||||
[0.45, 1.03],
|
||||
[0.05, 1.0],
|
||||
[-0.12, 0.86],
|
||||
];
|
||||
body.add(extrudeProfile(tank, 0.38, kit.paint, 0.05));
|
||||
const tail = [
|
||||
[-0.55, 0.86],
|
||||
[-0.98, 1.04],
|
||||
[-1.0, 0.98],
|
||||
[-0.6, 0.82],
|
||||
];
|
||||
body.add(extrudeProfile(tail, 0.22, kit.paint, 0.03));
|
||||
box(body, kit.seat, { size: [0.5, 0.08, 0.26], at: [-0.35, 0.93, 0] });
|
||||
box(body, kit.accent, { size: [0.32, 0.03, 0.39], at: [0.3, 0.9, 0], tilt: 0.2 });
|
||||
box(body, kit.engine, { size: [0.46, 0.34, 0.3], at: [0.05, 0.52, 0] });
|
||||
box(body, kit.engine, { size: [0.7, 0.05, 0.08], at: [-0.33, 0.38, 0], tilt: 0.12 });
|
||||
const exhaust = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.07, 0.38, 10), kit.chrome);
|
||||
exhaust.rotation.z = Math.PI / 2 - 0.15;
|
||||
exhaust.position.set(-0.05, 0.3, 0.12);
|
||||
body.add(exhaust);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the fork, handlebar, headlight and tail light.
|
||||
* @param {THREE.Group} body - Leaning body group.
|
||||
* @param {object} kit - Materials.
|
||||
*/
|
||||
function addFrontAndLights(body, kit) {
|
||||
for (const z of [0.08, -0.08])
|
||||
box(body, kit.chrome, { size: [0.04, 0.72, 0.04], at: [0.58, 0.65, z], tilt: 0.42 });
|
||||
const bar = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.7, 8), kit.engine);
|
||||
bar.rotation.x = Math.PI / 2;
|
||||
bar.position.set(0.44, 1.07, 0);
|
||||
body.add(bar);
|
||||
box(body, kit.paint, { size: [0.14, 0.2, 0.24], at: [0.62, 0.98, 0], tilt: -0.3 });
|
||||
box(body, kit.head, { size: [0.04, 0.08, 0.16], at: [0.7, 0.96, 0], tilt: -0.3 });
|
||||
box(body, kit.tail, { size: [0.03, 0.05, 0.12], at: [-1.0, 0.98, 0] });
|
||||
box(body, kit.paint, { size: [0.3, 0.03, 0.14], at: [0.68, 0.62, 0], tilt: -0.5 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the seated rider in a black jacket and helmet, holding the handlebar.
|
||||
* @returns {object} Person from createPerson.
|
||||
*/
|
||||
function createRider() {
|
||||
const rider = createPerson({ top: '#151515', bottom: '#28303a', shoes: '#1a1a1a' });
|
||||
const helmet = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.15, 16, 12),
|
||||
mat('#0d0d0f', { metalness: 0.6, roughness: 0.25 }),
|
||||
);
|
||||
helmet.position.set(0, 0.01, -0.01);
|
||||
rider.head.add(helmet);
|
||||
// Seated pose puts the pelvis 0.5 m up; lift it onto the 0.94 m seat, facing the bars (+x).
|
||||
rider.pose('type');
|
||||
rider.root.position.set(-0.32, 0.42, 0);
|
||||
rider.root.rotation.y = Math.PI / 2;
|
||||
rider.root.visible = false;
|
||||
return rider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the motorcycle's materials.
|
||||
* @param {string} color - Body paint colour.
|
||||
* @returns {object} Paint, trim and lamp materials.
|
||||
*/
|
||||
function createBikeKit(color) {
|
||||
return {
|
||||
paint: mat(color, { metalness: 0.7, roughness: 0.25 }),
|
||||
accent: mat('#c8171e', { metalness: 0.4, roughness: 0.35 }),
|
||||
seat: mat('#151617', { roughness: 0.85 }),
|
||||
engine: mat('#2b2e31', { metalness: 0.6, roughness: 0.45 }),
|
||||
chrome: mat('#c9cdd1', { metalness: 0.9, roughness: 0.2 }),
|
||||
head: mat('#eaf6ff', { emissive: '#cfe9ff', emissiveIntensity: 1.6 }),
|
||||
tail: mat('#9e1410', { emissive: '#e0261c', emissiveIntensity: 0.9 }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a black Apache-style motorcycle.
|
||||
* @param {string} color - Body paint colour.
|
||||
* @returns {{group: THREE.Group, body: THREE.Group, wheels: THREE.Group[], rider: object,
|
||||
* size: object}} The model and its footprint half extents (without the rider).
|
||||
*/
|
||||
export function createMotorcycle(color = VEHICLE_BLACK) {
|
||||
const kit = createBikeKit(color);
|
||||
const group = new THREE.Group(),
|
||||
body = new THREE.Group();
|
||||
group.add(body);
|
||||
addBodywork(body, kit);
|
||||
addFrontAndLights(body, kit);
|
||||
const rubber = mat('#151719', { roughness: 0.9 });
|
||||
const wheels = WHEEL_X.map((x) => {
|
||||
const wheel = createWheel(rubber, kit.engine);
|
||||
wheel.position.set(x, WHEEL_RADIUS, 0);
|
||||
body.add(wheel);
|
||||
return wheel;
|
||||
});
|
||||
const size = measureFootprint(group);
|
||||
const rider = createRider();
|
||||
body.add(rider.root);
|
||||
return { group, body, wheels, rider, size };
|
||||
}
|
||||
+11
-29
@@ -8,12 +8,11 @@ import {
|
||||
} from '../../constants/vehicles.js';
|
||||
import { createSportSedan, measureFootprint } from '../models/carModels.js';
|
||||
import { createLuxuryCar } from '../models/luxuryCars.js';
|
||||
import { createMotorcycle } from '../models/motorcycle.js';
|
||||
import { TRAFFIC_CAR } from '../systems/traffic.js';
|
||||
import { BIKE, CAR, createCar } from '../systems/vehicle.js';
|
||||
import { CAR, createCar } from '../systems/vehicle.js';
|
||||
import { boxOverlap, circleOutOfBox, vehicleBox } from '../world/vehicleCollision.js';
|
||||
|
||||
// Every vehicle the player can drive: the family BMW, the staff cars, the Apache motorcycles,
|
||||
// Every vehicle the player can drive: the family BMW, the staff cars,
|
||||
// Mr Mohammadi's Bugatti once he has parked it, and any traffic car the player has taken over.
|
||||
// `context.vehicle` is the one in use (or last used); `context.car` is its moving state.
|
||||
|
||||
@@ -25,26 +24,22 @@ const WALKER_RADIUS = 0.45;
|
||||
const HEAD_ON_SPEED_KEPT = 0.4;
|
||||
// A traffic car the player keeps pushing against pulls away (respawns) after this many seconds.
|
||||
const TRAFFIC_GIVE_WAY = 0.8;
|
||||
// How far a motorcycle leans per radian of steering at speed, and the speed of full lean.
|
||||
const LEAN_PER_STEER = 0.8;
|
||||
const FULL_LEAN_SPEED = 12;
|
||||
|
||||
/**
|
||||
* Adds a vehicle to the garage and the scene.
|
||||
* @param {object} context - Game context.
|
||||
* @param {{brand: string, kind: 'car' | 'bike', model: object, slot: object}} vehicle - Vehicle
|
||||
* @param {{brand: string, model: object, slot: object}} vehicle - Vehicle
|
||||
* fields; `slot` is where it is parked on reset (null for vehicles that are not reset).
|
||||
* @returns {object} The garage record.
|
||||
*/
|
||||
function addVehicle(context, { brand, kind, model, slot, ...extra }) {
|
||||
function addVehicle(context, { brand, model, slot, ...extra }) {
|
||||
const record = {
|
||||
brand,
|
||||
kind,
|
||||
model,
|
||||
slot,
|
||||
label: VEHICLE_LABELS[brand],
|
||||
// Handling from the kind, collision box from the model itself, so contact is exact.
|
||||
spec: { ...(kind === 'bike' ? BIKE : CAR), ...model.size },
|
||||
// Car handling, with the collision box measured from the model itself so contact is exact.
|
||||
spec: { ...CAR, ...model.size },
|
||||
state: createCar(slot.x, slot.z, slot.heading),
|
||||
...extra,
|
||||
};
|
||||
@@ -93,20 +88,16 @@ export function createGarage(context) {
|
||||
const { car: familySlot, parking } = context.factory;
|
||||
addVehicle(context, {
|
||||
brand: 'bmw',
|
||||
kind: 'car',
|
||||
model: withSize(createSportSedan(VEHICLE_BLACK)),
|
||||
slot: familySlot,
|
||||
});
|
||||
STAFF_CAR_BRANDS.forEach((brand, i) =>
|
||||
addVehicle(context, {
|
||||
brand,
|
||||
kind: 'car',
|
||||
model: createLuxuryCar(brand),
|
||||
slot: parking.cars[i],
|
||||
}),
|
||||
);
|
||||
for (const slot of parking.bikes)
|
||||
addVehicle(context, { brand: 'apache', kind: 'bike', model: createMotorcycle(), slot });
|
||||
setActive(context, context.garage.vehicles[0]);
|
||||
}
|
||||
|
||||
@@ -125,7 +116,6 @@ export function resetGarage(context) {
|
||||
for (const record of garage.vehicles) {
|
||||
record.state = createCar(record.slot.x, record.slot.z, record.slot.heading);
|
||||
placeModel(record);
|
||||
if (record.model.rider) record.model.rider.root.visible = false;
|
||||
}
|
||||
setActive(context, garage.vehicles[0]);
|
||||
}
|
||||
@@ -145,7 +135,6 @@ export function updateGarage(context) {
|
||||
const heading = Math.atan2(-(nose.z - at.z), nose.x - at.x);
|
||||
const record = addVehicle(context, {
|
||||
brand: MANAGER_CAR_BRAND,
|
||||
kind: 'car',
|
||||
model: manager.model,
|
||||
slot: { x: at.x, z: at.z, heading },
|
||||
});
|
||||
@@ -187,7 +176,7 @@ export function takeRide(context, { record, npc }) {
|
||||
if (npc) {
|
||||
const slot = { x: npc.x, z: npc.z, heading: npc.model.group.rotation.y };
|
||||
const model = { ...context.traffic.adopt(npc), size: TRAFFIC_CAR };
|
||||
record = addVehicle(context, { brand: 'traffic', kind: 'car', model, slot });
|
||||
record = addVehicle(context, { brand: 'traffic', model, slot });
|
||||
// Taken traffic cars stay where they are left until the next reset, which removes them.
|
||||
record.slot = null;
|
||||
}
|
||||
@@ -196,25 +185,18 @@ export function takeRide(context, { record, npc }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active vehicle's model: position, wheel spin and steering, motorcycle lean and rider.
|
||||
* Updates the active vehicle's model: position, wheel spin and front-wheel steering.
|
||||
* @param {object} context - Game context.
|
||||
* @param {number} dt - Frame time in seconds.
|
||||
*/
|
||||
export function syncActiveVehicle(context, dt) {
|
||||
const { model, state, spec, kind } = context.vehicle;
|
||||
const { model, state, spec } = context.vehicle;
|
||||
placeModel(context.vehicle);
|
||||
const firstFront = kind === 'bike' ? 1 : 2;
|
||||
// Wheels list the rear pair first, then the two front (steering) wheels.
|
||||
for (const [i, wheel] of model.wheels.entries()) {
|
||||
wheel.rotation.z -= (state.speed * dt) / spec.wheelRadius;
|
||||
if (i >= firstFront) wheel.rotation.y = -state.steer;
|
||||
if (i >= 2) wheel.rotation.y = -state.steer;
|
||||
}
|
||||
if (kind !== 'bike') return;
|
||||
const riding = context.mode === 'drive';
|
||||
model.rider.root.visible = riding;
|
||||
// Lean into the turn (towards local +z, the right-hand side, when steering right).
|
||||
const speedShare = Math.min(1, Math.abs(state.speed) / FULL_LEAN_SPEED);
|
||||
const lean = riding ? state.steer * LEAN_PER_STEER * speedShare * Math.sign(state.speed) : 0;
|
||||
model.body.rotation.x += (lean - model.body.rotation.x) * Math.min(1, dt * 8);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,18 +49,17 @@ function toggleCar(context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets on or in the closest vehicle within reach, if there is one.
|
||||
* 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;
|
||||
const { kind } = takeRide(context, ride);
|
||||
takeRide(context, ride);
|
||||
context.mode = 'drive';
|
||||
context.cam.distance = Math.max(context.cam.distance, kind === 'bike' ? 6 : 8);
|
||||
context.cam.distance = Math.max(context.cam.distance, 8);
|
||||
context.tone(320, 0.2, 0.05, 'square');
|
||||
const line = kind === 'bike' ? strings.dialogue.bikeOn : strings.dialogue.carOn;
|
||||
context.say('father', line);
|
||||
context.say('father', strings.dialogue.carOn);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -205,7 +205,6 @@ export function refreshHUD(context) {
|
||||
*/
|
||||
function modeLabel(context) {
|
||||
if (context.mode !== 'drive') return strings.common.walking;
|
||||
if (context.vehicle.kind === 'bike') return strings.travel.onBike;
|
||||
return strings.travel.inVehicle(context.vehicle.label);
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -427,7 +427,7 @@ export async function initializeFamily(context) {
|
||||
*/
|
||||
export async function initializeSpawnCar(context) {
|
||||
// The family car waits in the factory yard right beside them, pointing at the gate; the staff
|
||||
// cars and motorcycles are parked in front of the office.
|
||||
// cars are parked in front of the office.
|
||||
createGarage(context);
|
||||
// ---------- Input ----------
|
||||
context.isTouch = matchMedia('(pointer: coarse)').matches;
|
||||
|
||||
@@ -314,8 +314,8 @@ export function createTraffic(scene, data, count = 28) {
|
||||
const ox = o.x - car.x,
|
||||
oz = o.z - car.z,
|
||||
along = ox * fx + oz * fz;
|
||||
// Only something that really overlaps the lane stops the car, so a motorcycle
|
||||
// filtering between two lanes doesn't bring traffic to a halt.
|
||||
// Only something that really overlaps the lane stops the car, so a car or walker
|
||||
// beside the lane doesn't bring traffic to a halt.
|
||||
const reach =
|
||||
TRAFFIC_CAR.halfWidth + (o.halfWidth ?? DEFAULT_OBSTACLE_HALF_WIDTH) + PASSING_MARGIN;
|
||||
return along > 0 && along < 9 && Math.abs(ox * fz - oz * fx) < reach;
|
||||
|
||||
@@ -22,25 +22,6 @@ export const CAR = {
|
||||
],
|
||||
};
|
||||
|
||||
// Apache motorcycle: 130 km/h top speed, quick off the line, narrow enough to filter between cars.
|
||||
export const BIKE = {
|
||||
maxSpeed: 36.1,
|
||||
maxReverse: 1.5,
|
||||
accel: 11,
|
||||
brake: 13,
|
||||
drag: 1.4,
|
||||
maxSteer: 0.6,
|
||||
wheelBase: 1.35,
|
||||
radius: 0.6,
|
||||
halfLength: 1.0,
|
||||
halfWidth: 0.4,
|
||||
wheelRadius: 0.31,
|
||||
probes: [
|
||||
{ along: 0.55, radius: 0.42 },
|
||||
{ along: -0.55, radius: 0.42 },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the moving state of a vehicle.
|
||||
* @param {number} x - World x.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Oriented-box collisions between vehicles, so two bodies only touch when their outlines really
|
||||
// meet. A narrow motorcycle can therefore pass between two cars with a hand's width to spare.
|
||||
// meet. A car can therefore pass through any gap it actually fits, without slowing down.
|
||||
|
||||
/**
|
||||
* Builds the footprint box of a vehicle on the ground plane.
|
||||
|
||||
Reference in New Issue
Block a user