From 611960511e0dadf42fea0c7b08d7667f3a87322c Mon Sep 17 00:00:00 2001 From: "hamed@B" Date: Sat, 31 Oct 2026 08:27:52 +0330 Subject: [PATCH] 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 --- docs/02-game-rules.md | 6 +- docs/03-architecture.md | 3 +- src/constants/vehicles.js | 3 - src/game/models/factory.js | 9 +- src/game/models/motorcycle.js | 155 ----------------------------- src/game/runtime/garage.js | 40 ++------ src/game/runtime/movement.js | 9 +- src/game/runtime/navigationHud.js | 1 - src/game/runtime/runtimeSetup.js | 2 +- src/game/systems/traffic.js | 4 +- src/game/systems/vehicle.js | 19 ---- src/game/world/vehicleCollision.js | 2 +- src/i18n/fa.js | 3 - tests/vehicles.test.js | 47 ++++----- 14 files changed, 44 insertions(+), 259 deletions(-) delete mode 100644 src/game/models/motorcycle.js diff --git a/docs/02-game-rules.md b/docs/02-game-rules.md index a5130e0..49ebce0 100644 --- a/docs/02-game-rules.md +++ b/docs/02-game-rules.md @@ -11,13 +11,13 @@ - موبایل joystick و دکمه‌های F، E و toggle دویدن دارد. E موبایل در مرحلهٔ ready کارت می‌کشد. - سرعت هدف پیاده ۳٫۴ و دویدن ۶٫۵ متر/ثانیه با نرم‌سازی است. برخورد دایره‌ای شخصیت شعاع ۱ متر دارد. مرز جهان با فاصلهٔ ۱۵ متر محدود می‌شود. -## ماشین و موتور +## ماشین‌ها -همهٔ وسیله‌های حیاط کارخانه مشکی‌اند و سوارشدنی: بی‌ام‌وی خانواده کنار نقطهٔ شروع، لامبورگینی، مزدا و نیسان در پارکینگ کارکنان، شش موتور آپاچی کنار دفتر، و بوگاتی آقای محمدی پس از اینکه پارک کرد. هر ماشین ترافیک شهر هم با F گرفته می‌شود و تا reset همان‌جا که رها شده می‌ماند. کامیون بارگیری تزئینی است و سوار نمی‌شود. همه در `runtime/garage.js` نگه‌داری می‌شوند. +همهٔ وسیله‌های حیاط کارخانه مشکی‌اند و سوارشدنی: بی‌ام‌وی خانواده کنار نقطهٔ شروع، لامبورگینی، مزدا و نیسان در پارکینگ کارکنان، و بوگاتی آقای محمدی پس از اینکه پارک کرد. هر ماشین ترافیک شهر هم با F گرفته می‌شود و تا reset همان‌جا که رها شده می‌ماند. کامیون بارگیری تزئینی است و سوار نمی‌شود. همه در `runtime/garage.js` نگه‌داری می‌شوند. سوارشدن در فاصلهٔ حداکثر ۴٫۵ متر و خارج پرداخت/مشغولی؛ پیاده‌شدن در قدرمطلق سرعت حداکثر ۳ متر/ثانیه. UI پیشنهاد پیاده‌شدن را با شرط محدودتر سرعت زیر ۱ و دستگاه نزدیک نشان می‌دهد. سمت خروج راننده بررسی برخورد می‌شود. -ثابت‌های `CAR`: سقف جلو ۵۵٫۶ متر/ثانیه (حدود ۲۰۰ km/h)، عقب ۶، شتاب ۹٫۵، ترمز ۱۴، drag برابر ۱٫۶، فرمان حداکثر ۰٫۵۵ رادیان، wheelBase برابر ۲٫۷. فرمان در سرعت بالا محدودتر می‌شود. برخورد دیوار دو دایرهٔ جلو/عقب و حرکت زیرگام دارد. ثابت‌های `BIKE`: سقف ۳۶٫۱ (حدود ۱۳۰ km/h)، شتاب ۱۱، wheelBase برابر ۱٫۳۵؛ موتور در پیچ خم می‌شود. برخورد بین وسیله‌ها با جعبهٔ جهت‌دار به اندازهٔ واقعی مدل (SAT در `world/vehicleCollision.js`) است، پس فقط وقتی بدنه‌ها واقعاً به هم برسند سرعت کم می‌شود و موتور می‌تواند لایی بکشد. کاهش سرعت به زاویهٔ برخورد بستگی دارد: شاخ‌به‌شاخ ۶۰٪، مالیدن کناری تقریباً هیچ. ترافیک فقط برای چیزی که واقعاً در عرض خطش باشد ترمز می‌کند و NPC پس از تماس طولانی حذف/بازپدید می‌شود. جریمهٔ نقدی، خسارت قابل تعمیر و سوخت وجود ندارد. +ثابت‌های `CAR`: سقف جلو ۵۵٫۶ متر/ثانیه (حدود ۲۰۰ km/h)، عقب ۶، شتاب ۹٫۵، ترمز ۱۴، drag برابر ۱٫۶، فرمان حداکثر ۰٫۵۵ رادیان، wheelBase برابر ۲٫۷. فرمان در سرعت بالا محدودتر می‌شود. برخورد دیوار دو دایرهٔ جلو/عقب و حرکت زیرگام دارد. برخورد بین وسیله‌ها با جعبهٔ جهت‌دار به اندازهٔ واقعی مدل (SAT در `world/vehicleCollision.js`) است، پس فقط وقتی بدنه‌ها واقعاً به هم برسند سرعت کم می‌شود و ماشین از هر فاصله‌ای که واقعاً جا شود رد می‌شود. کاهش سرعت به زاویهٔ برخورد بستگی دارد: شاخ‌به‌شاخ ۶۰٪، مالیدن کناری تقریباً هیچ. ترافیک فقط برای چیزی که واقعاً در عرض خطش باشد ترمز می‌کند و NPC پس از تماس طولانی حذف/بازپدید می‌شود. جریمهٔ نقدی، خسارت قابل تعمیر و سوخت وجود ندارد. ## پول و محصول diff --git a/docs/03-architecture.md b/docs/03-architecture.md index 255912b..ed4f959 100644 --- a/docs/03-architecture.md +++ b/docs/03-architecture.md @@ -18,8 +18,7 @@ JavaScript ES Modules، Three.js/WebGL، Vite، HTML و CSS فارسی RTL و Ca | staff.js | مدل کارکنان مفصلی و crew به شکل InstancedMesh | | car-models.js | سدان خانواده، کامیون و helperهای مشترک بدنه | | luxuryCars.js | ماشین‌های لوکس مشکی به سبک بوگاتی، لامبورگینی، مزدا و نیسان | -| motorcycle.js | موتور آپاچی با راکب و بدنهٔ خم‌شونده | -| vehicle.js | state و فیزیک آرکید ماشین (`CAR`) و موتور (`BIKE`) | +| vehicle.js | state و فیزیک آرکید ماشین (`CAR`) | | vehicleCollision.js | برخورد جعبهٔ جهت‌دار وسیله‌ها و هل‌دادن پیاده از وسیلهٔ پارک‌شده | | garage.js | فهرست وسیله‌های سوارشدنی، سوار/پیاده، برخورد رانندگی و reset | | traffic.js | ماشین‌های NPC، spawn و حرکت روی شبکهٔ اصلی و one-way داده | diff --git a/src/constants/vehicles.js b/src/constants/vehicles.js index 144c970..bab66ef 100644 --- a/src/constants/vehicles.js +++ b/src/constants/vehicles.js @@ -15,6 +15,3 @@ export const VEHICLE_LABELS = strings.vehicles; // Brands parked in the staff bays, in bay order. Mr Mohammadi arrives in the Bugatti. export const STAFF_CAR_BRANDS = ['lamborghini', 'mazda', 'nissan']; export const MANAGER_CAR_BRAND = 'bugatti'; - -// Apache motorcycles lined up in the yard. -export const MOTORCYCLE_COUNT = 6; diff --git a/src/game/models/factory.js b/src/game/models/factory.js index bb6f2ff..201ce04 100644 --- a/src/game/models/factory.js +++ b/src/game/models/factory.js @@ -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 ---------- diff --git a/src/game/models/motorcycle.js b/src/game/models/motorcycle.js deleted file mode 100644 index 4effd46..0000000 --- a/src/game/models/motorcycle.js +++ /dev/null @@ -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 }; -} diff --git a/src/game/runtime/garage.js b/src/game/runtime/garage.js index 351eaa5..9ab9589 100644 --- a/src/game/runtime/garage.js +++ b/src/game/runtime/garage.js @@ -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); } /** diff --git a/src/game/runtime/movement.js b/src/game/runtime/movement.js index eef4ba9..7296f33 100644 --- a/src/game/runtime/movement.js +++ b/src/game/runtime/movement.js @@ -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); } /** diff --git a/src/game/runtime/navigationHud.js b/src/game/runtime/navigationHud.js index ebbaa7f..afba8f4 100644 --- a/src/game/runtime/navigationHud.js +++ b/src/game/runtime/navigationHud.js @@ -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); } /** diff --git a/src/game/runtime/runtimeSetup.js b/src/game/runtime/runtimeSetup.js index 0d437e5..eaa5556 100644 --- a/src/game/runtime/runtimeSetup.js +++ b/src/game/runtime/runtimeSetup.js @@ -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; diff --git a/src/game/systems/traffic.js b/src/game/systems/traffic.js index e9276f1..db2c7f8 100644 --- a/src/game/systems/traffic.js +++ b/src/game/systems/traffic.js @@ -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; diff --git a/src/game/systems/vehicle.js b/src/game/systems/vehicle.js index 3c91f88..62c195a 100644 --- a/src/game/systems/vehicle.js +++ b/src/game/systems/vehicle.js @@ -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. diff --git a/src/game/world/vehicleCollision.js b/src/game/world/vehicleCollision.js index 83ed189..3ef93b2 100644 --- a/src/game/world/vehicleCollision.js +++ b/src/game/world/vehicleCollision.js @@ -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. diff --git a/src/i18n/fa.js b/src/i18n/fa.js index 7e21797..79c8b1b 100644 --- a/src/i18n/fa.js +++ b/src/i18n/fa.js @@ -104,7 +104,6 @@ export const strings = { metroPrefix: 'ایستگاه مترو ', }, travel: { - onBike: 'روی موتور · F برای پیاده شدن', /** @type {(vehicle: string) => string} */ inVehicle: (vehicle) => `در ${vehicle} · F برای پیاده شدن`, }, @@ -125,7 +124,6 @@ export const strings = { lamborghini: 'لامبورگینی', mazda: 'مزدا', nissan: 'نیسان', - apache: 'موتور آپاچی', traffic: 'ماشین', }, map: { @@ -287,7 +285,6 @@ export const strings = { targetClosed: (machine, hours) => `${machine} الان بسته است (${hours}). از نقشه مقصد دیگری انتخاب کن.`, longWay: 'راه طولانیه؛ بهتره با ماشین بریم.', - bikeOn: 'کلاه ایمنی سرت باشه! با موتور می‌شه از لای ماشین‌ها رد شد.', carOn: 'کمربندها رو ببندیم! مسیر طلایی روی زمین راه رو نشون می‌ده.', stopFirst: 'اول نگه دار، بعد پیاده می‌شویم.', tooFastToBuy: 'داریم سریع حرکت می‌کنیم؛ اول بایستیم، بعد خرید کنیم.', diff --git a/tests/vehicles.test.js b/tests/vehicles.test.js index 1bee800..b4920d0 100644 --- a/tests/vehicles.test.js +++ b/tests/vehicles.test.js @@ -3,10 +3,8 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { Scene } from 'three'; import { boxOverlap, circleOutOfBox, vehicleBox } from '../src/game/world/vehicleCollision.js'; -import { BIKE, CAR, createCar, kmh, stepCar } from '../src/game/systems/vehicle.js'; import { createTraffic, TRAFFIC_CAR } from '../src/game/systems/traffic.js'; import { createLuxuryCar } from '../src/game/models/luxuryCars.js'; -import { createMotorcycle } from '../src/game/models/motorcycle.js'; const data = JSON.parse(readFileSync(new URL('../src/data/mellat.json', import.meta.url))); @@ -14,12 +12,12 @@ const data = JSON.parse(readFileSync(new URL('../src/data/mellat.json', import.m const leftCar = vehicleBox({ x: 0, z: -2.25, heading: 0 }, TRAFFIC_CAR); const rightCar = vehicleBox({ x: 0, z: 2.25, heading: Math.PI }, TRAFFIC_CAR); -test('A motorcycle filters between two cars without touching them', () => { - const bike = createMotorcycle(); - const between = vehicleBox({ x: 0, z: 0, heading: 0 }, bike.size); +test('A car drives between two traffic cars without touching them', () => { + const car = createLuxuryCar('mazda'); + const between = vehicleBox({ x: 0, z: 0, heading: 0 }, car.size); assert.equal(boxOverlap(between, leftCar), null); assert.equal(boxOverlap(between, rightCar), null); - // Regression: the old 3.4 m circle test counted this as a crash and stopped the rider. + // Regression: the old 3.4 m circle test counted this as a crash and stopped the driver. assert.ok(Math.hypot(0, 2.25) < 3.4); }); @@ -50,29 +48,13 @@ test('A walker is pushed out of a parked vehicle, even from inside it', () => { } }); -test('Motorcycles top out at 130 km/h and turn tighter than cars', () => { - const bike = createCar(0, 0, 0), - car = createCar(0, 0, 0); - for (let i = 0; i < 900; i++) stepCar(bike, { throttle: 1 }, 1 / 30, BIKE); - assert.equal(kmh(bike), 130); - bike.speed = car.speed = 8; - for (let i = 0; i < 15; i++) { - stepCar(bike, { steer: 1 }, 1 / 30, BIKE); - stepCar(car, { steer: 1 }, 1 / 30, CAR); - } - assert.ok(bike.heading < car.heading, 'the shorter wheelbase turns faster'); -}); - -test('Luxury cars and the motorcycle report footprints that match their size', () => { +test('Luxury cars report footprints that match their size', () => { for (const brand of ['bugatti', 'lamborghini', 'mazda', 'nissan']) { const model = createLuxuryCar(brand); assert.equal(model.wheels.length, 4, brand); assert.ok(model.size.halfLength > 2.1 && model.size.halfLength < 2.6, brand); assert.ok(model.size.halfWidth > 0.9 && model.size.halfWidth < 1.15, brand); } - const bike = createMotorcycle(); - assert.equal(bike.wheels.length, 2); - assert.ok(bike.size.halfWidth < 0.45 && bike.size.halfLength < 1.1); }); /** @@ -87,7 +69,7 @@ function loneCar(traffic) { ); } -test('Traffic stops for what blocks its lane but keeps going past a filtering motorcycle', () => { +test('Traffic stops for what blocks its lane but keeps going past what is beside it', () => { const traffic = createTraffic(new Scene(), data, 28), around = { x: -10008, z: -15049 }; for (let i = 0; i < 200; i++) traffic.update(1 / 30, [], around); @@ -103,10 +85,11 @@ test('Traffic stops for what blocks its lane but keeps going past a filtering mo halfWidth, }); const before = car.d; - traffic.update(1 / 30, [at(0.5, BIKE.halfWidth)], around); - assert.equal(car.d, before, 'a bike in the lane stops the car'); - traffic.update(1 / 30, [at(1.8, BIKE.halfWidth)], around); - assert.notEqual(car.d, before, 'a bike on the lane edge does not'); + // A walker (0.45 m half width) in the lane stops the car; one at the lane edge does not. + traffic.update(1 / 30, [at(0.5, 0.45)], around); + assert.equal(car.d, before, 'a walker in the lane stops the car'); + traffic.update(1 / 30, [at(1.8, 0.45)], around); + assert.notEqual(car.d, before, 'a walker on the lane edge does not'); }); test('Taking a traffic car hands its model to the player and refills the slot', () => { @@ -119,3 +102,11 @@ test('Taking a traffic car hands its model to the player and refills the slot', assert.equal(car.model.group.parent, traffic.group); assert.equal(car.road, null, 'the slot respawns elsewhere later'); }); + +test('The motorcycle is gone from the game', () => { + const src = new URL('../src/', import.meta.url); + const vehicleSource = ['game/runtime/garage.js', 'game/systems/vehicle.js', 'i18n/fa.js'] + .map((file) => readFileSync(new URL(file, src), 'utf8')) + .join('\n'); + assert.doesNotMatch(vehicleSource, /motorcycle|BIKE|apache/i); +});