@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.global.js
CHANGED
|
@@ -23,16 +23,23 @@ var GameVehicles = (() => {
|
|
|
23
23
|
// src/index.ts
|
|
24
24
|
var index_exports = {};
|
|
25
25
|
__export(index_exports, {
|
|
26
|
+
ARMOUR_LEVELS: () => ARMOUR_LEVELS,
|
|
26
27
|
CABIN_LEAK: () => CABIN_LEAK,
|
|
28
|
+
CRASH_RULES: () => CRASH_RULES,
|
|
27
29
|
JOINT_RULES: () => JOINT_RULES,
|
|
28
30
|
LEGACY_STATES: () => LEGACY_STATES,
|
|
29
31
|
LEGACY_ZONE_IDS: () => LEGACY_ZONE_IDS,
|
|
30
32
|
SEED_SALTS: () => SEED_SALTS,
|
|
33
|
+
armourLevelOf: () => armourLevelOf,
|
|
31
34
|
buildVehicleItem: () => buildVehicleItem,
|
|
32
35
|
buildVehicleModel: () => buildVehicleModel,
|
|
33
36
|
cabinBoxOf: () => cabinBoxOf,
|
|
34
37
|
cabinEnclosure: () => cabinEnclosure,
|
|
35
38
|
clamp: () => clamp,
|
|
39
|
+
crackedGlassMaterial: () => crackedGlassMaterial,
|
|
40
|
+
crashDeltaV: () => crashDeltaV,
|
|
41
|
+
crashVehicle: () => crashVehicle,
|
|
42
|
+
createCreaseQueue: () => createCreaseQueue,
|
|
36
43
|
createLod: () => createLod,
|
|
37
44
|
createTranslate: () => createTranslate,
|
|
38
45
|
createVehicleDestructible: () => createVehicleDestructible,
|
|
@@ -55,7 +62,7 @@ var GameVehicles = (() => {
|
|
|
55
62
|
wrapThreeForLod: () => wrapThreeForLod
|
|
56
63
|
});
|
|
57
64
|
|
|
58
|
-
// ../game-shapes/dist/chunk-
|
|
65
|
+
// ../game-shapes/dist/chunk-M5N2SHNL.js
|
|
59
66
|
function canonicalJson(value) {
|
|
60
67
|
if (value === null) return "null";
|
|
61
68
|
if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
|
|
@@ -1302,8 +1309,8 @@ var GameVehicles = (() => {
|
|
|
1302
1309
|
groups: []
|
|
1303
1310
|
};
|
|
1304
1311
|
}
|
|
1305
|
-
function make(
|
|
1306
|
-
switch (kind) {
|
|
1312
|
+
function make(p) {
|
|
1313
|
+
switch (p.kind) {
|
|
1307
1314
|
case "box": {
|
|
1308
1315
|
const size = p.size ?? [1, 1, 1];
|
|
1309
1316
|
const segments = p.segments ?? [1, 1, 1];
|
|
@@ -1332,7 +1339,7 @@ var GameVehicles = (() => {
|
|
|
1332
1339
|
case "mesh":
|
|
1333
1340
|
return mesh(p);
|
|
1334
1341
|
default:
|
|
1335
|
-
throw new Error(`game-shapes: unknown primitive kind "${String(kind)}"`);
|
|
1342
|
+
throw new Error(`game-shapes: unknown primitive kind "${String(p.kind)}"`);
|
|
1336
1343
|
}
|
|
1337
1344
|
}
|
|
1338
1345
|
var templates = /* @__PURE__ */ new Map();
|
|
@@ -1340,7 +1347,7 @@ var GameVehicles = (() => {
|
|
|
1340
1347
|
var stats = { hits: 0, misses: 0 };
|
|
1341
1348
|
function buildPrimitive(node, options = {}) {
|
|
1342
1349
|
const parameters = primitiveParameters(node, options);
|
|
1343
|
-
if (node.kind === "mesh" || templateLimit === 0) return make(node
|
|
1350
|
+
if (node.kind === "mesh" || templateLimit === 0) return make({ ...node, ...parameters });
|
|
1344
1351
|
const key = `${node.kind}:${canonicalJson(parameters)}`;
|
|
1345
1352
|
const cached = templates.get(key);
|
|
1346
1353
|
if (cached) {
|
|
@@ -1350,7 +1357,7 @@ var GameVehicles = (() => {
|
|
|
1350
1357
|
return cached;
|
|
1351
1358
|
}
|
|
1352
1359
|
stats.misses++;
|
|
1353
|
-
const built = make(node
|
|
1360
|
+
const built = make({ ...node, ...parameters });
|
|
1354
1361
|
templates.set(key, built);
|
|
1355
1362
|
if (templates.size > templateLimit) templates.delete(templates.keys().next().value);
|
|
1356
1363
|
return built;
|
|
@@ -1443,6 +1450,127 @@ var GameVehicles = (() => {
|
|
|
1443
1450
|
custom: { fields: { generator: str, params: () => null }, required: ["generator"] }
|
|
1444
1451
|
};
|
|
1445
1452
|
|
|
1453
|
+
// ../game-contracts/dist/index.js
|
|
1454
|
+
var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
|
|
1455
|
+
var DEFAULT_IGNITION_RULES = {
|
|
1456
|
+
fillCurve: [
|
|
1457
|
+
[0, 0.6],
|
|
1458
|
+
[0.1, 1],
|
|
1459
|
+
[0.35, 0.6],
|
|
1460
|
+
[0.65, 0.3],
|
|
1461
|
+
[0.9, 0.15],
|
|
1462
|
+
[1, 0.12]
|
|
1463
|
+
],
|
|
1464
|
+
flameFillInfluence: 0.5,
|
|
1465
|
+
flameDefaults: { incendiary: 0.7, tracer: 0.2 },
|
|
1466
|
+
sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
|
|
1467
|
+
sparkMetalDefault: 0.5,
|
|
1468
|
+
sparkFluid: { diesel: 0.15 },
|
|
1469
|
+
fireNear: { radius: 1.5, chancePerSecond: 0.5 }
|
|
1470
|
+
};
|
|
1471
|
+
var resolveIgnitionRules = (rules) => ({
|
|
1472
|
+
...DEFAULT_IGNITION_RULES,
|
|
1473
|
+
...rules,
|
|
1474
|
+
flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
|
|
1475
|
+
sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
|
|
1476
|
+
sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
|
|
1477
|
+
fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
|
|
1478
|
+
});
|
|
1479
|
+
var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
|
|
1480
|
+
if (curve.length === 0) return 1;
|
|
1481
|
+
const f = Math.min(1, Math.max(0, fill));
|
|
1482
|
+
if (f <= curve[0][0]) return curve[0][1];
|
|
1483
|
+
for (let i = 1; i < curve.length; i++) {
|
|
1484
|
+
const [x1, y1] = curve[i];
|
|
1485
|
+
const [x0, y0] = curve[i - 1];
|
|
1486
|
+
if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
|
|
1487
|
+
}
|
|
1488
|
+
return curve[curve.length - 1][1];
|
|
1489
|
+
};
|
|
1490
|
+
var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
|
|
1491
|
+
var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
|
|
1492
|
+
const k = fillFactor(target.fill, rules.fillCurve);
|
|
1493
|
+
const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
|
|
1494
|
+
const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
|
|
1495
|
+
const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
|
|
1496
|
+
return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
|
|
1497
|
+
};
|
|
1498
|
+
var DEFERRED_QUEUE_ID = {
|
|
1499
|
+
DESTRUCTION_EVENTS: "destruction.events",
|
|
1500
|
+
BREACH_PAINT: "floorplan.breach-paint",
|
|
1501
|
+
AUDIO_VOICES: "audio.voices",
|
|
1502
|
+
VEHICLE_CREASE: "vehicles.crease"
|
|
1503
|
+
};
|
|
1504
|
+
var DEFERRED_PRIORITY = {
|
|
1505
|
+
[DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
|
|
1506
|
+
[DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
|
|
1507
|
+
[DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
|
|
1508
|
+
[DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
|
|
1509
|
+
};
|
|
1510
|
+
var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
|
|
1511
|
+
var warnedKeys = () => {
|
|
1512
|
+
const holder = globalThis;
|
|
1513
|
+
return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
|
|
1514
|
+
};
|
|
1515
|
+
var isProduction = () => {
|
|
1516
|
+
try {
|
|
1517
|
+
return process.env.NODE_ENV === "production";
|
|
1518
|
+
} catch {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
|
|
1523
|
+
var warnOnce = (key, message) => {
|
|
1524
|
+
if (isQuiet() || isProduction()) return;
|
|
1525
|
+
const seen = warnedKeys();
|
|
1526
|
+
if (seen.has(key)) return;
|
|
1527
|
+
seen.add(key);
|
|
1528
|
+
console.warn(`[game-kit] ${message}`);
|
|
1529
|
+
};
|
|
1530
|
+
var KIT_ORIGIN_KEY = "kitOrigin";
|
|
1531
|
+
function markKitOrigin(object2, origin) {
|
|
1532
|
+
const node = object2;
|
|
1533
|
+
node.userData = node.userData ?? {};
|
|
1534
|
+
node.userData[KIT_ORIGIN_KEY] = { ...origin };
|
|
1535
|
+
return object2;
|
|
1536
|
+
}
|
|
1537
|
+
var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
|
|
1538
|
+
var GAIT_IDS = ["walk", "run"];
|
|
1539
|
+
var LANDING_IDS = ["light", "heavy"];
|
|
1540
|
+
var BODY_SOUND_IDS = [
|
|
1541
|
+
"jump",
|
|
1542
|
+
"cloth_rustle",
|
|
1543
|
+
"body_fall",
|
|
1544
|
+
"fall_damage",
|
|
1545
|
+
"player_hurt",
|
|
1546
|
+
"player_breath_run",
|
|
1547
|
+
"player_death",
|
|
1548
|
+
"bite",
|
|
1549
|
+
"grab",
|
|
1550
|
+
"punch_swing",
|
|
1551
|
+
"punch_hit_body",
|
|
1552
|
+
"punch_hit_face",
|
|
1553
|
+
"punch_hit_object",
|
|
1554
|
+
"kick_swing",
|
|
1555
|
+
"kick_hit",
|
|
1556
|
+
"kick_hit_object",
|
|
1557
|
+
"shove_two_hands",
|
|
1558
|
+
"knife_swing",
|
|
1559
|
+
"knife_stab",
|
|
1560
|
+
"knife_slash",
|
|
1561
|
+
"knife_hit_object",
|
|
1562
|
+
"blunt_swing",
|
|
1563
|
+
"blunt_hit",
|
|
1564
|
+
"blunt_hit_object",
|
|
1565
|
+
"axe_swing",
|
|
1566
|
+
"axe_chop",
|
|
1567
|
+
"axe_hit_object"
|
|
1568
|
+
];
|
|
1569
|
+
var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
|
|
1570
|
+
var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
|
|
1571
|
+
var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
|
|
1572
|
+
var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
|
|
1573
|
+
|
|
1446
1574
|
// ../game-shapes-three/dist/index.js
|
|
1447
1575
|
var UV_BEFORE_NORMAL = /* @__PURE__ */ new Set(["LatheGeometry", "ExtrudeGeometry"]);
|
|
1448
1576
|
var INDEX_FIRST = /* @__PURE__ */ new Set(["BoxGeometry", "CylinderGeometry", "ConeGeometry", "SphereGeometry", "PlaneGeometry", "CircleGeometry", "RingGeometry", "LatheGeometry", "TorusGeometry"]);
|
|
@@ -2459,7 +2587,7 @@ var GameVehicles = (() => {
|
|
|
2459
2587
|
}
|
|
2460
2588
|
return ti.builtBlocks === total;
|
|
2461
2589
|
}
|
|
2462
|
-
function start(geo) {
|
|
2590
|
+
function start(geo, store = cache) {
|
|
2463
2591
|
const pos = geo.attributes.position;
|
|
2464
2592
|
const triangles = trianglesOf(geo);
|
|
2465
2593
|
const ti = {
|
|
@@ -2472,7 +2600,7 @@ var GameVehicles = (() => {
|
|
|
2472
2600
|
blocks: new Float64Array(blockCount(triangles) * 6),
|
|
2473
2601
|
builtBlocks: 0
|
|
2474
2602
|
};
|
|
2475
|
-
|
|
2603
|
+
store.set(geo, ti);
|
|
2476
2604
|
return ti;
|
|
2477
2605
|
}
|
|
2478
2606
|
function triIndexOf(geo) {
|
|
@@ -2524,6 +2652,106 @@ var GameVehicles = (() => {
|
|
|
2524
2652
|
}
|
|
2525
2653
|
return { box: box2, version: ti.positionVersion };
|
|
2526
2654
|
}
|
|
2655
|
+
var rayCache = /* @__PURE__ */ new WeakMap();
|
|
2656
|
+
function rayIndexOf(geo) {
|
|
2657
|
+
const shared = cache.get(geo);
|
|
2658
|
+
if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
|
|
2659
|
+
let ti = rayCache.get(geo);
|
|
2660
|
+
if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
|
|
2661
|
+
advance(geo, ti, Infinity);
|
|
2662
|
+
return ti;
|
|
2663
|
+
}
|
|
2664
|
+
var BLOCK_SLACK = 1e-6;
|
|
2665
|
+
var scratch = /* @__PURE__ */ new WeakMap();
|
|
2666
|
+
function scratchOf(THREE) {
|
|
2667
|
+
let s = scratch.get(THREE);
|
|
2668
|
+
if (!s) {
|
|
2669
|
+
s = {
|
|
2670
|
+
inverse: new THREE.Matrix4(),
|
|
2671
|
+
local: new THREE.Ray(),
|
|
2672
|
+
box: new THREE.Box3(),
|
|
2673
|
+
a: new THREE.Vector3(),
|
|
2674
|
+
b: new THREE.Vector3(),
|
|
2675
|
+
c: new THREE.Vector3(),
|
|
2676
|
+
point: new THREE.Vector3(),
|
|
2677
|
+
world: new THREE.Vector3(),
|
|
2678
|
+
sphere: new THREE.Sphere()
|
|
2679
|
+
};
|
|
2680
|
+
scratch.set(THREE, s);
|
|
2681
|
+
}
|
|
2682
|
+
return s;
|
|
2683
|
+
}
|
|
2684
|
+
function indexable(THREE, mesh2) {
|
|
2685
|
+
const geo = mesh2.geometry;
|
|
2686
|
+
if (mesh2.raycast !== THREE.Mesh.prototype.raycast) return false;
|
|
2687
|
+
if (!mesh2.isMesh || mesh2.isSkinnedMesh || mesh2.isInstancedMesh || mesh2.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
|
|
2688
|
+
if (!mesh2.material || Array.isArray(mesh2.material)) return false;
|
|
2689
|
+
if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
|
|
2690
|
+
return supports(geo);
|
|
2691
|
+
}
|
|
2692
|
+
function raycastMesh(THREE, raycaster, mesh2, out) {
|
|
2693
|
+
const geo = mesh2.geometry;
|
|
2694
|
+
const s = scratchOf(THREE);
|
|
2695
|
+
if (geo.boundingSphere === null) geo.computeBoundingSphere();
|
|
2696
|
+
s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh2.matrixWorld);
|
|
2697
|
+
if (!raycaster.ray.intersectsSphere(s.sphere)) return;
|
|
2698
|
+
const ti = rayIndexOf(geo);
|
|
2699
|
+
if (!ti) return;
|
|
2700
|
+
s.inverse.copy(mesh2.matrixWorld).invert();
|
|
2701
|
+
s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
|
|
2702
|
+
const material = mesh2.material;
|
|
2703
|
+
const back = material.side === THREE.BackSide;
|
|
2704
|
+
const cull = material.side === THREE.FrontSide;
|
|
2705
|
+
const P = geo.attributes.position;
|
|
2706
|
+
const ix = geo.index;
|
|
2707
|
+
const count = ix ? ix.count : P.count;
|
|
2708
|
+
const first = Math.max(0, geo.drawRange.start);
|
|
2709
|
+
const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
|
|
2710
|
+
const t0 = Math.ceil(first / 3);
|
|
2711
|
+
const t1 = Math.floor(last / 3);
|
|
2712
|
+
const B = ti.boxes;
|
|
2713
|
+
const K = ti.blocks;
|
|
2714
|
+
for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
|
|
2715
|
+
const o = blk * 6;
|
|
2716
|
+
s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
|
|
2717
|
+
s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
|
|
2718
|
+
if (!s.local.intersectsBox(s.box)) continue;
|
|
2719
|
+
const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
|
|
2720
|
+
for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
|
|
2721
|
+
const j = t * 6;
|
|
2722
|
+
s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
|
|
2723
|
+
s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
|
|
2724
|
+
if (!s.local.intersectsBox(s.box)) continue;
|
|
2725
|
+
const a = ix ? ix.getX(t * 3) : t * 3;
|
|
2726
|
+
const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
|
|
2727
|
+
const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
|
|
2728
|
+
s.a.fromBufferAttribute(P, a);
|
|
2729
|
+
s.b.fromBufferAttribute(P, b);
|
|
2730
|
+
s.c.fromBufferAttribute(P, c);
|
|
2731
|
+
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);
|
|
2732
|
+
if (hit === null) continue;
|
|
2733
|
+
s.world.copy(s.point).applyMatrix4(mesh2.matrixWorld);
|
|
2734
|
+
const distance = raycaster.ray.origin.distanceTo(s.world);
|
|
2735
|
+
if (distance < raycaster.near || distance > raycaster.far) continue;
|
|
2736
|
+
const normal = new THREE.Vector3();
|
|
2737
|
+
THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
|
|
2738
|
+
out.push({ distance, point: s.world.clone(), object: mesh2, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
function collect(THREE, raycaster, object2, recursive, out) {
|
|
2743
|
+
if (object2.layers.test(raycaster.layers)) {
|
|
2744
|
+
if (indexable(THREE, object2)) raycastMesh(THREE, raycaster, object2, out);
|
|
2745
|
+
else object2.raycast(raycaster, out);
|
|
2746
|
+
}
|
|
2747
|
+
if (recursive) for (const child of object2.children) collect(THREE, raycaster, child, true, out);
|
|
2748
|
+
}
|
|
2749
|
+
function raycastIndexed(THREE, raycaster, objects, recursive = false) {
|
|
2750
|
+
const out = [];
|
|
2751
|
+
for (const object2 of objects) collect(THREE, raycaster, object2, recursive, out);
|
|
2752
|
+
out.sort((x, y) => x.distance - y.distance);
|
|
2753
|
+
return out;
|
|
2754
|
+
}
|
|
2527
2755
|
|
|
2528
2756
|
// src/geometry.ts
|
|
2529
2757
|
function installGeometry(kit) {
|
|
@@ -3263,58 +3491,6 @@ var GameVehicles = (() => {
|
|
|
3263
3491
|
Object.assign(I, { roundedBoxGeometry, taper, subdivide, crease, creaseTask, refreshBounds, pathShape, tumble, loftGeometry, hullGeometry, cutGeometry, mergeGeometries, uvProject });
|
|
3264
3492
|
}
|
|
3265
3493
|
|
|
3266
|
-
// ../game-contracts/dist/index.js
|
|
3267
|
-
var DEFAULT_IGNITION_RULES = {
|
|
3268
|
-
fillCurve: [
|
|
3269
|
-
[0, 0.6],
|
|
3270
|
-
[0.1, 1],
|
|
3271
|
-
[0.35, 0.6],
|
|
3272
|
-
[0.65, 0.3],
|
|
3273
|
-
[0.9, 0.15],
|
|
3274
|
-
[1, 0.12]
|
|
3275
|
-
],
|
|
3276
|
-
flameFillInfluence: 0.5,
|
|
3277
|
-
flameDefaults: { incendiary: 0.7, tracer: 0.2 },
|
|
3278
|
-
sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
|
|
3279
|
-
sparkMetalDefault: 0.5,
|
|
3280
|
-
sparkFluid: { diesel: 0.15 },
|
|
3281
|
-
fireNear: { radius: 1.5, chancePerSecond: 0.5 }
|
|
3282
|
-
};
|
|
3283
|
-
var resolveIgnitionRules = (rules) => ({
|
|
3284
|
-
...DEFAULT_IGNITION_RULES,
|
|
3285
|
-
...rules,
|
|
3286
|
-
flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
|
|
3287
|
-
sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
|
|
3288
|
-
sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
|
|
3289
|
-
fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
|
|
3290
|
-
});
|
|
3291
|
-
var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
|
|
3292
|
-
if (curve.length === 0) return 1;
|
|
3293
|
-
const f = Math.min(1, Math.max(0, fill));
|
|
3294
|
-
if (f <= curve[0][0]) return curve[0][1];
|
|
3295
|
-
for (let i = 1; i < curve.length; i++) {
|
|
3296
|
-
const [x1, y1] = curve[i];
|
|
3297
|
-
const [x0, y0] = curve[i - 1];
|
|
3298
|
-
if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
|
|
3299
|
-
}
|
|
3300
|
-
return curve[curve.length - 1][1];
|
|
3301
|
-
};
|
|
3302
|
-
var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
|
|
3303
|
-
var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
|
|
3304
|
-
const k = fillFactor(target.fill, rules.fillCurve);
|
|
3305
|
-
const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
|
|
3306
|
-
const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
|
|
3307
|
-
const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
|
|
3308
|
-
return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
|
|
3309
|
-
};
|
|
3310
|
-
var KIT_ORIGIN_KEY = "kitOrigin";
|
|
3311
|
-
function markKitOrigin(object2, origin) {
|
|
3312
|
-
const node = object2;
|
|
3313
|
-
node.userData = node.userData ?? {};
|
|
3314
|
-
node.userData[KIT_ORIGIN_KEY] = { ...origin };
|
|
3315
|
-
return object2;
|
|
3316
|
-
}
|
|
3317
|
-
|
|
3318
3494
|
// src/context.ts
|
|
3319
3495
|
function installContext(kit) {
|
|
3320
3496
|
const THREE = kit.THREE;
|
|
@@ -3908,6 +4084,7 @@ var GameVehicles = (() => {
|
|
|
3908
4084
|
}
|
|
3909
4085
|
kit.profX = profX;
|
|
3910
4086
|
const _sr = new THREE.Raycaster();
|
|
4087
|
+
_sr.layers.enableAll();
|
|
3911
4088
|
kit.surfaceAt = function(c, org, dir, o) {
|
|
3912
4089
|
o = o || {};
|
|
3913
4090
|
c.root.updateMatrixWorld(true);
|
|
@@ -3924,7 +4101,7 @@ var GameVehicles = (() => {
|
|
|
3924
4101
|
_sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
|
|
3925
4102
|
_sr.near = 0;
|
|
3926
4103
|
_sr.far = o.max || 20;
|
|
3927
|
-
const h =
|
|
4104
|
+
const h = raycastIndexed(THREE, _sr, cands)[0];
|
|
3928
4105
|
if (!h) return null;
|
|
3929
4106
|
const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
|
|
3930
4107
|
if (n.dot(d) > 0) n.negate();
|
|
@@ -3946,6 +4123,7 @@ var GameVehicles = (() => {
|
|
|
3946
4123
|
const V3 = THREE.Vector3;
|
|
3947
4124
|
const I = kit._;
|
|
3948
4125
|
const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
|
|
4126
|
+
I.paneSeals = /* @__PURE__ */ new WeakMap();
|
|
3949
4127
|
kit.addGlass = function(c, geo, label, type, o) {
|
|
3950
4128
|
o = o || {};
|
|
3951
4129
|
if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
|
|
@@ -4012,7 +4190,7 @@ var GameVehicles = (() => {
|
|
|
4012
4190
|
cy /= poly.length;
|
|
4013
4191
|
const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
|
|
4014
4192
|
const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
|
|
4015
|
-
kit.addGlass(c, geo, lab, o.type || "tempered", {
|
|
4193
|
+
const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
|
|
4016
4194
|
mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
|
|
4017
4195
|
pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
|
|
4018
4196
|
outline: shp.getSpacedPoints(56),
|
|
@@ -4028,6 +4206,7 @@ var GameVehicles = (() => {
|
|
|
4028
4206
|
pts.pop();
|
|
4029
4207
|
const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
|
|
4030
4208
|
seal.castShadow = false;
|
|
4209
|
+
I.paneSeals.set(pane, seal);
|
|
4031
4210
|
});
|
|
4032
4211
|
});
|
|
4033
4212
|
};
|
|
@@ -4703,6 +4882,7 @@ var GameVehicles = (() => {
|
|
|
4703
4882
|
}
|
|
4704
4883
|
|
|
4705
4884
|
// src/hood.ts
|
|
4885
|
+
var HOOD_LIFT = 8e-3;
|
|
4706
4886
|
function installHood(kit) {
|
|
4707
4887
|
const THREE = kit.THREE;
|
|
4708
4888
|
const V3 = THREE.Vector3;
|
|
@@ -4725,7 +4905,7 @@ var GameVehicles = (() => {
|
|
|
4725
4905
|
for (let i = 0; i < p.count; i++) {
|
|
4726
4906
|
p.setX(i, cx + (p.getX(i) - cx) * sx);
|
|
4727
4907
|
p.setZ(i, p.getZ(i) * sz);
|
|
4728
|
-
p.setY(i, p.getY(i) +
|
|
4908
|
+
p.setY(i, p.getY(i) + HOOD_LIFT);
|
|
4729
4909
|
}
|
|
4730
4910
|
geo.translate(-hx, -hy, 0);
|
|
4731
4911
|
const under = geo.clone();
|
|
@@ -4754,7 +4934,8 @@ var GameVehicles = (() => {
|
|
|
4754
4934
|
yMin: bb.min.y,
|
|
4755
4935
|
zw: bb.max.z,
|
|
4756
4936
|
label: o.label,
|
|
4757
|
-
noLatch: !!o.noLatch
|
|
4937
|
+
noLatch: !!o.noLatch,
|
|
4938
|
+
lift: HOOD_LIFT
|
|
4758
4939
|
};
|
|
4759
4940
|
c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
|
|
4760
4941
|
return c.hood;
|
|
@@ -5737,6 +5918,44 @@ var GameVehicles = (() => {
|
|
|
5737
5918
|
Object.assign(I, {});
|
|
5738
5919
|
}
|
|
5739
5920
|
|
|
5921
|
+
// src/routed-particles.ts
|
|
5922
|
+
var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
|
|
5923
|
+
var PUFF_LOOKS = {
|
|
5924
|
+
smokewhite: [10133927, 0.42],
|
|
5925
|
+
smokeblack: [1776412, 0.62],
|
|
5926
|
+
smokegray: [5593436, 0.5],
|
|
5927
|
+
smokesteam: [11975873, 0.3],
|
|
5928
|
+
dust: [7235160, 0.34],
|
|
5929
|
+
fire: [16777215, 0.9]
|
|
5930
|
+
};
|
|
5931
|
+
var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
|
|
5932
|
+
var FIRE_COLOR = 16753968;
|
|
5933
|
+
var SMOKE_START_SCALE = 0.18;
|
|
5934
|
+
function routedParticleRequest(input) {
|
|
5935
|
+
const { type } = input;
|
|
5936
|
+
const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
|
|
5937
|
+
const life = input.life ?? LIFE[type] ?? 99;
|
|
5938
|
+
const size = (input.size ?? 1) * (input.grow ?? 1);
|
|
5939
|
+
const isFire = type === "fire";
|
|
5940
|
+
const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
|
|
5941
|
+
return {
|
|
5942
|
+
effect: ROUTED_PARTICLE_TYPES[type],
|
|
5943
|
+
point: input.position,
|
|
5944
|
+
values: {
|
|
5945
|
+
vx: input.velocity[0],
|
|
5946
|
+
vy: input.velocity[1],
|
|
5947
|
+
vz: input.velocity[2],
|
|
5948
|
+
life,
|
|
5949
|
+
size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
|
|
5950
|
+
grow: growth,
|
|
5951
|
+
color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
|
|
5952
|
+
alpha: input.alpha ?? look[1],
|
|
5953
|
+
gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
|
|
5954
|
+
drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
|
|
5955
|
+
}
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
|
|
5740
5959
|
// src/particles.ts
|
|
5741
5960
|
function installParticles(kit) {
|
|
5742
5961
|
const THREE = kit.THREE;
|
|
@@ -5828,7 +6047,7 @@ var GameVehicles = (() => {
|
|
|
5828
6047
|
});
|
|
5829
6048
|
}
|
|
5830
6049
|
const list2 = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
|
|
5831
|
-
const
|
|
6050
|
+
const build = () => new THREE.SpriteMaterial({
|
|
5832
6051
|
map: list2[Math.floor(Math.random() * list2.length)],
|
|
5833
6052
|
color: new THREE.Color(col),
|
|
5834
6053
|
transparent: true,
|
|
@@ -5836,6 +6055,7 @@ var GameVehicles = (() => {
|
|
|
5836
6055
|
opacity: op,
|
|
5837
6056
|
blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
|
|
5838
6057
|
});
|
|
6058
|
+
const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
|
|
5839
6059
|
const dis = { value: 0 };
|
|
5840
6060
|
mat.userData.dis = dis;
|
|
5841
6061
|
mat.onBeforeCompile = function(sh) {
|
|
@@ -5863,11 +6083,37 @@ var GameVehicles = (() => {
|
|
|
5863
6083
|
function killParticle(c, i) {
|
|
5864
6084
|
const P = c.particles[i];
|
|
5865
6085
|
c.effects.remove(P.m);
|
|
5866
|
-
if (P.own)
|
|
6086
|
+
if (P.own) {
|
|
6087
|
+
if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
|
|
6088
|
+
else P.m.material.dispose();
|
|
6089
|
+
}
|
|
5867
6090
|
c.particles.splice(i, 1);
|
|
5868
6091
|
}
|
|
6092
|
+
function routeParticle(c, effects, type, pos, vel, o) {
|
|
6093
|
+
c.effects.updateWorldMatrix(true, false);
|
|
6094
|
+
const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
|
|
6095
|
+
const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
|
|
6096
|
+
const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
|
|
6097
|
+
effects.play(
|
|
6098
|
+
routedParticleRequest({
|
|
6099
|
+
type,
|
|
6100
|
+
position: [at.x, at.y, at.z],
|
|
6101
|
+
velocity: [to.x, to.y, to.z],
|
|
6102
|
+
...o.variant ? { variant: o.variant } : {},
|
|
6103
|
+
...colour !== void 0 ? { color: colour } : {},
|
|
6104
|
+
...o.op != null ? { alpha: o.op } : {},
|
|
6105
|
+
...o.s ? { size: o.s } : {},
|
|
6106
|
+
...o.life ? { life: o.life } : {},
|
|
6107
|
+
...o.grow ? { grow: o.grow } : {}
|
|
6108
|
+
})
|
|
6109
|
+
);
|
|
6110
|
+
return null;
|
|
6111
|
+
}
|
|
5869
6112
|
kit.spawn = function(c, type, pos, vel, o) {
|
|
5870
6113
|
o = o || {};
|
|
6114
|
+
const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
|
|
6115
|
+
if (effects) return routeParticle(c, effects, type, pos, vel, o);
|
|
6116
|
+
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");
|
|
5871
6117
|
if (c.particles.length >= PARTICLE_LIMIT) {
|
|
5872
6118
|
const idx = c.particles.findIndex(function(q) {
|
|
5873
6119
|
return TRANSIENT_TYPES[q.type];
|
|
@@ -6122,6 +6368,14 @@ var GameVehicles = (() => {
|
|
|
6122
6368
|
}
|
|
6123
6369
|
|
|
6124
6370
|
// src/fluids.ts
|
|
6371
|
+
var PUDDLE_COLORS = {
|
|
6372
|
+
coolant: 3123279,
|
|
6373
|
+
brake_fluid: 12093738,
|
|
6374
|
+
transmission_fluid: 10099746,
|
|
6375
|
+
power_steering_fluid: 9054768,
|
|
6376
|
+
washer_fluid: 2977744,
|
|
6377
|
+
battery_acid: 14211272
|
|
6378
|
+
};
|
|
6125
6379
|
function installFluids(kit) {
|
|
6126
6380
|
const THREE = kit.THREE;
|
|
6127
6381
|
const V3 = THREE.Vector3;
|
|
@@ -6139,6 +6393,7 @@ var GameVehicles = (() => {
|
|
|
6139
6393
|
};
|
|
6140
6394
|
kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
|
|
6141
6395
|
const _fr = new THREE.Raycaster();
|
|
6396
|
+
_fr.layers.enableAll();
|
|
6142
6397
|
kit.splat = function(c, p, n, r, o) {
|
|
6143
6398
|
o = o || {};
|
|
6144
6399
|
n = n.clone().normalize();
|
|
@@ -6340,6 +6595,13 @@ var GameVehicles = (() => {
|
|
|
6340
6595
|
transparent: true,
|
|
6341
6596
|
opacity: 0.9,
|
|
6342
6597
|
depthWrite: false
|
|
6598
|
+
}) : PUDDLE_COLORS[kind] ? new THREE.MeshStandardMaterial({
|
|
6599
|
+
color: PUDDLE_COLORS[kind],
|
|
6600
|
+
roughness: 0.05,
|
|
6601
|
+
metalness: 0.1,
|
|
6602
|
+
transparent: true,
|
|
6603
|
+
opacity: 0.7,
|
|
6604
|
+
depthWrite: false
|
|
6343
6605
|
}) : new THREE.MeshStandardMaterial({
|
|
6344
6606
|
color: 0,
|
|
6345
6607
|
roughness: 0.03,
|
|
@@ -6580,31 +6842,55 @@ var GameVehicles = (() => {
|
|
|
6580
6842
|
}
|
|
6581
6843
|
|
|
6582
6844
|
// src/glass-damage.ts
|
|
6845
|
+
function crackedGlassMaterial(THREE, texture) {
|
|
6846
|
+
return new THREE.MeshStandardMaterial({
|
|
6847
|
+
color: 16777215,
|
|
6848
|
+
map: texture,
|
|
6849
|
+
emissive: 6976118,
|
|
6850
|
+
emissiveMap: texture,
|
|
6851
|
+
transparent: true,
|
|
6852
|
+
roughness: 0.25,
|
|
6853
|
+
metalness: 0.1,
|
|
6854
|
+
side: THREE.DoubleSide,
|
|
6855
|
+
depthWrite: false
|
|
6856
|
+
});
|
|
6857
|
+
}
|
|
6583
6858
|
function installGlassDamage(kit) {
|
|
6584
6859
|
const THREE = kit.THREE;
|
|
6585
6860
|
const V3 = THREE.Vector3;
|
|
6586
6861
|
const PI = Math.PI;
|
|
6587
6862
|
const I = kit._;
|
|
6588
6863
|
const document2 = kit.document;
|
|
6589
|
-
const {
|
|
6590
|
-
function glassUV(msh, pLocal) {
|
|
6864
|
+
const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
|
|
6865
|
+
function glassUV(c, msh, pLocal) {
|
|
6591
6866
|
const pos = msh.geometry.attributes.position;
|
|
6592
6867
|
const uv = msh.geometry.attributes.uv;
|
|
6593
6868
|
if (!uv) return [0.5, 0.5];
|
|
6594
|
-
|
|
6595
|
-
const q = pLocal.clone()
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
const
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6869
|
+
c.root.updateMatrixWorld(true);
|
|
6870
|
+
const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
|
|
6871
|
+
const idx = msh.geometry.index;
|
|
6872
|
+
const tri = new THREE.Triangle();
|
|
6873
|
+
const near = new V3();
|
|
6874
|
+
const bary = new V3();
|
|
6875
|
+
let best = Infinity;
|
|
6876
|
+
const out = [0.5, 0.5];
|
|
6877
|
+
const count = idx ? idx.count : pos.count;
|
|
6878
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
6879
|
+
const ia = idx ? idx.getX(f) : f;
|
|
6880
|
+
const ib = idx ? idx.getX(f + 1) : f + 1;
|
|
6881
|
+
const ic = idx ? idx.getX(f + 2) : f + 2;
|
|
6882
|
+
tri.a.fromBufferAttribute(pos, ia);
|
|
6883
|
+
tri.b.fromBufferAttribute(pos, ib);
|
|
6884
|
+
tri.c.fromBufferAttribute(pos, ic);
|
|
6885
|
+
tri.closestPointToPoint(q, near);
|
|
6886
|
+
const d = near.distanceToSquared(q);
|
|
6887
|
+
if (d >= best) continue;
|
|
6888
|
+
best = d;
|
|
6889
|
+
tri.getBarycoord(near, bary);
|
|
6890
|
+
out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
|
|
6891
|
+
out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
|
|
6606
6892
|
}
|
|
6607
|
-
return
|
|
6893
|
+
return out;
|
|
6608
6894
|
}
|
|
6609
6895
|
function drawWeb(x, cx, cy, s, ax, rnd) {
|
|
6610
6896
|
const n = 16 + Math.floor(rnd() * 8);
|
|
@@ -6681,19 +6967,9 @@ var GameVehicles = (() => {
|
|
|
6681
6967
|
x0.fillStyle = "rgba(48,66,72,0.5)";
|
|
6682
6968
|
x0.fillRect(0, 0, 256, 256);
|
|
6683
6969
|
G.tex = new THREE.CanvasTexture(G.cv);
|
|
6684
|
-
msh.material =
|
|
6685
|
-
color: 16777215,
|
|
6686
|
-
map: G.tex,
|
|
6687
|
-
emissive: 6976118,
|
|
6688
|
-
emissiveMap: G.tex,
|
|
6689
|
-
transparent: true,
|
|
6690
|
-
roughness: 0.25,
|
|
6691
|
-
metalness: 0.1,
|
|
6692
|
-
side: DOUBLE_SIDE,
|
|
6693
|
-
depthWrite: false
|
|
6694
|
-
});
|
|
6970
|
+
msh.material = crackedGlassMaterial(THREE, G.tex);
|
|
6695
6971
|
}
|
|
6696
|
-
const uv = glassUV(msh, p);
|
|
6972
|
+
const uv = glassUV(c, msh, p);
|
|
6697
6973
|
const x = G.cv.getContext("2d");
|
|
6698
6974
|
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);
|
|
6699
6975
|
G.tex.needsUpdate = true;
|
|
@@ -6838,6 +7114,13 @@ var GameVehicles = (() => {
|
|
|
6838
7114
|
}
|
|
6839
7115
|
const seatGeo = [];
|
|
6840
7116
|
const d = dir.clone().normalize();
|
|
7117
|
+
if (msh.parent && msh.parent !== c.vehicle) {
|
|
7118
|
+
c.root.updateMatrixWorld(true);
|
|
7119
|
+
const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
|
|
7120
|
+
cubesAt.forEach(function(q) {
|
|
7121
|
+
q.applyMatrix4(toVehicle);
|
|
7122
|
+
});
|
|
7123
|
+
}
|
|
6841
7124
|
cubesAt.forEach(function(p, i) {
|
|
6842
7125
|
if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
|
|
6843
7126
|
let best = null;
|
|
@@ -7040,6 +7323,7 @@ var GameVehicles = (() => {
|
|
|
7040
7323
|
polygonOffsetUnits: -2
|
|
7041
7324
|
});
|
|
7042
7325
|
const _ray = new THREE.Raycaster();
|
|
7326
|
+
_ray.layers.enableAll();
|
|
7043
7327
|
function findSurface(c, p, dir) {
|
|
7044
7328
|
c.root.updateMatrixWorld(true);
|
|
7045
7329
|
const Mw = c.vehicle.matrixWorld;
|
|
@@ -7121,6 +7405,7 @@ var GameVehicles = (() => {
|
|
|
7121
7405
|
const v0 = new V3();
|
|
7122
7406
|
const w = new V3();
|
|
7123
7407
|
const sc = new V3();
|
|
7408
|
+
const local = new V3();
|
|
7124
7409
|
const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
|
|
7125
7410
|
c.deform.forEach(function(msh) {
|
|
7126
7411
|
if (!msh.parent || !msh.visible) return;
|
|
@@ -7135,7 +7420,15 @@ var GameVehicles = (() => {
|
|
|
7135
7420
|
irel.copy(rel).invert();
|
|
7136
7421
|
if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
|
|
7137
7422
|
const p0 = g.userData.p0;
|
|
7423
|
+
local.copy(p).applyMatrix4(irel);
|
|
7424
|
+
const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
|
|
7425
|
+
const reach2 = reach * reach;
|
|
7426
|
+
const arr = pos.array;
|
|
7138
7427
|
for (let i = 0; i < pos.count; i++) {
|
|
7428
|
+
const dx = arr[3 * i] - local.x;
|
|
7429
|
+
const dy = arr[3 * i + 1] - local.y;
|
|
7430
|
+
const dz = arr[3 * i + 2] - local.z;
|
|
7431
|
+
if (dx * dx + dy * dy + dz * dz >= reach2) continue;
|
|
7139
7432
|
v.fromBufferAttribute(pos, i).applyMatrix4(rel);
|
|
7140
7433
|
const a = dentField(v, H);
|
|
7141
7434
|
if (a <= 0) continue;
|
|
@@ -7151,10 +7444,39 @@ var GameVehicles = (() => {
|
|
|
7151
7444
|
}
|
|
7152
7445
|
if (changed) {
|
|
7153
7446
|
pos.needsUpdate = true;
|
|
7154
|
-
if (g.userData.crease)
|
|
7447
|
+
if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
|
|
7448
|
+
else if (g.userData.crease) crease(g, 42);
|
|
7155
7449
|
else g.computeVertexNormals();
|
|
7156
|
-
g.
|
|
7157
|
-
g.
|
|
7450
|
+
const sphere2 = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
|
|
7451
|
+
if (pos.isInterleavedBufferAttribute || pos.normalized || Object.keys(g.morphAttributes).length > 0) {
|
|
7452
|
+
g.computeBoundingBox();
|
|
7453
|
+
g.boundingBox.getCenter(sphere2.center);
|
|
7454
|
+
let far2 = 0;
|
|
7455
|
+
for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere2.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
|
|
7456
|
+
sphere2.radius = Math.sqrt(far2);
|
|
7457
|
+
} else {
|
|
7458
|
+
const box3 = g.boundingBox ?? (g.boundingBox = new THREE.Box3());
|
|
7459
|
+
let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
7460
|
+
for (let i = 0; i < pos.count * 3; i += 3) {
|
|
7461
|
+
const x = arr[i], y = arr[i + 1], z = arr[i + 2];
|
|
7462
|
+
if (x < x0) x0 = x;
|
|
7463
|
+
if (y < y0) y0 = y;
|
|
7464
|
+
if (z < z0) z0 = z;
|
|
7465
|
+
if (x > x1) x1 = x;
|
|
7466
|
+
if (y > y1) y1 = y;
|
|
7467
|
+
if (z > z1) z1 = z;
|
|
7468
|
+
}
|
|
7469
|
+
box3.min.set(x0, y0, z0);
|
|
7470
|
+
box3.max.set(x1, y1, z1);
|
|
7471
|
+
box3.getCenter(sphere2.center);
|
|
7472
|
+
const cx = sphere2.center.x, cy = sphere2.center.y, cz = sphere2.center.z;
|
|
7473
|
+
let far2 = 0;
|
|
7474
|
+
for (let i = 0; i < pos.count * 3; i += 3) {
|
|
7475
|
+
const dx = cx - arr[i], dy = cy - arr[i + 1], dz = cz - arr[i + 2];
|
|
7476
|
+
far2 = Math.max(far2, dx * dx + dy * dy + dz * dz);
|
|
7477
|
+
}
|
|
7478
|
+
sphere2.radius = Math.sqrt(far2);
|
|
7479
|
+
}
|
|
7158
7480
|
}
|
|
7159
7481
|
});
|
|
7160
7482
|
const dset = new Set(c.deform);
|
|
@@ -7238,13 +7560,14 @@ var GameVehicles = (() => {
|
|
|
7238
7560
|
kit.impact = impact;
|
|
7239
7561
|
kit.partEffects = {};
|
|
7240
7562
|
const _pr = new THREE.Raycaster();
|
|
7563
|
+
_pr.layers.enableAll();
|
|
7241
7564
|
function probe(c, o, d, far) {
|
|
7242
7565
|
c.root.updateMatrixWorld(true);
|
|
7243
7566
|
const Mw = c.vehicle.matrixWorld;
|
|
7244
7567
|
const inv = new THREE.Matrix4().copy(Mw).invert();
|
|
7245
7568
|
_pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
|
|
7246
7569
|
_pr.far = far || 10;
|
|
7247
|
-
const hits = _pr
|
|
7570
|
+
const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
|
|
7248
7571
|
for (let i = 0; i < hits.length; i++) {
|
|
7249
7572
|
const h = hits[i];
|
|
7250
7573
|
const ob = h.object;
|
|
@@ -7557,7 +7880,8 @@ var GameVehicles = (() => {
|
|
|
7557
7880
|
const G = gm2.userData.glass;
|
|
7558
7881
|
if (!G || G.broken || !gm2.visible || !gm2.parent) return;
|
|
7559
7882
|
if (G.type === "laminated") {
|
|
7560
|
-
|
|
7883
|
+
for (let k2 = 0; k2 < 3; k2++) I.crackGlass(c, gm2, I.glassPt ? I.glassPt(c, gm2, c.rand) : gm2.position.clone(), 1.2);
|
|
7884
|
+
return;
|
|
7561
7885
|
}
|
|
7562
7886
|
I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
|
|
7563
7887
|
});
|
|
@@ -7568,19 +7892,6 @@ var GameVehicles = (() => {
|
|
|
7568
7892
|
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)));
|
|
7569
7893
|
c.hood = null;
|
|
7570
7894
|
}
|
|
7571
|
-
const W = m.width || 1.6;
|
|
7572
|
-
const gh = m.glasshouse;
|
|
7573
|
-
if (m.kind === "car" && gh) {
|
|
7574
|
-
;
|
|
7575
|
-
[1, -1].forEach(function(sd) {
|
|
7576
|
-
const dh = Math.max(0.35, (m.belt || 1) - (m.clearance || 0.3) - 0.08);
|
|
7577
|
-
const dx = gh.fb - 0.55;
|
|
7578
|
-
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));
|
|
7579
|
-
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));
|
|
7580
|
-
hole.userData.noFollow = true;
|
|
7581
|
-
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)));
|
|
7582
|
-
});
|
|
7583
|
-
}
|
|
7584
7895
|
if (m.rearBumper && m.rearBumper.parent)
|
|
7585
7896
|
fling(
|
|
7586
7897
|
c,
|
|
@@ -7588,20 +7899,6 @@ var GameVehicles = (() => {
|
|
|
7588
7899
|
c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
|
|
7589
7900
|
new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
|
|
7590
7901
|
);
|
|
7591
|
-
if (c.wheels.length) {
|
|
7592
|
-
const wi = Math.floor(c.rand() * c.wheels.length);
|
|
7593
|
-
const wh = c.wheels[wi];
|
|
7594
|
-
c.wheels.splice(wi, 1);
|
|
7595
|
-
c.parts.forEach(function(p) {
|
|
7596
|
-
if (p.obj === wh) p.done = true;
|
|
7597
|
-
});
|
|
7598
|
-
fling(
|
|
7599
|
-
c,
|
|
7600
|
-
wh,
|
|
7601
|
-
c.dirToEffects(new V3(rv(-2, 2), rv(5, 7.5), (wh.position.z >= 0 ? 1 : -1) * rv(3, 5))),
|
|
7602
|
-
new V3(rv(-8, 8), rv(-3, 3), rv(-8, 8))
|
|
7603
|
-
);
|
|
7604
|
-
}
|
|
7605
7902
|
burnAll(c);
|
|
7606
7903
|
c.emitters = c.emitters.filter(function(e) {
|
|
7607
7904
|
return e.kind === "exhaust" ? false : true;
|
|
@@ -8610,6 +8907,7 @@ var GameVehicles = (() => {
|
|
|
8610
8907
|
return hole;
|
|
8611
8908
|
}
|
|
8612
8909
|
const _zr = new THREE.Raycaster();
|
|
8910
|
+
_zr.layers.enableAll();
|
|
8613
8911
|
const PROXY_MIN_TRIANGLES = 500;
|
|
8614
8912
|
const STROKE_REACH = 0.6;
|
|
8615
8913
|
const _pv = new V3();
|
|
@@ -8844,6 +9142,7 @@ var GameVehicles = (() => {
|
|
|
8844
9142
|
return out;
|
|
8845
9143
|
}
|
|
8846
9144
|
const _rt = new THREE.Raycaster();
|
|
9145
|
+
_rt.layers.enableAll();
|
|
8847
9146
|
function scratchPath(c, s, len, ang, cands, B) {
|
|
8848
9147
|
const n = s.n.clone().normalize();
|
|
8849
9148
|
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);
|
|
@@ -12125,6 +12424,8 @@ var GameVehicles = (() => {
|
|
|
12125
12424
|
V3: THREE.Vector3,
|
|
12126
12425
|
PI: Math.PI,
|
|
12127
12426
|
document: doc,
|
|
12427
|
+
particleMaterials: opts.particleMaterials,
|
|
12428
|
+
effects: opts.effects,
|
|
12128
12429
|
strings,
|
|
12129
12430
|
t,
|
|
12130
12431
|
matchers: createMatchers(strings),
|
|
@@ -12380,7 +12681,9 @@ var GameVehicles = (() => {
|
|
|
12380
12681
|
assignDeep(strings, resolveStrings(opts.locale));
|
|
12381
12682
|
return kit;
|
|
12382
12683
|
};
|
|
12383
|
-
const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
|
|
12684
|
+
const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
|
|
12685
|
+
void _particleMaterials;
|
|
12686
|
+
void _effects;
|
|
12384
12687
|
void _three;
|
|
12385
12688
|
void _lod;
|
|
12386
12689
|
void _locale;
|
|
@@ -12390,6 +12693,17 @@ var GameVehicles = (() => {
|
|
|
12390
12693
|
return kit;
|
|
12391
12694
|
}
|
|
12392
12695
|
|
|
12696
|
+
// src/destructible/armour.ts
|
|
12697
|
+
var ARMOUR_LEVELS = {
|
|
12698
|
+
IIIA: { glassStops: 1700, glassHp: 6e3, glassTakes: 0.25, steel: 8 },
|
|
12699
|
+
III: { glassStops: 3600, glassHp: 9e3, glassTakes: 0.25, steel: 30 }
|
|
12700
|
+
};
|
|
12701
|
+
function armourLevelOf(meta, option) {
|
|
12702
|
+
if (option !== void 0) return option;
|
|
12703
|
+
if (meta.armor === "IIIA" || meta.armor === "III") return meta.armor;
|
|
12704
|
+
return meta.armored || meta.bullet ? "III" : null;
|
|
12705
|
+
}
|
|
12706
|
+
|
|
12393
12707
|
// src/destructible/model.ts
|
|
12394
12708
|
var JOINT_RULES = {
|
|
12395
12709
|
hinge: { strength: 5e3, tools: ["wrench", "screwdriver"], seconds: 3 },
|
|
@@ -12433,6 +12747,12 @@ var GameVehicles = (() => {
|
|
|
12433
12747
|
center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
|
|
12434
12748
|
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)]
|
|
12435
12749
|
});
|
|
12750
|
+
var PART_FLUIDS = {
|
|
12751
|
+
gearbox: { fluid: "transmission_fluid", liters: 6 },
|
|
12752
|
+
brakes: { fluid: "brake_fluid", liters: 0.6 },
|
|
12753
|
+
steering: { fluid: "power_steering_fluid", liters: 1 },
|
|
12754
|
+
battery: { fluid: "battery_acid", liters: 3 }
|
|
12755
|
+
};
|
|
12436
12756
|
var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
|
|
12437
12757
|
var isFender = (id) => /^fender/.test(id);
|
|
12438
12758
|
var roleOfZone = (z) => {
|
|
@@ -12476,13 +12796,22 @@ var GameVehicles = (() => {
|
|
|
12476
12796
|
center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
|
|
12477
12797
|
size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
|
|
12478
12798
|
});
|
|
12799
|
+
function paneBox(c, pane, inverse) {
|
|
12800
|
+
pane.geometry.computeBoundingBox();
|
|
12801
|
+
const box2 = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
|
|
12802
|
+
box2.applyMatrix4(inverse);
|
|
12803
|
+
const center = [(box2.min.x + box2.max.x) / 2, (box2.min.y + box2.max.y) / 2, (box2.min.z + box2.max.z) / 2];
|
|
12804
|
+
const size = [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
|
|
12805
|
+
return { center, size };
|
|
12806
|
+
}
|
|
12479
12807
|
function buildVehicleModel(kit, c, options = {}) {
|
|
12480
12808
|
const kind = kindOf(c);
|
|
12481
12809
|
const m = c.meta;
|
|
12482
12810
|
const spec = c.spec;
|
|
12483
12811
|
const t = kit.t;
|
|
12484
12812
|
const skin = skinOf(kind);
|
|
12485
|
-
const
|
|
12813
|
+
const armoured = options.armour ?? null;
|
|
12814
|
+
const armour = armoured ? 12 : 1;
|
|
12486
12815
|
const mass = vehicleMass(c);
|
|
12487
12816
|
const W = m.width ?? 1.7;
|
|
12488
12817
|
const F = m.front ?? 2;
|
|
@@ -12548,8 +12877,8 @@ var GameVehicles = (() => {
|
|
|
12548
12877
|
}
|
|
12549
12878
|
if (role === "glass") {
|
|
12550
12879
|
const glass = (z.glass ?? [])[0];
|
|
12551
|
-
const laminated = glass?.userData?.glass?.type === "laminated" ||
|
|
12552
|
-
add({ id, role, name: label, material: "glass", box: box2, mass: 10 * area3, hp: laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
|
|
12880
|
+
const laminated = glass?.userData?.glass?.type === "laminated" || !!armoured;
|
|
12881
|
+
add({ id, role, name: label, material: "glass", box: box2, mass: 10 * area3, hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
|
|
12553
12882
|
continue;
|
|
12554
12883
|
}
|
|
12555
12884
|
if (role === "light") {
|
|
@@ -12587,7 +12916,7 @@ var GameVehicles = (() => {
|
|
|
12587
12916
|
zoneId: id,
|
|
12588
12917
|
visual: false,
|
|
12589
12918
|
contains: { fluid: diesel ? "diesel" : "fuel", liters },
|
|
12590
|
-
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
|
|
12919
|
+
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
|
|
12591
12920
|
ignitionC: diesel ? 330 : 260,
|
|
12592
12921
|
tags: ["fuel_tank", ...tags]
|
|
12593
12922
|
});
|
|
@@ -12616,6 +12945,16 @@ var GameVehicles = (() => {
|
|
|
12616
12945
|
tags: [role, ...tags]
|
|
12617
12946
|
});
|
|
12618
12947
|
}
|
|
12948
|
+
const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
|
|
12949
|
+
c.root.updateMatrixWorld(true);
|
|
12950
|
+
const inverse = c.vehicle.matrixWorld.clone().invert();
|
|
12951
|
+
for (const pane of c.glass ?? []) {
|
|
12952
|
+
if (claimed.has(pane)) continue;
|
|
12953
|
+
const G = pane.userData.glass;
|
|
12954
|
+
const bb = paneBox(c, pane, inverse);
|
|
12955
|
+
const laminated = G?.type === "laminated" || !!armoured;
|
|
12956
|
+
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"] });
|
|
12957
|
+
}
|
|
12619
12958
|
if (!hubZone) {
|
|
12620
12959
|
const h = Math.max(0.15, belt - clearance);
|
|
12621
12960
|
add({
|
|
@@ -12670,7 +13009,7 @@ var GameVehicles = (() => {
|
|
|
12670
13009
|
visual: false,
|
|
12671
13010
|
mechanical: true,
|
|
12672
13011
|
contains: { fluid: diesel ? "diesel" : "fuel", liters },
|
|
12673
|
-
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
|
|
13012
|
+
explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
|
|
12674
13013
|
ignitionC: diesel ? 330 : 260,
|
|
12675
13014
|
tags: ["fuel_tank"]
|
|
12676
13015
|
});
|
|
@@ -12947,10 +13286,163 @@ var GameVehicles = (() => {
|
|
|
12947
13286
|
}
|
|
12948
13287
|
}
|
|
12949
13288
|
void has;
|
|
13289
|
+
for (const part of parts) {
|
|
13290
|
+
const fluid = PART_FLUIDS[part.role];
|
|
13291
|
+
if (fluid && !part.contains && !(kind === "bike" && part.role === "steering")) part.contains = { ...fluid };
|
|
13292
|
+
}
|
|
12950
13293
|
return { parts, joints, hub: hubId, hitch };
|
|
12951
13294
|
}
|
|
12952
13295
|
|
|
13296
|
+
// src/destructible/subdivide-near.ts
|
|
13297
|
+
var SUBDIVIDE_DEPTH = 12;
|
|
13298
|
+
function subdivideNear(THREE, params) {
|
|
13299
|
+
const { mesh: mesh2, maxLen, near } = params;
|
|
13300
|
+
const src = mesh2.geometry.index ? mesh2.geometry.toNonIndexed() : mesh2.geometry;
|
|
13301
|
+
const a = src.attributes.position.array;
|
|
13302
|
+
const toWorld = mesh2.matrixWorld;
|
|
13303
|
+
const out = [];
|
|
13304
|
+
const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
|
|
13305
|
+
const split = (A, B, C, depth) => {
|
|
13306
|
+
const ab = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
|
|
13307
|
+
const bc = Math.hypot(B[0] - C[0], B[1] - C[1], B[2] - C[2]);
|
|
13308
|
+
const ca = Math.hypot(C[0] - A[0], C[1] - A[1], C[2] - A[2]);
|
|
13309
|
+
const longest = Math.max(ab, bc, ca);
|
|
13310
|
+
if (longest <= maxLen || depth > SUBDIVIDE_DEPTH) {
|
|
13311
|
+
out.push(...A, ...B, ...C);
|
|
13312
|
+
return;
|
|
13313
|
+
}
|
|
13314
|
+
if (longest === ab) {
|
|
13315
|
+
const D = mid(A, B);
|
|
13316
|
+
split(A, D, C, depth + 1);
|
|
13317
|
+
split(D, B, C, depth + 1);
|
|
13318
|
+
} else if (longest === bc) {
|
|
13319
|
+
const E = mid(B, C);
|
|
13320
|
+
split(A, B, E, depth + 1);
|
|
13321
|
+
split(A, E, C, depth + 1);
|
|
13322
|
+
} else {
|
|
13323
|
+
const F = mid(C, A);
|
|
13324
|
+
split(A, B, F, depth + 1);
|
|
13325
|
+
split(F, B, C, depth + 1);
|
|
13326
|
+
}
|
|
13327
|
+
};
|
|
13328
|
+
for (let i = 0; i < a.length; i += 9) {
|
|
13329
|
+
const A = [a[i], a[i + 1], a[i + 2]];
|
|
13330
|
+
const B = [a[i + 3], a[i + 4], a[i + 5]];
|
|
13331
|
+
const C = [a[i + 6], a[i + 7], a[i + 8]];
|
|
13332
|
+
const corners = [A, B, C].map((p) => new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(toWorld));
|
|
13333
|
+
if (near(corners)) split(A, B, C, 0);
|
|
13334
|
+
else out.push(...A, ...B, ...C);
|
|
13335
|
+
}
|
|
13336
|
+
const g = new THREE.BufferGeometry();
|
|
13337
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
|
|
13338
|
+
mesh2.geometry.dispose();
|
|
13339
|
+
mesh2.geometry = g;
|
|
13340
|
+
}
|
|
13341
|
+
|
|
12953
13342
|
// src/destructible/visual.ts
|
|
13343
|
+
var HOLE_DECAL_SCALE = 2.4;
|
|
13344
|
+
var DOOR_DEPTH = 0.07;
|
|
13345
|
+
var DOOR_EDGE_PIECES = 8;
|
|
13346
|
+
var DOOR_EDGE_REACH = 0.08;
|
|
13347
|
+
var DOOR_CARD_COLOR = "#3b3e42";
|
|
13348
|
+
var DOOR_LINER_COLOR = "#5a5e64";
|
|
13349
|
+
var DOOR_LINER_DARK = 0.15;
|
|
13350
|
+
var DOOR_STRIP_THIN = 0.08;
|
|
13351
|
+
var DOOR_STRIP_NEAR = 0.04;
|
|
13352
|
+
var DOOR_STRIP_TALL = 0.22;
|
|
13353
|
+
var STRIP_SMOOTH_DEG = 180;
|
|
13354
|
+
var DOOR_STRIP_VERTICES = 2e4;
|
|
13355
|
+
var DOOR_SILL = 0.25;
|
|
13356
|
+
var DOOR_STRIP_OVER = 0.3;
|
|
13357
|
+
var DOOR_TOP_OVER_PANE = 0.03;
|
|
13358
|
+
var HOLE_TEXTURE_SIZE = 64;
|
|
13359
|
+
var LAMINATED_HP = 1e3;
|
|
13360
|
+
var HOOD_SINK = 2e-3;
|
|
13361
|
+
var HOOD_GAP_CLOSED = 0.016;
|
|
13362
|
+
var DOOR_HANDLE_INSET = 0.12;
|
|
13363
|
+
var DOOR_HANDLE_MIN = 0.07;
|
|
13364
|
+
var DOOR_HANDLE_MAX = 0.24;
|
|
13365
|
+
var DOOR_HANDLE_THIN = 0.07;
|
|
13366
|
+
var DOOR_HANDLE_OUTSIDE = 0.15;
|
|
13367
|
+
var PANE_SEAT_OUT = 0.05;
|
|
13368
|
+
var PANE_SEAT_IN = -0.05;
|
|
13369
|
+
var PANE_SEAT_REACH = 0.14;
|
|
13370
|
+
var PANE_SEAT_SAMPLES = 24;
|
|
13371
|
+
var PANE_SEAT_LIFT = 4e-3;
|
|
13372
|
+
var PANE_FRAME_NEAR = 0.07;
|
|
13373
|
+
var PANE_MIN_SLOPE = 0.01;
|
|
13374
|
+
var PANE_EDGE_BAND = 0.03;
|
|
13375
|
+
var PANE_REACH_STEP = 0.01;
|
|
13376
|
+
var PANE_REACH_MAX = 0.25;
|
|
13377
|
+
var COWL_UNDER_GLASS = 0;
|
|
13378
|
+
var COWL_NEAR_PANE = 0.08;
|
|
13379
|
+
var COWL_PLATE = 0.06;
|
|
13380
|
+
var COWL_DASH = 0.5;
|
|
13381
|
+
var COWL_DEEP = 0.3;
|
|
13382
|
+
var WINDOW_FRAME_NEAR = 0.06;
|
|
13383
|
+
var WINDOW_FRAME_SPAN = 0.6;
|
|
13384
|
+
var DASHBOARD_COLOR = "#121314";
|
|
13385
|
+
var COWL_CUT_SUB = 0.1;
|
|
13386
|
+
var COWL_CUT_MARGIN = 0.08;
|
|
13387
|
+
var SKIN_UNDER_PANE = 0.025;
|
|
13388
|
+
var SKIN_EDGE_KEPT = 0.06;
|
|
13389
|
+
var SIDE_PANE_FACING = 0.6;
|
|
13390
|
+
var COWL_LONG = 0.8;
|
|
13391
|
+
var PANE_BAR_VERTICES = 48;
|
|
13392
|
+
var PANE_BAR_LENGTH = 0.3;
|
|
13393
|
+
var PANE_BORDER = 0.03;
|
|
13394
|
+
var PANE_BORDER_LIFT = 3e-3;
|
|
13395
|
+
var DRAWN_MIRROR_SIZE = 0.3;
|
|
13396
|
+
var DRAWN_MIRROR_REACH = 0.4;
|
|
13397
|
+
var DRAWN_MIRROR_MOUNT = 0.2;
|
|
13398
|
+
var MIRROR_GLASS_FIT = 0.76;
|
|
13399
|
+
var MIRROR_GLASS_PROUD = 1e-3;
|
|
13400
|
+
var MIRROR_FACE_DEPTH = 0.012;
|
|
13401
|
+
var MIRROR_GLASS_THIN = 0.04;
|
|
13402
|
+
var DEFAULT_WHEEL_RADIUS = 0.3;
|
|
13403
|
+
var SIDEWALL_SHARE = 0.3;
|
|
13404
|
+
var MIRROR_GLASS_MIN = 0.05;
|
|
13405
|
+
var MIRROR_GLASS_COLOR = "#59636a";
|
|
13406
|
+
var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
|
|
13407
|
+
var holeMaterials = /* @__PURE__ */ new WeakMap();
|
|
13408
|
+
var holeDisc = null;
|
|
13409
|
+
function holeDiscGeometry(THREE) {
|
|
13410
|
+
holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
|
|
13411
|
+
return holeDisc;
|
|
13412
|
+
}
|
|
13413
|
+
function holeMaterial(kit, material) {
|
|
13414
|
+
const key = kit;
|
|
13415
|
+
const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
|
|
13416
|
+
holeMaterials.set(key, cache2);
|
|
13417
|
+
const known = cache2.get(material);
|
|
13418
|
+
if (known) return known;
|
|
13419
|
+
const I = kit._;
|
|
13420
|
+
const THREE = kit.THREE;
|
|
13421
|
+
const canvas = kit.document?.createElement?.("canvas");
|
|
13422
|
+
const x = canvas?.getContext?.("2d");
|
|
13423
|
+
let made = I.MATERIALS.dark;
|
|
13424
|
+
if (x) {
|
|
13425
|
+
canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
|
|
13426
|
+
const mid = HOLE_TEXTURE_SIZE / 2;
|
|
13427
|
+
x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
|
|
13428
|
+
x.beginPath();
|
|
13429
|
+
for (let i = 0; i < 18; i++) {
|
|
13430
|
+
const a = i / 18 * Math.PI * 2;
|
|
13431
|
+
const r = mid * (i % 2 ? 0.98 : 0.76);
|
|
13432
|
+
x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
|
|
13433
|
+
}
|
|
13434
|
+
x.closePath();
|
|
13435
|
+
x.fill();
|
|
13436
|
+
x.fillStyle = "#050505";
|
|
13437
|
+
x.beginPath();
|
|
13438
|
+
x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
|
|
13439
|
+
x.fill();
|
|
13440
|
+
const map = new THREE.CanvasTexture(canvas);
|
|
13441
|
+
made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
|
|
13442
|
+
}
|
|
13443
|
+
cache2.set(material, made);
|
|
13444
|
+
return made;
|
|
13445
|
+
}
|
|
12954
13446
|
var VehicleVisuals = class {
|
|
12955
13447
|
constructor(kit, c, model) {
|
|
12956
13448
|
__publicField(this, "kit");
|
|
@@ -12968,14 +13460,23 @@ var GameVehicles = (() => {
|
|
|
12968
13460
|
__publicField(this, "syncPlates", /* @__PURE__ */ new Map());
|
|
12969
13461
|
/** 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). */
|
|
12970
13462
|
__publicField(this, "skins", /* @__PURE__ */ new Map());
|
|
13463
|
+
/** How high each door's cut reaches. */
|
|
13464
|
+
__publicField(this, "doorTops", /* @__PURE__ */ new Map());
|
|
12971
13465
|
__publicField(this, "lightsOn", true);
|
|
12972
13466
|
/** Heavy body meshes whose triangle index is still to build (null once done). */
|
|
12973
13467
|
__publicField(this, "warming");
|
|
12974
13468
|
/** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
|
|
12975
13469
|
__publicField(this, "tornThisFrame", false);
|
|
13470
|
+
/** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
|
|
13471
|
+
__publicField(this, "queued", false);
|
|
13472
|
+
/** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
|
|
13473
|
+
__publicField(this, "holdArmed", false);
|
|
13474
|
+
__publicField(this, "holdFrame", -1);
|
|
12976
13475
|
/** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
|
|
12977
13476
|
__publicField(this, "smoothing", /* @__PURE__ */ new Map());
|
|
12978
13477
|
__publicField(this, "hood");
|
|
13478
|
+
;
|
|
13479
|
+
c.smoothLater = (mesh2) => this.smoothLater(mesh2);
|
|
12979
13480
|
this.kit = kit;
|
|
12980
13481
|
this.c = c;
|
|
12981
13482
|
this.model = model;
|
|
@@ -12984,6 +13485,9 @@ var GameVehicles = (() => {
|
|
|
12984
13485
|
for (const p of model.parts) this.byId.set(p.id, p);
|
|
12985
13486
|
c.root.updateMatrixWorld(true);
|
|
12986
13487
|
this.hood = this.nameHood();
|
|
13488
|
+
this.seatPanes();
|
|
13489
|
+
this.seatDoorHandles();
|
|
13490
|
+
this.seatHood();
|
|
12987
13491
|
this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
|
|
12988
13492
|
if (!this.warming.length) this.warming = null;
|
|
12989
13493
|
for (const p of model.parts) {
|
|
@@ -13004,6 +13508,7 @@ var GameVehicles = (() => {
|
|
|
13004
13508
|
const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
|
|
13005
13509
|
if (!id) return null;
|
|
13006
13510
|
c.hood.piv.name = id;
|
|
13511
|
+
c.hood.piv.userData.plateOf = id;
|
|
13007
13512
|
return { id, open: c.hood.open, sign: c.hood.sign || 1 };
|
|
13008
13513
|
}
|
|
13009
13514
|
mirrorObjects(p) {
|
|
@@ -13091,6 +13596,7 @@ var GameVehicles = (() => {
|
|
|
13091
13596
|
recess.userData.noFollow = true;
|
|
13092
13597
|
const piv = new THREE.Group();
|
|
13093
13598
|
piv.name = p.id;
|
|
13599
|
+
piv.userData.plateOf = p.id;
|
|
13094
13600
|
piv.position.set(hx, cy, sk + s * 0.016);
|
|
13095
13601
|
piv.userData.rigid = true;
|
|
13096
13602
|
this.c.vehicle.add(piv);
|
|
@@ -13107,6 +13613,7 @@ var GameVehicles = (() => {
|
|
|
13107
13613
|
recess.userData.noFollow = true;
|
|
13108
13614
|
const piv = new THREE.Group();
|
|
13109
13615
|
piv.name = p.id;
|
|
13616
|
+
piv.userData.plateOf = p.id;
|
|
13110
13617
|
piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
|
|
13111
13618
|
piv.userData.rigid = true;
|
|
13112
13619
|
this.c.vehicle.add(piv);
|
|
@@ -13170,10 +13677,20 @@ var GameVehicles = (() => {
|
|
|
13170
13677
|
try {
|
|
13171
13678
|
if (p.role === "door") {
|
|
13172
13679
|
const dirty = /* @__PURE__ */ new Set();
|
|
13173
|
-
const
|
|
13174
|
-
const
|
|
13680
|
+
const glassBox = new this.THREE.Box3();
|
|
13681
|
+
for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
|
|
13682
|
+
const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
|
|
13683
|
+
this.doorTops.set(id, top);
|
|
13684
|
+
const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
|
|
13685
|
+
const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
|
|
13175
13686
|
this.mend(dirty);
|
|
13176
|
-
if (pcs.length)
|
|
13687
|
+
if (pcs.length) {
|
|
13688
|
+
const panes = I.windowsOver(this.c, z, inv);
|
|
13689
|
+
const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
|
|
13690
|
+
const mirrors = this.mirrorsOn(z);
|
|
13691
|
+
const frames = this.framesAround(panes, inv);
|
|
13692
|
+
objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
|
|
13693
|
+
}
|
|
13177
13694
|
} else {
|
|
13178
13695
|
const pcs = this.cutZone(z);
|
|
13179
13696
|
if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
|
|
@@ -13191,10 +13708,152 @@ var GameVehicles = (() => {
|
|
|
13191
13708
|
const di = deform.indexOf(o);
|
|
13192
13709
|
if (di >= 0) deform.splice(di, 1);
|
|
13193
13710
|
}
|
|
13711
|
+
if (p.role === "door" && objs.length > 0) {
|
|
13712
|
+
this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
|
|
13713
|
+
this.liftDoorTrim(objs, z);
|
|
13714
|
+
this.cutStripsAcross(piv, z, inv);
|
|
13715
|
+
}
|
|
13194
13716
|
piv.quaternion.copy(q);
|
|
13195
13717
|
this.c.root.updateMatrixWorld(true);
|
|
13196
13718
|
return objs.length > 0;
|
|
13197
13719
|
}
|
|
13720
|
+
/** 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. */
|
|
13721
|
+
doorEdges(piv, z, y1) {
|
|
13722
|
+
const THREE = this.THREE;
|
|
13723
|
+
const B = z.box;
|
|
13724
|
+
const side2 = z.side < 0 ? -1 : 1;
|
|
13725
|
+
const flat = piv.position.z - side2 * 0.016;
|
|
13726
|
+
const x0 = B.x0 + 0.03;
|
|
13727
|
+
const x1 = B.x1 - 0.03;
|
|
13728
|
+
const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
|
|
13729
|
+
const loop = [];
|
|
13730
|
+
const run = (ax, ay, bx, by) => {
|
|
13731
|
+
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]);
|
|
13732
|
+
};
|
|
13733
|
+
run(x0, y0, x1, y0);
|
|
13734
|
+
run(x1, y0, x1, y1);
|
|
13735
|
+
run(x1, y1, x0, y1);
|
|
13736
|
+
run(x0, y1, x0, y0);
|
|
13737
|
+
const heights = loop.map(([x, y]) => {
|
|
13738
|
+
const at = this.surface("z", side2, B, [[x, y]]);
|
|
13739
|
+
return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
|
|
13740
|
+
});
|
|
13741
|
+
const band = (lift) => {
|
|
13742
|
+
const position = [];
|
|
13743
|
+
const index = [];
|
|
13744
|
+
loop.forEach(([x, y], i) => {
|
|
13745
|
+
position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
|
|
13746
|
+
const a = i * 2;
|
|
13747
|
+
const b = (i + 1) % loop.length * 2;
|
|
13748
|
+
index.push(a, a + 1, b, b, a + 1, b + 1);
|
|
13749
|
+
});
|
|
13750
|
+
const g = new THREE.BufferGeometry();
|
|
13751
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
|
|
13752
|
+
g.setIndex(index);
|
|
13753
|
+
g.computeVertexNormals();
|
|
13754
|
+
return g;
|
|
13755
|
+
};
|
|
13756
|
+
const paint = this.paint().clone();
|
|
13757
|
+
paint.side = THREE.DoubleSide;
|
|
13758
|
+
const cardPosition = [];
|
|
13759
|
+
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);
|
|
13760
|
+
const cardZ = (z2) => z2 - side2 * DOOR_DEPTH;
|
|
13761
|
+
loop.forEach(([x, y], i) => {
|
|
13762
|
+
const j = (i + 1) % loop.length;
|
|
13763
|
+
cardPosition.push(mid[0], mid[1], cardZ(mid[2]), x, y, cardZ(heights[i]), loop[j][0], loop[j][1], cardZ(heights[j]));
|
|
13764
|
+
});
|
|
13765
|
+
const cardGeometry = new THREE.BufferGeometry();
|
|
13766
|
+
cardGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cardPosition, 3));
|
|
13767
|
+
cardGeometry.computeVertexNormals();
|
|
13768
|
+
const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.85 }).clone();
|
|
13769
|
+
cardMaterial.side = THREE.DoubleSide;
|
|
13770
|
+
const card = new THREE.Mesh(cardGeometry, cardMaterial);
|
|
13771
|
+
card.userData.noFollow = true;
|
|
13772
|
+
card.castShadow = false;
|
|
13773
|
+
this.c.vehicle.add(card);
|
|
13774
|
+
piv.attach(card);
|
|
13775
|
+
const edges = new THREE.Mesh(band(2e-3), paint);
|
|
13776
|
+
const lining = new THREE.Mesh(band(2e-3), paint);
|
|
13777
|
+
for (const m of [edges, lining]) {
|
|
13778
|
+
m.userData.noFollow = true;
|
|
13779
|
+
m.castShadow = false;
|
|
13780
|
+
this.c.vehicle.add(m);
|
|
13781
|
+
}
|
|
13782
|
+
piv.attach(edges);
|
|
13783
|
+
}
|
|
13784
|
+
/** 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. */
|
|
13785
|
+
framesAround(panes, inv) {
|
|
13786
|
+
const THREE = this.THREE;
|
|
13787
|
+
const boxes = panes.map((g) => this.I.localBox(this.c, g, inv).expandByScalar(WINDOW_FRAME_NEAR));
|
|
13788
|
+
return this.c.vehicle.children.filter((o) => {
|
|
13789
|
+
if (!o.isMesh || !o.visible || o.userData.glass || o.userData.decal || this.c.deform?.includes(o)) return false;
|
|
13790
|
+
const box2 = this.I.localBox(this.c, o, inv);
|
|
13791
|
+
if (box2.isEmpty()) return false;
|
|
13792
|
+
return boxes.some((b) => b.containsBox(box2) && box2.getSize(new THREE.Vector3()).x > b.getSize(new THREE.Vector3()).x * WINDOW_FRAME_SPAN);
|
|
13793
|
+
});
|
|
13794
|
+
}
|
|
13795
|
+
/** A front door carries the mirror drawn at its corner. */
|
|
13796
|
+
mirrorsOn(z) {
|
|
13797
|
+
if (!z.mirror) return [];
|
|
13798
|
+
const own = (z.mirrors ?? []).filter((o) => o.parent);
|
|
13799
|
+
if (own.length) return own;
|
|
13800
|
+
const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
|
|
13801
|
+
return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
|
|
13802
|
+
}
|
|
13803
|
+
/** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
|
|
13804
|
+
liftDoorTrim(objs, z) {
|
|
13805
|
+
const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
|
|
13806
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
13807
|
+
for (const o of objs) {
|
|
13808
|
+
if (!o.isMesh || o.userData.glass) continue;
|
|
13809
|
+
const box2 = this.I.localBox(this.c, o, inv);
|
|
13810
|
+
const size = box2.getSize(new this.THREE.Vector3());
|
|
13811
|
+
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;
|
|
13812
|
+
}
|
|
13813
|
+
}
|
|
13814
|
+
/** 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. */
|
|
13815
|
+
cutStripsAcross(piv, z, inv) {
|
|
13816
|
+
const THREE = this.THREE;
|
|
13817
|
+
const B = z.box;
|
|
13818
|
+
const x0 = B.x0 + 0.03;
|
|
13819
|
+
const x1 = B.x1 - 0.03;
|
|
13820
|
+
const y0 = B.y0 + DOOR_SILL;
|
|
13821
|
+
const glassTop = (I) => {
|
|
13822
|
+
let top = -Infinity;
|
|
13823
|
+
for (const g of I.windowsOver(this.c, z, inv)) top = Math.max(top, I.localBox(this.c, g, inv).max.y);
|
|
13824
|
+
return top;
|
|
13825
|
+
};
|
|
13826
|
+
const y1 = Math.max(B.y1, glassTop(this.I)) + DOOR_STRIP_NEAR;
|
|
13827
|
+
const deform = this.c.deform ?? [];
|
|
13828
|
+
for (const o of [...this.c.vehicle.children]) {
|
|
13829
|
+
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;
|
|
13830
|
+
const box2 = this.I.localBox(this.c, o, inv);
|
|
13831
|
+
const size = box2.getSize(new THREE.Vector3());
|
|
13832
|
+
const mid = box2.getCenter(new THREE.Vector3());
|
|
13833
|
+
if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
|
|
13834
|
+
const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
|
|
13835
|
+
const v = new THREE.Vector3();
|
|
13836
|
+
const sdf = (x, y, zz) => {
|
|
13837
|
+
v.set(x, y, zz).applyMatrix4(toVehicle);
|
|
13838
|
+
return Math.max(x0 - v.x, v.x - x1);
|
|
13839
|
+
};
|
|
13840
|
+
const over = new THREE.Mesh(o.geometry.clone(), o.material);
|
|
13841
|
+
over.position.copy(o.position);
|
|
13842
|
+
over.quaternion.copy(o.quaternion);
|
|
13843
|
+
over.scale.copy(o.scale);
|
|
13844
|
+
this.c.vehicle.add(over);
|
|
13845
|
+
over.updateMatrixWorld(true);
|
|
13846
|
+
const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
|
|
13847
|
+
const w = new THREE.Vector3();
|
|
13848
|
+
this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
|
|
13849
|
+
w.set(x, y, zz).applyMatrix4(overToVehicle);
|
|
13850
|
+
return -Math.max(x0 - w.x, w.x - x1);
|
|
13851
|
+
} }], STRIP_SMOOTH_DEG, {});
|
|
13852
|
+
this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
|
|
13853
|
+
over.userData.noFollow = true;
|
|
13854
|
+
piv.attach(over);
|
|
13855
|
+
}
|
|
13856
|
+
}
|
|
13198
13857
|
mend(dirty) {
|
|
13199
13858
|
dirty.forEach((msh) => {
|
|
13200
13859
|
const g = msh.geometry;
|
|
@@ -13204,7 +13863,470 @@ var GameVehicles = (() => {
|
|
|
13204
13863
|
g.computeBoundingBox();
|
|
13205
13864
|
});
|
|
13206
13865
|
}
|
|
13866
|
+
/**
|
|
13867
|
+
* 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
|
|
13868
|
+
* comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
|
|
13869
|
+
*/
|
|
13870
|
+
seatPanes() {
|
|
13871
|
+
const THREE = this.THREE;
|
|
13872
|
+
const c = this.c;
|
|
13873
|
+
c.root.updateMatrixWorld(true);
|
|
13874
|
+
const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
|
|
13875
|
+
if (!shell.length) return;
|
|
13876
|
+
const ray = new THREE.Raycaster();
|
|
13877
|
+
ray.layers.enableAll();
|
|
13878
|
+
for (const pane of c.glass) {
|
|
13879
|
+
if (pane.userData.glass?.outline) continue;
|
|
13880
|
+
const local = this.shellAround(pane, shell);
|
|
13881
|
+
if (!local) continue;
|
|
13882
|
+
const seat = this.paneSeat(pane, [local], ray);
|
|
13883
|
+
if (!seat) continue;
|
|
13884
|
+
const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
|
|
13885
|
+
for (const m of [pane, ...near]) this.movePane(m, seat);
|
|
13886
|
+
const past = this.shellPast(pane, [local], ray, seat.normal);
|
|
13887
|
+
if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
|
|
13888
|
+
this.reachShellBelow(pane, local, ray, seat.normal);
|
|
13889
|
+
this.paneBorder(pane, near, seat);
|
|
13890
|
+
this.keepUnderPane(pane, seat.normal);
|
|
13891
|
+
const across = new this.THREE.Vector3(0, 0, 1).transformDirection(this.c.vehicle.matrixWorld);
|
|
13892
|
+
if (Math.abs(seat.normal.dot(across)) < SIDE_PANE_FACING) {
|
|
13893
|
+
for (const body of shell) if (body.parent === this.c.vehicle) this.cutUnderPane(body, pane, seat.normal, SKIN_UNDER_PANE, SKIN_EDGE_KEPT);
|
|
13894
|
+
}
|
|
13895
|
+
}
|
|
13896
|
+
}
|
|
13897
|
+
/** 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. */
|
|
13898
|
+
shellAround(pane, shell) {
|
|
13899
|
+
const THREE = this.THREE;
|
|
13900
|
+
const box2 = new THREE.Box3().setFromObject(pane).expandByScalar(PANE_SEAT_REACH + PANE_SEAT_OUT);
|
|
13901
|
+
const out = [];
|
|
13902
|
+
const a = new THREE.Vector3();
|
|
13903
|
+
const b = new THREE.Vector3();
|
|
13904
|
+
const d = new THREE.Vector3();
|
|
13905
|
+
const tri = new THREE.Box3();
|
|
13906
|
+
for (const mesh2 of shell) {
|
|
13907
|
+
const pos = mesh2.geometry.attributes.position;
|
|
13908
|
+
const idx = mesh2.geometry.index;
|
|
13909
|
+
const count = idx ? idx.count : pos.count;
|
|
13910
|
+
const e = mesh2.matrixWorld.elements;
|
|
13911
|
+
const world = (v, i) => {
|
|
13912
|
+
const x = pos.getX(i);
|
|
13913
|
+
const y = pos.getY(i);
|
|
13914
|
+
const z = pos.getZ(i);
|
|
13915
|
+
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]);
|
|
13916
|
+
};
|
|
13917
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
13918
|
+
world(a, idx ? idx.getX(f) : f);
|
|
13919
|
+
world(b, idx ? idx.getX(f + 1) : f + 1);
|
|
13920
|
+
world(d, idx ? idx.getX(f + 2) : f + 2);
|
|
13921
|
+
tri.makeEmpty().expandByPoint(a).expandByPoint(b).expandByPoint(d);
|
|
13922
|
+
if (tri.intersectsBox(box2)) out.push(a.x, a.y, a.z, b.x, b.y, b.z, d.x, d.y, d.z);
|
|
13923
|
+
}
|
|
13924
|
+
}
|
|
13925
|
+
if (!out.length) return null;
|
|
13926
|
+
const g = new THREE.BufferGeometry();
|
|
13927
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
|
|
13928
|
+
const proxy = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }));
|
|
13929
|
+
proxy.updateMatrixWorld(true);
|
|
13930
|
+
return proxy;
|
|
13931
|
+
}
|
|
13932
|
+
/** The outward normal of a pane and how far the shell is at each sampled point (world). */
|
|
13933
|
+
paneSeat(pane, shell, ray) {
|
|
13934
|
+
const THREE = this.THREE;
|
|
13935
|
+
const pos = pane.geometry.attributes.position;
|
|
13936
|
+
const box2 = new THREE.Box3().setFromObject(pane);
|
|
13937
|
+
const centre = box2.getCenter(new THREE.Vector3());
|
|
13938
|
+
const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
|
|
13939
|
+
const tri = new THREE.Triangle();
|
|
13940
|
+
const normal = new THREE.Vector3();
|
|
13941
|
+
let largest = 0;
|
|
13942
|
+
const idx = pane.geometry.index;
|
|
13943
|
+
const count = idx ? idx.count : pos.count;
|
|
13944
|
+
for (let f = 0; f + 2 < count; f += 3) {
|
|
13945
|
+
tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
|
|
13946
|
+
tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
|
|
13947
|
+
tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
|
|
13948
|
+
if (tri.getArea() > largest) {
|
|
13949
|
+
largest = tri.getArea();
|
|
13950
|
+
tri.getNormal(normal);
|
|
13951
|
+
}
|
|
13952
|
+
}
|
|
13953
|
+
normal.transformDirection(pane.matrixWorld);
|
|
13954
|
+
if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
|
|
13955
|
+
const samples = [];
|
|
13956
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13957
|
+
const v = new THREE.Vector3();
|
|
13958
|
+
const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
|
|
13959
|
+
for (let i = 0; i < pos.count; i += step) {
|
|
13960
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
13961
|
+
const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
|
|
13962
|
+
if (seen.has(key)) continue;
|
|
13963
|
+
seen.add(key);
|
|
13964
|
+
const away = v.clone().sub(centre);
|
|
13965
|
+
away.addScaledVector(normal, -away.dot(normal)).normalize();
|
|
13966
|
+
let by = -Infinity;
|
|
13967
|
+
for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
|
|
13968
|
+
const at = v.clone().addScaledVector(away, off);
|
|
13969
|
+
ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
|
|
13970
|
+
ray.far = PANE_SEAT_REACH * 2;
|
|
13971
|
+
const hit = raycastIndexed(THREE, ray, shell)[0];
|
|
13972
|
+
if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
|
|
13973
|
+
}
|
|
13974
|
+
if (by > -Infinity) samples.push({ at: v.clone(), by });
|
|
13975
|
+
}
|
|
13976
|
+
return samples.length >= 3 ? { normal, samples } : null;
|
|
13977
|
+
}
|
|
13978
|
+
/** 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. */
|
|
13979
|
+
reachShellBelow(pane, shell, ray, normal) {
|
|
13980
|
+
const THREE = this.THREE;
|
|
13981
|
+
const down = new THREE.Vector3(0, -1, 0).applyQuaternion(this.c.root.getWorldQuaternion(new THREE.Quaternion()));
|
|
13982
|
+
down.addScaledVector(normal, -down.dot(normal));
|
|
13983
|
+
if (down.lengthSq() < PANE_MIN_SLOPE) return;
|
|
13984
|
+
down.normalize();
|
|
13985
|
+
const g = pane.geometry;
|
|
13986
|
+
const pos = g.attributes.position;
|
|
13987
|
+
const v = new THREE.Vector3();
|
|
13988
|
+
let lowest = -Infinity;
|
|
13989
|
+
for (let i = 0; i < pos.count; i++) lowest = Math.max(lowest, v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).dot(down));
|
|
13990
|
+
const edge = [];
|
|
13991
|
+
const mid = new THREE.Vector3();
|
|
13992
|
+
for (let i = 0; i < pos.count; i++) {
|
|
13993
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
13994
|
+
if (v.dot(down) > lowest - PANE_EDGE_BAND) {
|
|
13995
|
+
edge.push(i);
|
|
13996
|
+
mid.add(v);
|
|
13997
|
+
}
|
|
13998
|
+
}
|
|
13999
|
+
if (!edge.length) return;
|
|
14000
|
+
mid.divideScalar(edge.length);
|
|
14001
|
+
let reach = 0;
|
|
14002
|
+
for (let d = PANE_REACH_STEP; d <= PANE_REACH_MAX; d += PANE_REACH_STEP) {
|
|
14003
|
+
const at = mid.clone().addScaledVector(down, d).addScaledVector(normal, PANE_SEAT_REACH);
|
|
14004
|
+
ray.set(at, normal.clone().negate());
|
|
14005
|
+
ray.far = PANE_SEAT_REACH * 2;
|
|
14006
|
+
if (ray.intersectObject(shell, false)[0]) {
|
|
14007
|
+
reach = d + PANE_REACH_STEP;
|
|
14008
|
+
break;
|
|
14009
|
+
}
|
|
14010
|
+
}
|
|
14011
|
+
if (!reach) return;
|
|
14012
|
+
const inverse = pane.matrixWorld.clone().invert();
|
|
14013
|
+
for (const i of edge) {
|
|
14014
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).addScaledVector(down, reach).applyMatrix4(inverse);
|
|
14015
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
14016
|
+
}
|
|
14017
|
+
pos.needsUpdate = true;
|
|
14018
|
+
g.computeBoundingBox();
|
|
14019
|
+
g.computeBoundingSphere();
|
|
14020
|
+
}
|
|
14021
|
+
/** 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. */
|
|
14022
|
+
keepUnderPane(pane, normal) {
|
|
14023
|
+
const THREE = this.THREE;
|
|
14024
|
+
const paneBox2 = new THREE.Box3().setFromObject(pane).expandByScalar(COWL_NEAR_PANE);
|
|
14025
|
+
const pos = pane.geometry.attributes.position;
|
|
14026
|
+
const centre = new THREE.Vector3();
|
|
14027
|
+
const v = new THREE.Vector3();
|
|
14028
|
+
for (let i = 0; i < pos.count; i++) centre.add(v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld));
|
|
14029
|
+
centre.divideScalar(pos.count);
|
|
14030
|
+
const plane2 = centre.dot(normal);
|
|
14031
|
+
for (const o of this.c.vehicle.children) {
|
|
14032
|
+
if (!o.isMesh || o === pane || o.userData.glass || this.c.deform?.includes(o)) continue;
|
|
14033
|
+
const box2 = new THREE.Box3().setFromObject(o);
|
|
14034
|
+
const size = box2.getSize(new THREE.Vector3());
|
|
14035
|
+
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(box2)) continue;
|
|
14036
|
+
if (box2.getCenter(new THREE.Vector3()).dot(normal) - plane2 >= COWL_UNDER_GLASS) continue;
|
|
14037
|
+
if (size.y >= COWL_PLATE) {
|
|
14038
|
+
o.material = this.I.makeMaterial(DASHBOARD_COLOR, { roughness: 1, metalness: 0 });
|
|
14039
|
+
continue;
|
|
14040
|
+
}
|
|
14041
|
+
o.material = this.paint();
|
|
14042
|
+
this.cutUnderPane(o, pane, normal);
|
|
14043
|
+
}
|
|
14044
|
+
}
|
|
14045
|
+
/** Cuts away the part of a mesh that lies under a pane (behind its plane, inside its outline seen along its normal). */
|
|
14046
|
+
cutUnderPane(mesh2, pane, normal, behind = -Infinity, inset = 0) {
|
|
14047
|
+
const THREE = this.THREE;
|
|
14048
|
+
const outline = this.paneOutline(pane, normal);
|
|
14049
|
+
if (outline.length < 3) return;
|
|
14050
|
+
const u = new THREE.Vector3(0, 1, 0).cross(normal);
|
|
14051
|
+
if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
|
|
14052
|
+
u.normalize();
|
|
14053
|
+
const w = normal.clone().cross(u);
|
|
14054
|
+
const cx = outline.reduce((sum, p) => sum + p.dot(u), 0) / outline.length;
|
|
14055
|
+
const cy = outline.reduce((sum, p) => sum + p.dot(w), 0) / outline.length;
|
|
14056
|
+
const poly = outline.map((p) => {
|
|
14057
|
+
const x = p.dot(u);
|
|
14058
|
+
const y = p.dot(w);
|
|
14059
|
+
const d = Math.hypot(x - cx, y - cy) || 1;
|
|
14060
|
+
const k = Math.max(0, d - inset) / d;
|
|
14061
|
+
return [cx + (x - cx) * k, cy + (y - cy) * k];
|
|
14062
|
+
});
|
|
14063
|
+
const plane2 = outline.reduce((sum, p) => sum + p.dot(normal), 0) / outline.length;
|
|
14064
|
+
const inside = (x, y) => {
|
|
14065
|
+
let hit = false;
|
|
14066
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
14067
|
+
const [xi, yi] = poly[i];
|
|
14068
|
+
const [xj, yj] = poly[j];
|
|
14069
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) hit = !hit;
|
|
14070
|
+
}
|
|
14071
|
+
return hit;
|
|
14072
|
+
};
|
|
14073
|
+
const v = new THREE.Vector3();
|
|
14074
|
+
const toWorld = mesh2.matrixWorld;
|
|
14075
|
+
mesh2.updateMatrixWorld(true);
|
|
14076
|
+
const xs = poly.map((p) => p[0]);
|
|
14077
|
+
const ys = poly.map((p) => p[1]);
|
|
14078
|
+
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];
|
|
14079
|
+
subdivideNear(THREE, { mesh: mesh2, maxLen: COWL_CUT_SUB, near: (corners) => {
|
|
14080
|
+
const us = corners.map((c) => c.dot(u));
|
|
14081
|
+
const ws = corners.map((c) => c.dot(w));
|
|
14082
|
+
return Math.max(...us) >= x0 && Math.min(...us) <= x1 && Math.max(...ws) >= y0 && Math.min(...ws) <= y1;
|
|
14083
|
+
} });
|
|
14084
|
+
this.kit.cutMesh(
|
|
14085
|
+
mesh2,
|
|
14086
|
+
[
|
|
14087
|
+
{
|
|
14088
|
+
sdf: (x, y, z) => {
|
|
14089
|
+
v.set(x, y, z).applyMatrix4(toWorld);
|
|
14090
|
+
return inside(v.dot(u), v.dot(w)) && plane2 - v.dot(normal) > behind ? -1 : 1;
|
|
14091
|
+
}
|
|
14092
|
+
}
|
|
14093
|
+
],
|
|
14094
|
+
42,
|
|
14095
|
+
{}
|
|
14096
|
+
);
|
|
14097
|
+
}
|
|
14098
|
+
/** 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. */
|
|
14099
|
+
paneBorder(pane, near, seat) {
|
|
14100
|
+
const THREE = this.THREE;
|
|
14101
|
+
const bars = near.filter((o) => {
|
|
14102
|
+
if (o.geometry.attributes.position.count > PANE_BAR_VERTICES) return false;
|
|
14103
|
+
const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
|
|
14104
|
+
return Math.max(size.x, size.y, size.z) > PANE_BAR_LENGTH;
|
|
14105
|
+
});
|
|
14106
|
+
if (!bars.length) return;
|
|
14107
|
+
const outline = this.paneOutline(pane, seat.normal);
|
|
14108
|
+
if (outline.length < 3) return;
|
|
14109
|
+
const centre = new THREE.Vector3();
|
|
14110
|
+
for (const p of outline) centre.add(p);
|
|
14111
|
+
centre.divideScalar(outline.length);
|
|
14112
|
+
const position = [];
|
|
14113
|
+
const toVehicle = this.I.worldInverseOf(this.c);
|
|
14114
|
+
const put = (p) => {
|
|
14115
|
+
const q = p.clone().addScaledVector(seat.normal, PANE_BORDER_LIFT).applyMatrix4(toVehicle);
|
|
14116
|
+
position.push(q.x, q.y, q.z);
|
|
14117
|
+
};
|
|
14118
|
+
const inward = (p) => p.clone().add(centre.clone().sub(p).normalize().multiplyScalar(PANE_BORDER));
|
|
14119
|
+
outline.forEach((a, i) => {
|
|
14120
|
+
const b = outline[(i + 1) % outline.length];
|
|
14121
|
+
const c = inward(b);
|
|
14122
|
+
const d = inward(a);
|
|
14123
|
+
for (const p of [a, b, c, a, c, d]) put(p);
|
|
14124
|
+
});
|
|
14125
|
+
if (!position.length) return;
|
|
14126
|
+
for (const o of bars) o.visible = false;
|
|
14127
|
+
const band = new THREE.BufferGeometry();
|
|
14128
|
+
band.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
|
|
14129
|
+
band.computeVertexNormals();
|
|
14130
|
+
const material = this.I.MATERIALS.dark.clone();
|
|
14131
|
+
material.side = THREE.DoubleSide;
|
|
14132
|
+
const mesh2 = new THREE.Mesh(band, material);
|
|
14133
|
+
mesh2.userData.noFollow = true;
|
|
14134
|
+
mesh2.castShadow = false;
|
|
14135
|
+
mesh2.raycast = () => {
|
|
14136
|
+
};
|
|
14137
|
+
this.c.vehicle.add(mesh2);
|
|
14138
|
+
}
|
|
14139
|
+
/** The outline of a pane, the convex hull of its vertices seen along its normal, in world space. */
|
|
14140
|
+
paneOutline(pane, normal) {
|
|
14141
|
+
const THREE = this.THREE;
|
|
14142
|
+
const pos = pane.geometry.attributes.position;
|
|
14143
|
+
const u = new THREE.Vector3(0, 1, 0).cross(normal);
|
|
14144
|
+
if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
|
|
14145
|
+
u.normalize();
|
|
14146
|
+
const w = normal.clone().cross(u);
|
|
14147
|
+
const points = [];
|
|
14148
|
+
for (let i = 0; i < pos.count; i++) {
|
|
14149
|
+
const p = new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
|
|
14150
|
+
points.push({ x: p.dot(u), y: p.dot(w), p });
|
|
14151
|
+
}
|
|
14152
|
+
points.sort((a, b) => a.x - b.x || a.y - b.y);
|
|
14153
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
14154
|
+
const half = (list2) => {
|
|
14155
|
+
const out = [];
|
|
14156
|
+
for (const q of list2) {
|
|
14157
|
+
while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], q) <= 0) out.pop();
|
|
14158
|
+
out.push(q);
|
|
14159
|
+
}
|
|
14160
|
+
out.pop();
|
|
14161
|
+
return out;
|
|
14162
|
+
};
|
|
14163
|
+
return [...half(points), ...half([...points].reverse())].map((q) => q.p);
|
|
14164
|
+
}
|
|
14165
|
+
/** How far the shell still stands past a seated pane, over its triangles. */
|
|
14166
|
+
shellPast(pane, shell, ray, normal) {
|
|
14167
|
+
const THREE = this.THREE;
|
|
14168
|
+
const pos = pane.geometry.attributes.position;
|
|
14169
|
+
const idx = pane.geometry.index;
|
|
14170
|
+
const count = idx ? idx.count : pos.count;
|
|
14171
|
+
const tri = new THREE.Triangle();
|
|
14172
|
+
const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
|
|
14173
|
+
let past = 0;
|
|
14174
|
+
for (let f = 0; f + 2 < count; f += step) {
|
|
14175
|
+
tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
|
|
14176
|
+
tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
|
|
14177
|
+
tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
|
|
14178
|
+
const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
|
|
14179
|
+
ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
|
|
14180
|
+
ray.far = PANE_SEAT_REACH;
|
|
14181
|
+
const hit = raycastIndexed(THREE, ray, shell)[0];
|
|
14182
|
+
if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
|
|
14183
|
+
}
|
|
14184
|
+
return past;
|
|
14185
|
+
}
|
|
14186
|
+
/** A frame or a seal drawn on the pane: small, and close to its plane. */
|
|
14187
|
+
onPane(o, pane, seat) {
|
|
14188
|
+
const THREE = this.THREE;
|
|
14189
|
+
const box2 = new THREE.Box3().setFromObject(o);
|
|
14190
|
+
if (box2.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box2)) return false;
|
|
14191
|
+
const at = box2.getCenter(new THREE.Vector3());
|
|
14192
|
+
return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
|
|
14193
|
+
}
|
|
14194
|
+
movePane(mesh2, seat) {
|
|
14195
|
+
const THREE = this.THREE;
|
|
14196
|
+
const g = mesh2.geometry.clone();
|
|
14197
|
+
const pos = g.attributes.position;
|
|
14198
|
+
const inverse = mesh2.matrixWorld.clone().invert();
|
|
14199
|
+
const v = new THREE.Vector3();
|
|
14200
|
+
for (let i = 0; i < pos.count; i++) {
|
|
14201
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(mesh2.matrixWorld);
|
|
14202
|
+
let weight = 0;
|
|
14203
|
+
let by = 0;
|
|
14204
|
+
for (const s of seat.samples) {
|
|
14205
|
+
const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
|
|
14206
|
+
weight += w;
|
|
14207
|
+
by += s.by * w;
|
|
14208
|
+
}
|
|
14209
|
+
v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
|
|
14210
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
14211
|
+
}
|
|
14212
|
+
pos.needsUpdate = true;
|
|
14213
|
+
g.computeBoundingBox();
|
|
14214
|
+
g.computeBoundingSphere();
|
|
14215
|
+
mesh2.geometry = g;
|
|
14216
|
+
}
|
|
14217
|
+
/** 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. */
|
|
14218
|
+
seatHood() {
|
|
14219
|
+
const hood = this.c.hood;
|
|
14220
|
+
if (!hood?.lift) return;
|
|
14221
|
+
const THREE = this.THREE;
|
|
14222
|
+
const box2 = new THREE.Box3().setFromObject(hood.piv).getSize(new THREE.Vector3());
|
|
14223
|
+
for (const part of hood.piv.children) {
|
|
14224
|
+
part.position.y -= hood.lift + HOOD_SINK;
|
|
14225
|
+
part.scale.multiply(new THREE.Vector3(1 + HOOD_GAP_CLOSED / Math.max(0.3, box2.x), 1, 1 + HOOD_GAP_CLOSED / Math.max(0.3, box2.z)));
|
|
14226
|
+
}
|
|
14227
|
+
}
|
|
14228
|
+
/** A handle is at the free edge of its door, the end away from the hinge. */
|
|
14229
|
+
seatDoorHandles() {
|
|
14230
|
+
const THREE = this.THREE;
|
|
14231
|
+
const c = this.c;
|
|
14232
|
+
const inv = this.I.worldInverseOf(c);
|
|
14233
|
+
const bodyBox = new THREE.Box3();
|
|
14234
|
+
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));
|
|
14235
|
+
const outerHalf = Math.max(Math.abs(bodyBox.min.z), Math.abs(bodyBox.max.z));
|
|
14236
|
+
for (const p of this.model.parts) {
|
|
14237
|
+
if (p.role !== "door") continue;
|
|
14238
|
+
const z = this.zone(p);
|
|
14239
|
+
if (!z) continue;
|
|
14240
|
+
const B = z.box;
|
|
14241
|
+
const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
|
|
14242
|
+
for (const o of c.vehicle.children) {
|
|
14243
|
+
if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
|
|
14244
|
+
const box2 = this.I.localBox(c, o, inv);
|
|
14245
|
+
const size = box2.getSize(new THREE.Vector3());
|
|
14246
|
+
const at = box2.getCenter(new THREE.Vector3());
|
|
14247
|
+
const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
|
|
14248
|
+
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;
|
|
14249
|
+
if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
|
|
14250
|
+
const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
|
|
14251
|
+
o.position.x += free - at.x;
|
|
14252
|
+
if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
|
|
14253
|
+
}
|
|
14254
|
+
}
|
|
14255
|
+
c.root.updateMatrixWorld(true);
|
|
14256
|
+
}
|
|
14257
|
+
/** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
|
|
14258
|
+
seatMirrorGlass(glass, drawn) {
|
|
14259
|
+
const g = glass.geometry.clone();
|
|
14260
|
+
g.computeBoundingBox();
|
|
14261
|
+
const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
|
|
14262
|
+
g.translate(-mid.x, -mid.y, -mid.z);
|
|
14263
|
+
g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
|
|
14264
|
+
g.translate(mid.x, mid.y, mid.z);
|
|
14265
|
+
glass.geometry = g;
|
|
14266
|
+
const THREE = this.THREE;
|
|
14267
|
+
const volumeOf = (o) => {
|
|
14268
|
+
const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
|
|
14269
|
+
return size.x * size.y * size.z;
|
|
14270
|
+
};
|
|
14271
|
+
const housing = drawn.filter((o) => o !== glass && o.isMesh && o.geometry).sort((a, b) => volumeOf(b) - volumeOf(a))[0];
|
|
14272
|
+
if (housing) {
|
|
14273
|
+
this.c.root.updateMatrixWorld(true);
|
|
14274
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
14275
|
+
glass.updateMatrixWorld(true);
|
|
14276
|
+
const gb = this.I.localBox(this.c, glass, inv);
|
|
14277
|
+
const hb = this.I.localBox(this.c, housing, inv);
|
|
14278
|
+
const side2 = hb.getCenter(new THREE.Vector3()).x > gb.getCenter(new THREE.Vector3()).x ? -1 : 1;
|
|
14279
|
+
const face = side2 < 0 ? hb.min.x + MIRROR_FACE_DEPTH : hb.max.x - MIRROR_FACE_DEPTH;
|
|
14280
|
+
const shape = housing.geometry.clone();
|
|
14281
|
+
const pos = shape.attributes.position;
|
|
14282
|
+
const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, housing.matrixWorld);
|
|
14283
|
+
const back = toVehicle.clone().invert();
|
|
14284
|
+
const v = new THREE.Vector3();
|
|
14285
|
+
for (let i = 0; i < pos.count; i++) {
|
|
14286
|
+
v.fromBufferAttribute(pos, i).applyMatrix4(toVehicle);
|
|
14287
|
+
if ((v.x - face) * side2 <= 0) continue;
|
|
14288
|
+
v.x = face;
|
|
14289
|
+
v.applyMatrix4(back);
|
|
14290
|
+
pos.setXYZ(i, v.x, v.y, v.z);
|
|
14291
|
+
}
|
|
14292
|
+
shape.computeVertexNormals();
|
|
14293
|
+
housing.geometry = shape;
|
|
14294
|
+
const outer = side2 < 0 ? gb.min.x : gb.max.x;
|
|
14295
|
+
glass.position.x += face + side2 * MIRROR_GLASS_PROUD - outer;
|
|
14296
|
+
}
|
|
14297
|
+
glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
|
|
14298
|
+
}
|
|
14299
|
+
/** The mirror the model already draws around where the part says, if it draws one. */
|
|
14300
|
+
drawnMirror(p) {
|
|
14301
|
+
const THREE = this.THREE;
|
|
14302
|
+
const [cx, cy, cz] = p.box.center;
|
|
14303
|
+
this.c.root.updateMatrixWorld(true);
|
|
14304
|
+
const inv = this.I.worldInverseOf(this.c);
|
|
14305
|
+
const small = this.c.vehicle.children.flatMap((o) => {
|
|
14306
|
+
if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
|
|
14307
|
+
const box2 = this.I.localBox(this.c, o, inv);
|
|
14308
|
+
if (box2.isEmpty()) return [];
|
|
14309
|
+
const size = box2.getSize(new THREE.Vector3());
|
|
14310
|
+
return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box2.getCenter(new THREE.Vector3()) }] : [];
|
|
14311
|
+
});
|
|
14312
|
+
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);
|
|
14313
|
+
return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
|
|
14314
|
+
}
|
|
13207
14315
|
makeMirror(p) {
|
|
14316
|
+
const drawn = this.drawnMirror(p);
|
|
14317
|
+
if (drawn.length) {
|
|
14318
|
+
for (const o of drawn) {
|
|
14319
|
+
if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
|
|
14320
|
+
const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
|
|
14321
|
+
const [thin, mid] = size.toArray().sort((a, b) => a - b);
|
|
14322
|
+
if (thin < MIRROR_GLASS_THIN && mid > MIRROR_GLASS_MIN) {
|
|
14323
|
+
this.seatMirrorGlass(o, drawn);
|
|
14324
|
+
o.userData.mirrorGlass = true;
|
|
14325
|
+
}
|
|
14326
|
+
}
|
|
14327
|
+
this.extra.set(p.id, drawn);
|
|
14328
|
+
return;
|
|
14329
|
+
}
|
|
13208
14330
|
const [cx, cy, cz] = p.box.center;
|
|
13209
14331
|
const s = cz < 0 ? -1 : 1;
|
|
13210
14332
|
const mats = this.I.MATERIALS;
|
|
@@ -13212,14 +14334,26 @@ var GameVehicles = (() => {
|
|
|
13212
14334
|
g.name = `obj_${p.id}`;
|
|
13213
14335
|
g.position.set(cx, cy, cz);
|
|
13214
14336
|
this.c.vehicle.add(g);
|
|
13215
|
-
this.box(0.05, 0.
|
|
13216
|
-
const housing = this.box(0.
|
|
14337
|
+
this.box(0.05, 0.035, 0.05, mats.dark, 0, -0.04, -s * 0.06, g);
|
|
14338
|
+
const housing = this.box(0.08, 0.11, 0.125, mats.plastic ?? mats.dark, 0, 0, s * 0.0225, g);
|
|
13217
14339
|
housing.userData.mirrorHousing = true;
|
|
13218
|
-
const glass = this.box(0.095, 0.
|
|
14340
|
+
const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
|
|
13219
14341
|
glass.userData.mirrorGlass = true;
|
|
13220
14342
|
g.userData.rigid = true;
|
|
13221
14343
|
this.extra.set(p.id, [g]);
|
|
13222
14344
|
}
|
|
14345
|
+
/** 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. */
|
|
14346
|
+
shatterMirror(id) {
|
|
14347
|
+
let broke = false;
|
|
14348
|
+
for (const o of this.extra.get(id) ?? []) {
|
|
14349
|
+
o.traverse((m) => {
|
|
14350
|
+
if (!m.userData.mirrorGlass || !m.visible) return;
|
|
14351
|
+
m.visible = false;
|
|
14352
|
+
broke = true;
|
|
14353
|
+
});
|
|
14354
|
+
}
|
|
14355
|
+
return broke;
|
|
14356
|
+
}
|
|
13223
14357
|
makeBattery(p) {
|
|
13224
14358
|
const mats = this.I.MATERIALS;
|
|
13225
14359
|
const g = new this.THREE.Group();
|
|
@@ -13245,17 +14379,33 @@ var GameVehicles = (() => {
|
|
|
13245
14379
|
}
|
|
13246
14380
|
// ---- marks -------------------------------------------------------------------------------------------------------------------------------
|
|
13247
14381
|
/** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
|
|
13248
|
-
holeDecal(point, normal, diameter) {
|
|
14382
|
+
holeDecal(point, normal, diameter, part) {
|
|
13249
14383
|
const THREE = this.THREE;
|
|
13250
|
-
const
|
|
14384
|
+
const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
|
|
14385
|
+
if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
|
|
14386
|
+
const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
|
|
14387
|
+
const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
|
|
14388
|
+
m.scale.set(radius, radius, 1);
|
|
13251
14389
|
const n = new THREE.Vector3(...normal);
|
|
13252
|
-
m.position.set(...point).addScaledVector(n,
|
|
13253
|
-
|
|
14390
|
+
m.position.set(...point).addScaledVector(n, 3e-3);
|
|
14391
|
+
this.c.vehicle.add(m);
|
|
14392
|
+
m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
|
|
13254
14393
|
m.userData.decal = true;
|
|
13255
14394
|
m.userData.noFollow = true;
|
|
14395
|
+
m.renderOrder = 3;
|
|
13256
14396
|
m.raycast = () => {
|
|
13257
14397
|
};
|
|
13258
|
-
this.c.vehicle.
|
|
14398
|
+
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);
|
|
14399
|
+
if (carrier) carrier.attach(m);
|
|
14400
|
+
}
|
|
14401
|
+
/** The door or lid a hinge group stands for. */
|
|
14402
|
+
/** 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. */
|
|
14403
|
+
extraIdOf(o) {
|
|
14404
|
+
for (const [id, objs] of this.extra) if (objs.includes(o)) return id;
|
|
14405
|
+
return void 0;
|
|
14406
|
+
}
|
|
14407
|
+
plateIdOf(o) {
|
|
14408
|
+
return o.userData.plateOf;
|
|
13259
14409
|
}
|
|
13260
14410
|
spark(point, dir, count = 8) {
|
|
13261
14411
|
const V3 = this.THREE.Vector3;
|
|
@@ -13295,10 +14445,8 @@ var GameVehicles = (() => {
|
|
|
13295
14445
|
}
|
|
13296
14446
|
/** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
|
|
13297
14447
|
hitGlass(p, point, dir, force) {
|
|
13298
|
-
const z = this.zone(p);
|
|
13299
|
-
if (!z) return false;
|
|
13300
14448
|
const V3 = this.THREE.Vector3;
|
|
13301
|
-
const list2 =
|
|
14449
|
+
const list2 = this.panesOf(p).filter((g) => g.parent && g.visible);
|
|
13302
14450
|
if (!list2.length) return false;
|
|
13303
14451
|
const pt = new V3(...point);
|
|
13304
14452
|
let best = list2[0];
|
|
@@ -13312,12 +14460,20 @@ var GameVehicles = (() => {
|
|
|
13312
14460
|
best = g;
|
|
13313
14461
|
}
|
|
13314
14462
|
}
|
|
14463
|
+
if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
|
|
13315
14464
|
this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
|
|
13316
14465
|
return !!best.userData.glass?.broken;
|
|
13317
14466
|
}
|
|
14467
|
+
panesOf(p) {
|
|
14468
|
+
return p.panes ?? this.zone(p)?.glass ?? [];
|
|
14469
|
+
}
|
|
13318
14470
|
glassBreak(p) {
|
|
13319
14471
|
const z = this.zone(p);
|
|
13320
|
-
if (!z)
|
|
14472
|
+
if (!z) {
|
|
14473
|
+
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);
|
|
14474
|
+
this.gone.add(p.id);
|
|
14475
|
+
return;
|
|
14476
|
+
}
|
|
13321
14477
|
for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
|
|
13322
14478
|
this.gone.add(p.id);
|
|
13323
14479
|
}
|
|
@@ -13326,6 +14482,17 @@ var GameVehicles = (() => {
|
|
|
13326
14482
|
if (!z) return;
|
|
13327
14483
|
for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
|
|
13328
14484
|
}
|
|
14485
|
+
/** A tyre driven flat until it tore apart: the rubber is gone and the bare rim shows. */
|
|
14486
|
+
tyreTornOff(p) {
|
|
14487
|
+
const z = this.zone(p);
|
|
14488
|
+
if (!z) return;
|
|
14489
|
+
for (const w of z.wheels) {
|
|
14490
|
+
if (!w.parent || w.userData.tornOff) continue;
|
|
14491
|
+
w.userData.tornOff = true;
|
|
14492
|
+
for (const m of this.I.tireMeshes(w)) m.visible = false;
|
|
14493
|
+
w.position.y -= (w.userData.r ?? DEFAULT_WHEEL_RADIUS) * SIDEWALL_SHARE;
|
|
14494
|
+
}
|
|
14495
|
+
}
|
|
13329
14496
|
/** Burning rubber: the tyre sags and goes black as it melts. */
|
|
13330
14497
|
tyreMelt(p, melt) {
|
|
13331
14498
|
const z = this.zone(p);
|
|
@@ -13345,6 +14512,7 @@ var GameVehicles = (() => {
|
|
|
13345
14512
|
smoothLater(msh) {
|
|
13346
14513
|
this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
|
|
13347
14514
|
this.tornThisFrame = true;
|
|
14515
|
+
this.holdArmed = true;
|
|
13348
14516
|
}
|
|
13349
14517
|
/** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
|
|
13350
14518
|
pump(budgetMs = 2.5) {
|
|
@@ -13352,6 +14520,7 @@ var GameVehicles = (() => {
|
|
|
13352
14520
|
this.tornThisFrame = false;
|
|
13353
14521
|
return;
|
|
13354
14522
|
}
|
|
14523
|
+
if (this.queued) return;
|
|
13355
14524
|
if (!this.smoothing.size && !this.warming) return;
|
|
13356
14525
|
const deadline = performance.now() + budgetMs;
|
|
13357
14526
|
for (const [msh, task] of this.smoothing) {
|
|
@@ -13360,6 +14529,34 @@ var GameVehicles = (() => {
|
|
|
13360
14529
|
}
|
|
13361
14530
|
this.warmIndexes(deadline);
|
|
13362
14531
|
}
|
|
14532
|
+
setQueued(isQueued) {
|
|
14533
|
+
this.queued = isQueued;
|
|
14534
|
+
}
|
|
14535
|
+
/** 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). */
|
|
14536
|
+
pendingUnits(frame) {
|
|
14537
|
+
if (this.holdArmed && frame !== void 0) {
|
|
14538
|
+
this.holdArmed = false;
|
|
14539
|
+
this.holdFrame = frame;
|
|
14540
|
+
}
|
|
14541
|
+
if (frame !== void 0 && frame === this.holdFrame) return 0;
|
|
14542
|
+
if (frame === void 0 && this.tornThisFrame) return 0;
|
|
14543
|
+
return this.smoothing.size + (this.warming ? 1 : 0);
|
|
14544
|
+
}
|
|
14545
|
+
/** One slice of the pending work, for the shared queue. */
|
|
14546
|
+
runStep(deadline) {
|
|
14547
|
+
const next = this.smoothing.entries().next();
|
|
14548
|
+
if (!next.done) {
|
|
14549
|
+
const [msh, task] = next.value;
|
|
14550
|
+
if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
|
|
14551
|
+
return true;
|
|
14552
|
+
}
|
|
14553
|
+
if (!this.warming) return false;
|
|
14554
|
+
this.warmIndexes(deadline);
|
|
14555
|
+
return true;
|
|
14556
|
+
}
|
|
14557
|
+
finishAll() {
|
|
14558
|
+
while (this.runStep(Infinity)) ;
|
|
14559
|
+
}
|
|
13363
14560
|
/** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
|
|
13364
14561
|
warmIndexes(deadline) {
|
|
13365
14562
|
if (!this.warming) return;
|
|
@@ -13531,6 +14728,7 @@ var GameVehicles = (() => {
|
|
|
13531
14728
|
}
|
|
13532
14729
|
pieceTasks.forEach((task, mesh2) => this.smoothing.set(mesh2, task));
|
|
13533
14730
|
this.tornThisFrame = true;
|
|
14731
|
+
this.holdArmed = true;
|
|
13534
14732
|
dirty.forEach((msh) => {
|
|
13535
14733
|
const g = msh.geometry;
|
|
13536
14734
|
if (g.userData.crease || !g.index) this.smoothLater(msh);
|
|
@@ -13618,6 +14816,41 @@ var GameVehicles = (() => {
|
|
|
13618
14816
|
var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
|
|
13619
14817
|
var AMBIENT = 20;
|
|
13620
14818
|
var FLAME = 700;
|
|
14819
|
+
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 };
|
|
14820
|
+
var ARMOUR_STEEL_MM = { IIIA: 4, III: 7 };
|
|
14821
|
+
var SOFT_LEAD = 0.8;
|
|
14822
|
+
var TYRE_FLAT_HEALTH = 0.1;
|
|
14823
|
+
var TYRE_BURST_JOULES = 1500;
|
|
14824
|
+
var TYRE_BURST_CALIBER = 12;
|
|
14825
|
+
var TYRE_LEAK_PER_MM2 = 26e-5;
|
|
14826
|
+
var FLAT_TYRE_LIFE_M = 1500;
|
|
14827
|
+
var FLAT_TYRE_TOWN_SPEED = 8;
|
|
14828
|
+
var RUN_FLAT_LIFE_M = 8e4;
|
|
14829
|
+
var RUN_FLAT_SPEED = 22;
|
|
14830
|
+
var TYRE_RUNFLAT_HEALTH = 0.6;
|
|
14831
|
+
var CRUMPLE_BASE_M = 0.04;
|
|
14832
|
+
var CRUMPLE_PER_FORCE_M = 0.15;
|
|
14833
|
+
var CRUMPLE_MIN_FORCE = 0.3;
|
|
14834
|
+
var CRUMPLE_MAX_FORCE = 1.4;
|
|
14835
|
+
var RIM_HEALTH = 0.05;
|
|
14836
|
+
var RIM_GRIND_PER_M = 4e-4;
|
|
14837
|
+
var TYRE_FLAT_SAG = 0.24;
|
|
14838
|
+
var TYRE_FLAT_AIR = 0.2;
|
|
14839
|
+
var BRAKE_FLUID_NEEDED = 0.6;
|
|
14840
|
+
var BRAKE_NO_FLUID = 0.15;
|
|
14841
|
+
var ENGINE_WORKING_C = 90;
|
|
14842
|
+
var ENGINE_SETTLE_S = 30;
|
|
14843
|
+
var ENGINE_HEATING = 0.5;
|
|
14844
|
+
var ENGINE_STEAM_C = 120;
|
|
14845
|
+
var ENGINE_SEIZE_C = 150;
|
|
14846
|
+
var COOLANT_NEEDED = 0.4;
|
|
14847
|
+
var OIL_NEEDED = 0.3;
|
|
14848
|
+
var OIL_WEAR = 4e-3;
|
|
14849
|
+
var ENGINE_SEIZE_HP = 0.2;
|
|
14850
|
+
var EXPANDED_HOLE = 1.6;
|
|
14851
|
+
var MIN_SPENT = 0.08;
|
|
14852
|
+
var WELDED = /* @__PURE__ */ new Set(["roof", "pillar", "frame", "panel"]);
|
|
14853
|
+
var CRUMPLES = /* @__PURE__ */ new Set(["door", "hood", "lid", "fender", "bumper"]);
|
|
13621
14854
|
var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
|
|
13622
14855
|
var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
13623
14856
|
var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
|
|
@@ -13629,6 +14862,8 @@ var GameVehicles = (() => {
|
|
|
13629
14862
|
let c = vehicleCtx;
|
|
13630
14863
|
const id = options.id ?? c.key;
|
|
13631
14864
|
const headless = !!options.headless;
|
|
14865
|
+
const armourId = armourLevelOf(c.meta, options.armour);
|
|
14866
|
+
const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
|
|
13632
14867
|
const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
|
|
13633
14868
|
let rand = rng(seedBase);
|
|
13634
14869
|
const ignitionRules = resolveIgnitionRules(options.ignition);
|
|
@@ -13637,6 +14872,7 @@ var GameVehicles = (() => {
|
|
|
13637
14872
|
let clock = 0;
|
|
13638
14873
|
let model;
|
|
13639
14874
|
let vis = null;
|
|
14875
|
+
let releaseCrease;
|
|
13640
14876
|
const ps = /* @__PURE__ */ new Map();
|
|
13641
14877
|
let js = [];
|
|
13642
14878
|
let leaks = /* @__PURE__ */ new Map();
|
|
@@ -13685,7 +14921,7 @@ var GameVehicles = (() => {
|
|
|
13685
14921
|
return d.ignitionC ? 1 : 0;
|
|
13686
14922
|
};
|
|
13687
14923
|
const setup = () => {
|
|
13688
|
-
model = buildVehicleModel(kit, c, { hitch: options.hitch });
|
|
14924
|
+
model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
|
|
13689
14925
|
ps.clear();
|
|
13690
14926
|
for (const d of model.parts) {
|
|
13691
14927
|
const kg = burnKg(d);
|
|
@@ -13705,6 +14941,8 @@ var GameVehicles = (() => {
|
|
|
13705
14941
|
lost: false,
|
|
13706
14942
|
status: "intact",
|
|
13707
14943
|
flat: false,
|
|
14944
|
+
air: 1,
|
|
14945
|
+
airLoss: 0,
|
|
13708
14946
|
fireLoss: 0
|
|
13709
14947
|
});
|
|
13710
14948
|
}
|
|
@@ -13721,7 +14959,9 @@ var GameVehicles = (() => {
|
|
|
13721
14959
|
rand = rng(seedBase);
|
|
13722
14960
|
igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
|
|
13723
14961
|
steerSign = rand() < 0.5 ? -1 : 1;
|
|
14962
|
+
releaseCrease?.();
|
|
13724
14963
|
vis = headless ? null : new VehicleVisuals(kit, c, model);
|
|
14964
|
+
releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
|
|
13725
14965
|
recomputeGroups();
|
|
13726
14966
|
};
|
|
13727
14967
|
const recomputeGroups = () => {
|
|
@@ -13803,7 +15043,8 @@ var GameVehicles = (() => {
|
|
|
13803
15043
|
vis.breakLight(d, true);
|
|
13804
15044
|
break;
|
|
13805
15045
|
case "tyre":
|
|
13806
|
-
vis.
|
|
15046
|
+
if (status === "broken") vis.tyreTornOff(d);
|
|
15047
|
+
else vis.tyreFlat(d, 0.34);
|
|
13807
15048
|
break;
|
|
13808
15049
|
case "mirror":
|
|
13809
15050
|
case "bumper":
|
|
@@ -13833,7 +15074,8 @@ var GameVehicles = (() => {
|
|
|
13833
15074
|
const damage = (p, amount, dtype, source, out, cause) => {
|
|
13834
15075
|
if (p.lost || !(amount > 0)) return;
|
|
13835
15076
|
const before = p.hp;
|
|
13836
|
-
|
|
15077
|
+
const crumples = dtype === "blunt" && CRUMPLES.has(p.def.role) && (p.def.material === "steel" || p.def.material === "aluminium");
|
|
15078
|
+
p.hp = Math.max(WELDED.has(p.def.role) || crumples ? 1 : 0, p.hp - amount);
|
|
13837
15079
|
out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
|
|
13838
15080
|
wear(p, amount, out);
|
|
13839
15081
|
syncZone(p);
|
|
@@ -13876,11 +15118,22 @@ var GameVehicles = (() => {
|
|
|
13876
15118
|
const p = v.clone().applyMatrix4(M);
|
|
13877
15119
|
return [p.x, p.y, p.z];
|
|
13878
15120
|
};
|
|
13879
|
-
const armour = () =>
|
|
15121
|
+
const armour = () => armourLevel?.steel ?? 1;
|
|
15122
|
+
const steelMm = (d) => {
|
|
15123
|
+
const base = STEEL_MM[d.role] ?? STEEL_MM.panel;
|
|
15124
|
+
return d.material === "steel" || d.material === "aluminium" ? base * (armourLevel ? ARMOUR_STEEL_MM[armourId === "III" ? "III" : "IIIA"] / STEEL_MM.panel : 1) : base;
|
|
15125
|
+
};
|
|
15126
|
+
const goesThrough = (h, d, p, energy, res) => {
|
|
15127
|
+
const piercing = h.piercing ?? 1;
|
|
15128
|
+
const metal = d.material === "steel" || d.material === "aluminium";
|
|
15129
|
+
const mm = h.penetration?.steel;
|
|
15130
|
+
if (metal && mm != null) return mm >= steelMm(d) * (p.dents.length > 3 ? 0.9 : 1);
|
|
15131
|
+
return energy * (metal || d.material === "glass" ? piercing : 1) >= res;
|
|
15132
|
+
};
|
|
13880
15133
|
const resist = (d, p) => {
|
|
13881
15134
|
let base = RESIST[d.material] ?? 100;
|
|
13882
15135
|
if (d.material === "glass" && d.hp > 1e3) base = 120;
|
|
13883
|
-
if (d.material === "glass" &&
|
|
15136
|
+
if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
|
|
13884
15137
|
const k = ROLE_RESIST[d.role] ?? 1;
|
|
13885
15138
|
const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
|
|
13886
15139
|
return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
|
|
@@ -13903,7 +15156,7 @@ var GameVehicles = (() => {
|
|
|
13903
15156
|
if (vis) {
|
|
13904
15157
|
const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
|
|
13905
15158
|
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 });
|
|
13906
|
-
|
|
15159
|
+
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);
|
|
13907
15160
|
}
|
|
13908
15161
|
}
|
|
13909
15162
|
const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
|
|
@@ -13940,7 +15193,7 @@ var GameVehicles = (() => {
|
|
|
13940
15193
|
exploded = true;
|
|
13941
15194
|
const d = tank.def;
|
|
13942
15195
|
const spec = d.explodes ?? { energy: 8e5, radius: 5, atC: 320 };
|
|
13943
|
-
out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius }));
|
|
15196
|
+
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 } : {} }));
|
|
13944
15197
|
tank.liters = 0;
|
|
13945
15198
|
leaks.delete(d.id);
|
|
13946
15199
|
if (vis) kit.explode(c, { delay: 0.6 });
|
|
@@ -14032,15 +15285,15 @@ var GameVehicles = (() => {
|
|
|
14032
15285
|
sync();
|
|
14033
15286
|
const M = toFrameMatrix();
|
|
14034
15287
|
const v = toLocal(h.point);
|
|
14035
|
-
const p = locate(v, 0.2);
|
|
15288
|
+
const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
|
|
14036
15289
|
if (!p) return;
|
|
14037
15290
|
const d = p.def;
|
|
14038
15291
|
const E = h.energy ?? 0;
|
|
14039
15292
|
const cal = h.caliber ?? 9;
|
|
14040
15293
|
const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
|
|
14041
|
-
const n = normalOf(d, v);
|
|
15294
|
+
const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
|
|
14042
15295
|
const res = resist(d, p);
|
|
14043
|
-
const through = E
|
|
15296
|
+
const through = goesThrough(h, d, p, E, res);
|
|
14044
15297
|
const cosInc = Math.abs(dir.dot(n));
|
|
14045
15298
|
const metal = d.material === "steel" || d.material === "aluminium";
|
|
14046
15299
|
const worldPoint = toWorld(v);
|
|
@@ -14051,13 +15304,21 @@ var GameVehicles = (() => {
|
|
|
14051
15304
|
vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
|
|
14052
15305
|
return;
|
|
14053
15306
|
}
|
|
14054
|
-
const
|
|
15307
|
+
const opens = (h.piercing ?? 1) < SOFT_LEAD ? EXPANDED_HOLE : 1;
|
|
15308
|
+
const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1) * opens);
|
|
15309
|
+
const energyLeft = () => {
|
|
15310
|
+
const metal2 = d.material === "steel" || d.material === "aluminium";
|
|
15311
|
+
const mm = h.penetration?.steel;
|
|
15312
|
+
const spent = metal2 && mm ? clamp(steelMm(d) / mm, MIN_SPENT, 1) : clamp(res / Math.max(E, 1), MIN_SPENT, 1);
|
|
15313
|
+
return E * (1 - spent);
|
|
15314
|
+
};
|
|
14055
15315
|
const push = (hole) => {
|
|
14056
15316
|
if (!hole) return;
|
|
14057
15317
|
const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
|
|
14058
15318
|
p.holes.push(hp);
|
|
14059
|
-
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])))))) } : {} }));
|
|
14060
|
-
vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
|
|
15319
|
+
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])))))) } : {} }));
|
|
15320
|
+
vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
|
|
15321
|
+
if (d.role === "mirror") vis?.shatterMirror(d.id);
|
|
14061
15322
|
startLeak(p, v, cal, out, dir);
|
|
14062
15323
|
};
|
|
14063
15324
|
switch (d.material) {
|
|
@@ -14070,7 +15331,7 @@ var GameVehicles = (() => {
|
|
|
14070
15331
|
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) };
|
|
14071
15332
|
p.cracks.push(crack);
|
|
14072
15333
|
out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
|
|
14073
|
-
damage(p, E * 0.5, "ballistic", h.source, out, worldPoint);
|
|
15334
|
+
damage(p, E * (armourLevel && !through ? armourLevel.glassTakes : 0.5), "ballistic", h.source, out, worldPoint);
|
|
14074
15335
|
} else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
|
|
14075
15336
|
break;
|
|
14076
15337
|
}
|
|
@@ -14078,8 +15339,8 @@ var GameVehicles = (() => {
|
|
|
14078
15339
|
push(through);
|
|
14079
15340
|
damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
|
|
14080
15341
|
if (through && !p.lost && !p.flat) {
|
|
14081
|
-
p
|
|
14082
|
-
|
|
15342
|
+
if (E >= TYRE_BURST_JOULES || cal >= TYRE_BURST_CALIBER) deflate(p, 1);
|
|
15343
|
+
else p.airLoss += TYRE_LEAK_PER_MM2 * Math.PI * (cal / 2) ** 2;
|
|
14083
15344
|
}
|
|
14084
15345
|
break;
|
|
14085
15346
|
}
|
|
@@ -14195,7 +15456,7 @@ var GameVehicles = (() => {
|
|
|
14195
15456
|
const electric = c.spec.fuel === "electric";
|
|
14196
15457
|
const tank = tankPart();
|
|
14197
15458
|
const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
|
|
14198
|
-
const engineOk = sound("engine") && !exploded && fuel > 0.05;
|
|
15459
|
+
const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
|
|
14199
15460
|
const powered = sound("battery") && sound("alternator");
|
|
14200
15461
|
const gearOk = sound("gearbox");
|
|
14201
15462
|
const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
|
|
@@ -14355,9 +15616,62 @@ var GameVehicles = (() => {
|
|
|
14355
15616
|
const state = () => stateCache ?? (stateCache = buildState());
|
|
14356
15617
|
const isSimulating = () => {
|
|
14357
15618
|
if (leaks.size > 0) return true;
|
|
14358
|
-
for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
|
|
15619
|
+
for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT || p.airLoss > 0 && p.air > 0)) return true;
|
|
14359
15620
|
return false;
|
|
14360
15621
|
};
|
|
15622
|
+
let engineHeat = ENGINE_WORKING_C;
|
|
15623
|
+
let seized = false;
|
|
15624
|
+
let steaming = false;
|
|
15625
|
+
const surfaceRays = new THREE.Raycaster();
|
|
15626
|
+
surfaceRays.layers.enableAll();
|
|
15627
|
+
surfaceRays.camera = new THREE.PerspectiveCamera();
|
|
15628
|
+
let meshParts = null;
|
|
15629
|
+
const paneParts = () => {
|
|
15630
|
+
if (meshParts) return meshParts;
|
|
15631
|
+
meshParts = /* @__PURE__ */ new Map();
|
|
15632
|
+
for (const d of model.parts) {
|
|
15633
|
+
if (d.role !== "glass") continue;
|
|
15634
|
+
const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
|
|
15635
|
+
for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
|
|
15636
|
+
}
|
|
15637
|
+
return meshParts;
|
|
15638
|
+
};
|
|
15639
|
+
const isDrawnSurface = (o) => {
|
|
15640
|
+
for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
|
|
15641
|
+
return true;
|
|
15642
|
+
};
|
|
15643
|
+
const partOfSurface = (o, v) => {
|
|
15644
|
+
const pane = paneParts().get(o);
|
|
15645
|
+
if (pane) return ps.get(pane) ?? null;
|
|
15646
|
+
for (let q = o; q && q !== c.vehicle; q = q.parent) {
|
|
15647
|
+
if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
|
|
15648
|
+
const piece = vis?.extraIdOf(q);
|
|
15649
|
+
if (piece) return ps.get(piece) ?? null;
|
|
15650
|
+
}
|
|
15651
|
+
return locate(v, 0.08, (p) => p.def.role === "glass");
|
|
15652
|
+
};
|
|
15653
|
+
const surfaceMeshes = [];
|
|
15654
|
+
const collectSurfaces = (o) => {
|
|
15655
|
+
if (!o.visible || o.userData.decal || o.userData.ghost || o.userData.noSurfaceRay) return;
|
|
15656
|
+
if (o.isMesh) surfaceMeshes.push(o);
|
|
15657
|
+
for (const child of o.children) collectSurfaces(child);
|
|
15658
|
+
};
|
|
15659
|
+
const surfaceRay = (ray) => {
|
|
15660
|
+
sync();
|
|
15661
|
+
surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
|
|
15662
|
+
surfaceRays.far = ray.maxDistance;
|
|
15663
|
+
surfaceMeshes.length = 0;
|
|
15664
|
+
for (const child of c.vehicle.children) collectSurfaces(child);
|
|
15665
|
+
for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
|
|
15666
|
+
if (!h.face || !isDrawnSurface(h.object)) continue;
|
|
15667
|
+
const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
|
|
15668
|
+
if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
|
|
15669
|
+
const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
|
|
15670
|
+
if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
|
|
15671
|
+
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 };
|
|
15672
|
+
}
|
|
15673
|
+
return null;
|
|
15674
|
+
};
|
|
14361
15675
|
const hit = (h) => {
|
|
14362
15676
|
stateCache = void 0;
|
|
14363
15677
|
const out = [];
|
|
@@ -14382,7 +15696,29 @@ var GameVehicles = (() => {
|
|
|
14382
15696
|
applyEffects();
|
|
14383
15697
|
return out;
|
|
14384
15698
|
};
|
|
15699
|
+
const runFlat = armourId != null;
|
|
15700
|
+
const tyreHealth = (p) => {
|
|
15701
|
+
if (p.lost) return RIM_HEALTH;
|
|
15702
|
+
if (p.flat) return runFlat ? TYRE_RUNFLAT_HEALTH : TYRE_FLAT_HEALTH;
|
|
15703
|
+
return Math.min(p.hp / p.def.hp, 0.2 + 0.8 * p.air);
|
|
15704
|
+
};
|
|
15705
|
+
const tyreOf = (rim) => {
|
|
15706
|
+
let best;
|
|
15707
|
+
let bestD = Infinity;
|
|
15708
|
+
for (const p of ps.values()) {
|
|
15709
|
+
if (p.def.role !== "tyre") continue;
|
|
15710
|
+
const d = Math.hypot(p.def.box.center[0] - rim.def.box.center[0], p.def.box.center[2] - rim.def.box.center[2]);
|
|
15711
|
+
if (d < bestD) [best, bestD] = [p, d];
|
|
15712
|
+
}
|
|
15713
|
+
return best;
|
|
15714
|
+
};
|
|
15715
|
+
const deflate = (p, share) => {
|
|
15716
|
+
p.air = Math.max(0, p.air - share);
|
|
15717
|
+
vis?.tyreFlat(p.def, TYRE_FLAT_SAG * (1 - p.air));
|
|
15718
|
+
if (p.air <= TYRE_FLAT_AIR) p.flat = true;
|
|
15719
|
+
};
|
|
14385
15720
|
const update = (dt) => {
|
|
15721
|
+
for (const p of ps.values()) if (p.airLoss > 0 && p.air > 0 && !p.lost) deflate(p, p.airLoss * dt);
|
|
14386
15722
|
clock += dt;
|
|
14387
15723
|
const out = queue;
|
|
14388
15724
|
queue = [];
|
|
@@ -14480,9 +15816,14 @@ var GameVehicles = (() => {
|
|
|
14480
15816
|
},
|
|
14481
15817
|
describe,
|
|
14482
15818
|
hit,
|
|
15819
|
+
surfaceRay,
|
|
14483
15820
|
update,
|
|
14484
15821
|
state,
|
|
14485
15822
|
reset,
|
|
15823
|
+
dispose: () => {
|
|
15824
|
+
releaseCrease?.();
|
|
15825
|
+
releaseCrease = void 0;
|
|
15826
|
+
},
|
|
14486
15827
|
disassemble,
|
|
14487
15828
|
extinguish,
|
|
14488
15829
|
serialize: () => ({
|
|
@@ -14493,6 +15834,70 @@ var GameVehicles = (() => {
|
|
|
14493
15834
|
partName: (pid) => ps.get(pid)?.def.name ?? pid,
|
|
14494
15835
|
status: computeStatus,
|
|
14495
15836
|
animations: () => createVehicleClips(model, vis, kit, c),
|
|
15837
|
+
crumple: (point, direction, depth) => {
|
|
15838
|
+
if (!vis || depth <= 0) return;
|
|
15839
|
+
sync();
|
|
15840
|
+
const force = clamp((depth - CRUMPLE_BASE_M) / CRUMPLE_PER_FORCE_M, CRUMPLE_MIN_FORCE, CRUMPLE_MAX_FORCE);
|
|
15841
|
+
kit.impact(c, toLocal(point), dirToLocal(direction).normalize(), force, { maxDepth: depth });
|
|
15842
|
+
stateCache = void 0;
|
|
15843
|
+
},
|
|
15844
|
+
rollFlat: (meters, speed) => {
|
|
15845
|
+
for (const p of ps.values()) {
|
|
15846
|
+
if (p.def.role !== "tyre" || !p.flat || p.lost) continue;
|
|
15847
|
+
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);
|
|
15848
|
+
p.hp = Math.max(0, p.hp - p.def.hp * meters / life);
|
|
15849
|
+
if (p.hp > 0) continue;
|
|
15850
|
+
const at = toWorld(p.def.box.center);
|
|
15851
|
+
lose(p, "broken", queue, at);
|
|
15852
|
+
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) }));
|
|
15853
|
+
}
|
|
15854
|
+
let grinding = 0;
|
|
15855
|
+
for (const rim of ps.values()) {
|
|
15856
|
+
if (rim.def.role !== "rim" || rim.lost || !tyreOf(rim)?.lost) continue;
|
|
15857
|
+
grinding += 1;
|
|
15858
|
+
rim.hp = Math.max(1, rim.hp - rim.def.hp * RIM_GRIND_PER_M * meters);
|
|
15859
|
+
const [x, y, z] = rim.def.box.center;
|
|
15860
|
+
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) }));
|
|
15861
|
+
}
|
|
15862
|
+
stateCache = void 0;
|
|
15863
|
+
return grinding;
|
|
15864
|
+
},
|
|
15865
|
+
runEngine: (seconds, load) => {
|
|
15866
|
+
const engine = [...ps.values()].find((p) => p.def.role === "engine");
|
|
15867
|
+
if (!engine || engine.lost || seized) return engineHeat;
|
|
15868
|
+
const share = (fluid) => {
|
|
15869
|
+
const list2 = [...ps.values()].filter((p) => p.def.contains?.fluid === fluid);
|
|
15870
|
+
return list2.length ? Math.min(...list2.map((p) => (p.liters ?? 0) / p.def.contains.liters)) : 1;
|
|
15871
|
+
};
|
|
15872
|
+
const coolant = share("coolant");
|
|
15873
|
+
const oil = share("oil");
|
|
15874
|
+
const work = 0.4 + 0.6 * clamp(load, 0, 1);
|
|
15875
|
+
if (coolant < COOLANT_NEEDED) engineHeat += (COOLANT_NEEDED - coolant) / COOLANT_NEEDED * ENGINE_HEATING * work * seconds;
|
|
15876
|
+
else engineHeat += (ENGINE_WORKING_C - engineHeat) * Math.min(1, seconds / ENGINE_SETTLE_S);
|
|
15877
|
+
if (oil < OIL_NEEDED) engine.hp = Math.max(0, engine.hp - engine.def.hp * OIL_WEAR * work * seconds * ((OIL_NEEDED - oil) / OIL_NEEDED));
|
|
15878
|
+
if (engineHeat >= ENGINE_STEAM_C && vis && !steaming) {
|
|
15879
|
+
steaming = true;
|
|
15880
|
+
kit.addEmitter(c, new V3(engine.def.box.center[0], engine.def.box.center[1] + 0.4, 0), "white", { rate: 8, s: 1 });
|
|
15881
|
+
}
|
|
15882
|
+
if (engineHeat >= ENGINE_SEIZE_C || engine.hp <= engine.def.hp * ENGINE_SEIZE_HP) {
|
|
15883
|
+
seized = true;
|
|
15884
|
+
queue.push(ev({ type: "damage", part: engine.def.id, amount: 0, hp: r2(engine.hp), damageType: "heat" }));
|
|
15885
|
+
}
|
|
15886
|
+
stateCache = void 0;
|
|
15887
|
+
return engineHeat;
|
|
15888
|
+
},
|
|
15889
|
+
driveReport: () => {
|
|
15890
|
+
const health = (role) => {
|
|
15891
|
+
const list2 = [...ps.values()].filter((p) => p.def.role === role);
|
|
15892
|
+
if (!list2.length) return 1;
|
|
15893
|
+
return Math.min(...list2.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
|
|
15894
|
+
};
|
|
15895
|
+
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) }));
|
|
15896
|
+
const status = computeStatus();
|
|
15897
|
+
const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
|
|
15898
|
+
const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
|
|
15899
|
+
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") };
|
|
15900
|
+
},
|
|
14496
15901
|
partWorldCenter: (pid) => {
|
|
14497
15902
|
sync();
|
|
14498
15903
|
const d = ps.get(pid)?.def;
|
|
@@ -14521,6 +15926,89 @@ var GameVehicles = (() => {
|
|
|
14521
15926
|
return { root, vehicle };
|
|
14522
15927
|
}
|
|
14523
15928
|
|
|
15929
|
+
// src/destructible/crease-queue.ts
|
|
15930
|
+
function createCreaseQueue() {
|
|
15931
|
+
const tracked = [];
|
|
15932
|
+
let cursor = 0;
|
|
15933
|
+
let lastFrame;
|
|
15934
|
+
let attached = false;
|
|
15935
|
+
const deferred = {
|
|
15936
|
+
id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
|
|
15937
|
+
pending(frame) {
|
|
15938
|
+
lastFrame = frame;
|
|
15939
|
+
let units = 0;
|
|
15940
|
+
for (const work of tracked) units += work.pendingUnits(frame);
|
|
15941
|
+
return units;
|
|
15942
|
+
},
|
|
15943
|
+
run(deadline, clock) {
|
|
15944
|
+
let ran = 0;
|
|
15945
|
+
let skipped = 0;
|
|
15946
|
+
while (tracked.length > 0 && skipped < tracked.length) {
|
|
15947
|
+
if (ran > 0 && clock() >= deadline) break;
|
|
15948
|
+
const work = tracked[cursor % tracked.length];
|
|
15949
|
+
cursor = (cursor + 1) % tracked.length;
|
|
15950
|
+
if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
|
|
15951
|
+
ran++;
|
|
15952
|
+
skipped = 0;
|
|
15953
|
+
} else skipped++;
|
|
15954
|
+
}
|
|
15955
|
+
return ran;
|
|
15956
|
+
},
|
|
15957
|
+
flush() {
|
|
15958
|
+
for (const work of tracked) work.finishAll();
|
|
15959
|
+
},
|
|
15960
|
+
attach() {
|
|
15961
|
+
attached = true;
|
|
15962
|
+
for (const work of tracked) work.setQueued(true);
|
|
15963
|
+
},
|
|
15964
|
+
detach() {
|
|
15965
|
+
attached = false;
|
|
15966
|
+
for (const work of tracked) work.setQueued(false);
|
|
15967
|
+
}
|
|
15968
|
+
};
|
|
15969
|
+
return {
|
|
15970
|
+
deferredQueue: () => deferred,
|
|
15971
|
+
track(work) {
|
|
15972
|
+
tracked.push(work);
|
|
15973
|
+
work.setQueued(attached);
|
|
15974
|
+
return () => {
|
|
15975
|
+
const at = tracked.indexOf(work);
|
|
15976
|
+
if (at < 0) return;
|
|
15977
|
+
tracked.splice(at, 1);
|
|
15978
|
+
work.setQueued(false);
|
|
15979
|
+
};
|
|
15980
|
+
}
|
|
15981
|
+
};
|
|
15982
|
+
}
|
|
15983
|
+
|
|
15984
|
+
// src/destructible/crash.ts
|
|
15985
|
+
var CRASH_RULES = { energyToDamage: 0.3, hitSpacing: 0.6, minSpeed: 1.5, rows: [0, 0.3], crushPerMs: 0.035, maxCrush: 0.6 };
|
|
15986
|
+
function crashDeltaV(speed, massKg, otherMassKg) {
|
|
15987
|
+
if (!Number.isFinite(otherMassKg)) return speed;
|
|
15988
|
+
return speed * otherMassKg / (massKg + otherMassKg);
|
|
15989
|
+
}
|
|
15990
|
+
function crashVehicle(d, crash) {
|
|
15991
|
+
const rules = { ...CRASH_RULES, ...crash.rules };
|
|
15992
|
+
const deltaV = crashDeltaV(crash.speed, crash.massKg, crash.otherMassKg);
|
|
15993
|
+
const energy = 0.5 * crash.massKg * deltaV * deltaV;
|
|
15994
|
+
if (crash.speed < rules.minSpeed) return { deltaV, energy, events: [] };
|
|
15995
|
+
const [dx, , dz] = crash.direction;
|
|
15996
|
+
const length = Math.hypot(dx, dz) || 1;
|
|
15997
|
+
const across = [-dz / length, 0, dx / length];
|
|
15998
|
+
const hits = Math.max(1, Math.round(crash.contactWidth / rules.hitSpacing));
|
|
15999
|
+
const each = energy * rules.energyToDamage / (hits * rules.rows.length);
|
|
16000
|
+
const events = [];
|
|
16001
|
+
for (const up of rules.rows)
|
|
16002
|
+
for (let i = 0; i < hits; i++) {
|
|
16003
|
+
const offset = hits === 1 ? 0 : (i / (hits - 1) - 0.5) * crash.contactWidth;
|
|
16004
|
+
const point = [crash.point[0] + across[0] * offset, crash.point[1] + up, crash.point[2] + across[2] * offset];
|
|
16005
|
+
events.push(...d.hit({ type: "blunt", point, direction: [dx / length, 0, dz / length], energy: each, radius: rules.hitSpacing / 2, ...crash.source ? { source: crash.source } : {} }));
|
|
16006
|
+
}
|
|
16007
|
+
const crush = Math.min(rules.maxCrush, deltaV * rules.crushPerMs);
|
|
16008
|
+
d.crumple(crash.point, [dx / length, 0, dz / length], crush);
|
|
16009
|
+
return { deltaV, energy, events };
|
|
16010
|
+
}
|
|
16011
|
+
|
|
14524
16012
|
// src/destructible/enclosure.ts
|
|
14525
16013
|
var CABIN_LEAK = 0.02;
|
|
14526
16014
|
var HOLE_RATE_PER_AREA = 3e3;
|