@adatechnology/game-vehicles-driving 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -2
- package/dist/index.d.ts +405 -0
- package/dist/index.global.js +529 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +477 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -3
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var GameVehiclesDriving = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
bodyDamage: () => bodyDamage,
|
|
25
|
+
createArcadeDriver: () => createArcadeDriver,
|
|
26
|
+
createSemiSimDriver: () => createSemiSimDriver,
|
|
27
|
+
createSurfaces: () => createSurfaces,
|
|
28
|
+
defaultArcadeSpec: () => defaultArcadeSpec,
|
|
29
|
+
defaultFuelModel: () => defaultFuelModel,
|
|
30
|
+
defaultSemiSpec: () => defaultSemiSpec,
|
|
31
|
+
defaultSurfaces: () => defaultSurfaces,
|
|
32
|
+
fuelRate: () => fuelRate,
|
|
33
|
+
impactEnergy: () => impactEnergy,
|
|
34
|
+
runOver: () => runOver,
|
|
35
|
+
semiSpecFromStats: () => semiSpecFromStats,
|
|
36
|
+
speedLoss: () => speedLoss,
|
|
37
|
+
surfaceGrip: () => surfaceGrip,
|
|
38
|
+
vehicleCorners: () => vehicleCorners,
|
|
39
|
+
vehicleDamageFromImpact: () => vehicleDamageFromImpact
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// src/arcade.ts
|
|
43
|
+
var defaultArcadeSpec = {
|
|
44
|
+
length: 4.3,
|
|
45
|
+
width: 1.9,
|
|
46
|
+
accel: 5.5,
|
|
47
|
+
brakeAgainstMotion: 12,
|
|
48
|
+
reverseAccel: 3,
|
|
49
|
+
drag: 0.5,
|
|
50
|
+
handbrakeDrag: 3,
|
|
51
|
+
maxForward: 17,
|
|
52
|
+
maxReverse: -5,
|
|
53
|
+
steerMax: 0.55,
|
|
54
|
+
steerResponse: 5,
|
|
55
|
+
wheelbase: 2.6,
|
|
56
|
+
engineNoiseInterval: 0.5,
|
|
57
|
+
engineNoiseBase: 25,
|
|
58
|
+
engineNoiseSpeedFactor: 2,
|
|
59
|
+
hornNoiseRadius: 120,
|
|
60
|
+
crash: { minSpeed: 0.5, damageSpeed: 4, damage: 9, shakeDivisor: 20, hurtSpeed: 9, hurtFactor: 1.2, bounce: -0.25, noiseRadius: 25 },
|
|
61
|
+
runOver: { knockSpeed: 3, killSpeed: 8, knockDamage: 2.5, stunSeconds: 1.4, throwFactor: 0.8, sideKick: 2, knockSlow: 0.93, killVehicleDamage: 3, holdDrag: 2.5, margin: 0.3 }
|
|
62
|
+
};
|
|
63
|
+
function vehicleCorners(spec, x, z, yaw) {
|
|
64
|
+
const c = Math.cos(yaw);
|
|
65
|
+
const s = Math.sin(yaw);
|
|
66
|
+
const hl = spec.length / 2;
|
|
67
|
+
const hw = spec.width / 2;
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const [a, b] of [[hl, hw], [hl, -hw], [-hl, hw], [-hl, -hw], [hl, 0], [-hl, 0], [0, hw], [0, -hw]]) out.push([x + a * c + b * s, z - a * s + b * c]);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
var clamp = (x, a, b) => Math.max(a, Math.min(b, x));
|
|
73
|
+
var lerp = (a, b, t) => a + (b - a) * t;
|
|
74
|
+
function createArcadeDriver(options = {}) {
|
|
75
|
+
const spec = {
|
|
76
|
+
...defaultArcadeSpec,
|
|
77
|
+
...options,
|
|
78
|
+
crash: { ...defaultArcadeSpec.crash, ...options.crash || {} },
|
|
79
|
+
runOver: { ...defaultArcadeSpec.runOver, ...options.runOver || {} }
|
|
80
|
+
};
|
|
81
|
+
const state = { x: 0, z: 0, yaw: 0, v: 0, steer: 0, noiseTimer: 0 };
|
|
82
|
+
const step = (input, dt, world) => {
|
|
83
|
+
const events = [];
|
|
84
|
+
const f = (input.throttle > 0 ? 1 : 0) - (input.brake > 0 ? 1 : 0);
|
|
85
|
+
if (f > 0) state.v += (state.v < 0 ? spec.brakeAgainstMotion : spec.accel) * dt * Math.min(1, input.throttle);
|
|
86
|
+
else if (f < 0) state.v -= (state.v > 0 ? spec.brakeAgainstMotion : spec.reverseAccel) * dt * Math.min(1, input.brake);
|
|
87
|
+
else state.v *= Math.exp(-spec.drag * dt);
|
|
88
|
+
if (input.handbrake) state.v *= Math.exp(-spec.handbrakeDrag * dt);
|
|
89
|
+
state.v = clamp(state.v, spec.maxReverse, spec.maxForward);
|
|
90
|
+
state.steer = lerp(state.steer, clamp(input.steer, -1, 1) * spec.steerMax, Math.min(1, dt * spec.steerResponse));
|
|
91
|
+
const nyaw = state.yaw + state.steer * state.v / spec.wheelbase * dt;
|
|
92
|
+
const fx = Math.cos(nyaw);
|
|
93
|
+
const fz = -Math.sin(nyaw);
|
|
94
|
+
const nx = state.x + fx * state.v * dt;
|
|
95
|
+
const nz = state.z + fz * state.v * dt;
|
|
96
|
+
if (!world.blocked(nx, nz, nyaw)) {
|
|
97
|
+
state.x = nx;
|
|
98
|
+
state.z = nz;
|
|
99
|
+
state.yaw = nyaw;
|
|
100
|
+
} else if (Math.abs(state.v) > spec.crash.minSpeed) {
|
|
101
|
+
const sp2 = Math.abs(state.v);
|
|
102
|
+
if (sp2 > spec.crash.damageSpeed) {
|
|
103
|
+
events.push({ type: "crash", speed: sp2, damage: sp2 * spec.crash.damage, shake: sp2 / spec.crash.shakeDivisor, hurtDriver: sp2 > spec.crash.hurtSpeed ? sp2 * spec.crash.hurtFactor : 0 });
|
|
104
|
+
events.push({ type: "noise", radius: spec.crash.noiseRadius, x: state.x, z: state.z, source: "crash" });
|
|
105
|
+
}
|
|
106
|
+
state.v *= spec.crash.bounce;
|
|
107
|
+
}
|
|
108
|
+
const sp = Math.abs(state.v);
|
|
109
|
+
const ro = spec.runOver;
|
|
110
|
+
const bodies = world.bodies ? world.bodies() : [];
|
|
111
|
+
for (let i = 0; i < bodies.length; i++) {
|
|
112
|
+
const body = bodies[i];
|
|
113
|
+
if (!body) continue;
|
|
114
|
+
const dx = body.x - state.x;
|
|
115
|
+
const dz = body.z - state.z;
|
|
116
|
+
const a = dx * fx + dz * fz;
|
|
117
|
+
const b = -dx * fz + dz * fx;
|
|
118
|
+
if (Math.abs(a) > spec.length / 2 + ro.margin || Math.abs(b) > spec.width / 2 + ro.margin) continue;
|
|
119
|
+
if (sp > ro.killSpeed) events.push({ type: "run_over", outcome: "kill", index: i, speed: sp, vehicleDamage: ro.killVehicleDamage });
|
|
120
|
+
else if (sp > ro.knockSpeed) {
|
|
121
|
+
events.push({
|
|
122
|
+
type: "run_over",
|
|
123
|
+
outcome: "knock",
|
|
124
|
+
index: i,
|
|
125
|
+
speed: sp,
|
|
126
|
+
damage: sp * sp * ro.knockDamage,
|
|
127
|
+
stunSeconds: ro.stunSeconds,
|
|
128
|
+
vx: fx * sp * ro.throwFactor + (b > 0 ? -fz : fz) * ro.sideKick,
|
|
129
|
+
vz: fz * sp * ro.throwFactor + (b > 0 ? fx : -fx) * ro.sideKick,
|
|
130
|
+
slow: ro.knockSlow
|
|
131
|
+
});
|
|
132
|
+
state.v *= ro.knockSlow;
|
|
133
|
+
} else {
|
|
134
|
+
state.v *= Math.exp(-ro.holdDrag * dt);
|
|
135
|
+
events.push({ type: "run_over", outcome: "push", index: i, speed: sp, dx: (b > 0 ? -fz : fz) * dt, dz: (b > 0 ? fx : -fx) * dt });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
state.noiseTimer -= dt;
|
|
139
|
+
if (state.noiseTimer <= 0) {
|
|
140
|
+
state.noiseTimer = spec.engineNoiseInterval;
|
|
141
|
+
events.push({ type: "noise", radius: spec.engineNoiseBase + sp * spec.engineNoiseSpeedFactor, x: state.x, z: state.z, source: "engine" });
|
|
142
|
+
}
|
|
143
|
+
if (input.horn) {
|
|
144
|
+
events.push({ type: "noise", radius: spec.hornNoiseRadius, x: state.x, z: state.z, source: "horn" });
|
|
145
|
+
events.push({ type: "horn" });
|
|
146
|
+
}
|
|
147
|
+
return { state, events };
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
spec,
|
|
151
|
+
state,
|
|
152
|
+
step,
|
|
153
|
+
place(x, z, yaw) {
|
|
154
|
+
state.x = x;
|
|
155
|
+
state.z = z;
|
|
156
|
+
state.yaw = yaw;
|
|
157
|
+
state.v = 0;
|
|
158
|
+
state.steer = 0;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/run-over.ts
|
|
164
|
+
function runOver(speed, rules = {}) {
|
|
165
|
+
const { knockSpeed, killSpeed } = { ...defaultArcadeSpec.runOver, ...rules };
|
|
166
|
+
const s = Math.abs(speed);
|
|
167
|
+
return s > killSpeed ? "kill" : s > knockSpeed ? "knock" : "push";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/semi/fuel.ts
|
|
171
|
+
var defaultFuelModel = { idleRate: 3e-4, fullRate: 35e-4, loadFactor: 0.5, damagedEngineExtra: 0.4 };
|
|
172
|
+
function fuelRate(model, rpmFraction, throttle, loadRatio, engineHealth) {
|
|
173
|
+
const base = model.idleRate + model.fullRate * Math.max(0, Math.min(1, rpmFraction)) * Math.max(0, Math.min(1, throttle));
|
|
174
|
+
return base * (1 + model.loadFactor * Math.max(0, loadRatio)) * (1 + model.damagedEngineExtra * (1 - Math.max(0, Math.min(1, engineHealth))));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ../game-contracts/dist/index.js
|
|
178
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
179
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
180
|
+
var deepMerge = (base, patch) => {
|
|
181
|
+
if (!patch) return base;
|
|
182
|
+
const result = { ...base };
|
|
183
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
184
|
+
if (value === void 0 || UNSAFE_KEYS.has(key)) continue;
|
|
185
|
+
const current = result[key];
|
|
186
|
+
result[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
};
|
|
190
|
+
var mergeById = (shipped, extra) => {
|
|
191
|
+
const entries = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
|
|
193
|
+
return entries;
|
|
194
|
+
};
|
|
195
|
+
var applyPatches = (entries, patches) => {
|
|
196
|
+
for (const [id, patch] of Object.entries(patches ?? {})) {
|
|
197
|
+
const entry = entries.get(id);
|
|
198
|
+
if (entry) entries.set(id, deepMerge(entry, patch));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var removeIds = (entries, ids) => {
|
|
202
|
+
for (const id of ids ?? []) entries.delete(id);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/semi/surfaces.ts
|
|
206
|
+
var defaultSurfaces = [
|
|
207
|
+
{ id: "dry_asphalt", grip: 1 },
|
|
208
|
+
{ id: "wet_asphalt", grip: 0.75, aquaplaningKmh: 90, aquaplaningFactor: 0.4, aquaplaningBand: 30 },
|
|
209
|
+
{ id: "dirt", grip: 0.65 },
|
|
210
|
+
{ id: "grass", grip: 0.55 },
|
|
211
|
+
{ id: "mud", grip: 0.4, bogs: true },
|
|
212
|
+
/** Blood and bodies after a run-over; a driver applies it for a few seconds with `contaminate`. */
|
|
213
|
+
{ id: "blood", grip: 0.5 }
|
|
214
|
+
];
|
|
215
|
+
function createSurfaces(options = {}) {
|
|
216
|
+
const entries = mergeById(defaultSurfaces, options.packs?.flatMap((pack) => pack.surfaces ?? []));
|
|
217
|
+
applyPatches(entries, options.patch);
|
|
218
|
+
removeIds(entries, options.remove);
|
|
219
|
+
const fallback = entries.get("dry_asphalt") ?? { id: "dry_asphalt", grip: 1 };
|
|
220
|
+
return {
|
|
221
|
+
get: (id) => entries.get(id) ?? fallback,
|
|
222
|
+
get surfaces() {
|
|
223
|
+
return [...entries.values()];
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function surfaceGrip(surface, speedMetresPerSecond) {
|
|
228
|
+
if (surface.aquaplaningKmh == null) return surface.grip;
|
|
229
|
+
const kmh = Math.abs(speedMetresPerSecond) * 3.6;
|
|
230
|
+
const band = surface.aquaplaningBand ?? 30;
|
|
231
|
+
const t = Math.max(0, Math.min(1, (kmh - surface.aquaplaningKmh) / band));
|
|
232
|
+
return surface.grip * (1 - (1 - (surface.aquaplaningFactor ?? 0.4)) * t);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/semi/driver.ts
|
|
236
|
+
var G = 9.81;
|
|
237
|
+
var clamp2 = (x, a, b) => Math.max(a, Math.min(b, x));
|
|
238
|
+
var MAX_SUBSTEP = 1 / 240;
|
|
239
|
+
var FLAT_BELOW = 0.2;
|
|
240
|
+
var STALL_HEALTH = 0.3;
|
|
241
|
+
var defaultSemiSpec = {
|
|
242
|
+
mass: 1300,
|
|
243
|
+
centreHeight: 0.5,
|
|
244
|
+
wheels: [
|
|
245
|
+
{ x: 1.3, z: -0.75, steer: true },
|
|
246
|
+
{ x: 1.3, z: 0.75, steer: true },
|
|
247
|
+
{ x: -1.3, z: -0.75, drive: true, handbrake: true },
|
|
248
|
+
{ x: -1.3, z: 0.75, drive: true, handbrake: true }
|
|
249
|
+
],
|
|
250
|
+
wheelRadius: 0.32,
|
|
251
|
+
fourWheelDrive: false,
|
|
252
|
+
drag: 0.4,
|
|
253
|
+
rolling: 0.015,
|
|
254
|
+
bogResistance: 0.3,
|
|
255
|
+
maxSteer: 0.55,
|
|
256
|
+
brakeForce: 14e3,
|
|
257
|
+
engine: { idleRpm: 900, peakRpm: 4200, redlineRpm: 6500, peakTorque: 220 },
|
|
258
|
+
gears: [3.6, 2.2, 1.5, 1.1, 0.85],
|
|
259
|
+
reverseRatio: 3.4,
|
|
260
|
+
finalDrive: 3.7,
|
|
261
|
+
efficiency: 0.85,
|
|
262
|
+
upshiftFraction: 0.85,
|
|
263
|
+
downshiftFraction: 0.35,
|
|
264
|
+
suspension: { restLength: 0.4, spring: 32e3, damper: 3200 },
|
|
265
|
+
tyre: { friction: 1.05, corneringStiffness: 9 },
|
|
266
|
+
tank: { capacity: 45 }
|
|
267
|
+
};
|
|
268
|
+
var torqueAt = (spec, rpm) => {
|
|
269
|
+
const e = spec.engine;
|
|
270
|
+
if (rpm >= e.redlineRpm) return 0;
|
|
271
|
+
const t = rpm <= e.peakRpm ? 0.7 + 0.3 * clamp2((rpm - e.idleRpm) / (e.peakRpm - e.idleRpm), 0, 1) : 1 - 0.4 * ((rpm - e.peakRpm) / (e.redlineRpm - e.peakRpm));
|
|
272
|
+
return e.peakTorque * t;
|
|
273
|
+
};
|
|
274
|
+
function createSemiSimDriver(specification = {}, options = {}) {
|
|
275
|
+
const spec = { ...defaultSemiSpec, ...specification };
|
|
276
|
+
const surfaces = options.surfaces ?? createSurfaces();
|
|
277
|
+
const fuelModel = { ...defaultFuelModel, ...spec.tank };
|
|
278
|
+
const wheelbase = Math.max(...spec.wheels.map((w) => w.x)) - Math.min(...spec.wheels.map((w) => w.x));
|
|
279
|
+
const track = Math.max(...spec.wheels.map((w) => w.z)) - Math.min(...spec.wheels.map((w) => w.z));
|
|
280
|
+
const baseMass = spec.mass;
|
|
281
|
+
let cargo = 0;
|
|
282
|
+
let contamination = null;
|
|
283
|
+
const n = spec.wheels.length;
|
|
284
|
+
const state = {
|
|
285
|
+
x: 0,
|
|
286
|
+
z: 0,
|
|
287
|
+
yaw: 0,
|
|
288
|
+
u: 0,
|
|
289
|
+
w: 0,
|
|
290
|
+
yawRate: 0,
|
|
291
|
+
height: spec.suspension.restLength + spec.wheelRadius - spec.mass * G / (specification.wheels ? specification.wheels.length : defaultSemiSpec.wheels.length) / spec.suspension.spring,
|
|
292
|
+
heightRate: 0,
|
|
293
|
+
pitch: 0,
|
|
294
|
+
pitchRate: 0,
|
|
295
|
+
roll: 0,
|
|
296
|
+
rollRate: 0,
|
|
297
|
+
rpm: spec.engine.idleRpm,
|
|
298
|
+
gear: 1,
|
|
299
|
+
engineOn: true,
|
|
300
|
+
bogged: false,
|
|
301
|
+
fuel: spec.tank.capacity,
|
|
302
|
+
loads: new Array(n).fill(baseMass * G / n),
|
|
303
|
+
slip: new Array(n).fill(0),
|
|
304
|
+
steerAngle: 0
|
|
305
|
+
};
|
|
306
|
+
const driver = {
|
|
307
|
+
spec,
|
|
308
|
+
state,
|
|
309
|
+
condition: {},
|
|
310
|
+
setCargo(kg) {
|
|
311
|
+
cargo = Math.max(0, kg);
|
|
312
|
+
},
|
|
313
|
+
contaminate(surfaceId, seconds) {
|
|
314
|
+
contamination = { id: surfaceId, left: seconds };
|
|
315
|
+
},
|
|
316
|
+
refuel(litres) {
|
|
317
|
+
state.fuel = Math.min(spec.tank.capacity, state.fuel + litres);
|
|
318
|
+
},
|
|
319
|
+
place(x, z, yaw) {
|
|
320
|
+
state.x = x;
|
|
321
|
+
state.z = z;
|
|
322
|
+
state.yaw = yaw;
|
|
323
|
+
state.u = state.w = state.yawRate = 0;
|
|
324
|
+
},
|
|
325
|
+
step(input, dt, world = {}) {
|
|
326
|
+
const events = [];
|
|
327
|
+
const steps = Math.max(1, Math.ceil(dt / MAX_SUBSTEP));
|
|
328
|
+
const h = dt / steps;
|
|
329
|
+
for (let s = 0; s < steps; s++) substep(input, h, world, events);
|
|
330
|
+
return { state, events };
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const stallWatch = { throttleTime: 0 };
|
|
334
|
+
function substep(input, dt, world, events) {
|
|
335
|
+
const cond = driver.condition;
|
|
336
|
+
const mass = baseMass + cargo;
|
|
337
|
+
const engineHealth = clamp2(cond.engine ?? 1, 0, 1);
|
|
338
|
+
const suspensionHealth = clamp2(cond.suspension ?? 1, 0, 1);
|
|
339
|
+
const inertia = (spec.yawInertia ?? baseMass * (wheelbase * wheelbase + track * track) / 12) * (mass / baseMass);
|
|
340
|
+
const pitchInertia = mass * (wheelbase * wheelbase + 1) / 12;
|
|
341
|
+
const rollInertia = mass * (track * track + 1) / 12;
|
|
342
|
+
if (contamination) {
|
|
343
|
+
contamination.left -= dt;
|
|
344
|
+
if (contamination.left <= 0) contamination = null;
|
|
345
|
+
}
|
|
346
|
+
const cs = Math.cos(state.yaw);
|
|
347
|
+
const sn = Math.sin(state.yaw);
|
|
348
|
+
const target = clamp2(input.steer, -1, 1) * spec.maxSteer;
|
|
349
|
+
state.steerAngle += (target - state.steerAngle) * Math.min(1, dt * 8);
|
|
350
|
+
let gear = state.gear;
|
|
351
|
+
if (input.gear != null) gear = input.gear;
|
|
352
|
+
else if (state.u < 0.5 && input.brake > 0 && input.throttle <= 0) gear = -1;
|
|
353
|
+
else if (gear <= 0 && !(gear === -1 && state.u < 0.5 && input.throttle <= 0)) gear = 1;
|
|
354
|
+
const ratioOf = (g) => g < 0 ? spec.reverseRatio : g === 0 ? 0 : spec.gears[g - 1];
|
|
355
|
+
const driveWheels = spec.wheels.map((w, i) => ({ w, i })).filter(({ w }) => spec.fourWheelDrive ? true : w.drive);
|
|
356
|
+
let driveSpeed = 0;
|
|
357
|
+
for (const { w } of driveWheels) driveSpeed += state.u + state.yawRate * w.z;
|
|
358
|
+
driveSpeed /= Math.max(1, driveWheels.length);
|
|
359
|
+
const wheelRpm = Math.abs(driveSpeed) / spec.wheelRadius * (60 / (2 * Math.PI));
|
|
360
|
+
if (input.gear == null && gear > 0) {
|
|
361
|
+
if (wheelRpm * ratioOf(gear) * spec.finalDrive > spec.engine.redlineRpm * spec.upshiftFraction && gear < spec.gears.length) gear++;
|
|
362
|
+
else if (gear > 1 && wheelRpm * ratioOf(gear) * spec.finalDrive < spec.engine.redlineRpm * spec.downshiftFraction) gear--;
|
|
363
|
+
}
|
|
364
|
+
if (gear !== state.gear) {
|
|
365
|
+
state.gear = gear;
|
|
366
|
+
events.push({ type: "shift", gear });
|
|
367
|
+
}
|
|
368
|
+
const reversing = state.gear < 0;
|
|
369
|
+
const accelInput = reversing ? input.brake : input.throttle;
|
|
370
|
+
const brakeInput = clamp2(reversing ? input.throttle : input.brake, 0, 1);
|
|
371
|
+
const ratio = ratioOf(state.gear) * spec.finalDrive;
|
|
372
|
+
state.rpm = clamp2(wheelRpm * ratio, spec.engine.idleRpm, spec.engine.redlineRpm + 200);
|
|
373
|
+
if (!state.engineOn || state.fuel <= 0) state.rpm = 0;
|
|
374
|
+
let throttle = clamp2(accelInput, 0, 1);
|
|
375
|
+
if (engineHealth < STALL_HEALTH && throttle > 0.8) {
|
|
376
|
+
stallWatch.throttleTime += dt;
|
|
377
|
+
if (state.pitch > 0.06 && stallWatch.throttleTime > 0.5 && state.engineOn) {
|
|
378
|
+
state.engineOn = false;
|
|
379
|
+
events.push({ type: "stall" });
|
|
380
|
+
}
|
|
381
|
+
throttle *= 0.5;
|
|
382
|
+
} else stallWatch.throttleTime = 0;
|
|
383
|
+
if (!state.engineOn && accelInput > 0 && Math.abs(state.u) < 0.5 && engineHealth >= STALL_HEALTH && state.fuel > 0) state.engineOn = true;
|
|
384
|
+
if (state.fuel <= 0 && state.engineOn) {
|
|
385
|
+
state.engineOn = false;
|
|
386
|
+
events.push({ type: "out_of_fuel" });
|
|
387
|
+
}
|
|
388
|
+
const engineTorque = state.engineOn ? torqueAt(spec, state.rpm) * throttle * (0.55 + 0.45 * engineHealth) : 0;
|
|
389
|
+
const driveForceTotal = state.gear === 0 ? 0 : (reversing ? -1 : 1) * engineTorque * ratio * spec.efficiency / spec.wheelRadius;
|
|
390
|
+
if (state.engineOn) {
|
|
391
|
+
const burned = fuelRate(fuelModel, state.rpm / spec.engine.redlineRpm, throttle, cargo / baseMass, engineHealth) * dt;
|
|
392
|
+
state.fuel = Math.max(0, state.fuel - burned);
|
|
393
|
+
}
|
|
394
|
+
const ride = spec.suspension.restLength;
|
|
395
|
+
let sumN = 0;
|
|
396
|
+
let sumNA = 0;
|
|
397
|
+
let sumNB = 0;
|
|
398
|
+
const wheelState = [];
|
|
399
|
+
let bogging = false;
|
|
400
|
+
for (let i = 0; i < n; i++) {
|
|
401
|
+
const w = spec.wheels[i];
|
|
402
|
+
const wx = state.x + w.x * cs + w.z * sn;
|
|
403
|
+
const wz = state.z - w.x * sn + w.z * cs;
|
|
404
|
+
const ground = world.ground ? world.ground(wx, wz) : {};
|
|
405
|
+
const surface = surfaces.get(contamination && contamination.left > 0 && surfaces.get(contamination.id).grip < surfaces.get(ground.surface ?? "dry_asphalt").grip ? contamination.id : ground.surface ?? "dry_asphalt");
|
|
406
|
+
const top = state.height + w.x * state.pitch - w.z * state.roll;
|
|
407
|
+
const length = top - (ground.height ?? 0) - spec.wheelRadius;
|
|
408
|
+
const compression = ride - length;
|
|
409
|
+
const rate = -(state.heightRate + w.x * state.pitchRate - w.z * state.rollRate);
|
|
410
|
+
const k = spec.suspension.spring * (0.5 + 0.5 * suspensionHealth);
|
|
411
|
+
const c = spec.suspension.damper * (0.5 + 0.5 * suspensionHealth);
|
|
412
|
+
const load = Math.max(0, k * compression + c * rate);
|
|
413
|
+
const tyreHealth = cond.tyres ? clamp2(cond.tyres[i] ?? 1, 0, 1) : 1;
|
|
414
|
+
const flat = tyreHealth < FLAT_BELOW;
|
|
415
|
+
const bog = !!surface.bogs && !spec.fourWheelDrive;
|
|
416
|
+
if (bog) bogging = true;
|
|
417
|
+
const grip = surfaceGrip(surface, state.u) * (flat ? 0.5 : 1);
|
|
418
|
+
wheelState.push({ n: load, grip, surface: surface.id, bog, flat });
|
|
419
|
+
sumN += load;
|
|
420
|
+
sumNA += load * w.x;
|
|
421
|
+
sumNB += load * w.z;
|
|
422
|
+
}
|
|
423
|
+
let fFwd = 0;
|
|
424
|
+
let fRight = 0;
|
|
425
|
+
let torque = 0;
|
|
426
|
+
let pitchTorque = 0;
|
|
427
|
+
let rollTorque = 0;
|
|
428
|
+
const handbrake = !!input.handbrake;
|
|
429
|
+
const loadSum = Math.max(1, wheelState.reduce((a, b) => a + b.n, 0));
|
|
430
|
+
const drivenCount = Math.max(1, driveWheels.length);
|
|
431
|
+
for (let i = 0; i < n; i++) {
|
|
432
|
+
const w = spec.wheels[i];
|
|
433
|
+
const ws = wheelState[i];
|
|
434
|
+
const delta = w.steer ? state.steerAngle : 0;
|
|
435
|
+
const cd = Math.cos(delta);
|
|
436
|
+
const sd = Math.sin(delta);
|
|
437
|
+
const vxw = state.u + state.yawRate * w.z;
|
|
438
|
+
const vyw = state.w - state.yawRate * w.x;
|
|
439
|
+
const vl = vxw * cd - vyw * sd;
|
|
440
|
+
const vc = vxw * sd + vyw * cd;
|
|
441
|
+
const capacity = spec.tyre.friction * ws.grip * ws.n;
|
|
442
|
+
let fl = 0;
|
|
443
|
+
if (driveWheels.some((d) => d.i === i)) fl += driveForceTotal / drivenCount;
|
|
444
|
+
const locked = handbrake && w.handbrake;
|
|
445
|
+
const brakeShare = spec.brakeForce * brakeInput * ws.n / loadSum;
|
|
446
|
+
const moving = Math.abs(vl) > 0.05;
|
|
447
|
+
if (locked) fl += moving ? -Math.sign(vl) * capacity : 0;
|
|
448
|
+
else if (brakeShare > 0 && moving) fl += -Math.sign(vl) * brakeShare;
|
|
449
|
+
const rolling = ws.bog && accelInput > 0 ? spec.bogResistance : spec.rolling * (ws.flat ? 6 : 1);
|
|
450
|
+
const alpha = Math.atan2(vc, Math.abs(vl) + 0.5);
|
|
451
|
+
let fc = -Math.tanh(spec.tyre.corneringStiffness * alpha) * capacity;
|
|
452
|
+
if (locked) fc *= 0.6;
|
|
453
|
+
const total = Math.hypot(fl, fc);
|
|
454
|
+
let used = 1;
|
|
455
|
+
if (total > capacity && total > 0) used = capacity / total;
|
|
456
|
+
fl *= used;
|
|
457
|
+
fc *= used;
|
|
458
|
+
if (moving) fl += -Math.sign(vl) * rolling * ws.n;
|
|
459
|
+
const slipRatio = capacity > 0 ? clamp2(total / capacity, 0, 1) : 0;
|
|
460
|
+
state.slip[i] = slipRatio;
|
|
461
|
+
if (ws.surface === "wet_asphalt" && Math.abs(state.u) * 3.6 > (surfaces.get(ws.surface).aquaplaningKmh ?? 1e9) && slipRatio >= 0.99) events.push({ type: "aquaplaning", wheel: i });
|
|
462
|
+
const fx = fl * cd + fc * sd;
|
|
463
|
+
const fy = -fl * sd + fc * cd;
|
|
464
|
+
fFwd += fx;
|
|
465
|
+
fRight += fy;
|
|
466
|
+
torque += w.z * fx - w.x * fy;
|
|
467
|
+
pitchTorque += fx * spec.centreHeight;
|
|
468
|
+
rollTorque += -fy * spec.centreHeight;
|
|
469
|
+
}
|
|
470
|
+
const bogNow = bogging && Math.abs(state.u) < 1 && accelInput > 0;
|
|
471
|
+
if (bogNow && !state.bogged) events.push({ type: "bogged" });
|
|
472
|
+
state.bogged = bogNow;
|
|
473
|
+
fFwd += -spec.drag * state.u * Math.abs(state.u);
|
|
474
|
+
fRight += -spec.drag * state.w * Math.abs(state.w);
|
|
475
|
+
const du = fFwd / mass - state.yawRate * state.w;
|
|
476
|
+
const dw = fRight / mass + state.yawRate * state.u;
|
|
477
|
+
state.u += du * dt;
|
|
478
|
+
state.w += dw * dt;
|
|
479
|
+
if (Math.abs(state.u) < 0.02 && accelInput <= 0) state.u = 0;
|
|
480
|
+
state.yawRate += torque / inertia * dt;
|
|
481
|
+
state.yaw += state.yawRate * dt;
|
|
482
|
+
const csn = Math.cos(state.yaw);
|
|
483
|
+
const snn = Math.sin(state.yaw);
|
|
484
|
+
state.x += (state.u * csn + state.w * snn) * dt;
|
|
485
|
+
state.z += (-state.u * snn + state.w * csn) * dt;
|
|
486
|
+
const ay = (sumN - mass * G) / mass;
|
|
487
|
+
state.heightRate += ay * dt;
|
|
488
|
+
state.height += state.heightRate * dt;
|
|
489
|
+
state.pitchRate += (sumNA + pitchTorque) / pitchInertia * dt;
|
|
490
|
+
state.pitch += state.pitchRate * dt;
|
|
491
|
+
state.rollRate += (-sumNB + rollTorque) / rollInertia * dt;
|
|
492
|
+
state.roll += state.rollRate * dt;
|
|
493
|
+
for (let i = 0; i < n; i++) state.loads[i] = wheelState[i].n;
|
|
494
|
+
}
|
|
495
|
+
return driver;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/semi/impact.ts
|
|
499
|
+
var impactEnergy = (vehicleMass, relativeSpeed) => 0.5 * vehicleMass * relativeSpeed * relativeSpeed;
|
|
500
|
+
var bodyDamage = (energy, bumperMultiplier, zoneFactor) => energy * bumperMultiplier * zoneFactor;
|
|
501
|
+
var vehicleDamageFromImpact = (energy, frontArmor, factor = 0.04) => energy * factor * (1 - frontArmor);
|
|
502
|
+
var speedLoss = (bodyMass, vehicleMass, speed) => bodyMass / (vehicleMass + bodyMass) * speed;
|
|
503
|
+
|
|
504
|
+
// src/semi/from-stats.ts
|
|
505
|
+
function semiSpecFromStats(stats, body, base = {}) {
|
|
506
|
+
const spec = { ...defaultSemiSpec, ...base };
|
|
507
|
+
const half = body.wheelbase / 2;
|
|
508
|
+
const side = body.track / 2;
|
|
509
|
+
const driveAll = !!body.fourWheelDrive;
|
|
510
|
+
const wheels = [
|
|
511
|
+
{ x: half, z: -side, steer: true, drive: driveAll },
|
|
512
|
+
{ x: half, z: side, steer: true, drive: driveAll },
|
|
513
|
+
{ x: -half, z: -side, drive: true, handbrake: true },
|
|
514
|
+
{ x: -half, z: side, drive: true, handbrake: true }
|
|
515
|
+
];
|
|
516
|
+
const peakTorque = body.mass * (0.06 + 0.015 * stats.acceleration);
|
|
517
|
+
const engine = { ...spec.engine, peakTorque };
|
|
518
|
+
const top = stats.maxSpeedKmh / 3.6;
|
|
519
|
+
const lastRatio = engine.redlineRpm * 0.95 * 2 * Math.PI * spec.wheelRadius / (60 * top * spec.finalDrive);
|
|
520
|
+
const gears = [...spec.gears.slice(0, -1).map((g, i, all) => Math.max(g, lastRatio * 1.25 ** (all.length - i))), lastRatio];
|
|
521
|
+
const torqueAtTop = peakTorque * 0.76;
|
|
522
|
+
const thrust = torqueAtTop * lastRatio * spec.finalDrive * spec.efficiency / spec.wheelRadius;
|
|
523
|
+
const rollingForce = spec.rolling * body.mass * 9.81;
|
|
524
|
+
const drag = Math.max(0.01, (thrust - rollingForce) / (top * top));
|
|
525
|
+
return { ...spec, mass: body.mass, wheels, fourWheelDrive: driveAll, engine, gears, drag, tyre: { ...spec.tyre, friction: 0.8 + 0.03 * stats.handling } };
|
|
526
|
+
}
|
|
527
|
+
return __toCommonJS(index_exports);
|
|
528
|
+
})();
|
|
529
|
+
//# sourceMappingURL=index.global.js.map
|