@adatechnology/game-vehicles 0.5.0 → 0.6.1
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/dist/index.d.ts +169 -4
- package/dist/index.global.js +1652 -164
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1529 -106
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -892,7 +892,7 @@ function advance(geo, ti, deadline) {
|
|
|
892
892
|
}
|
|
893
893
|
return ti.builtBlocks === total;
|
|
894
894
|
}
|
|
895
|
-
function start(geo) {
|
|
895
|
+
function start(geo, store = cache) {
|
|
896
896
|
const pos = geo.attributes.position;
|
|
897
897
|
const triangles = trianglesOf(geo);
|
|
898
898
|
const ti = {
|
|
@@ -905,7 +905,7 @@ function start(geo) {
|
|
|
905
905
|
blocks: new Float64Array(blockCount(triangles) * 6),
|
|
906
906
|
builtBlocks: 0
|
|
907
907
|
};
|
|
908
|
-
|
|
908
|
+
store.set(geo, ti);
|
|
909
909
|
return ti;
|
|
910
910
|
}
|
|
911
911
|
function triIndexOf(geo) {
|
|
@@ -957,6 +957,106 @@ function boundsFromIndex(geo) {
|
|
|
957
957
|
}
|
|
958
958
|
return { box, version: ti.positionVersion };
|
|
959
959
|
}
|
|
960
|
+
var rayCache = /* @__PURE__ */ new WeakMap();
|
|
961
|
+
function rayIndexOf(geo) {
|
|
962
|
+
const shared = cache.get(geo);
|
|
963
|
+
if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
|
|
964
|
+
let ti = rayCache.get(geo);
|
|
965
|
+
if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
|
|
966
|
+
advance(geo, ti, Infinity);
|
|
967
|
+
return ti;
|
|
968
|
+
}
|
|
969
|
+
var BLOCK_SLACK = 1e-6;
|
|
970
|
+
var scratch = /* @__PURE__ */ new WeakMap();
|
|
971
|
+
function scratchOf(THREE) {
|
|
972
|
+
let s = scratch.get(THREE);
|
|
973
|
+
if (!s) {
|
|
974
|
+
s = {
|
|
975
|
+
inverse: new THREE.Matrix4(),
|
|
976
|
+
local: new THREE.Ray(),
|
|
977
|
+
box: new THREE.Box3(),
|
|
978
|
+
a: new THREE.Vector3(),
|
|
979
|
+
b: new THREE.Vector3(),
|
|
980
|
+
c: new THREE.Vector3(),
|
|
981
|
+
point: new THREE.Vector3(),
|
|
982
|
+
world: new THREE.Vector3(),
|
|
983
|
+
sphere: new THREE.Sphere()
|
|
984
|
+
};
|
|
985
|
+
scratch.set(THREE, s);
|
|
986
|
+
}
|
|
987
|
+
return s;
|
|
988
|
+
}
|
|
989
|
+
function indexable(THREE, mesh) {
|
|
990
|
+
const geo = mesh.geometry;
|
|
991
|
+
if (mesh.raycast !== THREE.Mesh.prototype.raycast) return false;
|
|
992
|
+
if (!mesh.isMesh || mesh.isSkinnedMesh || mesh.isInstancedMesh || mesh.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
|
|
993
|
+
if (!mesh.material || Array.isArray(mesh.material)) return false;
|
|
994
|
+
if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
|
|
995
|
+
return supports(geo);
|
|
996
|
+
}
|
|
997
|
+
function raycastMesh(THREE, raycaster, mesh, out) {
|
|
998
|
+
const geo = mesh.geometry;
|
|
999
|
+
const s = scratchOf(THREE);
|
|
1000
|
+
if (geo.boundingSphere === null) geo.computeBoundingSphere();
|
|
1001
|
+
s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh.matrixWorld);
|
|
1002
|
+
if (!raycaster.ray.intersectsSphere(s.sphere)) return;
|
|
1003
|
+
const ti = rayIndexOf(geo);
|
|
1004
|
+
if (!ti) return;
|
|
1005
|
+
s.inverse.copy(mesh.matrixWorld).invert();
|
|
1006
|
+
s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
|
|
1007
|
+
const material = mesh.material;
|
|
1008
|
+
const back = material.side === THREE.BackSide;
|
|
1009
|
+
const cull = material.side === THREE.FrontSide;
|
|
1010
|
+
const P = geo.attributes.position;
|
|
1011
|
+
const ix = geo.index;
|
|
1012
|
+
const count = ix ? ix.count : P.count;
|
|
1013
|
+
const first = Math.max(0, geo.drawRange.start);
|
|
1014
|
+
const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
|
|
1015
|
+
const t0 = Math.ceil(first / 3);
|
|
1016
|
+
const t1 = Math.floor(last / 3);
|
|
1017
|
+
const B = ti.boxes;
|
|
1018
|
+
const K = ti.blocks;
|
|
1019
|
+
for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
|
|
1020
|
+
const o = blk * 6;
|
|
1021
|
+
s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
|
|
1022
|
+
s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
|
|
1023
|
+
if (!s.local.intersectsBox(s.box)) continue;
|
|
1024
|
+
const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
|
|
1025
|
+
for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
|
|
1026
|
+
const j = t * 6;
|
|
1027
|
+
s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
|
|
1028
|
+
s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
|
|
1029
|
+
if (!s.local.intersectsBox(s.box)) continue;
|
|
1030
|
+
const a = ix ? ix.getX(t * 3) : t * 3;
|
|
1031
|
+
const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
|
|
1032
|
+
const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
|
|
1033
|
+
s.a.fromBufferAttribute(P, a);
|
|
1034
|
+
s.b.fromBufferAttribute(P, b);
|
|
1035
|
+
s.c.fromBufferAttribute(P, c);
|
|
1036
|
+
const hit = back ? s.local.intersectTriangle(s.c, s.b, s.a, true, s.point) : s.local.intersectTriangle(s.a, s.b, s.c, cull, s.point);
|
|
1037
|
+
if (hit === null) continue;
|
|
1038
|
+
s.world.copy(s.point).applyMatrix4(mesh.matrixWorld);
|
|
1039
|
+
const distance = raycaster.ray.origin.distanceTo(s.world);
|
|
1040
|
+
if (distance < raycaster.near || distance > raycaster.far) continue;
|
|
1041
|
+
const normal = new THREE.Vector3();
|
|
1042
|
+
THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
|
|
1043
|
+
out.push({ distance, point: s.world.clone(), object: mesh, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function collect(THREE, raycaster, object, recursive, out) {
|
|
1048
|
+
if (object.layers.test(raycaster.layers)) {
|
|
1049
|
+
if (indexable(THREE, object)) raycastMesh(THREE, raycaster, object, out);
|
|
1050
|
+
else object.raycast(raycaster, out);
|
|
1051
|
+
}
|
|
1052
|
+
if (recursive) for (const child of object.children) collect(THREE, raycaster, child, true, out);
|
|
1053
|
+
}
|
|
1054
|
+
function raycastIndexed(THREE, raycaster, objects, recursive = false) {
|
|
1055
|
+
const out = [];
|
|
1056
|
+
for (const object of objects) collect(THREE, raycaster, object, recursive, out);
|
|
1057
|
+
out.sort((x, y) => x.distance - y.distance);
|
|
1058
|
+
return out;
|
|
1059
|
+
}
|
|
960
1060
|
|
|
961
1061
|
// src/geometry.ts
|
|
962
1062
|
function installGeometry(kit) {
|
|
@@ -2290,6 +2390,7 @@ function installBody(kit) {
|
|
|
2290
2390
|
}
|
|
2291
2391
|
kit.profX = profX;
|
|
2292
2392
|
const _sr = new THREE.Raycaster();
|
|
2393
|
+
_sr.layers.enableAll();
|
|
2293
2394
|
kit.surfaceAt = function(c, org, dir, o) {
|
|
2294
2395
|
o = o || {};
|
|
2295
2396
|
c.root.updateMatrixWorld(true);
|
|
@@ -2306,7 +2407,7 @@ function installBody(kit) {
|
|
|
2306
2407
|
_sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
|
|
2307
2408
|
_sr.near = 0;
|
|
2308
2409
|
_sr.far = o.max || 20;
|
|
2309
|
-
const h =
|
|
2410
|
+
const h = raycastIndexed(THREE, _sr, cands)[0];
|
|
2310
2411
|
if (!h) return null;
|
|
2311
2412
|
const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
|
|
2312
2413
|
if (n.dot(d) > 0) n.negate();
|
|
@@ -2328,6 +2429,7 @@ function installGlass(kit) {
|
|
|
2328
2429
|
const V3 = THREE.Vector3;
|
|
2329
2430
|
const I = kit._;
|
|
2330
2431
|
const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
|
|
2432
|
+
I.paneSeals = /* @__PURE__ */ new WeakMap();
|
|
2331
2433
|
kit.addGlass = function(c, geo, label, type, o) {
|
|
2332
2434
|
o = o || {};
|
|
2333
2435
|
if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
|
|
@@ -2394,7 +2496,7 @@ function installGlass(kit) {
|
|
|
2394
2496
|
cy /= poly.length;
|
|
2395
2497
|
const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
|
|
2396
2498
|
const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
|
|
2397
|
-
kit.addGlass(c, geo, lab, o.type || "tempered", {
|
|
2499
|
+
const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
|
|
2398
2500
|
mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
|
|
2399
2501
|
pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
|
|
2400
2502
|
outline: shp.getSpacedPoints(56),
|
|
@@ -2410,6 +2512,7 @@ function installGlass(kit) {
|
|
|
2410
2512
|
pts.pop();
|
|
2411
2513
|
const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
|
|
2412
2514
|
seal.castShadow = false;
|
|
2515
|
+
I.paneSeals.set(pane, seal);
|
|
2413
2516
|
});
|
|
2414
2517
|
});
|
|
2415
2518
|
};
|
|
@@ -3085,6 +3188,7 @@ function installInterior(kit) {
|
|
|
3085
3188
|
}
|
|
3086
3189
|
|
|
3087
3190
|
// src/hood.ts
|
|
3191
|
+
var HOOD_LIFT = 8e-3;
|
|
3088
3192
|
function installHood(kit) {
|
|
3089
3193
|
const THREE = kit.THREE;
|
|
3090
3194
|
const V3 = THREE.Vector3;
|
|
@@ -3107,7 +3211,7 @@ function installHood(kit) {
|
|
|
3107
3211
|
for (let i = 0; i < p.count; i++) {
|
|
3108
3212
|
p.setX(i, cx + (p.getX(i) - cx) * sx);
|
|
3109
3213
|
p.setZ(i, p.getZ(i) * sz);
|
|
3110
|
-
p.setY(i, p.getY(i) +
|
|
3214
|
+
p.setY(i, p.getY(i) + HOOD_LIFT);
|
|
3111
3215
|
}
|
|
3112
3216
|
geo.translate(-hx, -hy, 0);
|
|
3113
3217
|
const under = geo.clone();
|
|
@@ -3136,7 +3240,8 @@ function installHood(kit) {
|
|
|
3136
3240
|
yMin: bb.min.y,
|
|
3137
3241
|
zw: bb.max.z,
|
|
3138
3242
|
label: o.label,
|
|
3139
|
-
noLatch: !!o.noLatch
|
|
3243
|
+
noLatch: !!o.noLatch,
|
|
3244
|
+
lift: HOOD_LIFT
|
|
3140
3245
|
};
|
|
3141
3246
|
c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
|
|
3142
3247
|
return c.hood;
|
|
@@ -4119,6 +4224,47 @@ function installWheels(kit) {
|
|
|
4119
4224
|
Object.assign(I, {});
|
|
4120
4225
|
}
|
|
4121
4226
|
|
|
4227
|
+
// src/particles.ts
|
|
4228
|
+
import { warnOnce } from "@adatechnology/game-contracts";
|
|
4229
|
+
|
|
4230
|
+
// src/routed-particles.ts
|
|
4231
|
+
var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
|
|
4232
|
+
var PUFF_LOOKS = {
|
|
4233
|
+
smokewhite: [10133927, 0.42],
|
|
4234
|
+
smokeblack: [1776412, 0.62],
|
|
4235
|
+
smokegray: [5593436, 0.5],
|
|
4236
|
+
smokesteam: [11975873, 0.3],
|
|
4237
|
+
dust: [7235160, 0.34],
|
|
4238
|
+
fire: [16777215, 0.9]
|
|
4239
|
+
};
|
|
4240
|
+
var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
|
|
4241
|
+
var FIRE_COLOR = 16753968;
|
|
4242
|
+
var SMOKE_START_SCALE = 0.18;
|
|
4243
|
+
function routedParticleRequest(input) {
|
|
4244
|
+
const { type } = input;
|
|
4245
|
+
const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
|
|
4246
|
+
const life = input.life ?? LIFE[type] ?? 99;
|
|
4247
|
+
const size = (input.size ?? 1) * (input.grow ?? 1);
|
|
4248
|
+
const isFire = type === "fire";
|
|
4249
|
+
const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
|
|
4250
|
+
return {
|
|
4251
|
+
effect: ROUTED_PARTICLE_TYPES[type],
|
|
4252
|
+
point: input.position,
|
|
4253
|
+
values: {
|
|
4254
|
+
vx: input.velocity[0],
|
|
4255
|
+
vy: input.velocity[1],
|
|
4256
|
+
vz: input.velocity[2],
|
|
4257
|
+
life,
|
|
4258
|
+
size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
|
|
4259
|
+
grow: growth,
|
|
4260
|
+
color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
|
|
4261
|
+
alpha: input.alpha ?? look[1],
|
|
4262
|
+
gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
|
|
4263
|
+
drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
|
|
4264
|
+
}
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4122
4268
|
// src/particles.ts
|
|
4123
4269
|
function installParticles(kit) {
|
|
4124
4270
|
const THREE = kit.THREE;
|
|
@@ -4210,7 +4356,7 @@ function installParticles(kit) {
|
|
|
4210
4356
|
});
|
|
4211
4357
|
}
|
|
4212
4358
|
const list = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
|
|
4213
|
-
const
|
|
4359
|
+
const build = () => new THREE.SpriteMaterial({
|
|
4214
4360
|
map: list[Math.floor(Math.random() * list.length)],
|
|
4215
4361
|
color: new THREE.Color(col),
|
|
4216
4362
|
transparent: true,
|
|
@@ -4218,6 +4364,7 @@ function installParticles(kit) {
|
|
|
4218
4364
|
opacity: op,
|
|
4219
4365
|
blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
|
|
4220
4366
|
});
|
|
4367
|
+
const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
|
|
4221
4368
|
const dis = { value: 0 };
|
|
4222
4369
|
mat.userData.dis = dis;
|
|
4223
4370
|
mat.onBeforeCompile = function(sh) {
|
|
@@ -4245,11 +4392,37 @@ function installParticles(kit) {
|
|
|
4245
4392
|
function killParticle(c, i) {
|
|
4246
4393
|
const P = c.particles[i];
|
|
4247
4394
|
c.effects.remove(P.m);
|
|
4248
|
-
if (P.own)
|
|
4395
|
+
if (P.own) {
|
|
4396
|
+
if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
|
|
4397
|
+
else P.m.material.dispose();
|
|
4398
|
+
}
|
|
4249
4399
|
c.particles.splice(i, 1);
|
|
4250
4400
|
}
|
|
4401
|
+
function routeParticle(c, effects, type, pos, vel, o) {
|
|
4402
|
+
c.effects.updateWorldMatrix(true, false);
|
|
4403
|
+
const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
|
|
4404
|
+
const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
|
|
4405
|
+
const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
|
|
4406
|
+
effects.play(
|
|
4407
|
+
routedParticleRequest({
|
|
4408
|
+
type,
|
|
4409
|
+
position: [at.x, at.y, at.z],
|
|
4410
|
+
velocity: [to.x, to.y, to.z],
|
|
4411
|
+
...o.variant ? { variant: o.variant } : {},
|
|
4412
|
+
...colour !== void 0 ? { color: colour } : {},
|
|
4413
|
+
...o.op != null ? { alpha: o.op } : {},
|
|
4414
|
+
...o.s ? { size: o.s } : {},
|
|
4415
|
+
...o.life ? { life: o.life } : {},
|
|
4416
|
+
...o.grow ? { grow: o.grow } : {}
|
|
4417
|
+
})
|
|
4418
|
+
);
|
|
4419
|
+
return null;
|
|
4420
|
+
}
|
|
4251
4421
|
kit.spawn = function(c, type, pos, vel, o) {
|
|
4252
4422
|
o = o || {};
|
|
4423
|
+
const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
|
|
4424
|
+
if (effects) return routeParticle(c, effects, type, pos, vel, o);
|
|
4425
|
+
if (ROUTED_PARTICLE_TYPES[type] && !kit.effects) warnOnce("game-vehicles.own-particles", "game-vehicles drew its smoke, fire and dust with sprites of its own because the host lent no effects world (options.effects); pass the host effects so the puffs share one budget");
|
|
4253
4426
|
if (c.particles.length >= PARTICLE_LIMIT) {
|
|
4254
4427
|
const idx = c.particles.findIndex(function(q) {
|
|
4255
4428
|
return TRANSIENT_TYPES[q.type];
|
|
@@ -4504,6 +4677,14 @@ function installParticles(kit) {
|
|
|
4504
4677
|
}
|
|
4505
4678
|
|
|
4506
4679
|
// src/fluids.ts
|
|
4680
|
+
var PUDDLE_COLORS = {
|
|
4681
|
+
coolant: 3123279,
|
|
4682
|
+
brake_fluid: 12093738,
|
|
4683
|
+
transmission_fluid: 10099746,
|
|
4684
|
+
power_steering_fluid: 9054768,
|
|
4685
|
+
washer_fluid: 2977744,
|
|
4686
|
+
battery_acid: 14211272
|
|
4687
|
+
};
|
|
4507
4688
|
function installFluids(kit) {
|
|
4508
4689
|
const THREE = kit.THREE;
|
|
4509
4690
|
const V3 = THREE.Vector3;
|
|
@@ -4521,6 +4702,7 @@ function installFluids(kit) {
|
|
|
4521
4702
|
};
|
|
4522
4703
|
kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
|
|
4523
4704
|
const _fr = new THREE.Raycaster();
|
|
4705
|
+
_fr.layers.enableAll();
|
|
4524
4706
|
kit.splat = function(c, p, n, r, o) {
|
|
4525
4707
|
o = o || {};
|
|
4526
4708
|
n = n.clone().normalize();
|
|
@@ -4722,6 +4904,13 @@ function installFluids(kit) {
|
|
|
4722
4904
|
transparent: true,
|
|
4723
4905
|
opacity: 0.9,
|
|
4724
4906
|
depthWrite: false
|
|
4907
|
+
}) : PUDDLE_COLORS[kind] ? new THREE.MeshStandardMaterial({
|
|
4908
|
+
color: PUDDLE_COLORS[kind],
|
|
4909
|
+
roughness: 0.05,
|
|
4910
|
+
metalness: 0.1,
|
|
4911
|
+
transparent: true,
|
|
4912
|
+
opacity: 0.7,
|
|
4913
|
+
depthWrite: false
|
|
4725
4914
|
}) : new THREE.MeshStandardMaterial({
|
|
4726
4915
|
color: 0,
|
|
4727
4916
|
roughness: 0.03,
|
|
@@ -4962,31 +5151,55 @@ function installFluids(kit) {
|
|
|
4962
5151
|
}
|
|
4963
5152
|
|
|
4964
5153
|
// src/glass-damage.ts
|
|
5154
|
+
function crackedGlassMaterial(THREE, texture) {
|
|
5155
|
+
return new THREE.MeshStandardMaterial({
|
|
5156
|
+
color: 16777215,
|
|
5157
|
+
map: texture,
|
|
5158
|
+
emissive: 6976118,
|
|
5159
|
+
emissiveMap: texture,
|
|
5160
|
+
transparent: true,
|
|
5161
|
+
roughness: 0.25,
|
|
5162
|
+
metalness: 0.1,
|
|
5163
|
+
side: THREE.DoubleSide,
|
|
5164
|
+
depthWrite: false
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
4965
5167
|
function installGlassDamage(kit) {
|
|
4966
5168
|
const THREE = kit.THREE;
|
|
4967
5169
|
const V3 = THREE.Vector3;
|
|
4968
5170
|
const PI = Math.PI;
|
|
4969
5171
|
const I = kit._;
|
|
4970
5172
|
const document2 = kit.document;
|
|
4971
|
-
const {
|
|
4972
|
-
function glassUV(msh, pLocal) {
|
|
5173
|
+
const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
|
|
5174
|
+
function glassUV(c, msh, pLocal) {
|
|
4973
5175
|
const pos = msh.geometry.attributes.position;
|
|
4974
5176
|
const uv = msh.geometry.attributes.uv;
|
|
4975
5177
|
if (!uv) return [0.5, 0.5];
|
|
4976
|
-
|
|
4977
|
-
const q = pLocal.clone()
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
const
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5178
|
+
c.root.updateMatrixWorld(true);
|
|
5179
|
+
const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
|
|
5180
|
+
const idx = msh.geometry.index;
|
|
5181
|
+
const tri = new THREE.Triangle();
|
|
5182
|
+
const near = new V3();
|
|
5183
|
+
const bary = new V3();
|
|
5184
|
+
let best = Infinity;
|
|
5185
|
+
const out = [0.5, 0.5];
|
|
5186
|
+
const count = idx ? idx.count : pos.count;
|
|
5187
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
5188
|
+
const ia = idx ? idx.getX(f) : f;
|
|
5189
|
+
const ib = idx ? idx.getX(f + 1) : f + 1;
|
|
5190
|
+
const ic = idx ? idx.getX(f + 2) : f + 2;
|
|
5191
|
+
tri.a.fromBufferAttribute(pos, ia);
|
|
5192
|
+
tri.b.fromBufferAttribute(pos, ib);
|
|
5193
|
+
tri.c.fromBufferAttribute(pos, ic);
|
|
5194
|
+
tri.closestPointToPoint(q, near);
|
|
5195
|
+
const d = near.distanceToSquared(q);
|
|
5196
|
+
if (d >= best) continue;
|
|
5197
|
+
best = d;
|
|
5198
|
+
tri.getBarycoord(near, bary);
|
|
5199
|
+
out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
|
|
5200
|
+
out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
|
|
4988
5201
|
}
|
|
4989
|
-
return
|
|
5202
|
+
return out;
|
|
4990
5203
|
}
|
|
4991
5204
|
function drawWeb(x, cx, cy, s, ax, rnd) {
|
|
4992
5205
|
const n = 16 + Math.floor(rnd() * 8);
|
|
@@ -5063,19 +5276,9 @@ function installGlassDamage(kit) {
|
|
|
5063
5276
|
x0.fillStyle = "rgba(48,66,72,0.5)";
|
|
5064
5277
|
x0.fillRect(0, 0, 256, 256);
|
|
5065
5278
|
G.tex = new THREE.CanvasTexture(G.cv);
|
|
5066
|
-
msh.material =
|
|
5067
|
-
color: 16777215,
|
|
5068
|
-
map: G.tex,
|
|
5069
|
-
emissive: 6976118,
|
|
5070
|
-
emissiveMap: G.tex,
|
|
5071
|
-
transparent: true,
|
|
5072
|
-
roughness: 0.25,
|
|
5073
|
-
metalness: 0.1,
|
|
5074
|
-
side: DOUBLE_SIDE,
|
|
5075
|
-
depthWrite: false
|
|
5076
|
-
});
|
|
5279
|
+
msh.material = crackedGlassMaterial(THREE, G.tex);
|
|
5077
5280
|
}
|
|
5078
|
-
const uv = glassUV(msh, p);
|
|
5281
|
+
const uv = glassUV(c, msh, p);
|
|
5079
5282
|
const x = G.cv.getContext("2d");
|
|
5080
5283
|
drawWeb(x, uv[0] * 256, (1 - uv[1]) * 256, sc != null ? sc : 0.6 + force * 0.5, Math.max(0.4, G.aspect || 1), c.rand);
|
|
5081
5284
|
G.tex.needsUpdate = true;
|
|
@@ -5220,6 +5423,13 @@ function installGlassDamage(kit) {
|
|
|
5220
5423
|
}
|
|
5221
5424
|
const seatGeo = [];
|
|
5222
5425
|
const d = dir.clone().normalize();
|
|
5426
|
+
if (msh.parent && msh.parent !== c.vehicle) {
|
|
5427
|
+
c.root.updateMatrixWorld(true);
|
|
5428
|
+
const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
|
|
5429
|
+
cubesAt.forEach(function(q) {
|
|
5430
|
+
q.applyMatrix4(toVehicle);
|
|
5431
|
+
});
|
|
5432
|
+
}
|
|
5223
5433
|
cubesAt.forEach(function(p, i) {
|
|
5224
5434
|
if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
|
|
5225
5435
|
let best = null;
|
|
@@ -5422,6 +5632,7 @@ function installImpact(kit) {
|
|
|
5422
5632
|
polygonOffsetUnits: -2
|
|
5423
5633
|
});
|
|
5424
5634
|
const _ray = new THREE.Raycaster();
|
|
5635
|
+
_ray.layers.enableAll();
|
|
5425
5636
|
function findSurface(c, p, dir) {
|
|
5426
5637
|
c.root.updateMatrixWorld(true);
|
|
5427
5638
|
const Mw = c.vehicle.matrixWorld;
|
|
@@ -5503,6 +5714,7 @@ function installImpact(kit) {
|
|
|
5503
5714
|
const v0 = new V3();
|
|
5504
5715
|
const w = new V3();
|
|
5505
5716
|
const sc = new V3();
|
|
5717
|
+
const local = new V3();
|
|
5506
5718
|
const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
|
|
5507
5719
|
c.deform.forEach(function(msh) {
|
|
5508
5720
|
if (!msh.parent || !msh.visible) return;
|
|
@@ -5517,7 +5729,15 @@ function installImpact(kit) {
|
|
|
5517
5729
|
irel.copy(rel).invert();
|
|
5518
5730
|
if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
|
|
5519
5731
|
const p0 = g.userData.p0;
|
|
5732
|
+
local.copy(p).applyMatrix4(irel);
|
|
5733
|
+
const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
|
|
5734
|
+
const reach2 = reach * reach;
|
|
5735
|
+
const arr = pos.array;
|
|
5520
5736
|
for (let i = 0; i < pos.count; i++) {
|
|
5737
|
+
const dx = arr[3 * i] - local.x;
|
|
5738
|
+
const dy = arr[3 * i + 1] - local.y;
|
|
5739
|
+
const dz = arr[3 * i + 2] - local.z;
|
|
5740
|
+
if (dx * dx + dy * dy + dz * dz >= reach2) continue;
|
|
5521
5741
|
v.fromBufferAttribute(pos, i).applyMatrix4(rel);
|
|
5522
5742
|
const a = dentField(v, H);
|
|
5523
5743
|
if (a <= 0) continue;
|
|
@@ -5533,10 +5753,39 @@ function installImpact(kit) {
|
|
|
5533
5753
|
}
|
|
5534
5754
|
if (changed) {
|
|
5535
5755
|
pos.needsUpdate = true;
|
|
5536
|
-
if (g.userData.crease)
|
|
5756
|
+
if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
|
|
5757
|
+
else if (g.userData.crease) crease(g, 42);
|
|
5537
5758
|
else g.computeVertexNormals();
|
|
5538
|
-
g.
|
|
5539
|
-
g.
|
|
5759
|
+
const sphere = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
|
|
5760
|
+
if (pos.isInterleavedBufferAttribute || pos.normalized || Object.keys(g.morphAttributes).length > 0) {
|
|
5761
|
+
g.computeBoundingBox();
|
|
5762
|
+
g.boundingBox.getCenter(sphere.center);
|
|
5763
|
+
let far2 = 0;
|
|
5764
|
+
for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
|
|
5765
|
+
sphere.radius = Math.sqrt(far2);
|
|
5766
|
+
} else {
|
|
5767
|
+
const box2 = g.boundingBox ?? (g.boundingBox = new THREE.Box3());
|
|
5768
|
+
let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
5769
|
+
for (let i = 0; i < pos.count * 3; i += 3) {
|
|
5770
|
+
const x = arr[i], y = arr[i + 1], z = arr[i + 2];
|
|
5771
|
+
if (x < x0) x0 = x;
|
|
5772
|
+
if (y < y0) y0 = y;
|
|
5773
|
+
if (z < z0) z0 = z;
|
|
5774
|
+
if (x > x1) x1 = x;
|
|
5775
|
+
if (y > y1) y1 = y;
|
|
5776
|
+
if (z > z1) z1 = z;
|
|
5777
|
+
}
|
|
5778
|
+
box2.min.set(x0, y0, z0);
|
|
5779
|
+
box2.max.set(x1, y1, z1);
|
|
5780
|
+
box2.getCenter(sphere.center);
|
|
5781
|
+
const cx = sphere.center.x, cy = sphere.center.y, cz = sphere.center.z;
|
|
5782
|
+
let far2 = 0;
|
|
5783
|
+
for (let i = 0; i < pos.count * 3; i += 3) {
|
|
5784
|
+
const dx = cx - arr[i], dy = cy - arr[i + 1], dz = cz - arr[i + 2];
|
|
5785
|
+
far2 = Math.max(far2, dx * dx + dy * dy + dz * dz);
|
|
5786
|
+
}
|
|
5787
|
+
sphere.radius = Math.sqrt(far2);
|
|
5788
|
+
}
|
|
5540
5789
|
}
|
|
5541
5790
|
});
|
|
5542
5791
|
const dset = new Set(c.deform);
|
|
@@ -5620,13 +5869,14 @@ function installImpact(kit) {
|
|
|
5620
5869
|
kit.impact = impact;
|
|
5621
5870
|
kit.partEffects = {};
|
|
5622
5871
|
const _pr = new THREE.Raycaster();
|
|
5872
|
+
_pr.layers.enableAll();
|
|
5623
5873
|
function probe(c, o, d, far) {
|
|
5624
5874
|
c.root.updateMatrixWorld(true);
|
|
5625
5875
|
const Mw = c.vehicle.matrixWorld;
|
|
5626
5876
|
const inv = new THREE.Matrix4().copy(Mw).invert();
|
|
5627
5877
|
_pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
|
|
5628
5878
|
_pr.far = far || 10;
|
|
5629
|
-
const hits = _pr
|
|
5879
|
+
const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
|
|
5630
5880
|
for (let i = 0; i < hits.length; i++) {
|
|
5631
5881
|
const h = hits[i];
|
|
5632
5882
|
const ob = h.object;
|
|
@@ -5939,7 +6189,8 @@ function installExplosion(kit) {
|
|
|
5939
6189
|
const G = gm2.userData.glass;
|
|
5940
6190
|
if (!G || G.broken || !gm2.visible || !gm2.parent) return;
|
|
5941
6191
|
if (G.type === "laminated") {
|
|
5942
|
-
|
|
6192
|
+
for (let k2 = 0; k2 < 3; k2++) I.crackGlass(c, gm2, I.glassPt ? I.glassPt(c, gm2, c.rand) : gm2.position.clone(), 1.2);
|
|
6193
|
+
return;
|
|
5943
6194
|
}
|
|
5944
6195
|
I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
|
|
5945
6196
|
});
|
|
@@ -5950,19 +6201,6 @@ function installExplosion(kit) {
|
|
|
5950
6201
|
fling(c, c.hood.piv, new V3(rv(1, 3), rv(6, 8.5), rv(-1.5, 1.5)), new V3(rv(-4, 4), rv(-2, 2), rv(3, 7)));
|
|
5951
6202
|
c.hood = null;
|
|
5952
6203
|
}
|
|
5953
|
-
const W = m.width || 1.6;
|
|
5954
|
-
const gh = m.glasshouse;
|
|
5955
|
-
if (m.kind === "car" && gh) {
|
|
5956
|
-
;
|
|
5957
|
-
[1, -1].forEach(function(sd) {
|
|
5958
|
-
const dh = Math.max(0.35, (m.belt || 1) - (m.clearance || 0.3) - 0.08);
|
|
5959
|
-
const dx = gh.fb - 0.55;
|
|
5960
|
-
const door = c.roundBox(0.95, dh, 0.045, 0.03, c.paint(m.color || "#777"), dx, (m.clearance || 0.3) + dh / 2 + 0.04, sd * (W / 2 + 0.01));
|
|
5961
|
-
const hole = c.roundBox(0.9, dh * 0.95, 0.02, 0.02, MATERIALS.dark, dx, (m.clearance || 0.3) + dh / 2 + 0.04, sd * (W / 2 - 0.01));
|
|
5962
|
-
hole.userData.noFollow = true;
|
|
5963
|
-
fling(c, door, c.dirToEffects(new V3(rv(-1, 1), rv(3.5, 6), sd * rv(4, 6.5))), new V3(rv(-5, 5), rv(-4, 4), rv(-5, 5)));
|
|
5964
|
-
});
|
|
5965
|
-
}
|
|
5966
6204
|
if (m.rearBumper && m.rearBumper.parent)
|
|
5967
6205
|
fling(
|
|
5968
6206
|
c,
|
|
@@ -5970,20 +6208,6 @@ function installExplosion(kit) {
|
|
|
5970
6208
|
c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
|
|
5971
6209
|
new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
|
|
5972
6210
|
);
|
|
5973
|
-
if (c.wheels.length) {
|
|
5974
|
-
const wi = Math.floor(c.rand() * c.wheels.length);
|
|
5975
|
-
const wh = c.wheels[wi];
|
|
5976
|
-
c.wheels.splice(wi, 1);
|
|
5977
|
-
c.parts.forEach(function(p) {
|
|
5978
|
-
if (p.obj === wh) p.done = true;
|
|
5979
|
-
});
|
|
5980
|
-
fling(
|
|
5981
|
-
c,
|
|
5982
|
-
wh,
|
|
5983
|
-
c.dirToEffects(new V3(rv(-2, 2), rv(5, 7.5), (wh.position.z >= 0 ? 1 : -1) * rv(3, 5))),
|
|
5984
|
-
new V3(rv(-8, 8), rv(-3, 3), rv(-8, 8))
|
|
5985
|
-
);
|
|
5986
|
-
}
|
|
5987
6211
|
burnAll(c);
|
|
5988
6212
|
c.emitters = c.emitters.filter(function(e) {
|
|
5989
6213
|
return e.kind === "exhaust" ? false : true;
|
|
@@ -6992,6 +7216,7 @@ function installZoneDecals(kit) {
|
|
|
6992
7216
|
return hole;
|
|
6993
7217
|
}
|
|
6994
7218
|
const _zr = new THREE.Raycaster();
|
|
7219
|
+
_zr.layers.enableAll();
|
|
6995
7220
|
const PROXY_MIN_TRIANGLES = 500;
|
|
6996
7221
|
const STROKE_REACH = 0.6;
|
|
6997
7222
|
const _pv = new V3();
|
|
@@ -7226,6 +7451,7 @@ function installZoneDecals(kit) {
|
|
|
7226
7451
|
return out;
|
|
7227
7452
|
}
|
|
7228
7453
|
const _rt = new THREE.Raycaster();
|
|
7454
|
+
_rt.layers.enableAll();
|
|
7229
7455
|
function scratchPath(c, s, len, ang, cands, B) {
|
|
7230
7456
|
const n = s.n.clone().normalize();
|
|
7231
7457
|
const t = Math.abs(n.y) < 0.9 ? new V3().crossVectors(new V3(0, 1, 0), n) : new V3().crossVectors(new V3(1, 0, 0), n);
|
|
@@ -10507,6 +10733,8 @@ function createVehicleKit(options) {
|
|
|
10507
10733
|
V3: THREE.Vector3,
|
|
10508
10734
|
PI: Math.PI,
|
|
10509
10735
|
document: doc,
|
|
10736
|
+
particleMaterials: opts.particleMaterials,
|
|
10737
|
+
effects: opts.effects,
|
|
10510
10738
|
strings,
|
|
10511
10739
|
t,
|
|
10512
10740
|
matchers: createMatchers(strings),
|
|
@@ -10762,7 +10990,9 @@ function createVehicleKit(options) {
|
|
|
10762
10990
|
assignDeep(strings, resolveStrings(opts.locale));
|
|
10763
10991
|
return kit;
|
|
10764
10992
|
};
|
|
10765
|
-
const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
|
|
10993
|
+
const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
|
|
10994
|
+
void _particleMaterials;
|
|
10995
|
+
void _effects;
|
|
10766
10996
|
void _three;
|
|
10767
10997
|
void _lod;
|
|
10768
10998
|
void _locale;
|
|
@@ -10772,6 +11002,17 @@ function createVehicleKit(options) {
|
|
|
10772
11002
|
return kit;
|
|
10773
11003
|
}
|
|
10774
11004
|
|
|
11005
|
+
// src/destructible/armour.ts
|
|
11006
|
+
var ARMOUR_LEVELS = {
|
|
11007
|
+
IIIA: { glassStops: 1700, glassHp: 6e3, glassTakes: 0.25, steel: 8 },
|
|
11008
|
+
III: { glassStops: 3600, glassHp: 9e3, glassTakes: 0.25, steel: 30 }
|
|
11009
|
+
};
|
|
11010
|
+
function armourLevelOf(meta, option) {
|
|
11011
|
+
if (option !== void 0) return option;
|
|
11012
|
+
if (meta.armor === "IIIA" || meta.armor === "III") return meta.armor;
|
|
11013
|
+
return meta.armored || meta.bullet ? "III" : null;
|
|
11014
|
+
}
|
|
11015
|
+
|
|
10775
11016
|
// src/destructible/impl.ts
|
|
10776
11017
|
import { ignitionChance, resolveIgnitionRules, sparkTargetFactor } from "@adatechnology/game-contracts";
|
|
10777
11018
|
|
|
@@ -10818,6 +11059,12 @@ var boxOf = (z) => ({
|
|
|
10818
11059
|
center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
|
|
10819
11060
|
size: [Math.max(0.02, z.box.x1 - z.box.x0), Math.max(0.02, z.box.y1 - z.box.y0), Math.max(0.02, z.box.z1 - z.box.z0)]
|
|
10820
11061
|
});
|
|
11062
|
+
var PART_FLUIDS = {
|
|
11063
|
+
gearbox: { fluid: "transmission_fluid", liters: 6 },
|
|
11064
|
+
brakes: { fluid: "brake_fluid", liters: 0.6 },
|
|
11065
|
+
steering: { fluid: "power_steering_fluid", liters: 1 },
|
|
11066
|
+
battery: { fluid: "battery_acid", liters: 3 }
|
|
11067
|
+
};
|
|
10821
11068
|
var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
|
|
10822
11069
|
var isFender = (id) => /^fender/.test(id);
|
|
10823
11070
|
var roleOfZone = (z) => {
|
|
@@ -10861,13 +11108,22 @@ var floorPan = (b) => ({
|
|
|
10861
11108
|
center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
|
|
10862
11109
|
size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
|
|
10863
11110
|
});
|
|
11111
|
+
function paneBox(c, pane, inverse) {
|
|
11112
|
+
pane.geometry.computeBoundingBox();
|
|
11113
|
+
const box = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
|
|
11114
|
+
box.applyMatrix4(inverse);
|
|
11115
|
+
const center = [(box.min.x + box.max.x) / 2, (box.min.y + box.max.y) / 2, (box.min.z + box.max.z) / 2];
|
|
11116
|
+
const size = [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
|
|
11117
|
+
return { center, size };
|
|
11118
|
+
}
|
|
10864
11119
|
function buildVehicleModel(kit, c, options = {}) {
|
|
10865
11120
|
const kind = kindOf(c);
|
|
10866
11121
|
const m = c.meta;
|
|
10867
11122
|
const spec = c.spec;
|
|
10868
11123
|
const t = kit.t;
|
|
10869
11124
|
const skin = skinOf(kind);
|
|
10870
|
-
const
|
|
11125
|
+
const armoured = options.armour ?? null;
|
|
11126
|
+
const armour = armoured ? 12 : 1;
|
|
10871
11127
|
const mass = vehicleMass(c);
|
|
10872
11128
|
const W = m.width ?? 1.7;
|
|
10873
11129
|
const F = m.front ?? 2;
|
|
@@ -10933,8 +11189,8 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
10933
11189
|
}
|
|
10934
11190
|
if (role === "glass") {
|
|
10935
11191
|
const glass = (z.glass ?? [])[0];
|
|
10936
|
-
const laminated = glass?.userData?.glass?.type === "laminated" ||
|
|
10937
|
-
add({ id, role, name: label, material: "glass", box, mass: 10 * area, hp: laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
|
|
11192
|
+
const laminated = glass?.userData?.glass?.type === "laminated" || !!armoured;
|
|
11193
|
+
add({ id, role, name: label, material: "glass", box, mass: 10 * area, hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
|
|
10938
11194
|
continue;
|
|
10939
11195
|
}
|
|
10940
11196
|
if (role === "light") {
|
|
@@ -10972,7 +11228,7 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
10972
11228
|
zoneId: id,
|
|
10973
11229
|
visual: false,
|
|
10974
11230
|
contains: { fluid: diesel ? "diesel" : "fuel", liters },
|
|
10975
|
-
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
|
|
11231
|
+
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
|
|
10976
11232
|
ignitionC: diesel ? 330 : 260,
|
|
10977
11233
|
tags: ["fuel_tank", ...tags]
|
|
10978
11234
|
});
|
|
@@ -11001,6 +11257,16 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
11001
11257
|
tags: [role, ...tags]
|
|
11002
11258
|
});
|
|
11003
11259
|
}
|
|
11260
|
+
const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
|
|
11261
|
+
c.root.updateMatrixWorld(true);
|
|
11262
|
+
const inverse = c.vehicle.matrixWorld.clone().invert();
|
|
11263
|
+
for (const pane of c.glass ?? []) {
|
|
11264
|
+
if (claimed.has(pane)) continue;
|
|
11265
|
+
const G = pane.userData.glass;
|
|
11266
|
+
const bb = paneBox(c, pane, inverse);
|
|
11267
|
+
const laminated = G?.type === "laminated" || !!armoured;
|
|
11268
|
+
add({ id: `pane_${G?.key ?? "glass"}`, role: "glass", name: String(G?.label ?? G?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
|
|
11269
|
+
}
|
|
11004
11270
|
if (!hubZone) {
|
|
11005
11271
|
const h = Math.max(0.15, belt - clearance);
|
|
11006
11272
|
add({
|
|
@@ -11055,7 +11321,7 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
11055
11321
|
visual: false,
|
|
11056
11322
|
mechanical: true,
|
|
11057
11323
|
contains: { fluid: diesel ? "diesel" : "fuel", liters },
|
|
11058
|
-
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
|
|
11324
|
+
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
|
|
11059
11325
|
ignitionC: diesel ? 330 : 260,
|
|
11060
11326
|
tags: ["fuel_tank"]
|
|
11061
11327
|
});
|
|
@@ -11332,10 +11598,163 @@ function buildVehicleModel(kit, c, options = {}) {
|
|
|
11332
11598
|
}
|
|
11333
11599
|
}
|
|
11334
11600
|
void has;
|
|
11601
|
+
for (const part of parts) {
|
|
11602
|
+
const fluid = PART_FLUIDS[part.role];
|
|
11603
|
+
if (fluid && !part.contains && !(kind === "bike" && part.role === "steering")) part.contains = { ...fluid };
|
|
11604
|
+
}
|
|
11335
11605
|
return { parts, joints, hub: hubId, hitch };
|
|
11336
11606
|
}
|
|
11337
11607
|
|
|
11608
|
+
// src/destructible/subdivide-near.ts
|
|
11609
|
+
var SUBDIVIDE_DEPTH = 12;
|
|
11610
|
+
function subdivideNear(THREE, params) {
|
|
11611
|
+
const { mesh, maxLen, near } = params;
|
|
11612
|
+
const src = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
|
|
11613
|
+
const a = src.attributes.position.array;
|
|
11614
|
+
const toWorld = mesh.matrixWorld;
|
|
11615
|
+
const out = [];
|
|
11616
|
+
const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
|
|
11617
|
+
const split = (A, B, C, depth) => {
|
|
11618
|
+
const ab = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
|
|
11619
|
+
const bc = Math.hypot(B[0] - C[0], B[1] - C[1], B[2] - C[2]);
|
|
11620
|
+
const ca = Math.hypot(C[0] - A[0], C[1] - A[1], C[2] - A[2]);
|
|
11621
|
+
const longest = Math.max(ab, bc, ca);
|
|
11622
|
+
if (longest <= maxLen || depth > SUBDIVIDE_DEPTH) {
|
|
11623
|
+
out.push(...A, ...B, ...C);
|
|
11624
|
+
return;
|
|
11625
|
+
}
|
|
11626
|
+
if (longest === ab) {
|
|
11627
|
+
const D = mid(A, B);
|
|
11628
|
+
split(A, D, C, depth + 1);
|
|
11629
|
+
split(D, B, C, depth + 1);
|
|
11630
|
+
} else if (longest === bc) {
|
|
11631
|
+
const E = mid(B, C);
|
|
11632
|
+
split(A, B, E, depth + 1);
|
|
11633
|
+
split(A, E, C, depth + 1);
|
|
11634
|
+
} else {
|
|
11635
|
+
const F = mid(C, A);
|
|
11636
|
+
split(A, B, F, depth + 1);
|
|
11637
|
+
split(F, B, C, depth + 1);
|
|
11638
|
+
}
|
|
11639
|
+
};
|
|
11640
|
+
for (let i = 0; i < a.length; i += 9) {
|
|
11641
|
+
const A = [a[i], a[i + 1], a[i + 2]];
|
|
11642
|
+
const B = [a[i + 3], a[i + 4], a[i + 5]];
|
|
11643
|
+
const C = [a[i + 6], a[i + 7], a[i + 8]];
|
|
11644
|
+
const corners = [A, B, C].map((p) => new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(toWorld));
|
|
11645
|
+
if (near(corners)) split(A, B, C, 0);
|
|
11646
|
+
else out.push(...A, ...B, ...C);
|
|
11647
|
+
}
|
|
11648
|
+
const g = new THREE.BufferGeometry();
|
|
11649
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
|
|
11650
|
+
mesh.geometry.dispose();
|
|
11651
|
+
mesh.geometry = g;
|
|
11652
|
+
}
|
|
11653
|
+
|
|
11338
11654
|
// src/destructible/visual.ts
|
|
11655
|
+
var HOLE_DECAL_SCALE = 2.4;
|
|
11656
|
+
var DOOR_DEPTH = 0.07;
|
|
11657
|
+
var DOOR_EDGE_PIECES = 8;
|
|
11658
|
+
var DOOR_EDGE_REACH = 0.08;
|
|
11659
|
+
var DOOR_CARD_COLOR = "#3b3e42";
|
|
11660
|
+
var DOOR_LINER_COLOR = "#5a5e64";
|
|
11661
|
+
var DOOR_LINER_DARK = 0.15;
|
|
11662
|
+
var DOOR_STRIP_THIN = 0.08;
|
|
11663
|
+
var DOOR_STRIP_NEAR = 0.04;
|
|
11664
|
+
var DOOR_STRIP_TALL = 0.22;
|
|
11665
|
+
var STRIP_SMOOTH_DEG = 180;
|
|
11666
|
+
var DOOR_STRIP_VERTICES = 2e4;
|
|
11667
|
+
var DOOR_SILL = 0.25;
|
|
11668
|
+
var DOOR_STRIP_OVER = 0.3;
|
|
11669
|
+
var DOOR_TOP_OVER_PANE = 0.03;
|
|
11670
|
+
var HOLE_TEXTURE_SIZE = 64;
|
|
11671
|
+
var LAMINATED_HP = 1e3;
|
|
11672
|
+
var HOOD_SINK = 2e-3;
|
|
11673
|
+
var HOOD_GAP_CLOSED = 0.016;
|
|
11674
|
+
var DOOR_HANDLE_INSET = 0.12;
|
|
11675
|
+
var DOOR_HANDLE_MIN = 0.07;
|
|
11676
|
+
var DOOR_HANDLE_MAX = 0.24;
|
|
11677
|
+
var DOOR_HANDLE_THIN = 0.07;
|
|
11678
|
+
var DOOR_HANDLE_OUTSIDE = 0.15;
|
|
11679
|
+
var PANE_SEAT_OUT = 0.05;
|
|
11680
|
+
var PANE_SEAT_IN = -0.05;
|
|
11681
|
+
var PANE_SEAT_REACH = 0.14;
|
|
11682
|
+
var PANE_SEAT_SAMPLES = 24;
|
|
11683
|
+
var PANE_SEAT_LIFT = 4e-3;
|
|
11684
|
+
var PANE_FRAME_NEAR = 0.07;
|
|
11685
|
+
var PANE_MIN_SLOPE = 0.01;
|
|
11686
|
+
var PANE_EDGE_BAND = 0.03;
|
|
11687
|
+
var PANE_REACH_STEP = 0.01;
|
|
11688
|
+
var PANE_REACH_MAX = 0.25;
|
|
11689
|
+
var COWL_UNDER_GLASS = 0;
|
|
11690
|
+
var COWL_NEAR_PANE = 0.08;
|
|
11691
|
+
var COWL_PLATE = 0.06;
|
|
11692
|
+
var COWL_DASH = 0.5;
|
|
11693
|
+
var COWL_DEEP = 0.3;
|
|
11694
|
+
var WINDOW_FRAME_NEAR = 0.06;
|
|
11695
|
+
var WINDOW_FRAME_SPAN = 0.6;
|
|
11696
|
+
var DASHBOARD_COLOR = "#121314";
|
|
11697
|
+
var COWL_CUT_SUB = 0.1;
|
|
11698
|
+
var COWL_CUT_MARGIN = 0.08;
|
|
11699
|
+
var SKIN_UNDER_PANE = 0.025;
|
|
11700
|
+
var SKIN_EDGE_KEPT = 0.06;
|
|
11701
|
+
var SIDE_PANE_FACING = 0.6;
|
|
11702
|
+
var COWL_LONG = 0.8;
|
|
11703
|
+
var PANE_BAR_VERTICES = 48;
|
|
11704
|
+
var PANE_BAR_LENGTH = 0.3;
|
|
11705
|
+
var PANE_BORDER = 0.03;
|
|
11706
|
+
var PANE_BORDER_LIFT = 3e-3;
|
|
11707
|
+
var DRAWN_MIRROR_SIZE = 0.3;
|
|
11708
|
+
var DRAWN_MIRROR_REACH = 0.4;
|
|
11709
|
+
var DRAWN_MIRROR_MOUNT = 0.2;
|
|
11710
|
+
var MIRROR_GLASS_FIT = 0.76;
|
|
11711
|
+
var MIRROR_GLASS_PROUD = 1e-3;
|
|
11712
|
+
var MIRROR_FACE_DEPTH = 0.012;
|
|
11713
|
+
var MIRROR_GLASS_THIN = 0.04;
|
|
11714
|
+
var DEFAULT_WHEEL_RADIUS = 0.3;
|
|
11715
|
+
var SIDEWALL_SHARE = 0.3;
|
|
11716
|
+
var MIRROR_GLASS_MIN = 0.05;
|
|
11717
|
+
var MIRROR_GLASS_COLOR = "#59636a";
|
|
11718
|
+
var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
|
|
11719
|
+
var holeMaterials = /* @__PURE__ */ new WeakMap();
|
|
11720
|
+
var holeDisc = null;
|
|
11721
|
+
function holeDiscGeometry(THREE) {
|
|
11722
|
+
holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
|
|
11723
|
+
return holeDisc;
|
|
11724
|
+
}
|
|
11725
|
+
function holeMaterial(kit, material) {
|
|
11726
|
+
const key = kit;
|
|
11727
|
+
const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
|
|
11728
|
+
holeMaterials.set(key, cache2);
|
|
11729
|
+
const known = cache2.get(material);
|
|
11730
|
+
if (known) return known;
|
|
11731
|
+
const I = kit._;
|
|
11732
|
+
const THREE = kit.THREE;
|
|
11733
|
+
const canvas = kit.document?.createElement?.("canvas");
|
|
11734
|
+
const x = canvas?.getContext?.("2d");
|
|
11735
|
+
let made = I.MATERIALS.dark;
|
|
11736
|
+
if (x) {
|
|
11737
|
+
canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
|
|
11738
|
+
const mid = HOLE_TEXTURE_SIZE / 2;
|
|
11739
|
+
x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
|
|
11740
|
+
x.beginPath();
|
|
11741
|
+
for (let i = 0; i < 18; i++) {
|
|
11742
|
+
const a = i / 18 * Math.PI * 2;
|
|
11743
|
+
const r = mid * (i % 2 ? 0.98 : 0.76);
|
|
11744
|
+
x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
|
|
11745
|
+
}
|
|
11746
|
+
x.closePath();
|
|
11747
|
+
x.fill();
|
|
11748
|
+
x.fillStyle = "#050505";
|
|
11749
|
+
x.beginPath();
|
|
11750
|
+
x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
|
|
11751
|
+
x.fill();
|
|
11752
|
+
const map = new THREE.CanvasTexture(canvas);
|
|
11753
|
+
made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
|
|
11754
|
+
}
|
|
11755
|
+
cache2.set(material, made);
|
|
11756
|
+
return made;
|
|
11757
|
+
}
|
|
11339
11758
|
var VehicleVisuals = class {
|
|
11340
11759
|
constructor(kit, c, model) {
|
|
11341
11760
|
__publicField(this, "kit");
|
|
@@ -11353,14 +11772,23 @@ var VehicleVisuals = class {
|
|
|
11353
11772
|
__publicField(this, "syncPlates", /* @__PURE__ */ new Map());
|
|
11354
11773
|
/** Doors and lids whose own skin was cut from the body and hung on the hinge (true), or that had none to take and keep the plate (false). */
|
|
11355
11774
|
__publicField(this, "skins", /* @__PURE__ */ new Map());
|
|
11775
|
+
/** How high each door's cut reaches. */
|
|
11776
|
+
__publicField(this, "doorTops", /* @__PURE__ */ new Map());
|
|
11356
11777
|
__publicField(this, "lightsOn", true);
|
|
11357
11778
|
/** Heavy body meshes whose triangle index is still to build (null once done). */
|
|
11358
11779
|
__publicField(this, "warming");
|
|
11359
11780
|
/** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
|
|
11360
11781
|
__publicField(this, "tornThisFrame", false);
|
|
11782
|
+
/** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
|
|
11783
|
+
__publicField(this, "queued", false);
|
|
11784
|
+
/** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
|
|
11785
|
+
__publicField(this, "holdArmed", false);
|
|
11786
|
+
__publicField(this, "holdFrame", -1);
|
|
11361
11787
|
/** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
|
|
11362
11788
|
__publicField(this, "smoothing", /* @__PURE__ */ new Map());
|
|
11363
11789
|
__publicField(this, "hood");
|
|
11790
|
+
;
|
|
11791
|
+
c.smoothLater = (mesh) => this.smoothLater(mesh);
|
|
11364
11792
|
this.kit = kit;
|
|
11365
11793
|
this.c = c;
|
|
11366
11794
|
this.model = model;
|
|
@@ -11369,6 +11797,9 @@ var VehicleVisuals = class {
|
|
|
11369
11797
|
for (const p of model.parts) this.byId.set(p.id, p);
|
|
11370
11798
|
c.root.updateMatrixWorld(true);
|
|
11371
11799
|
this.hood = this.nameHood();
|
|
11800
|
+
this.seatPanes();
|
|
11801
|
+
this.seatDoorHandles();
|
|
11802
|
+
this.seatHood();
|
|
11372
11803
|
this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
|
|
11373
11804
|
if (!this.warming.length) this.warming = null;
|
|
11374
11805
|
for (const p of model.parts) {
|
|
@@ -11389,6 +11820,7 @@ var VehicleVisuals = class {
|
|
|
11389
11820
|
const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
|
|
11390
11821
|
if (!id) return null;
|
|
11391
11822
|
c.hood.piv.name = id;
|
|
11823
|
+
c.hood.piv.userData.plateOf = id;
|
|
11392
11824
|
return { id, open: c.hood.open, sign: c.hood.sign || 1 };
|
|
11393
11825
|
}
|
|
11394
11826
|
mirrorObjects(p) {
|
|
@@ -11476,6 +11908,7 @@ var VehicleVisuals = class {
|
|
|
11476
11908
|
recess.userData.noFollow = true;
|
|
11477
11909
|
const piv = new THREE.Group();
|
|
11478
11910
|
piv.name = p.id;
|
|
11911
|
+
piv.userData.plateOf = p.id;
|
|
11479
11912
|
piv.position.set(hx, cy, sk + s * 0.016);
|
|
11480
11913
|
piv.userData.rigid = true;
|
|
11481
11914
|
this.c.vehicle.add(piv);
|
|
@@ -11492,6 +11925,7 @@ var VehicleVisuals = class {
|
|
|
11492
11925
|
recess.userData.noFollow = true;
|
|
11493
11926
|
const piv = new THREE.Group();
|
|
11494
11927
|
piv.name = p.id;
|
|
11928
|
+
piv.userData.plateOf = p.id;
|
|
11495
11929
|
piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
|
|
11496
11930
|
piv.userData.rigid = true;
|
|
11497
11931
|
this.c.vehicle.add(piv);
|
|
@@ -11555,10 +11989,20 @@ var VehicleVisuals = class {
|
|
|
11555
11989
|
try {
|
|
11556
11990
|
if (p.role === "door") {
|
|
11557
11991
|
const dirty = /* @__PURE__ */ new Set();
|
|
11558
|
-
const
|
|
11559
|
-
const
|
|
11992
|
+
const glassBox = new this.THREE.Box3();
|
|
11993
|
+
for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
|
|
11994
|
+
const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
|
|
11995
|
+
this.doorTops.set(id, top);
|
|
11996
|
+
const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
|
|
11997
|
+
const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
|
|
11560
11998
|
this.mend(dirty);
|
|
11561
|
-
if (pcs.length)
|
|
11999
|
+
if (pcs.length) {
|
|
12000
|
+
const panes = I.windowsOver(this.c, z, inv);
|
|
12001
|
+
const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
|
|
12002
|
+
const mirrors = this.mirrorsOn(z);
|
|
12003
|
+
const frames = this.framesAround(panes, inv);
|
|
12004
|
+
objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
|
|
12005
|
+
}
|
|
11562
12006
|
} else {
|
|
11563
12007
|
const pcs = this.cutZone(z);
|
|
11564
12008
|
if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
|
|
@@ -11576,10 +12020,152 @@ var VehicleVisuals = class {
|
|
|
11576
12020
|
const di = deform.indexOf(o);
|
|
11577
12021
|
if (di >= 0) deform.splice(di, 1);
|
|
11578
12022
|
}
|
|
12023
|
+
if (p.role === "door" && objs.length > 0) {
|
|
12024
|
+
this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
|
|
12025
|
+
this.liftDoorTrim(objs, z);
|
|
12026
|
+
this.cutStripsAcross(piv, z, inv);
|
|
12027
|
+
}
|
|
11579
12028
|
piv.quaternion.copy(q);
|
|
11580
12029
|
this.c.root.updateMatrixWorld(true);
|
|
11581
12030
|
return objs.length > 0;
|
|
11582
12031
|
}
|
|
12032
|
+
/** A door has edges: a band that follows the skin all round closes it to its card and hides where the cut followed the triangles of the shell; the same band, in the same paint as on a real car, lines the opening. */
|
|
12033
|
+
doorEdges(piv, z, y1) {
|
|
12034
|
+
const THREE = this.THREE;
|
|
12035
|
+
const B = z.box;
|
|
12036
|
+
const side2 = z.side < 0 ? -1 : 1;
|
|
12037
|
+
const flat = piv.position.z - side2 * 0.016;
|
|
12038
|
+
const x0 = B.x0 + 0.03;
|
|
12039
|
+
const x1 = B.x1 - 0.03;
|
|
12040
|
+
const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
|
|
12041
|
+
const loop = [];
|
|
12042
|
+
const run = (ax, ay, bx, by) => {
|
|
12043
|
+
for (let i = 0; i < DOOR_EDGE_PIECES; i++) loop.push([ax + (bx - ax) * i / DOOR_EDGE_PIECES, ay + (by - ay) * i / DOOR_EDGE_PIECES]);
|
|
12044
|
+
};
|
|
12045
|
+
run(x0, y0, x1, y0);
|
|
12046
|
+
run(x1, y0, x1, y1);
|
|
12047
|
+
run(x1, y1, x0, y1);
|
|
12048
|
+
run(x0, y1, x0, y0);
|
|
12049
|
+
const heights = loop.map(([x, y]) => {
|
|
12050
|
+
const at = this.surface("z", side2, B, [[x, y]]);
|
|
12051
|
+
return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
|
|
12052
|
+
});
|
|
12053
|
+
const band = (lift) => {
|
|
12054
|
+
const position = [];
|
|
12055
|
+
const index = [];
|
|
12056
|
+
loop.forEach(([x, y], i) => {
|
|
12057
|
+
position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
|
|
12058
|
+
const a = i * 2;
|
|
12059
|
+
const b = (i + 1) % loop.length * 2;
|
|
12060
|
+
index.push(a, a + 1, b, b, a + 1, b + 1);
|
|
12061
|
+
});
|
|
12062
|
+
const g = new THREE.BufferGeometry();
|
|
12063
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
|
|
12064
|
+
g.setIndex(index);
|
|
12065
|
+
g.computeVertexNormals();
|
|
12066
|
+
return g;
|
|
12067
|
+
};
|
|
12068
|
+
const paint = this.paint().clone();
|
|
12069
|
+
paint.side = THREE.DoubleSide;
|
|
12070
|
+
const cardPosition = [];
|
|
12071
|
+
const mid = loop.reduce((acc, [x, y], i) => [acc[0] + x, acc[1] + y, acc[2] + heights[i]], [0, 0, 0]).map((v) => v / loop.length);
|
|
12072
|
+
const cardZ = (z2) => z2 - side2 * DOOR_DEPTH;
|
|
12073
|
+
loop.forEach(([x, y], i) => {
|
|
12074
|
+
const j = (i + 1) % loop.length;
|
|
12075
|
+
cardPosition.push(mid[0], mid[1], cardZ(mid[2]), x, y, cardZ(heights[i]), loop[j][0], loop[j][1], cardZ(heights[j]));
|
|
12076
|
+
});
|
|
12077
|
+
const cardGeometry = new THREE.BufferGeometry();
|
|
12078
|
+
cardGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cardPosition, 3));
|
|
12079
|
+
cardGeometry.computeVertexNormals();
|
|
12080
|
+
const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.85 }).clone();
|
|
12081
|
+
cardMaterial.side = THREE.DoubleSide;
|
|
12082
|
+
const card = new THREE.Mesh(cardGeometry, cardMaterial);
|
|
12083
|
+
card.userData.noFollow = true;
|
|
12084
|
+
card.castShadow = false;
|
|
12085
|
+
this.c.vehicle.add(card);
|
|
12086
|
+
piv.attach(card);
|
|
12087
|
+
const edges = new THREE.Mesh(band(2e-3), paint);
|
|
12088
|
+
const lining = new THREE.Mesh(band(2e-3), paint);
|
|
12089
|
+
for (const m of [edges, lining]) {
|
|
12090
|
+
m.userData.noFollow = true;
|
|
12091
|
+
m.castShadow = false;
|
|
12092
|
+
this.c.vehicle.add(m);
|
|
12093
|
+
}
|
|
12094
|
+
piv.attach(edges);
|
|
12095
|
+
}
|
|
12096
|
+
/** The frame the model draws round a door's window (a dark strip over the glass) goes with the door: left behind it hangs in the air over the opening. */
|
|
12097
|
+
framesAround(panes, inv) {
|
|
12098
|
+
const THREE = this.THREE;
|
|
12099
|
+
const boxes = panes.map((g) => this.I.localBox(this.c, g, inv).expandByScalar(WINDOW_FRAME_NEAR));
|
|
12100
|
+
return this.c.vehicle.children.filter((o) => {
|
|
12101
|
+
if (!o.isMesh || !o.visible || o.userData.glass || o.userData.decal || this.c.deform?.includes(o)) return false;
|
|
12102
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
12103
|
+
if (box.isEmpty()) return false;
|
|
12104
|
+
return boxes.some((b) => b.containsBox(box) && box.getSize(new THREE.Vector3()).x > b.getSize(new THREE.Vector3()).x * WINDOW_FRAME_SPAN);
|
|
12105
|
+
});
|
|
12106
|
+
}
|
|
12107
|
+
/** A front door carries the mirror drawn at its corner. */
|
|
12108
|
+
mirrorsOn(z) {
|
|
12109
|
+
if (!z.mirror) return [];
|
|
12110
|
+
const own = (z.mirrors ?? []).filter((o) => o.parent);
|
|
12111
|
+
if (own.length) return own;
|
|
12112
|
+
const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
|
|
12113
|
+
return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
|
|
12114
|
+
}
|
|
12115
|
+
/** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
|
|
12116
|
+
liftDoorTrim(objs, z) {
|
|
12117
|
+
const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
|
|
12118
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
12119
|
+
for (const o of objs) {
|
|
12120
|
+
if (!o.isMesh || o.userData.glass) continue;
|
|
12121
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
12122
|
+
const size = box.getSize(new this.THREE.Vector3());
|
|
12123
|
+
if (size.x > (z.box.x1 - z.box.x0) * 0.4 && size.y > (z.box.y1 - z.box.y0) * 0.3 && size.z < DOOR_DEPTH * 1.5 && o.material?.color?.getHSL({ h: 0, s: 0, l: 0 }).l < DOOR_LINER_DARK) o.material = card;
|
|
12124
|
+
}
|
|
12125
|
+
}
|
|
12126
|
+
/** A long strip drawn along the side of the car (the chrome of the belt): the part over the door goes with the door, and nothing stays across the opening. */
|
|
12127
|
+
cutStripsAcross(piv, z, inv) {
|
|
12128
|
+
const THREE = this.THREE;
|
|
12129
|
+
const B = z.box;
|
|
12130
|
+
const x0 = B.x0 + 0.03;
|
|
12131
|
+
const x1 = B.x1 - 0.03;
|
|
12132
|
+
const y0 = B.y0 + DOOR_SILL;
|
|
12133
|
+
const glassTop = (I) => {
|
|
12134
|
+
let top = -Infinity;
|
|
12135
|
+
for (const g of I.windowsOver(this.c, z, inv)) top = Math.max(top, I.localBox(this.c, g, inv).max.y);
|
|
12136
|
+
return top;
|
|
12137
|
+
};
|
|
12138
|
+
const y1 = Math.max(B.y1, glassTop(this.I)) + DOOR_STRIP_NEAR;
|
|
12139
|
+
const deform = this.c.deform ?? [];
|
|
12140
|
+
for (const o of [...this.c.vehicle.children]) {
|
|
12141
|
+
if (!o.isMesh || o === piv || o.userData.glass || o.userData.decal || !o.visible || deform.includes(o) && o.geometry.attributes.position.count > DOOR_STRIP_VERTICES) continue;
|
|
12142
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
12143
|
+
const size = box.getSize(new THREE.Vector3());
|
|
12144
|
+
const mid = box.getCenter(new THREE.Vector3());
|
|
12145
|
+
if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box.max.x, x1) - Math.max(box.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
|
|
12146
|
+
const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
|
|
12147
|
+
const v = new THREE.Vector3();
|
|
12148
|
+
const sdf = (x, y, zz) => {
|
|
12149
|
+
v.set(x, y, zz).applyMatrix4(toVehicle);
|
|
12150
|
+
return Math.max(x0 - v.x, v.x - x1);
|
|
12151
|
+
};
|
|
12152
|
+
const over = new THREE.Mesh(o.geometry.clone(), o.material);
|
|
12153
|
+
over.position.copy(o.position);
|
|
12154
|
+
over.quaternion.copy(o.quaternion);
|
|
12155
|
+
over.scale.copy(o.scale);
|
|
12156
|
+
this.c.vehicle.add(over);
|
|
12157
|
+
over.updateMatrixWorld(true);
|
|
12158
|
+
const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
|
|
12159
|
+
const w = new THREE.Vector3();
|
|
12160
|
+
this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
|
|
12161
|
+
w.set(x, y, zz).applyMatrix4(overToVehicle);
|
|
12162
|
+
return -Math.max(x0 - w.x, w.x - x1);
|
|
12163
|
+
} }], STRIP_SMOOTH_DEG, {});
|
|
12164
|
+
this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
|
|
12165
|
+
over.userData.noFollow = true;
|
|
12166
|
+
piv.attach(over);
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
11583
12169
|
mend(dirty) {
|
|
11584
12170
|
dirty.forEach((msh) => {
|
|
11585
12171
|
const g = msh.geometry;
|
|
@@ -11589,7 +12175,470 @@ var VehicleVisuals = class {
|
|
|
11589
12175
|
g.computeBoundingBox();
|
|
11590
12176
|
});
|
|
11591
12177
|
}
|
|
12178
|
+
/**
|
|
12179
|
+
* A windshield or rear window is drawn as a flat quad, and the body under its edge is curved: the pane and its frame stand off the shell or rise past the roof. Each one
|
|
12180
|
+
* comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
|
|
12181
|
+
*/
|
|
12182
|
+
seatPanes() {
|
|
12183
|
+
const THREE = this.THREE;
|
|
12184
|
+
const c = this.c;
|
|
12185
|
+
c.root.updateMatrixWorld(true);
|
|
12186
|
+
const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
|
|
12187
|
+
if (!shell.length) return;
|
|
12188
|
+
const ray = new THREE.Raycaster();
|
|
12189
|
+
ray.layers.enableAll();
|
|
12190
|
+
for (const pane of c.glass) {
|
|
12191
|
+
if (pane.userData.glass?.outline) continue;
|
|
12192
|
+
const local = this.shellAround(pane, shell);
|
|
12193
|
+
if (!local) continue;
|
|
12194
|
+
const seat = this.paneSeat(pane, [local], ray);
|
|
12195
|
+
if (!seat) continue;
|
|
12196
|
+
const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
|
|
12197
|
+
for (const m of [pane, ...near]) this.movePane(m, seat);
|
|
12198
|
+
const past = this.shellPast(pane, [local], ray, seat.normal);
|
|
12199
|
+
if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
|
|
12200
|
+
this.reachShellBelow(pane, local, ray, seat.normal);
|
|
12201
|
+
this.paneBorder(pane, near, seat);
|
|
12202
|
+
this.keepUnderPane(pane, seat.normal);
|
|
12203
|
+
const across = new this.THREE.Vector3(0, 0, 1).transformDirection(this.c.vehicle.matrixWorld);
|
|
12204
|
+
if (Math.abs(seat.normal.dot(across)) < SIDE_PANE_FACING) {
|
|
12205
|
+
for (const body of shell) if (body.parent === this.c.vehicle) this.cutUnderPane(body, pane, seat.normal, SKIN_UNDER_PANE, SKIN_EDGE_KEPT);
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
/** Only the triangles of the shell around a pane, in world space: seating casts many rays and the whole hull has too many triangles for each. */
|
|
12210
|
+
shellAround(pane, shell) {
|
|
12211
|
+
const THREE = this.THREE;
|
|
12212
|
+
const box = new THREE.Box3().setFromObject(pane).expandByScalar(PANE_SEAT_REACH + PANE_SEAT_OUT);
|
|
12213
|
+
const out = [];
|
|
12214
|
+
const a = new THREE.Vector3();
|
|
12215
|
+
const b = new THREE.Vector3();
|
|
12216
|
+
const d = new THREE.Vector3();
|
|
12217
|
+
const tri = new THREE.Box3();
|
|
12218
|
+
for (const mesh of shell) {
|
|
12219
|
+
const pos = mesh.geometry.attributes.position;
|
|
12220
|
+
const idx = mesh.geometry.index;
|
|
12221
|
+
const count = idx ? idx.count : pos.count;
|
|
12222
|
+
const e = mesh.matrixWorld.elements;
|
|
12223
|
+
const world = (v, i) => {
|
|
12224
|
+
const x = pos.getX(i);
|
|
12225
|
+
const y = pos.getY(i);
|
|
12226
|
+
const z = pos.getZ(i);
|
|
12227
|
+
v.set(e[0] * x + e[4] * y + e[8] * z + e[12], e[1] * x + e[5] * y + e[9] * z + e[13], e[2] * x + e[6] * y + e[10] * z + e[14]);
|
|
12228
|
+
};
|
|
12229
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
12230
|
+
world(a, idx ? idx.getX(f) : f);
|
|
12231
|
+
world(b, idx ? idx.getX(f + 1) : f + 1);
|
|
12232
|
+
world(d, idx ? idx.getX(f + 2) : f + 2);
|
|
12233
|
+
tri.makeEmpty().expandByPoint(a).expandByPoint(b).expandByPoint(d);
|
|
12234
|
+
if (tri.intersectsBox(box)) out.push(a.x, a.y, a.z, b.x, b.y, b.z, d.x, d.y, d.z);
|
|
12235
|
+
}
|
|
12236
|
+
}
|
|
12237
|
+
if (!out.length) return null;
|
|
12238
|
+
const g = new THREE.BufferGeometry();
|
|
12239
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
|
|
12240
|
+
const proxy = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }));
|
|
12241
|
+
proxy.updateMatrixWorld(true);
|
|
12242
|
+
return proxy;
|
|
12243
|
+
}
|
|
12244
|
+
/** The outward normal of a pane and how far the shell is at each sampled point (world). */
|
|
12245
|
+
paneSeat(pane, shell, ray) {
|
|
12246
|
+
const THREE = this.THREE;
|
|
12247
|
+
const pos = pane.geometry.attributes.position;
|
|
12248
|
+
const box = new THREE.Box3().setFromObject(pane);
|
|
12249
|
+
const centre = box.getCenter(new THREE.Vector3());
|
|
12250
|
+
const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
|
|
12251
|
+
const tri = new THREE.Triangle();
|
|
12252
|
+
const normal = new THREE.Vector3();
|
|
12253
|
+
let largest = 0;
|
|
12254
|
+
const idx = pane.geometry.index;
|
|
12255
|
+
const count = idx ? idx.count : pos.count;
|
|
12256
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
12257
|
+
tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
|
|
12258
|
+
tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
|
|
12259
|
+
tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
|
|
12260
|
+
if (tri.getArea() > largest) {
|
|
12261
|
+
largest = tri.getArea();
|
|
12262
|
+
tri.getNormal(normal);
|
|
12263
|
+
}
|
|
12264
|
+
}
|
|
12265
|
+
normal.transformDirection(pane.matrixWorld);
|
|
12266
|
+
if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
|
|
12267
|
+
const samples = [];
|
|
12268
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12269
|
+
const v = new THREE.Vector3();
|
|
12270
|
+
const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
|
|
12271
|
+
for (let i = 0; i < pos.count; i += step) {
|
|
12272
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
12273
|
+
const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
|
|
12274
|
+
if (seen.has(key)) continue;
|
|
12275
|
+
seen.add(key);
|
|
12276
|
+
const away = v.clone().sub(centre);
|
|
12277
|
+
away.addScaledVector(normal, -away.dot(normal)).normalize();
|
|
12278
|
+
let by = -Infinity;
|
|
12279
|
+
for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
|
|
12280
|
+
const at = v.clone().addScaledVector(away, off);
|
|
12281
|
+
ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
|
|
12282
|
+
ray.far = PANE_SEAT_REACH * 2;
|
|
12283
|
+
const hit = raycastIndexed(THREE, ray, shell)[0];
|
|
12284
|
+
if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
|
|
12285
|
+
}
|
|
12286
|
+
if (by > -Infinity) samples.push({ at: v.clone(), by });
|
|
12287
|
+
}
|
|
12288
|
+
return samples.length >= 3 ? { normal, samples } : null;
|
|
12289
|
+
}
|
|
12290
|
+
/** The lower edge of a pane comes down to the shell: left short, the dashboard shows through the slit as a black bar under the glass. */
|
|
12291
|
+
reachShellBelow(pane, shell, ray, normal) {
|
|
12292
|
+
const THREE = this.THREE;
|
|
12293
|
+
const down = new THREE.Vector3(0, -1, 0).applyQuaternion(this.c.root.getWorldQuaternion(new THREE.Quaternion()));
|
|
12294
|
+
down.addScaledVector(normal, -down.dot(normal));
|
|
12295
|
+
if (down.lengthSq() < PANE_MIN_SLOPE) return;
|
|
12296
|
+
down.normalize();
|
|
12297
|
+
const g = pane.geometry;
|
|
12298
|
+
const pos = g.attributes.position;
|
|
12299
|
+
const v = new THREE.Vector3();
|
|
12300
|
+
let lowest = -Infinity;
|
|
12301
|
+
for (let i = 0; i < pos.count; i++) lowest = Math.max(lowest, v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).dot(down));
|
|
12302
|
+
const edge = [];
|
|
12303
|
+
const mid = new THREE.Vector3();
|
|
12304
|
+
for (let i = 0; i < pos.count; i++) {
|
|
12305
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
12306
|
+
if (v.dot(down) > lowest - PANE_EDGE_BAND) {
|
|
12307
|
+
edge.push(i);
|
|
12308
|
+
mid.add(v);
|
|
12309
|
+
}
|
|
12310
|
+
}
|
|
12311
|
+
if (!edge.length) return;
|
|
12312
|
+
mid.divideScalar(edge.length);
|
|
12313
|
+
let reach = 0;
|
|
12314
|
+
for (let d = PANE_REACH_STEP; d <= PANE_REACH_MAX; d += PANE_REACH_STEP) {
|
|
12315
|
+
const at = mid.clone().addScaledVector(down, d).addScaledVector(normal, PANE_SEAT_REACH);
|
|
12316
|
+
ray.set(at, normal.clone().negate());
|
|
12317
|
+
ray.far = PANE_SEAT_REACH * 2;
|
|
12318
|
+
if (ray.intersectObject(shell, false)[0]) {
|
|
12319
|
+
reach = d + PANE_REACH_STEP;
|
|
12320
|
+
break;
|
|
12321
|
+
}
|
|
12322
|
+
}
|
|
12323
|
+
if (!reach) return;
|
|
12324
|
+
const inverse = pane.matrixWorld.clone().invert();
|
|
12325
|
+
for (const i of edge) {
|
|
12326
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).addScaledVector(down, reach).applyMatrix4(inverse);
|
|
12327
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
12328
|
+
}
|
|
12329
|
+
pos.needsUpdate = true;
|
|
12330
|
+
g.computeBoundingBox();
|
|
12331
|
+
g.computeBoundingSphere();
|
|
12332
|
+
}
|
|
12333
|
+
/** The plate drawn right under a windshield stays only outside the glass, in the body's paint: under the glass it would show inside the cabin. */
|
|
12334
|
+
keepUnderPane(pane, normal) {
|
|
12335
|
+
const THREE = this.THREE;
|
|
12336
|
+
const paneBox2 = new THREE.Box3().setFromObject(pane).expandByScalar(COWL_NEAR_PANE);
|
|
12337
|
+
const pos = pane.geometry.attributes.position;
|
|
12338
|
+
const centre = new THREE.Vector3();
|
|
12339
|
+
const v = new THREE.Vector3();
|
|
12340
|
+
for (let i = 0; i < pos.count; i++) centre.add(v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld));
|
|
12341
|
+
centre.divideScalar(pos.count);
|
|
12342
|
+
const plane = centre.dot(normal);
|
|
12343
|
+
for (const o of this.c.vehicle.children) {
|
|
12344
|
+
if (!o.isMesh || o === pane || o.userData.glass || this.c.deform?.includes(o)) continue;
|
|
12345
|
+
const box = new THREE.Box3().setFromObject(o);
|
|
12346
|
+
const size = box.getSize(new THREE.Vector3());
|
|
12347
|
+
if (o.geometry.attributes.position.count <= PANE_BAR_VERTICES || size.y > COWL_DASH || Math.max(size.x, size.z) < COWL_LONG || Math.min(size.x, size.z) < COWL_DEEP || !paneBox2.intersectsBox(box)) continue;
|
|
12348
|
+
if (box.getCenter(new THREE.Vector3()).dot(normal) - plane >= COWL_UNDER_GLASS) continue;
|
|
12349
|
+
if (size.y >= COWL_PLATE) {
|
|
12350
|
+
o.material = this.I.makeMaterial(DASHBOARD_COLOR, { roughness: 1, metalness: 0 });
|
|
12351
|
+
continue;
|
|
12352
|
+
}
|
|
12353
|
+
o.material = this.paint();
|
|
12354
|
+
this.cutUnderPane(o, pane, normal);
|
|
12355
|
+
}
|
|
12356
|
+
}
|
|
12357
|
+
/** Cuts away the part of a mesh that lies under a pane (behind its plane, inside its outline seen along its normal). */
|
|
12358
|
+
cutUnderPane(mesh, pane, normal, behind = -Infinity, inset = 0) {
|
|
12359
|
+
const THREE = this.THREE;
|
|
12360
|
+
const outline = this.paneOutline(pane, normal);
|
|
12361
|
+
if (outline.length < 3) return;
|
|
12362
|
+
const u = new THREE.Vector3(0, 1, 0).cross(normal);
|
|
12363
|
+
if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
|
|
12364
|
+
u.normalize();
|
|
12365
|
+
const w = normal.clone().cross(u);
|
|
12366
|
+
const cx = outline.reduce((sum, p) => sum + p.dot(u), 0) / outline.length;
|
|
12367
|
+
const cy = outline.reduce((sum, p) => sum + p.dot(w), 0) / outline.length;
|
|
12368
|
+
const poly = outline.map((p) => {
|
|
12369
|
+
const x = p.dot(u);
|
|
12370
|
+
const y = p.dot(w);
|
|
12371
|
+
const d = Math.hypot(x - cx, y - cy) || 1;
|
|
12372
|
+
const k = Math.max(0, d - inset) / d;
|
|
12373
|
+
return [cx + (x - cx) * k, cy + (y - cy) * k];
|
|
12374
|
+
});
|
|
12375
|
+
const plane = outline.reduce((sum, p) => sum + p.dot(normal), 0) / outline.length;
|
|
12376
|
+
const inside = (x, y) => {
|
|
12377
|
+
let hit = false;
|
|
12378
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
12379
|
+
const [xi, yi] = poly[i];
|
|
12380
|
+
const [xj, yj] = poly[j];
|
|
12381
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) hit = !hit;
|
|
12382
|
+
}
|
|
12383
|
+
return hit;
|
|
12384
|
+
};
|
|
12385
|
+
const v = new THREE.Vector3();
|
|
12386
|
+
const toWorld = mesh.matrixWorld;
|
|
12387
|
+
mesh.updateMatrixWorld(true);
|
|
12388
|
+
const xs = poly.map((p) => p[0]);
|
|
12389
|
+
const ys = poly.map((p) => p[1]);
|
|
12390
|
+
const [x0, x1, y0, y1] = [Math.min(...xs) - COWL_CUT_MARGIN, Math.max(...xs) + COWL_CUT_MARGIN, Math.min(...ys) - COWL_CUT_MARGIN, Math.max(...ys) + COWL_CUT_MARGIN];
|
|
12391
|
+
subdivideNear(THREE, { mesh, maxLen: COWL_CUT_SUB, near: (corners) => {
|
|
12392
|
+
const us = corners.map((c) => c.dot(u));
|
|
12393
|
+
const ws = corners.map((c) => c.dot(w));
|
|
12394
|
+
return Math.max(...us) >= x0 && Math.min(...us) <= x1 && Math.max(...ws) >= y0 && Math.min(...ws) <= y1;
|
|
12395
|
+
} });
|
|
12396
|
+
this.kit.cutMesh(
|
|
12397
|
+
mesh,
|
|
12398
|
+
[
|
|
12399
|
+
{
|
|
12400
|
+
sdf: (x, y, z) => {
|
|
12401
|
+
v.set(x, y, z).applyMatrix4(toWorld);
|
|
12402
|
+
return inside(v.dot(u), v.dot(w)) && plane - v.dot(normal) > behind ? -1 : 1;
|
|
12403
|
+
}
|
|
12404
|
+
}
|
|
12405
|
+
],
|
|
12406
|
+
42,
|
|
12407
|
+
{}
|
|
12408
|
+
);
|
|
12409
|
+
}
|
|
12410
|
+
/** The straight bars drawn round a quad pane stand off its edges: they give way to a thin dark band that lies on the glass, as on a bonded windshield. */
|
|
12411
|
+
paneBorder(pane, near, seat) {
|
|
12412
|
+
const THREE = this.THREE;
|
|
12413
|
+
const bars = near.filter((o) => {
|
|
12414
|
+
if (o.geometry.attributes.position.count > PANE_BAR_VERTICES) return false;
|
|
12415
|
+
const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
|
|
12416
|
+
return Math.max(size.x, size.y, size.z) > PANE_BAR_LENGTH;
|
|
12417
|
+
});
|
|
12418
|
+
if (!bars.length) return;
|
|
12419
|
+
const outline = this.paneOutline(pane, seat.normal);
|
|
12420
|
+
if (outline.length < 3) return;
|
|
12421
|
+
const centre = new THREE.Vector3();
|
|
12422
|
+
for (const p of outline) centre.add(p);
|
|
12423
|
+
centre.divideScalar(outline.length);
|
|
12424
|
+
const position = [];
|
|
12425
|
+
const toVehicle = this.I.worldInverseOf(this.c);
|
|
12426
|
+
const put = (p) => {
|
|
12427
|
+
const q = p.clone().addScaledVector(seat.normal, PANE_BORDER_LIFT).applyMatrix4(toVehicle);
|
|
12428
|
+
position.push(q.x, q.y, q.z);
|
|
12429
|
+
};
|
|
12430
|
+
const inward = (p) => p.clone().add(centre.clone().sub(p).normalize().multiplyScalar(PANE_BORDER));
|
|
12431
|
+
outline.forEach((a, i) => {
|
|
12432
|
+
const b = outline[(i + 1) % outline.length];
|
|
12433
|
+
const c = inward(b);
|
|
12434
|
+
const d = inward(a);
|
|
12435
|
+
for (const p of [a, b, c, a, c, d]) put(p);
|
|
12436
|
+
});
|
|
12437
|
+
if (!position.length) return;
|
|
12438
|
+
for (const o of bars) o.visible = false;
|
|
12439
|
+
const band = new THREE.BufferGeometry();
|
|
12440
|
+
band.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
|
|
12441
|
+
band.computeVertexNormals();
|
|
12442
|
+
const material = this.I.MATERIALS.dark.clone();
|
|
12443
|
+
material.side = THREE.DoubleSide;
|
|
12444
|
+
const mesh = new THREE.Mesh(band, material);
|
|
12445
|
+
mesh.userData.noFollow = true;
|
|
12446
|
+
mesh.castShadow = false;
|
|
12447
|
+
mesh.raycast = () => {
|
|
12448
|
+
};
|
|
12449
|
+
this.c.vehicle.add(mesh);
|
|
12450
|
+
}
|
|
12451
|
+
/** The outline of a pane, the convex hull of its vertices seen along its normal, in world space. */
|
|
12452
|
+
paneOutline(pane, normal) {
|
|
12453
|
+
const THREE = this.THREE;
|
|
12454
|
+
const pos = pane.geometry.attributes.position;
|
|
12455
|
+
const u = new THREE.Vector3(0, 1, 0).cross(normal);
|
|
12456
|
+
if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
|
|
12457
|
+
u.normalize();
|
|
12458
|
+
const w = normal.clone().cross(u);
|
|
12459
|
+
const points = [];
|
|
12460
|
+
for (let i = 0; i < pos.count; i++) {
|
|
12461
|
+
const p = new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
12462
|
+
points.push({ x: p.dot(u), y: p.dot(w), p });
|
|
12463
|
+
}
|
|
12464
|
+
points.sort((a, b) => a.x - b.x || a.y - b.y);
|
|
12465
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
12466
|
+
const half = (list) => {
|
|
12467
|
+
const out = [];
|
|
12468
|
+
for (const q of list) {
|
|
12469
|
+
while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], q) <= 0) out.pop();
|
|
12470
|
+
out.push(q);
|
|
12471
|
+
}
|
|
12472
|
+
out.pop();
|
|
12473
|
+
return out;
|
|
12474
|
+
};
|
|
12475
|
+
return [...half(points), ...half([...points].reverse())].map((q) => q.p);
|
|
12476
|
+
}
|
|
12477
|
+
/** How far the shell still stands past a seated pane, over its triangles. */
|
|
12478
|
+
shellPast(pane, shell, ray, normal) {
|
|
12479
|
+
const THREE = this.THREE;
|
|
12480
|
+
const pos = pane.geometry.attributes.position;
|
|
12481
|
+
const idx = pane.geometry.index;
|
|
12482
|
+
const count = idx ? idx.count : pos.count;
|
|
12483
|
+
const tri = new THREE.Triangle();
|
|
12484
|
+
const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
|
|
12485
|
+
let past = 0;
|
|
12486
|
+
for (let f = 0; f + 2 < count; f += step) {
|
|
12487
|
+
tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
|
|
12488
|
+
tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
|
|
12489
|
+
tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
|
|
12490
|
+
const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
|
|
12491
|
+
ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
|
|
12492
|
+
ray.far = PANE_SEAT_REACH;
|
|
12493
|
+
const hit = raycastIndexed(THREE, ray, shell)[0];
|
|
12494
|
+
if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
|
|
12495
|
+
}
|
|
12496
|
+
return past;
|
|
12497
|
+
}
|
|
12498
|
+
/** A frame or a seal drawn on the pane: small, and close to its plane. */
|
|
12499
|
+
onPane(o, pane, seat) {
|
|
12500
|
+
const THREE = this.THREE;
|
|
12501
|
+
const box = new THREE.Box3().setFromObject(o);
|
|
12502
|
+
if (box.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box)) return false;
|
|
12503
|
+
const at = box.getCenter(new THREE.Vector3());
|
|
12504
|
+
return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
|
|
12505
|
+
}
|
|
12506
|
+
movePane(mesh, seat) {
|
|
12507
|
+
const THREE = this.THREE;
|
|
12508
|
+
const g = mesh.geometry.clone();
|
|
12509
|
+
const pos = g.attributes.position;
|
|
12510
|
+
const inverse = mesh.matrixWorld.clone().invert();
|
|
12511
|
+
const v = new THREE.Vector3();
|
|
12512
|
+
for (let i = 0; i < pos.count; i++) {
|
|
12513
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(mesh.matrixWorld);
|
|
12514
|
+
let weight = 0;
|
|
12515
|
+
let by = 0;
|
|
12516
|
+
for (const s of seat.samples) {
|
|
12517
|
+
const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
|
|
12518
|
+
weight += w;
|
|
12519
|
+
by += s.by * w;
|
|
12520
|
+
}
|
|
12521
|
+
v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
|
|
12522
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
12523
|
+
}
|
|
12524
|
+
pos.needsUpdate = true;
|
|
12525
|
+
g.computeBoundingBox();
|
|
12526
|
+
g.computeBoundingSphere();
|
|
12527
|
+
mesh.geometry = g;
|
|
12528
|
+
}
|
|
12529
|
+
/** A closed hood is flush with the fenders beside it: the height it was drawn with to keep clear of the hull goes, the hull has its hole. */
|
|
12530
|
+
seatHood() {
|
|
12531
|
+
const hood = this.c.hood;
|
|
12532
|
+
if (!hood?.lift) return;
|
|
12533
|
+
const THREE = this.THREE;
|
|
12534
|
+
const box = new THREE.Box3().setFromObject(hood.piv).getSize(new THREE.Vector3());
|
|
12535
|
+
for (const part of hood.piv.children) {
|
|
12536
|
+
part.position.y -= hood.lift + HOOD_SINK;
|
|
12537
|
+
part.scale.multiply(new THREE.Vector3(1 + HOOD_GAP_CLOSED / Math.max(0.3, box.x), 1, 1 + HOOD_GAP_CLOSED / Math.max(0.3, box.z)));
|
|
12538
|
+
}
|
|
12539
|
+
}
|
|
12540
|
+
/** A handle is at the free edge of its door, the end away from the hinge. */
|
|
12541
|
+
seatDoorHandles() {
|
|
12542
|
+
const THREE = this.THREE;
|
|
12543
|
+
const c = this.c;
|
|
12544
|
+
const inv = this.I.worldInverseOf(c);
|
|
12545
|
+
const bodyBox = new THREE.Box3();
|
|
12546
|
+
for (const o of c.deform) if (o.isMesh && !o.userData.glass && c.paints?.has(o.material)) bodyBox.union(this.I.localBox(c, o, inv));
|
|
12547
|
+
const outerHalf = Math.max(Math.abs(bodyBox.min.z), Math.abs(bodyBox.max.z));
|
|
12548
|
+
for (const p of this.model.parts) {
|
|
12549
|
+
if (p.role !== "door") continue;
|
|
12550
|
+
const z = this.zone(p);
|
|
12551
|
+
if (!z) continue;
|
|
12552
|
+
const B = z.box;
|
|
12553
|
+
const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
|
|
12554
|
+
for (const o of c.vehicle.children) {
|
|
12555
|
+
if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
|
|
12556
|
+
const box = this.I.localBox(c, o, inv);
|
|
12557
|
+
const size = box.getSize(new THREE.Vector3());
|
|
12558
|
+
const at = box.getCenter(new THREE.Vector3());
|
|
12559
|
+
const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
|
|
12560
|
+
if (!isHandle || Math.sign(at.z) !== Math.sign(z.side) || at.x < B.x0 || at.x > B.x1 || at.y < B.y0 || at.y > B.y1) continue;
|
|
12561
|
+
if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
|
|
12562
|
+
const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
|
|
12563
|
+
o.position.x += free - at.x;
|
|
12564
|
+
if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
|
|
12565
|
+
}
|
|
12566
|
+
}
|
|
12567
|
+
c.root.updateMatrixWorld(true);
|
|
12568
|
+
}
|
|
12569
|
+
/** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
|
|
12570
|
+
seatMirrorGlass(glass, drawn) {
|
|
12571
|
+
const g = glass.geometry.clone();
|
|
12572
|
+
g.computeBoundingBox();
|
|
12573
|
+
const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
|
|
12574
|
+
g.translate(-mid.x, -mid.y, -mid.z);
|
|
12575
|
+
g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
|
|
12576
|
+
g.translate(mid.x, mid.y, mid.z);
|
|
12577
|
+
glass.geometry = g;
|
|
12578
|
+
const THREE = this.THREE;
|
|
12579
|
+
const volumeOf = (o) => {
|
|
12580
|
+
const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
|
|
12581
|
+
return size.x * size.y * size.z;
|
|
12582
|
+
};
|
|
12583
|
+
const housing = drawn.filter((o) => o !== glass && o.isMesh && o.geometry).sort((a, b) => volumeOf(b) - volumeOf(a))[0];
|
|
12584
|
+
if (housing) {
|
|
12585
|
+
this.c.root.updateMatrixWorld(true);
|
|
12586
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
12587
|
+
glass.updateMatrixWorld(true);
|
|
12588
|
+
const gb = this.I.localBox(this.c, glass, inv);
|
|
12589
|
+
const hb = this.I.localBox(this.c, housing, inv);
|
|
12590
|
+
const side2 = hb.getCenter(new THREE.Vector3()).x > gb.getCenter(new THREE.Vector3()).x ? -1 : 1;
|
|
12591
|
+
const face = side2 < 0 ? hb.min.x + MIRROR_FACE_DEPTH : hb.max.x - MIRROR_FACE_DEPTH;
|
|
12592
|
+
const shape = housing.geometry.clone();
|
|
12593
|
+
const pos = shape.attributes.position;
|
|
12594
|
+
const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, housing.matrixWorld);
|
|
12595
|
+
const back = toVehicle.clone().invert();
|
|
12596
|
+
const v = new THREE.Vector3();
|
|
12597
|
+
for (let i = 0; i < pos.count; i++) {
|
|
12598
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(toVehicle);
|
|
12599
|
+
if ((v.x - face) * side2 <= 0) continue;
|
|
12600
|
+
v.x = face;
|
|
12601
|
+
v.applyMatrix4(back);
|
|
12602
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
12603
|
+
}
|
|
12604
|
+
shape.computeVertexNormals();
|
|
12605
|
+
housing.geometry = shape;
|
|
12606
|
+
const outer = side2 < 0 ? gb.min.x : gb.max.x;
|
|
12607
|
+
glass.position.x += face + side2 * MIRROR_GLASS_PROUD - outer;
|
|
12608
|
+
}
|
|
12609
|
+
glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
|
|
12610
|
+
}
|
|
12611
|
+
/** The mirror the model already draws around where the part says, if it draws one. */
|
|
12612
|
+
drawnMirror(p) {
|
|
12613
|
+
const THREE = this.THREE;
|
|
12614
|
+
const [cx, cy, cz] = p.box.center;
|
|
12615
|
+
this.c.root.updateMatrixWorld(true);
|
|
12616
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
12617
|
+
const small = this.c.vehicle.children.flatMap((o) => {
|
|
12618
|
+
if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
|
|
12619
|
+
const box = this.I.localBox(this.c, o, inv);
|
|
12620
|
+
if (box.isEmpty()) return [];
|
|
12621
|
+
const size = box.getSize(new THREE.Vector3());
|
|
12622
|
+
return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box.getCenter(new THREE.Vector3()) }] : [];
|
|
12623
|
+
});
|
|
12624
|
+
const housing = small.filter(({ at }) => Math.hypot(at.x - cx, at.y - cy, at.z - cz) < DRAWN_MIRROR_REACH && Math.abs(at.z) > Math.abs(cz) - DRAWN_MIRROR_REACH / 2);
|
|
12625
|
+
return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
|
|
12626
|
+
}
|
|
11592
12627
|
makeMirror(p) {
|
|
12628
|
+
const drawn = this.drawnMirror(p);
|
|
12629
|
+
if (drawn.length) {
|
|
12630
|
+
for (const o of drawn) {
|
|
12631
|
+
if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
|
|
12632
|
+
const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
|
|
12633
|
+
const [thin, mid] = size.toArray().sort((a, b) => a - b);
|
|
12634
|
+
if (thin < MIRROR_GLASS_THIN && mid > MIRROR_GLASS_MIN) {
|
|
12635
|
+
this.seatMirrorGlass(o, drawn);
|
|
12636
|
+
o.userData.mirrorGlass = true;
|
|
12637
|
+
}
|
|
12638
|
+
}
|
|
12639
|
+
this.extra.set(p.id, drawn);
|
|
12640
|
+
return;
|
|
12641
|
+
}
|
|
11593
12642
|
const [cx, cy, cz] = p.box.center;
|
|
11594
12643
|
const s = cz < 0 ? -1 : 1;
|
|
11595
12644
|
const mats = this.I.MATERIALS;
|
|
@@ -11597,14 +12646,26 @@ var VehicleVisuals = class {
|
|
|
11597
12646
|
g.name = `obj_${p.id}`;
|
|
11598
12647
|
g.position.set(cx, cy, cz);
|
|
11599
12648
|
this.c.vehicle.add(g);
|
|
11600
|
-
this.box(0.05, 0.
|
|
11601
|
-
const housing = this.box(0.
|
|
12649
|
+
this.box(0.05, 0.035, 0.05, mats.dark, 0, -0.04, -s * 0.06, g);
|
|
12650
|
+
const housing = this.box(0.08, 0.11, 0.125, mats.plastic ?? mats.dark, 0, 0, s * 0.0225, g);
|
|
11602
12651
|
housing.userData.mirrorHousing = true;
|
|
11603
|
-
const glass = this.box(0.095, 0.
|
|
12652
|
+
const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
|
|
11604
12653
|
glass.userData.mirrorGlass = true;
|
|
11605
12654
|
g.userData.rigid = true;
|
|
11606
12655
|
this.extra.set(p.id, [g]);
|
|
11607
12656
|
}
|
|
12657
|
+
/** A round through the mirror shatters its glass: the shards drop out of the housing, which stays on with the hole. Returns whether there was glass to break. */
|
|
12658
|
+
shatterMirror(id) {
|
|
12659
|
+
let broke = false;
|
|
12660
|
+
for (const o of this.extra.get(id) ?? []) {
|
|
12661
|
+
o.traverse((m) => {
|
|
12662
|
+
if (!m.userData.mirrorGlass || !m.visible) return;
|
|
12663
|
+
m.visible = false;
|
|
12664
|
+
broke = true;
|
|
12665
|
+
});
|
|
12666
|
+
}
|
|
12667
|
+
return broke;
|
|
12668
|
+
}
|
|
11608
12669
|
makeBattery(p) {
|
|
11609
12670
|
const mats = this.I.MATERIALS;
|
|
11610
12671
|
const g = new this.THREE.Group();
|
|
@@ -11630,17 +12691,33 @@ var VehicleVisuals = class {
|
|
|
11630
12691
|
}
|
|
11631
12692
|
// ---- marks -------------------------------------------------------------------------------------------------------------------------------
|
|
11632
12693
|
/** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
|
|
11633
|
-
holeDecal(point, normal, diameter) {
|
|
12694
|
+
holeDecal(point, normal, diameter, part) {
|
|
11634
12695
|
const THREE = this.THREE;
|
|
11635
|
-
const
|
|
12696
|
+
const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
|
|
12697
|
+
if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
|
|
12698
|
+
const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
|
|
12699
|
+
const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
|
|
12700
|
+
m.scale.set(radius, radius, 1);
|
|
11636
12701
|
const n = new THREE.Vector3(...normal);
|
|
11637
|
-
m.position.set(...point).addScaledVector(n,
|
|
11638
|
-
|
|
12702
|
+
m.position.set(...point).addScaledVector(n, 3e-3);
|
|
12703
|
+
this.c.vehicle.add(m);
|
|
12704
|
+
m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
|
|
11639
12705
|
m.userData.decal = true;
|
|
11640
12706
|
m.userData.noFollow = true;
|
|
12707
|
+
m.renderOrder = 3;
|
|
11641
12708
|
m.raycast = () => {
|
|
11642
12709
|
};
|
|
11643
|
-
this.c.vehicle.
|
|
12710
|
+
const carrier = (part && (this.plates.get(part.id) ?? (part.id === this.hood?.id ? this.c.hood?.piv : void 0))) ?? (pane && pane.parent !== this.c.vehicle ? pane.parent : null);
|
|
12711
|
+
if (carrier) carrier.attach(m);
|
|
12712
|
+
}
|
|
12713
|
+
/** The door or lid a hinge group stands for. */
|
|
12714
|
+
/** The part a separately drawn piece stands for (a mirror on the door, a battery under the hood), so a shot at it lands on that part. */
|
|
12715
|
+
extraIdOf(o) {
|
|
12716
|
+
for (const [id, objs] of this.extra) if (objs.includes(o)) return id;
|
|
12717
|
+
return void 0;
|
|
12718
|
+
}
|
|
12719
|
+
plateIdOf(o) {
|
|
12720
|
+
return o.userData.plateOf;
|
|
11644
12721
|
}
|
|
11645
12722
|
spark(point, dir, count = 8) {
|
|
11646
12723
|
const V3 = this.THREE.Vector3;
|
|
@@ -11680,10 +12757,8 @@ var VehicleVisuals = class {
|
|
|
11680
12757
|
}
|
|
11681
12758
|
/** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
|
|
11682
12759
|
hitGlass(p, point, dir, force) {
|
|
11683
|
-
const z = this.zone(p);
|
|
11684
|
-
if (!z) return false;
|
|
11685
12760
|
const V3 = this.THREE.Vector3;
|
|
11686
|
-
const list =
|
|
12761
|
+
const list = this.panesOf(p).filter((g) => g.parent && g.visible);
|
|
11687
12762
|
if (!list.length) return false;
|
|
11688
12763
|
const pt = new V3(...point);
|
|
11689
12764
|
let best = list[0];
|
|
@@ -11697,12 +12772,20 @@ var VehicleVisuals = class {
|
|
|
11697
12772
|
best = g;
|
|
11698
12773
|
}
|
|
11699
12774
|
}
|
|
12775
|
+
if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
|
|
11700
12776
|
this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
|
|
11701
12777
|
return !!best.userData.glass?.broken;
|
|
11702
12778
|
}
|
|
12779
|
+
panesOf(p) {
|
|
12780
|
+
return p.panes ?? this.zone(p)?.glass ?? [];
|
|
12781
|
+
}
|
|
11703
12782
|
glassBreak(p) {
|
|
11704
12783
|
const z = this.zone(p);
|
|
11705
|
-
if (!z)
|
|
12784
|
+
if (!z) {
|
|
12785
|
+
for (const g of p.panes ?? []) if (g.parent && g.visible) this.kit.hitGlass(this.c, g, g.position.clone(), new this.THREE.Vector3(0, -1, 0), 0.6);
|
|
12786
|
+
this.gone.add(p.id);
|
|
12787
|
+
return;
|
|
12788
|
+
}
|
|
11706
12789
|
for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
|
|
11707
12790
|
this.gone.add(p.id);
|
|
11708
12791
|
}
|
|
@@ -11711,6 +12794,17 @@ var VehicleVisuals = class {
|
|
|
11711
12794
|
if (!z) return;
|
|
11712
12795
|
for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
|
|
11713
12796
|
}
|
|
12797
|
+
/** A tyre driven flat until it tore apart: the rubber is gone and the bare rim shows. */
|
|
12798
|
+
tyreTornOff(p) {
|
|
12799
|
+
const z = this.zone(p);
|
|
12800
|
+
if (!z) return;
|
|
12801
|
+
for (const w of z.wheels) {
|
|
12802
|
+
if (!w.parent || w.userData.tornOff) continue;
|
|
12803
|
+
w.userData.tornOff = true;
|
|
12804
|
+
for (const m of this.I.tireMeshes(w)) m.visible = false;
|
|
12805
|
+
w.position.y -= (w.userData.r ?? DEFAULT_WHEEL_RADIUS) * SIDEWALL_SHARE;
|
|
12806
|
+
}
|
|
12807
|
+
}
|
|
11714
12808
|
/** Burning rubber: the tyre sags and goes black as it melts. */
|
|
11715
12809
|
tyreMelt(p, melt) {
|
|
11716
12810
|
const z = this.zone(p);
|
|
@@ -11730,6 +12824,7 @@ var VehicleVisuals = class {
|
|
|
11730
12824
|
smoothLater(msh) {
|
|
11731
12825
|
this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
|
|
11732
12826
|
this.tornThisFrame = true;
|
|
12827
|
+
this.holdArmed = true;
|
|
11733
12828
|
}
|
|
11734
12829
|
/** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
|
|
11735
12830
|
pump(budgetMs = 2.5) {
|
|
@@ -11737,6 +12832,7 @@ var VehicleVisuals = class {
|
|
|
11737
12832
|
this.tornThisFrame = false;
|
|
11738
12833
|
return;
|
|
11739
12834
|
}
|
|
12835
|
+
if (this.queued) return;
|
|
11740
12836
|
if (!this.smoothing.size && !this.warming) return;
|
|
11741
12837
|
const deadline = performance.now() + budgetMs;
|
|
11742
12838
|
for (const [msh, task] of this.smoothing) {
|
|
@@ -11745,6 +12841,34 @@ var VehicleVisuals = class {
|
|
|
11745
12841
|
}
|
|
11746
12842
|
this.warmIndexes(deadline);
|
|
11747
12843
|
}
|
|
12844
|
+
setQueued(isQueued) {
|
|
12845
|
+
this.queued = isQueued;
|
|
12846
|
+
}
|
|
12847
|
+
/** Units the shared queue may run now: 0 in the frame of the cut (the first ask after it holds the work for that frame, or `pump` closes it when there is no frame). */
|
|
12848
|
+
pendingUnits(frame) {
|
|
12849
|
+
if (this.holdArmed && frame !== void 0) {
|
|
12850
|
+
this.holdArmed = false;
|
|
12851
|
+
this.holdFrame = frame;
|
|
12852
|
+
}
|
|
12853
|
+
if (frame !== void 0 && frame === this.holdFrame) return 0;
|
|
12854
|
+
if (frame === void 0 && this.tornThisFrame) return 0;
|
|
12855
|
+
return this.smoothing.size + (this.warming ? 1 : 0);
|
|
12856
|
+
}
|
|
12857
|
+
/** One slice of the pending work, for the shared queue. */
|
|
12858
|
+
runStep(deadline) {
|
|
12859
|
+
const next = this.smoothing.entries().next();
|
|
12860
|
+
if (!next.done) {
|
|
12861
|
+
const [msh, task] = next.value;
|
|
12862
|
+
if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
|
|
12863
|
+
return true;
|
|
12864
|
+
}
|
|
12865
|
+
if (!this.warming) return false;
|
|
12866
|
+
this.warmIndexes(deadline);
|
|
12867
|
+
return true;
|
|
12868
|
+
}
|
|
12869
|
+
finishAll() {
|
|
12870
|
+
while (this.runStep(Infinity)) ;
|
|
12871
|
+
}
|
|
11748
12872
|
/** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
|
|
11749
12873
|
warmIndexes(deadline) {
|
|
11750
12874
|
if (!this.warming) return;
|
|
@@ -11916,6 +13040,7 @@ var VehicleVisuals = class {
|
|
|
11916
13040
|
}
|
|
11917
13041
|
pieceTasks.forEach((task, mesh) => this.smoothing.set(mesh, task));
|
|
11918
13042
|
this.tornThisFrame = true;
|
|
13043
|
+
this.holdArmed = true;
|
|
11919
13044
|
dirty.forEach((msh) => {
|
|
11920
13045
|
const g = msh.geometry;
|
|
11921
13046
|
if (g.userData.crease || !g.index) this.smoothLater(msh);
|
|
@@ -12003,6 +13128,41 @@ var BLUNT = { steel: 0.8, aluminium: 1, glass: 1.5, plastic: 1.2, rubber: 0.3 };
|
|
|
12003
13128
|
var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
|
|
12004
13129
|
var AMBIENT = 20;
|
|
12005
13130
|
var FLAME = 700;
|
|
13131
|
+
var STEEL_MM = { panel: 0.8, door: 1.6, hood: 0.8, lid: 0.8, roof: 0.8, fender: 0.8, pillar: 2, frame: 4, rim: 3, engine: 40, gearbox: 25, fuel_tank: 1, radiator: 1, battery: 2, brakes: 10, steering: 8, shocks: 6, oil_pan: 2, alternator: 4, hitch: 8, other: 2 };
|
|
13132
|
+
var ARMOUR_STEEL_MM = { IIIA: 4, III: 7 };
|
|
13133
|
+
var SOFT_LEAD = 0.8;
|
|
13134
|
+
var TYRE_FLAT_HEALTH = 0.1;
|
|
13135
|
+
var TYRE_BURST_JOULES = 1500;
|
|
13136
|
+
var TYRE_BURST_CALIBER = 12;
|
|
13137
|
+
var TYRE_LEAK_PER_MM2 = 26e-5;
|
|
13138
|
+
var FLAT_TYRE_LIFE_M = 1500;
|
|
13139
|
+
var FLAT_TYRE_TOWN_SPEED = 8;
|
|
13140
|
+
var RUN_FLAT_LIFE_M = 8e4;
|
|
13141
|
+
var RUN_FLAT_SPEED = 22;
|
|
13142
|
+
var TYRE_RUNFLAT_HEALTH = 0.6;
|
|
13143
|
+
var CRUMPLE_BASE_M = 0.04;
|
|
13144
|
+
var CRUMPLE_PER_FORCE_M = 0.15;
|
|
13145
|
+
var CRUMPLE_MIN_FORCE = 0.3;
|
|
13146
|
+
var CRUMPLE_MAX_FORCE = 1.4;
|
|
13147
|
+
var RIM_HEALTH = 0.05;
|
|
13148
|
+
var RIM_GRIND_PER_M = 4e-4;
|
|
13149
|
+
var TYRE_FLAT_SAG = 0.24;
|
|
13150
|
+
var TYRE_FLAT_AIR = 0.2;
|
|
13151
|
+
var BRAKE_FLUID_NEEDED = 0.6;
|
|
13152
|
+
var BRAKE_NO_FLUID = 0.15;
|
|
13153
|
+
var ENGINE_WORKING_C = 90;
|
|
13154
|
+
var ENGINE_SETTLE_S = 30;
|
|
13155
|
+
var ENGINE_HEATING = 0.5;
|
|
13156
|
+
var ENGINE_STEAM_C = 120;
|
|
13157
|
+
var ENGINE_SEIZE_C = 150;
|
|
13158
|
+
var COOLANT_NEEDED = 0.4;
|
|
13159
|
+
var OIL_NEEDED = 0.3;
|
|
13160
|
+
var OIL_WEAR = 4e-3;
|
|
13161
|
+
var ENGINE_SEIZE_HP = 0.2;
|
|
13162
|
+
var EXPANDED_HOLE = 1.6;
|
|
13163
|
+
var MIN_SPENT = 0.08;
|
|
13164
|
+
var WELDED = /* @__PURE__ */ new Set(["roof", "pillar", "frame", "panel"]);
|
|
13165
|
+
var CRUMPLES = /* @__PURE__ */ new Set(["door", "hood", "lid", "fender", "bumper"]);
|
|
12006
13166
|
var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
|
|
12007
13167
|
var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
12008
13168
|
var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
|
|
@@ -12014,6 +13174,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12014
13174
|
let c = vehicleCtx;
|
|
12015
13175
|
const id = options.id ?? c.key;
|
|
12016
13176
|
const headless = !!options.headless;
|
|
13177
|
+
const armourId = armourLevelOf(c.meta, options.armour);
|
|
13178
|
+
const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
|
|
12017
13179
|
const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
|
|
12018
13180
|
let rand = rng(seedBase);
|
|
12019
13181
|
const ignitionRules = resolveIgnitionRules(options.ignition);
|
|
@@ -12022,6 +13184,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12022
13184
|
let clock = 0;
|
|
12023
13185
|
let model;
|
|
12024
13186
|
let vis = null;
|
|
13187
|
+
let releaseCrease;
|
|
12025
13188
|
const ps = /* @__PURE__ */ new Map();
|
|
12026
13189
|
let js = [];
|
|
12027
13190
|
let leaks = /* @__PURE__ */ new Map();
|
|
@@ -12070,7 +13233,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12070
13233
|
return d.ignitionC ? 1 : 0;
|
|
12071
13234
|
};
|
|
12072
13235
|
const setup = () => {
|
|
12073
|
-
model = buildVehicleModel(kit, c, { hitch: options.hitch });
|
|
13236
|
+
model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
|
|
12074
13237
|
ps.clear();
|
|
12075
13238
|
for (const d of model.parts) {
|
|
12076
13239
|
const kg = burnKg(d);
|
|
@@ -12090,6 +13253,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12090
13253
|
lost: false,
|
|
12091
13254
|
status: "intact",
|
|
12092
13255
|
flat: false,
|
|
13256
|
+
air: 1,
|
|
13257
|
+
airLoss: 0,
|
|
12093
13258
|
fireLoss: 0
|
|
12094
13259
|
});
|
|
12095
13260
|
}
|
|
@@ -12106,7 +13271,9 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12106
13271
|
rand = rng(seedBase);
|
|
12107
13272
|
igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
|
|
12108
13273
|
steerSign = rand() < 0.5 ? -1 : 1;
|
|
13274
|
+
releaseCrease?.();
|
|
12109
13275
|
vis = headless ? null : new VehicleVisuals(kit, c, model);
|
|
13276
|
+
releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
|
|
12110
13277
|
recomputeGroups();
|
|
12111
13278
|
};
|
|
12112
13279
|
const recomputeGroups = () => {
|
|
@@ -12188,7 +13355,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12188
13355
|
vis.breakLight(d, true);
|
|
12189
13356
|
break;
|
|
12190
13357
|
case "tyre":
|
|
12191
|
-
vis.
|
|
13358
|
+
if (status === "broken") vis.tyreTornOff(d);
|
|
13359
|
+
else vis.tyreFlat(d, 0.34);
|
|
12192
13360
|
break;
|
|
12193
13361
|
case "mirror":
|
|
12194
13362
|
case "bumper":
|
|
@@ -12218,7 +13386,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12218
13386
|
const damage = (p, amount, dtype, source, out, cause) => {
|
|
12219
13387
|
if (p.lost || !(amount > 0)) return;
|
|
12220
13388
|
const before = p.hp;
|
|
12221
|
-
|
|
13389
|
+
const crumples = dtype === "blunt" && CRUMPLES.has(p.def.role) && (p.def.material === "steel" || p.def.material === "aluminium");
|
|
13390
|
+
p.hp = Math.max(WELDED.has(p.def.role) || crumples ? 1 : 0, p.hp - amount);
|
|
12222
13391
|
out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
|
|
12223
13392
|
wear(p, amount, out);
|
|
12224
13393
|
syncZone(p);
|
|
@@ -12261,11 +13430,22 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12261
13430
|
const p = v.clone().applyMatrix4(M);
|
|
12262
13431
|
return [p.x, p.y, p.z];
|
|
12263
13432
|
};
|
|
12264
|
-
const armour = () =>
|
|
13433
|
+
const armour = () => armourLevel?.steel ?? 1;
|
|
13434
|
+
const steelMm = (d) => {
|
|
13435
|
+
const base = STEEL_MM[d.role] ?? STEEL_MM.panel;
|
|
13436
|
+
return d.material === "steel" || d.material === "aluminium" ? base * (armourLevel ? ARMOUR_STEEL_MM[armourId === "III" ? "III" : "IIIA"] / STEEL_MM.panel : 1) : base;
|
|
13437
|
+
};
|
|
13438
|
+
const goesThrough = (h, d, p, energy, res) => {
|
|
13439
|
+
const piercing = h.piercing ?? 1;
|
|
13440
|
+
const metal = d.material === "steel" || d.material === "aluminium";
|
|
13441
|
+
const mm = h.penetration?.steel;
|
|
13442
|
+
if (metal && mm != null) return mm >= steelMm(d) * (p.dents.length > 3 ? 0.9 : 1);
|
|
13443
|
+
return energy * (metal || d.material === "glass" ? piercing : 1) >= res;
|
|
13444
|
+
};
|
|
12265
13445
|
const resist = (d, p) => {
|
|
12266
13446
|
let base = RESIST[d.material] ?? 100;
|
|
12267
13447
|
if (d.material === "glass" && d.hp > 1e3) base = 120;
|
|
12268
|
-
if (d.material === "glass" &&
|
|
13448
|
+
if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
|
|
12269
13449
|
const k = ROLE_RESIST[d.role] ?? 1;
|
|
12270
13450
|
const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
|
|
12271
13451
|
return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
|
|
@@ -12288,7 +13468,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12288
13468
|
if (vis) {
|
|
12289
13469
|
const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
|
|
12290
13470
|
if (fluid === "coolant") kit.addEmitter(c, new V3(d.box.center[0], d.box.center[1] + 0.2, d.box.center[2]), "white", { rate: 5, s: 0.8 });
|
|
12291
|
-
|
|
13471
|
+
if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : fluid === "brake_fluid" ? 0.4 : 1.1);
|
|
12292
13472
|
}
|
|
12293
13473
|
}
|
|
12294
13474
|
const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
|
|
@@ -12325,7 +13505,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12325
13505
|
exploded = true;
|
|
12326
13506
|
const d = tank.def;
|
|
12327
13507
|
const spec = d.explodes ?? { energy: 8e5, radius: 5, atC: 320 };
|
|
12328
|
-
out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius }));
|
|
13508
|
+
out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius, ..."effect" in spec && spec.effect !== void 0 ? { effect: spec.effect } : {} }));
|
|
12329
13509
|
tank.liters = 0;
|
|
12330
13510
|
leaks.delete(d.id);
|
|
12331
13511
|
if (vis) kit.explode(c, { delay: 0.6 });
|
|
@@ -12417,15 +13597,15 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12417
13597
|
sync();
|
|
12418
13598
|
const M = toFrameMatrix();
|
|
12419
13599
|
const v = toLocal(h.point);
|
|
12420
|
-
const p = locate(v, 0.2);
|
|
13600
|
+
const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
|
|
12421
13601
|
if (!p) return;
|
|
12422
13602
|
const d = p.def;
|
|
12423
13603
|
const E = h.energy ?? 0;
|
|
12424
13604
|
const cal = h.caliber ?? 9;
|
|
12425
13605
|
const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
|
|
12426
|
-
const n = normalOf(d, v);
|
|
13606
|
+
const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
|
|
12427
13607
|
const res = resist(d, p);
|
|
12428
|
-
const through = E
|
|
13608
|
+
const through = goesThrough(h, d, p, E, res);
|
|
12429
13609
|
const cosInc = Math.abs(dir.dot(n));
|
|
12430
13610
|
const metal = d.material === "steel" || d.material === "aluminium";
|
|
12431
13611
|
const worldPoint = toWorld(v);
|
|
@@ -12436,13 +13616,21 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12436
13616
|
vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
|
|
12437
13617
|
return;
|
|
12438
13618
|
}
|
|
12439
|
-
const
|
|
13619
|
+
const opens = (h.piercing ?? 1) < SOFT_LEAD ? EXPANDED_HOLE : 1;
|
|
13620
|
+
const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1) * opens);
|
|
13621
|
+
const energyLeft = () => {
|
|
13622
|
+
const metal2 = d.material === "steel" || d.material === "aluminium";
|
|
13623
|
+
const mm = h.penetration?.steel;
|
|
13624
|
+
const spent = metal2 && mm ? clamp(steelMm(d) / mm, MIN_SPENT, 1) : clamp(res / Math.max(E, 1), MIN_SPENT, 1);
|
|
13625
|
+
return E * (1 - spent);
|
|
13626
|
+
};
|
|
12440
13627
|
const push = (hole) => {
|
|
12441
13628
|
if (!hole) return;
|
|
12442
13629
|
const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
|
|
12443
13630
|
p.holes.push(hp);
|
|
12444
|
-
out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
|
|
12445
|
-
vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
|
|
13631
|
+
out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { energyLeft: r2(energyLeft()) } : {}, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
|
|
13632
|
+
vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
|
|
13633
|
+
if (d.role === "mirror") vis?.shatterMirror(d.id);
|
|
12446
13634
|
startLeak(p, v, cal, out, dir);
|
|
12447
13635
|
};
|
|
12448
13636
|
switch (d.material) {
|
|
@@ -12455,7 +13643,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12455
13643
|
const crack = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), length: r2(0.15 + force * 0.35), rays: 6 + Math.round(force * 6) };
|
|
12456
13644
|
p.cracks.push(crack);
|
|
12457
13645
|
out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
|
|
12458
|
-
damage(p, E * 0.5, "ballistic", h.source, out, worldPoint);
|
|
13646
|
+
damage(p, E * (armourLevel && !through ? armourLevel.glassTakes : 0.5), "ballistic", h.source, out, worldPoint);
|
|
12459
13647
|
} else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
|
|
12460
13648
|
break;
|
|
12461
13649
|
}
|
|
@@ -12463,8 +13651,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12463
13651
|
push(through);
|
|
12464
13652
|
damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
|
|
12465
13653
|
if (through && !p.lost && !p.flat) {
|
|
12466
|
-
p
|
|
12467
|
-
|
|
13654
|
+
if (E >= TYRE_BURST_JOULES || cal >= TYRE_BURST_CALIBER) deflate(p, 1);
|
|
13655
|
+
else p.airLoss += TYRE_LEAK_PER_MM2 * Math.PI * (cal / 2) ** 2;
|
|
12468
13656
|
}
|
|
12469
13657
|
break;
|
|
12470
13658
|
}
|
|
@@ -12580,7 +13768,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12580
13768
|
const electric = c.spec.fuel === "electric";
|
|
12581
13769
|
const tank = tankPart();
|
|
12582
13770
|
const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
|
|
12583
|
-
const engineOk = sound("engine") && !exploded && fuel > 0.05;
|
|
13771
|
+
const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
|
|
12584
13772
|
const powered = sound("battery") && sound("alternator");
|
|
12585
13773
|
const gearOk = sound("gearbox");
|
|
12586
13774
|
const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
|
|
@@ -12740,9 +13928,62 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12740
13928
|
const state = () => stateCache ?? (stateCache = buildState());
|
|
12741
13929
|
const isSimulating = () => {
|
|
12742
13930
|
if (leaks.size > 0) return true;
|
|
12743
|
-
for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
|
|
13931
|
+
for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT || p.airLoss > 0 && p.air > 0)) return true;
|
|
12744
13932
|
return false;
|
|
12745
13933
|
};
|
|
13934
|
+
let engineHeat = ENGINE_WORKING_C;
|
|
13935
|
+
let seized = false;
|
|
13936
|
+
let steaming = false;
|
|
13937
|
+
const surfaceRays = new THREE.Raycaster();
|
|
13938
|
+
surfaceRays.layers.enableAll();
|
|
13939
|
+
surfaceRays.camera = new THREE.PerspectiveCamera();
|
|
13940
|
+
let meshParts = null;
|
|
13941
|
+
const paneParts = () => {
|
|
13942
|
+
if (meshParts) return meshParts;
|
|
13943
|
+
meshParts = /* @__PURE__ */ new Map();
|
|
13944
|
+
for (const d of model.parts) {
|
|
13945
|
+
if (d.role !== "glass") continue;
|
|
13946
|
+
const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
|
|
13947
|
+
for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
|
|
13948
|
+
}
|
|
13949
|
+
return meshParts;
|
|
13950
|
+
};
|
|
13951
|
+
const isDrawnSurface = (o) => {
|
|
13952
|
+
for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
|
|
13953
|
+
return true;
|
|
13954
|
+
};
|
|
13955
|
+
const partOfSurface = (o, v) => {
|
|
13956
|
+
const pane = paneParts().get(o);
|
|
13957
|
+
if (pane) return ps.get(pane) ?? null;
|
|
13958
|
+
for (let q = o; q && q !== c.vehicle; q = q.parent) {
|
|
13959
|
+
if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
|
|
13960
|
+
const piece = vis?.extraIdOf(q);
|
|
13961
|
+
if (piece) return ps.get(piece) ?? null;
|
|
13962
|
+
}
|
|
13963
|
+
return locate(v, 0.08, (p) => p.def.role === "glass");
|
|
13964
|
+
};
|
|
13965
|
+
const surfaceMeshes = [];
|
|
13966
|
+
const collectSurfaces = (o) => {
|
|
13967
|
+
if (!o.visible || o.userData.decal || o.userData.ghost || o.userData.noSurfaceRay) return;
|
|
13968
|
+
if (o.isMesh) surfaceMeshes.push(o);
|
|
13969
|
+
for (const child of o.children) collectSurfaces(child);
|
|
13970
|
+
};
|
|
13971
|
+
const surfaceRay = (ray) => {
|
|
13972
|
+
sync();
|
|
13973
|
+
surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
|
|
13974
|
+
surfaceRays.far = ray.maxDistance;
|
|
13975
|
+
surfaceMeshes.length = 0;
|
|
13976
|
+
for (const child of c.vehicle.children) collectSurfaces(child);
|
|
13977
|
+
for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
|
|
13978
|
+
if (!h.face || !isDrawnSurface(h.object)) continue;
|
|
13979
|
+
const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
|
|
13980
|
+
if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
|
|
13981
|
+
const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
|
|
13982
|
+
if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
|
|
13983
|
+
return { part: p.def.id, point: [h.point.x, h.point.y, h.point.z], normal: [n.x, n.y, n.z], distance: h.distance, pathLen: 0.02 };
|
|
13984
|
+
}
|
|
13985
|
+
return null;
|
|
13986
|
+
};
|
|
12746
13987
|
const hit = (h) => {
|
|
12747
13988
|
stateCache = void 0;
|
|
12748
13989
|
const out = [];
|
|
@@ -12767,7 +14008,29 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12767
14008
|
applyEffects();
|
|
12768
14009
|
return out;
|
|
12769
14010
|
};
|
|
14011
|
+
const runFlat = armourId != null;
|
|
14012
|
+
const tyreHealth = (p) => {
|
|
14013
|
+
if (p.lost) return RIM_HEALTH;
|
|
14014
|
+
if (p.flat) return runFlat ? TYRE_RUNFLAT_HEALTH : TYRE_FLAT_HEALTH;
|
|
14015
|
+
return Math.min(p.hp / p.def.hp, 0.2 + 0.8 * p.air);
|
|
14016
|
+
};
|
|
14017
|
+
const tyreOf = (rim) => {
|
|
14018
|
+
let best;
|
|
14019
|
+
let bestD = Infinity;
|
|
14020
|
+
for (const p of ps.values()) {
|
|
14021
|
+
if (p.def.role !== "tyre") continue;
|
|
14022
|
+
const d = Math.hypot(p.def.box.center[0] - rim.def.box.center[0], p.def.box.center[2] - rim.def.box.center[2]);
|
|
14023
|
+
if (d < bestD) [best, bestD] = [p, d];
|
|
14024
|
+
}
|
|
14025
|
+
return best;
|
|
14026
|
+
};
|
|
14027
|
+
const deflate = (p, share) => {
|
|
14028
|
+
p.air = Math.max(0, p.air - share);
|
|
14029
|
+
vis?.tyreFlat(p.def, TYRE_FLAT_SAG * (1 - p.air));
|
|
14030
|
+
if (p.air <= TYRE_FLAT_AIR) p.flat = true;
|
|
14031
|
+
};
|
|
12770
14032
|
const update = (dt) => {
|
|
14033
|
+
for (const p of ps.values()) if (p.airLoss > 0 && p.air > 0 && !p.lost) deflate(p, p.airLoss * dt);
|
|
12771
14034
|
clock += dt;
|
|
12772
14035
|
const out = queue;
|
|
12773
14036
|
queue = [];
|
|
@@ -12865,9 +14128,14 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12865
14128
|
},
|
|
12866
14129
|
describe,
|
|
12867
14130
|
hit,
|
|
14131
|
+
surfaceRay,
|
|
12868
14132
|
update,
|
|
12869
14133
|
state,
|
|
12870
14134
|
reset,
|
|
14135
|
+
dispose: () => {
|
|
14136
|
+
releaseCrease?.();
|
|
14137
|
+
releaseCrease = void 0;
|
|
14138
|
+
},
|
|
12871
14139
|
disassemble,
|
|
12872
14140
|
extinguish,
|
|
12873
14141
|
serialize: () => ({
|
|
@@ -12878,6 +14146,70 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
|
|
|
12878
14146
|
partName: (pid) => ps.get(pid)?.def.name ?? pid,
|
|
12879
14147
|
status: computeStatus,
|
|
12880
14148
|
animations: () => createVehicleClips(model, vis, kit, c),
|
|
14149
|
+
crumple: (point, direction, depth) => {
|
|
14150
|
+
if (!vis || depth <= 0) return;
|
|
14151
|
+
sync();
|
|
14152
|
+
const force = clamp((depth - CRUMPLE_BASE_M) / CRUMPLE_PER_FORCE_M, CRUMPLE_MIN_FORCE, CRUMPLE_MAX_FORCE);
|
|
14153
|
+
kit.impact(c, toLocal(point), dirToLocal(direction).normalize(), force, { maxDepth: depth });
|
|
14154
|
+
stateCache = void 0;
|
|
14155
|
+
},
|
|
14156
|
+
rollFlat: (meters, speed) => {
|
|
14157
|
+
for (const p of ps.values()) {
|
|
14158
|
+
if (p.def.role !== "tyre" || !p.flat || p.lost) continue;
|
|
14159
|
+
const life = runFlat ? RUN_FLAT_LIFE_M * Math.min(1, RUN_FLAT_SPEED / Math.max(speed, 1)) : FLAT_TYRE_LIFE_M * clamp(FLAT_TYRE_TOWN_SPEED / Math.max(speed, 1), 0.2, 1.5);
|
|
14160
|
+
p.hp = Math.max(0, p.hp - p.def.hp * meters / life);
|
|
14161
|
+
if (p.hp > 0) continue;
|
|
14162
|
+
const at = toWorld(p.def.box.center);
|
|
14163
|
+
lose(p, "broken", queue, at);
|
|
14164
|
+
queue.push(ev({ type: "debris", part: p.def.id, kind: "fibers", point: at, normal: [0, 1, 0], count: 12, speed: Math.max(2, speed * 0.5) }));
|
|
14165
|
+
}
|
|
14166
|
+
let grinding = 0;
|
|
14167
|
+
for (const rim of ps.values()) {
|
|
14168
|
+
if (rim.def.role !== "rim" || rim.lost || !tyreOf(rim)?.lost) continue;
|
|
14169
|
+
grinding += 1;
|
|
14170
|
+
rim.hp = Math.max(1, rim.hp - rim.def.hp * RIM_GRIND_PER_M * meters);
|
|
14171
|
+
const [x, y, z] = rim.def.box.center;
|
|
14172
|
+
queue.push(ev({ type: "debris", part: rim.def.id, kind: "sparks", point: toWorld([x, y - rim.def.box.size[1] / 2, z]), normal: [0, 1, 0], count: Math.min(30, Math.ceil(4 + speed)), speed: Math.max(2, speed * 0.4) }));
|
|
14173
|
+
}
|
|
14174
|
+
stateCache = void 0;
|
|
14175
|
+
return grinding;
|
|
14176
|
+
},
|
|
14177
|
+
runEngine: (seconds, load) => {
|
|
14178
|
+
const engine = [...ps.values()].find((p) => p.def.role === "engine");
|
|
14179
|
+
if (!engine || engine.lost || seized) return engineHeat;
|
|
14180
|
+
const share = (fluid) => {
|
|
14181
|
+
const list = [...ps.values()].filter((p) => p.def.contains?.fluid === fluid);
|
|
14182
|
+
return list.length ? Math.min(...list.map((p) => (p.liters ?? 0) / p.def.contains.liters)) : 1;
|
|
14183
|
+
};
|
|
14184
|
+
const coolant = share("coolant");
|
|
14185
|
+
const oil = share("oil");
|
|
14186
|
+
const work = 0.4 + 0.6 * clamp(load, 0, 1);
|
|
14187
|
+
if (coolant < COOLANT_NEEDED) engineHeat += (COOLANT_NEEDED - coolant) / COOLANT_NEEDED * ENGINE_HEATING * work * seconds;
|
|
14188
|
+
else engineHeat += (ENGINE_WORKING_C - engineHeat) * Math.min(1, seconds / ENGINE_SETTLE_S);
|
|
14189
|
+
if (oil < OIL_NEEDED) engine.hp = Math.max(0, engine.hp - engine.def.hp * OIL_WEAR * work * seconds * ((OIL_NEEDED - oil) / OIL_NEEDED));
|
|
14190
|
+
if (engineHeat >= ENGINE_STEAM_C && vis && !steaming) {
|
|
14191
|
+
steaming = true;
|
|
14192
|
+
kit.addEmitter(c, new V3(engine.def.box.center[0], engine.def.box.center[1] + 0.4, 0), "white", { rate: 8, s: 1 });
|
|
14193
|
+
}
|
|
14194
|
+
if (engineHeat >= ENGINE_SEIZE_C || engine.hp <= engine.def.hp * ENGINE_SEIZE_HP) {
|
|
14195
|
+
seized = true;
|
|
14196
|
+
queue.push(ev({ type: "damage", part: engine.def.id, amount: 0, hp: r2(engine.hp), damageType: "heat" }));
|
|
14197
|
+
}
|
|
14198
|
+
stateCache = void 0;
|
|
14199
|
+
return engineHeat;
|
|
14200
|
+
},
|
|
14201
|
+
driveReport: () => {
|
|
14202
|
+
const health = (role) => {
|
|
14203
|
+
const list = [...ps.values()].filter((p) => p.def.role === role);
|
|
14204
|
+
if (!list.length) return 1;
|
|
14205
|
+
return Math.min(...list.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
|
|
14206
|
+
};
|
|
14207
|
+
const tyres = [...ps.values()].filter((p) => p.def.role === "tyre").map((p) => ({ x: p.def.box.center[0], z: p.def.box.center[2], health: tyreHealth(p) }));
|
|
14208
|
+
const status = computeStatus();
|
|
14209
|
+
const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
|
|
14210
|
+
const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
|
|
14211
|
+
return { tyres, brakes: Math.min(1, 1 / status.brakeFactor) * (BRAKE_NO_FLUID + (1 - BRAKE_NO_FLUID) * fluidLeft), engine: status.canStart ? health("engine") : 0, suspension: health("shocks") };
|
|
14212
|
+
},
|
|
12881
14213
|
partWorldCenter: (pid) => {
|
|
12882
14214
|
sync();
|
|
12883
14215
|
const d = ps.get(pid)?.def;
|
|
@@ -12906,6 +14238,90 @@ function buildVehicleItem(kit, key, options = {}) {
|
|
|
12906
14238
|
return { root, vehicle };
|
|
12907
14239
|
}
|
|
12908
14240
|
|
|
14241
|
+
// src/destructible/crease-queue.ts
|
|
14242
|
+
import { DEFERRED_QUEUE_ID } from "@adatechnology/game-contracts";
|
|
14243
|
+
function createCreaseQueue() {
|
|
14244
|
+
const tracked = [];
|
|
14245
|
+
let cursor = 0;
|
|
14246
|
+
let lastFrame;
|
|
14247
|
+
let attached = false;
|
|
14248
|
+
const deferred = {
|
|
14249
|
+
id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
|
|
14250
|
+
pending(frame) {
|
|
14251
|
+
lastFrame = frame;
|
|
14252
|
+
let units = 0;
|
|
14253
|
+
for (const work of tracked) units += work.pendingUnits(frame);
|
|
14254
|
+
return units;
|
|
14255
|
+
},
|
|
14256
|
+
run(deadline, clock) {
|
|
14257
|
+
let ran = 0;
|
|
14258
|
+
let skipped = 0;
|
|
14259
|
+
while (tracked.length > 0 && skipped < tracked.length) {
|
|
14260
|
+
if (ran > 0 && clock() >= deadline) break;
|
|
14261
|
+
const work = tracked[cursor % tracked.length];
|
|
14262
|
+
cursor = (cursor + 1) % tracked.length;
|
|
14263
|
+
if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
|
|
14264
|
+
ran++;
|
|
14265
|
+
skipped = 0;
|
|
14266
|
+
} else skipped++;
|
|
14267
|
+
}
|
|
14268
|
+
return ran;
|
|
14269
|
+
},
|
|
14270
|
+
flush() {
|
|
14271
|
+
for (const work of tracked) work.finishAll();
|
|
14272
|
+
},
|
|
14273
|
+
attach() {
|
|
14274
|
+
attached = true;
|
|
14275
|
+
for (const work of tracked) work.setQueued(true);
|
|
14276
|
+
},
|
|
14277
|
+
detach() {
|
|
14278
|
+
attached = false;
|
|
14279
|
+
for (const work of tracked) work.setQueued(false);
|
|
14280
|
+
}
|
|
14281
|
+
};
|
|
14282
|
+
return {
|
|
14283
|
+
deferredQueue: () => deferred,
|
|
14284
|
+
track(work) {
|
|
14285
|
+
tracked.push(work);
|
|
14286
|
+
work.setQueued(attached);
|
|
14287
|
+
return () => {
|
|
14288
|
+
const at = tracked.indexOf(work);
|
|
14289
|
+
if (at < 0) return;
|
|
14290
|
+
tracked.splice(at, 1);
|
|
14291
|
+
work.setQueued(false);
|
|
14292
|
+
};
|
|
14293
|
+
}
|
|
14294
|
+
};
|
|
14295
|
+
}
|
|
14296
|
+
|
|
14297
|
+
// src/destructible/crash.ts
|
|
14298
|
+
var CRASH_RULES = { energyToDamage: 0.3, hitSpacing: 0.6, minSpeed: 1.5, rows: [0, 0.3], crushPerMs: 0.035, maxCrush: 0.6 };
|
|
14299
|
+
function crashDeltaV(speed, massKg, otherMassKg) {
|
|
14300
|
+
if (!Number.isFinite(otherMassKg)) return speed;
|
|
14301
|
+
return speed * otherMassKg / (massKg + otherMassKg);
|
|
14302
|
+
}
|
|
14303
|
+
function crashVehicle(d, crash) {
|
|
14304
|
+
const rules = { ...CRASH_RULES, ...crash.rules };
|
|
14305
|
+
const deltaV = crashDeltaV(crash.speed, crash.massKg, crash.otherMassKg);
|
|
14306
|
+
const energy = 0.5 * crash.massKg * deltaV * deltaV;
|
|
14307
|
+
if (crash.speed < rules.minSpeed) return { deltaV, energy, events: [] };
|
|
14308
|
+
const [dx, , dz] = crash.direction;
|
|
14309
|
+
const length = Math.hypot(dx, dz) || 1;
|
|
14310
|
+
const across = [-dz / length, 0, dx / length];
|
|
14311
|
+
const hits = Math.max(1, Math.round(crash.contactWidth / rules.hitSpacing));
|
|
14312
|
+
const each = energy * rules.energyToDamage / (hits * rules.rows.length);
|
|
14313
|
+
const events = [];
|
|
14314
|
+
for (const up of rules.rows)
|
|
14315
|
+
for (let i = 0; i < hits; i++) {
|
|
14316
|
+
const offset = hits === 1 ? 0 : (i / (hits - 1) - 0.5) * crash.contactWidth;
|
|
14317
|
+
const point = [crash.point[0] + across[0] * offset, crash.point[1] + up, crash.point[2] + across[2] * offset];
|
|
14318
|
+
events.push(...d.hit({ type: "blunt", point, direction: [dx / length, 0, dz / length], energy: each, radius: rules.hitSpacing / 2, ...crash.source ? { source: crash.source } : {} }));
|
|
14319
|
+
}
|
|
14320
|
+
const crush = Math.min(rules.maxCrush, deltaV * rules.crushPerMs);
|
|
14321
|
+
d.crumple(crash.point, [dx / length, 0, dz / length], crush);
|
|
14322
|
+
return { deltaV, energy, events };
|
|
14323
|
+
}
|
|
14324
|
+
|
|
12909
14325
|
// src/destructible/enclosure.ts
|
|
12910
14326
|
var CABIN_LEAK = 0.02;
|
|
12911
14327
|
var HOLE_RATE_PER_AREA = 3e3;
|
|
@@ -12990,16 +14406,23 @@ function cabinEnclosure(source, options = {}) {
|
|
|
12990
14406
|
};
|
|
12991
14407
|
}
|
|
12992
14408
|
export {
|
|
14409
|
+
ARMOUR_LEVELS,
|
|
12993
14410
|
CABIN_LEAK,
|
|
14411
|
+
CRASH_RULES,
|
|
12994
14412
|
JOINT_RULES,
|
|
12995
14413
|
LEGACY_STATES,
|
|
12996
14414
|
LEGACY_ZONE_IDS,
|
|
12997
14415
|
SEED_SALTS,
|
|
14416
|
+
armourLevelOf,
|
|
12998
14417
|
buildVehicleItem,
|
|
12999
14418
|
buildVehicleModel,
|
|
13000
14419
|
cabinBoxOf,
|
|
13001
14420
|
cabinEnclosure,
|
|
13002
14421
|
clamp,
|
|
14422
|
+
crackedGlassMaterial,
|
|
14423
|
+
crashDeltaV,
|
|
14424
|
+
crashVehicle,
|
|
14425
|
+
createCreaseQueue,
|
|
13003
14426
|
createLod,
|
|
13004
14427
|
createTranslate,
|
|
13005
14428
|
createVehicleDestructible,
|