@adatechnology/game-vehicles 0.6.2 → 0.6.3

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 CHANGED
@@ -391,8 +391,11 @@ interface VehicleSpec {
391
391
  /** Fuel id (a key of `configure({ fuels })`) or `'electric'` / `'none'`. */
392
392
  fuel?: string;
393
393
  build?: BuildFunction;
394
+ /** What the headlights' lenses are made of (default `polycarbonate`, the lens of cars since the 1990s; `glass` on older ones): glass shatters, polycarbonate cracks. */
395
+ headlightLens?: HeadlightLens;
394
396
  [key: string]: any;
395
397
  }
398
+ type HeadlightLens = 'glass' | 'polycarbonate';
396
399
  /** Builds the geometry of a vehicle into the context. */
397
400
  type BuildFunction = (c: VehicleContext, kit: VehicleKit) => void;
398
401
  interface VehiclePart {
@@ -999,10 +1002,12 @@ interface VehicleDestructible extends DestructibleImpl {
999
1002
  crumple(point: Vec3, direction: Vec3, depth: number): void;
1000
1003
  }
1001
1004
  type DriveReport = {
1005
+ /** `rim`: the tyre is torn off and the bare rim is on the road. */
1002
1006
  readonly tyres: ReadonlyArray<{
1003
1007
  readonly x: number;
1004
1008
  readonly z: number;
1005
1009
  readonly health: number;
1010
+ readonly rim: boolean;
1006
1011
  }>;
1007
1012
  readonly brakes: number;
1008
1013
  readonly engine: number;
@@ -1122,4 +1127,4 @@ declare function cabinEnclosure(source: CabinSource, options?: CabinEnclosureOpt
1122
1127
  /** The material of a pane with cracks drawn on it. A host warms its program at load with any texture: the first crack is in the middle of a blast. */
1123
1128
  declare function crackedGlassMaterial(THREE: any, texture: unknown): any;
1124
1129
 
1125
- export { ARMOUR_LEVELS, type ArmourLevel, type ArmourLevelId, type Band, type BandId, type BandIndex, type BandOverride, type BuildFunction, type BuildOptions, type BuildState, type BuildZoneState, CABIN_LEAK, CRASH_RULES, type CabinBox, type CabinEnclosureOptions, type CabinSource, type CrashOptions, type CrashResult, type CrashRules, type CreaseQueue, type CreaseWork, type DamageMapOptions, type DamageZone, type Damageable, type DamageableKind, type DamageableMaterial, type DamageableSide, type DriveReport, type ExplodeOptions, type FlagEffect, type FluidDefinition, type ImpactOptions, JOINT_RULES, type JointDef, type KitConfiguration, type KitInternals, type KitTexts, LEGACY_STATES, LEGACY_ZONE_IDS, type LocalBox, type LocaleStrings, type LodSettings, type OverallHealth, type PartDef, type PartLabel, type PartRole, type PartialLocaleStrings, type ParticleEffectRequest, type ParticleEffects, type ParticleMaterials, type Plugin, type ResolvedConfiguration, SEED_SALTS, type ScenarioContext, type ScenarioDefinition, type ScenarioResult, type SplatOptions, type ThreeObject, type ThreeVector, type Translate, type UiHelpers, type VehicleContext, type VehicleDestructible, type VehicleDestructibleOptions, type VehicleItem, type VehicleItemOptions, type VehicleKit, type VehicleKitOptions, type VehicleMeta, type VehicleModel, type VehiclePart, type VehicleSpec, type VehicleStatus, type ZoneDamageEntry, type ZoneEffect, type ZoneState, armourLevelOf, buildVehicleItem, buildVehicleModel, cabinBoxOf, cabinEnclosure, clamp, crackedGlassMaterial, crashDeltaV, crashVehicle, createCreaseQueue, createLod, createTranslate, createVehicleDestructible, createVehicleKit, defaultDamageableKinds, defaultLodSettings, format, hash, legacyGlassId, legacyLightId, legacyZoneId, listLocales, registerLegacySeeds, registerLocale, resolveStrings, rng, smooth, strHash, swingOf, wrapThreeForLod };
1130
+ export { ARMOUR_LEVELS, type ArmourLevel, type ArmourLevelId, type Band, type BandId, type BandIndex, type BandOverride, type BuildFunction, type BuildOptions, type BuildState, type BuildZoneState, CABIN_LEAK, CRASH_RULES, type CabinBox, type CabinEnclosureOptions, type CabinSource, type CrashOptions, type CrashResult, type CrashRules, type CreaseQueue, type CreaseWork, type DamageMapOptions, type DamageZone, type Damageable, type DamageableKind, type DamageableMaterial, type DamageableSide, type DriveReport, type ExplodeOptions, type FlagEffect, type FluidDefinition, type HeadlightLens, type ImpactOptions, JOINT_RULES, type JointDef, type KitConfiguration, type KitInternals, type KitTexts, LEGACY_STATES, LEGACY_ZONE_IDS, type LocalBox, type LocaleStrings, type LodSettings, type OverallHealth, type PartDef, type PartLabel, type PartRole, type PartialLocaleStrings, type ParticleEffectRequest, type ParticleEffects, type ParticleMaterials, type Plugin, type ResolvedConfiguration, SEED_SALTS, type ScenarioContext, type ScenarioDefinition, type ScenarioResult, type SplatOptions, type ThreeObject, type ThreeVector, type Translate, type UiHelpers, type VehicleContext, type VehicleDestructible, type VehicleDestructibleOptions, type VehicleItem, type VehicleItemOptions, type VehicleKit, type VehicleKitOptions, type VehicleMeta, type VehicleModel, type VehiclePart, type VehicleSpec, type VehicleStatus, type ZoneDamageEntry, type ZoneEffect, type ZoneState, armourLevelOf, buildVehicleItem, buildVehicleModel, cabinBoxOf, cabinEnclosure, clamp, crackedGlassMaterial, crashDeltaV, crashVehicle, createCreaseQueue, createLod, createTranslate, createVehicleDestructible, createVehicleKit, defaultDamageableKinds, defaultLodSettings, format, hash, legacyGlassId, legacyLightId, legacyZoneId, listLocales, registerLegacySeeds, registerLocale, resolveStrings, rng, smooth, strHash, swingOf, wrapThreeForLod };
@@ -7163,8 +7163,20 @@ var GameVehicles = (() => {
7163
7163
  if (!L || L.broken) return;
7164
7164
  L.broken = true;
7165
7165
  L.lens.forEach(function(m) {
7166
- m.visible = false;
7166
+ const rim = L.lensMaterial ? glassRim(m.geometry, c.rand) : null;
7167
+ if (!rim) {
7168
+ m.visible = false;
7169
+ return;
7170
+ }
7171
+ m.geometry.dispose();
7172
+ m.geometry = rim;
7173
+ m.material = MATERIALS.glassEdge;
7174
+ m.userData.lensRim = true;
7167
7175
  });
7176
+ if (L.lensMaterial)
7177
+ (L.reflector || []).forEach(function(m) {
7178
+ m.material = MATERIALS.broken;
7179
+ });
7168
7180
  L.inner.forEach(function(m) {
7169
7181
  m.material = MATERIALS.bulbBroken;
7170
7182
  });
@@ -7172,11 +7184,38 @@ var GameVehicles = (() => {
7172
7184
  h.visible = false;
7173
7185
  });
7174
7186
  const p = L.group ? L.group.position.clone() : L.inner[0] ? L.inner[0].position.clone() : new V3();
7175
- I.burst(c, "lens", c.toEffects(p), 10, { dir: c.dirToEffects(dir.clone().negate()), speed: 1.4, variant: L.tint === "red" ? "red" : "clear" });
7176
- I.burst(c, "plastic", c.toEffects(p), 2, { dir: c.dirToEffects(dir.clone().negate()), speed: 1 });
7187
+ const glass = L.lensMaterial !== "plastic";
7188
+ I.burst(c, "lens", c.toEffects(p), glass ? 10 : 5, { dir: c.dirToEffects(dir.clone().negate()), speed: glass ? 1.4 : 1.1, variant: L.tint === "red" ? "red" : "clear" });
7189
+ I.burst(c, "plastic", c.toEffects(p), glass ? 2 : 5, { dir: c.dirToEffects(dir.clone().negate()), speed: 1 });
7177
7190
  }
7178
7191
  kit.breakLight = breakLight;
7179
- Object.assign(I, { glassUV, drawWeb, crackGlass, glassRim, shatterGlass, hitGlass, breakLight });
7192
+ const crazedCache = /* @__PURE__ */ new Map();
7193
+ const CRAZED_OPACITY = 0.6;
7194
+ function crazed(material) {
7195
+ const known = crazedCache.get(material);
7196
+ if (known) return known;
7197
+ const m = I.crackLens(material).clone();
7198
+ m.opacity = Math.max(m.opacity ?? 1, CRAZED_OPACITY);
7199
+ m.transparent = true;
7200
+ m.userData.shared = true;
7201
+ crazedCache.set(material, m);
7202
+ return m;
7203
+ }
7204
+ function crackLight(c, L, dir, pierced) {
7205
+ if (!L || L.broken) return;
7206
+ if (!L.cracked) {
7207
+ L.cracked = true;
7208
+ for (const m of L.lens) if (m.material && !Array.isArray(m.material)) m.material = crazed(m.material);
7209
+ }
7210
+ const p = L.group ? L.group.position.clone() : L.inner[0] ? L.inner[0].position.clone() : new V3();
7211
+ I.burst(c, "lens", c.toEffects(p), 3, { dir: c.dirToEffects(dir.clone().negate()), speed: 0.9, variant: L.tint === "red" ? "red" : "clear" });
7212
+ if (!pierced || L.dark) return;
7213
+ L.dark = true;
7214
+ for (const m of L.inner) m.material = MATERIALS.bulbBroken;
7215
+ for (const h of L.halo ?? []) h.visible = false;
7216
+ }
7217
+ kit.crackLight = crackLight;
7218
+ Object.assign(I, { glassUV, drawWeb, crackGlass, glassRim, shatterGlass, hitGlass, breakLight, crackLight });
7180
7219
  }
7181
7220
 
7182
7221
  // src/impact.ts
@@ -12726,6 +12765,8 @@ var GameVehicles = (() => {
12726
12765
  heli: ["battery", "alternator", "gearbox"]
12727
12766
  };
12728
12767
  var HITCH_CHANCE = { car: 30, truck: 60, bus: 0, tractor: 100, quad: 40, bike: 0, tank: 0, boat: 0, plane: 0, heli: 0 };
12768
+ var GLASS_LENS_HP = 320;
12769
+ var PLASTIC_LENS_HP = 900;
12729
12770
  var STEEL_KG_M2 = 7;
12730
12771
  var ALU_KG_M2 = 3.2;
12731
12772
  var kindOf = (c) => {
@@ -12883,7 +12924,9 @@ var GameVehicles = (() => {
12883
12924
  continue;
12884
12925
  }
12885
12926
  if (role === "light") {
12886
- add({ id, role, name: label, material: "glass", box: box2, mass: 1.2 + 4 * area3, hp: 320, zoneId: id, visual: true, tags: ["light", ...tags] });
12927
+ const lens = /^head/.test(id) ? spec.headlightLens ?? "polycarbonate" : "acrylic";
12928
+ const glassLens = lens === "glass";
12929
+ add({ id, role, name: label, material: glassLens ? "glass" : "plastic", box: box2, mass: 1.2 + 4 * area3, hp: glassLens ? GLASS_LENS_HP : PLASTIC_LENS_HP, zoneId: id, visual: true, tags: ["light", `lens_${lens}`, ...tags] });
12887
12930
  continue;
12888
12931
  }
12889
12932
  if (role === "bumper") {
@@ -13340,21 +13383,188 @@ var GameVehicles = (() => {
13340
13383
  mesh2.geometry = g;
13341
13384
  }
13342
13385
 
13386
+ // src/destructible/door-open.ts
13387
+ var STRIP_SMOOTH_DEG = 180;
13388
+ var SKIN_BAND = 0.05;
13389
+ var BODY_FACING = 0.5;
13390
+ var FIELDS = 4;
13391
+ var SEAM_THIN = 0.03;
13392
+ var SEAM_AT_EDGE = 0.04;
13393
+ var SEAM_TALL = 0.4;
13394
+ var SLIVER_AREA = 1e-10;
13395
+ function cutStripOverDoor(vehicleKit, params) {
13396
+ const kit = vehicleKit;
13397
+ const strip = params.strip;
13398
+ const toVehicle = params.toVehicle;
13399
+ const { x0, x1 } = params;
13400
+ const THREE = kit.THREE;
13401
+ const at = new THREE.Vector3();
13402
+ const along = (x, y, z) => at.set(x, y, z).applyMatrix4(toVehicle).x;
13403
+ const piece = (tests) => {
13404
+ const mesh2 = new THREE.Mesh(strip.geometry.clone(), strip.material);
13405
+ kit.cutMesh(mesh2, tests, STRIP_SMOOTH_DEG, {});
13406
+ return mesh2;
13407
+ };
13408
+ const over = piece([{ sdf: (x, y, z) => along(x, y, z) - x0 }, { sdf: (x, y, z) => x1 - along(x, y, z) }]);
13409
+ const after = piece([{ sdf: (x, y, z) => along(x, y, z) - x1 }]);
13410
+ kit.cutMesh(strip, [{ sdf: (x, y, z) => x0 - along(x, y, z) }], STRIP_SMOOTH_DEG, {});
13411
+ const joined = joinTriangles(THREE, strip.geometry, after.geometry);
13412
+ strip.geometry.dispose();
13413
+ after.geometry.dispose();
13414
+ strip.geometry = joined;
13415
+ return over;
13416
+ }
13417
+ function joinTriangles(THREE, first, second) {
13418
+ const a = first.index ? first.toNonIndexed() : first;
13419
+ const b = second.index ? second.toNonIndexed() : second;
13420
+ const flags = [...solidTriangles(THREE, a), ...solidTriangles(THREE, b)];
13421
+ const both = new THREE.BufferGeometry();
13422
+ for (const name of Object.keys(a.attributes)) {
13423
+ const p = a.getAttribute(name);
13424
+ const q = b.getAttribute(name);
13425
+ if (!q || q.itemSize !== p.itemSize) continue;
13426
+ const array = new Float32Array(p.array.length + q.array.length);
13427
+ array.set(p.array, 0);
13428
+ array.set(q.array, p.array.length);
13429
+ both.setAttribute(name, new THREE.Float32BufferAttribute(array, p.itemSize));
13430
+ }
13431
+ if (a !== first) a.dispose();
13432
+ if (b !== second) b.dispose();
13433
+ const out = pickTriangles(THREE, both, flags, true) ?? new THREE.BufferGeometry();
13434
+ both.dispose();
13435
+ return out;
13436
+ }
13437
+ function solidTriangles(THREE, geometry) {
13438
+ const pos = geometry.getAttribute("position");
13439
+ const triangle = new THREE.Triangle();
13440
+ const flags = [];
13441
+ for (let t = 0; t * 3 + 2 < pos.count; t++) {
13442
+ triangle.a.fromBufferAttribute(pos, t * 3);
13443
+ triangle.b.fromBufferAttribute(pos, t * 3 + 1);
13444
+ triangle.c.fromBufferAttribute(pos, t * 3 + 2);
13445
+ flags.push(triangle.getArea() > SLIVER_AREA);
13446
+ }
13447
+ return flags;
13448
+ }
13449
+ function splitDoorSkin(three, meshes, toVehicleOf, side2, depth) {
13450
+ const THREE = three;
13451
+ const pieces = meshes;
13452
+ const toVehicle = toVehicleOf;
13453
+ const v = new THREE.Vector3();
13454
+ const corners = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
13455
+ const triangle = new THREE.Triangle();
13456
+ const centres = pieces.map((mesh2) => {
13457
+ const geometry = mesh2.geometry.index ? mesh2.geometry.toNonIndexed() : mesh2.geometry;
13458
+ const matrix = toVehicle(mesh2);
13459
+ const pos = geometry.getAttribute("position");
13460
+ const out = new Float32Array(pos.count / 3 * FIELDS);
13461
+ for (let t = 0; t * 3 + 2 < pos.count; t++) {
13462
+ let y = 0;
13463
+ let across = 0;
13464
+ for (let k = 0; k < 3; k++) {
13465
+ corners[k].fromBufferAttribute(pos, t * 3 + k).applyMatrix4(matrix);
13466
+ y += corners[k].y / 3;
13467
+ across += side2 * corners[k].z / 3;
13468
+ }
13469
+ out[t * FIELDS] = y;
13470
+ out[t * FIELDS + 1] = across;
13471
+ triangle.set(corners[0], corners[1], corners[2]).getNormal(v);
13472
+ out[t * FIELDS + 2] = v.y;
13473
+ out[t * FIELDS + 3] = v.x;
13474
+ }
13475
+ return { geometry, out };
13476
+ });
13477
+ const outer = /* @__PURE__ */ new Map();
13478
+ for (const { out } of centres)
13479
+ for (let t = 0; t * FIELDS < out.length; t++) {
13480
+ const band = Math.floor(out[t * FIELDS] / SKIN_BAND);
13481
+ outer.set(band, Math.max(outer.get(band) ?? -Infinity, out[t * FIELDS + 1]));
13482
+ }
13483
+ const surfaceAt = (y) => {
13484
+ const band = Math.floor(y / SKIN_BAND);
13485
+ return Math.max(outer.get(band - 1) ?? -Infinity, outer.get(band) ?? -Infinity, outer.get(band + 1) ?? -Infinity);
13486
+ };
13487
+ return centres.map(({ geometry, out }, i) => {
13488
+ const near = [];
13489
+ for (let t = 0; t * FIELDS < out.length; t++) {
13490
+ const f = t * FIELDS;
13491
+ const body = out[f + 1] < surfaceAt(out[f]) - depth && (out[f + 2] < -BODY_FACING || Math.abs(out[f + 3]) > BODY_FACING);
13492
+ near.push(!body);
13493
+ }
13494
+ const result = { skin: pickTriangles(THREE, geometry, near, true), deep: pickTriangles(THREE, geometry, near, false) };
13495
+ if (geometry !== pieces[i].geometry) geometry.dispose();
13496
+ return result;
13497
+ });
13498
+ }
13499
+ function leaveDeepSkinOnBody(three, meshes, params) {
13500
+ const THREE = three;
13501
+ const pieces = meshes;
13502
+ const toVehicle = params.toVehicle;
13503
+ const { side: side2, depth } = params;
13504
+ const split = splitDoorSkin(THREE, pieces, (mesh2) => new THREE.Matrix4().multiplyMatrices(toVehicle, mesh2.matrixWorld), side2, depth);
13505
+ split.forEach(({ skin, deep }, i) => {
13506
+ const piece = pieces[i];
13507
+ if (!deep || !skin || !piece.parent) return;
13508
+ const body = new THREE.Mesh(deep, piece.material);
13509
+ body.position.copy(piece.position);
13510
+ body.quaternion.copy(piece.quaternion);
13511
+ body.scale.copy(piece.scale);
13512
+ body.castShadow = piece.castShadow;
13513
+ body.receiveShadow = piece.receiveShadow;
13514
+ piece.parent.add(body);
13515
+ piece.geometry.dispose();
13516
+ piece.geometry = skin;
13517
+ });
13518
+ }
13519
+ function pickTriangles(THREE, geometry, flags, wanted) {
13520
+ const count = flags.filter((flag) => flag === wanted).length;
13521
+ if (!count) return null;
13522
+ const out = new THREE.BufferGeometry();
13523
+ for (const name of Object.keys(geometry.attributes)) {
13524
+ const attribute = geometry.getAttribute(name);
13525
+ const size = attribute.itemSize;
13526
+ const source = attribute.array;
13527
+ const array = new Float32Array(count * 3 * size);
13528
+ let w = 0;
13529
+ flags.forEach((flag, t) => {
13530
+ if (flag !== wanted) return;
13531
+ array.set(source.subarray(t * 3 * size, (t + 1) * 3 * size), w);
13532
+ w += 3 * size;
13533
+ });
13534
+ out.setAttribute(name, new THREE.Float32BufferAttribute(array, size));
13535
+ }
13536
+ out.computeBoundingBox();
13537
+ out.computeBoundingSphere();
13538
+ return out;
13539
+ }
13540
+ function isDoorSeam(three, params) {
13541
+ const THREE = three;
13542
+ const box2 = params.box;
13543
+ const { zone, side: side2, dark } = params;
13544
+ if (!dark || box2.isEmpty()) return false;
13545
+ const size = box2.getSize(new THREE.Vector3());
13546
+ const centre = box2.getCenter(new THREE.Vector3());
13547
+ if (Math.sign(centre.z) !== Math.sign(side2) || size.x > SEAM_THIN || size.z > SEAM_THIN || size.y < (zone.y1 - zone.y0) * SEAM_TALL) return false;
13548
+ if (centre.y < zone.y0 || centre.y > zone.y1) return false;
13549
+ return Math.abs(centre.x - zone.x0) < SEAM_AT_EDGE || Math.abs(centre.x - zone.x1) < SEAM_AT_EDGE;
13550
+ }
13551
+
13343
13552
  // src/destructible/visual.ts
13344
13553
  var HOLE_DECAL_SCALE = 2.4;
13345
13554
  var DOOR_DEPTH = 0.07;
13555
+ var DOOR_SKIN_DEPTH = 0.09;
13556
+ var DOOR_FLOOR_DEPTH = 0.2;
13346
13557
  var DOOR_EDGE_PIECES = 8;
13347
13558
  var DOOR_EDGE_REACH = 0.08;
13348
13559
  var DOOR_CARD_COLOR = "#3b3e42";
13349
13560
  var DOOR_LINER_COLOR = "#5a5e64";
13350
13561
  var DOOR_LINER_DARK = 0.15;
13351
- var DOOR_STRIP_THIN = 0.08;
13562
+ var DOOR_STRIP_THIN = 0.13;
13352
13563
  var DOOR_STRIP_NEAR = 0.04;
13353
13564
  var DOOR_STRIP_TALL = 0.22;
13354
- var STRIP_SMOOTH_DEG = 180;
13355
- var DOOR_STRIP_VERTICES = 2e4;
13565
+ var DOOR_STRIP_VERTICES = 6e4;
13356
13566
  var DOOR_SILL = 0.25;
13357
- var DOOR_STRIP_OVER = 0.3;
13567
+ var DOOR_STRIP_OVER = 0.05;
13358
13568
  var DOOR_TOP_OVER_PANE = 0.03;
13359
13569
  var HOLE_TEXTURE_SIZE = 64;
13360
13570
  var LAMINATED_HP = 1e3;
@@ -13463,6 +13673,10 @@ var GameVehicles = (() => {
13463
13673
  __publicField(this, "skins", /* @__PURE__ */ new Map());
13464
13674
  /** How high each door's cut reaches. */
13465
13675
  __publicField(this, "doorTops", /* @__PURE__ */ new Map());
13676
+ /** The shut lines drawn on the body at a door's edges, by line: the doors whose edge it marks. Shown only while all of them are closed. */
13677
+ __publicField(this, "seams", /* @__PURE__ */ new Map());
13678
+ /** Doors and lids standing open now. */
13679
+ __publicField(this, "openNow", /* @__PURE__ */ new Set());
13466
13680
  __publicField(this, "lightsOn", true);
13467
13681
  /** Heavy body meshes whose triangle index is still to build (null once done). */
13468
13682
  __publicField(this, "warming");
@@ -13644,6 +13858,11 @@ var GameVehicles = (() => {
13644
13858
  }
13645
13859
  panel.visible = open && !this.skins.get(id);
13646
13860
  recess.visible = open && !this.skins.get(id);
13861
+ if (open) this.openNow.add(id);
13862
+ else this.openNow.delete(id);
13863
+ this.seams.forEach((doors, seam) => {
13864
+ if (doors.has(id)) seam.visible = ![...doors].some((door) => this.openNow.has(door));
13865
+ });
13647
13866
  };
13648
13867
  const euler = piv.rotation;
13649
13868
  const quaternion = piv.quaternion;
@@ -13686,11 +13905,13 @@ var GameVehicles = (() => {
13686
13905
  const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
13687
13906
  this.mend(dirty);
13688
13907
  if (pcs.length) {
13908
+ leaveDeepSkinOnBody(this.THREE, pcs, { toVehicle: inv, side: z.side < 0 ? -1 : 1, depth: DOOR_SKIN_DEPTH });
13689
13909
  const panes = I.windowsOver(this.c, z, inv);
13690
13910
  const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
13691
13911
  const mirrors = this.mirrorsOn(z);
13692
13912
  const frames = this.framesAround(panes, inv);
13693
- objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
13913
+ const followers = I.followersIn(this.c, z, inv).filter((o) => !this.takeSeam(o, id, z, inv) && !this.inFloor(o, piv, z, inv));
13914
+ objs = [...pcs, ...followers, ...panes, ...seals, ...mirrors, ...frames];
13694
13915
  }
13695
13916
  } else {
13696
13917
  const pcs = this.cutZone(z);
@@ -13718,6 +13939,25 @@ var GameVehicles = (() => {
13718
13939
  this.c.root.updateMatrixWorld(true);
13719
13940
  return objs.length > 0;
13720
13941
  }
13942
+ /** A thin part at the foot of the opening, well inside the skin (a step, a sill trim, a rail on the floor), is the floor's: it does not swing with the door. */
13943
+ inFloor(o, piv, z, inv) {
13944
+ const side2 = z.side < 0 ? -1 : 1;
13945
+ const centre = this.I.localBox(this.c, o, inv).getCenter(new this.THREE.Vector3());
13946
+ const skin = side2 * (piv.position.z - side2 * 0.016);
13947
+ return centre.y < z.box.y0 + DOOR_SILL && side2 * centre.z < skin - DOOR_FLOOR_DEPTH;
13948
+ }
13949
+ /** A shut line drawn at the door's edge stays on the body, hidden while a door it marks stands open; answers whether `o` is one. */
13950
+ takeSeam(o, id, z, inv) {
13951
+ const B = z.box;
13952
+ const box2 = this.I.localBox(this.c, o, inv);
13953
+ const color2 = o.material?.color;
13954
+ const dark = !!color2 && color2.getHSL({ h: 0, s: 0, l: 0 }).l < DOOR_LINER_DARK;
13955
+ if (!o.isMesh || !isDoorSeam(this.THREE, { box: box2, zone: { x0: B.x0, x1: B.x1, y0: B.y0, y1: B.y1 }, side: z.side, dark })) return false;
13956
+ const doors = this.seams.get(o) ?? /* @__PURE__ */ new Set();
13957
+ doors.add(id);
13958
+ this.seams.set(o, doors);
13959
+ return true;
13960
+ }
13721
13961
  /** 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. */
13722
13962
  doorEdges(piv, z, y1) {
13723
13963
  const THREE = this.THREE;
@@ -13739,13 +13979,15 @@ var GameVehicles = (() => {
13739
13979
  const at = this.surface("z", side2, B, [[x, y]]);
13740
13980
  return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
13741
13981
  });
13742
- const band = (lift) => {
13982
+ const band = (lift, path, closed) => {
13743
13983
  const position = [];
13744
13984
  const index = [];
13745
- loop.forEach(([x, y], i) => {
13746
- position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
13985
+ path.forEach((at, i) => {
13986
+ const [x, y] = loop[at];
13987
+ position.push(x, y, heights[at] + side2 * lift, x, y, heights[at] + side2 * (lift - DOOR_DEPTH));
13988
+ if (!closed && i === path.length - 1) return;
13747
13989
  const a = i * 2;
13748
- const b = (i + 1) % loop.length * 2;
13990
+ const b = (i + 1) % path.length * 2;
13749
13991
  index.push(a, a + 1, b, b, a + 1, b + 1);
13750
13992
  });
13751
13993
  const g = new THREE.BufferGeometry();
@@ -13773,8 +14015,11 @@ var GameVehicles = (() => {
13773
14015
  card.castShadow = false;
13774
14016
  this.c.vehicle.add(card);
13775
14017
  piv.attach(card);
13776
- const edges = new THREE.Mesh(band(2e-3), paint);
13777
- const lining = new THREE.Mesh(band(2e-3), paint);
14018
+ const all = loop.map((_, i) => i);
14019
+ const top = 2 * DOOR_EDGE_PIECES;
14020
+ const sides = [...all.slice(3 * DOOR_EDGE_PIECES), ...all.slice(0, top + 1)];
14021
+ const edges = new THREE.Mesh(band(2e-3, all, true), paint);
14022
+ const lining = new THREE.Mesh(band(2e-3, sides, false), paint);
13778
14023
  for (const m of [edges, lining]) {
13779
14024
  m.userData.noFollow = true;
13780
14025
  m.castShadow = false;
@@ -13831,26 +14076,14 @@ var GameVehicles = (() => {
13831
14076
  const box2 = this.I.localBox(this.c, o, inv);
13832
14077
  const size = box2.getSize(new THREE.Vector3());
13833
14078
  const mid = box2.getCenter(new THREE.Vector3());
13834
- 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;
14079
+ 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) < DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
13835
14080
  const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
13836
- const v = new THREE.Vector3();
13837
- const sdf = (x, y, zz) => {
13838
- v.set(x, y, zz).applyMatrix4(toVehicle);
13839
- return Math.max(x0 - v.x, v.x - x1);
13840
- };
13841
- const over = new THREE.Mesh(o.geometry.clone(), o.material);
14081
+ const over = cutStripOverDoor(this.kit, { strip: o, toVehicle, x0, x1 });
13842
14082
  over.position.copy(o.position);
13843
14083
  over.quaternion.copy(o.quaternion);
13844
14084
  over.scale.copy(o.scale);
13845
14085
  this.c.vehicle.add(over);
13846
14086
  over.updateMatrixWorld(true);
13847
- const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
13848
- const w = new THREE.Vector3();
13849
- this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
13850
- w.set(x, y, zz).applyMatrix4(overToVehicle);
13851
- return -Math.max(x0 - w.x, w.x - x1);
13852
- } }], STRIP_SMOOTH_DEG, {});
13853
- this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
13854
14087
  over.userData.noFollow = true;
13855
14088
  piv.attach(over);
13856
14089
  }
@@ -14431,11 +14664,18 @@ var GameVehicles = (() => {
14431
14664
  }
14432
14665
  }
14433
14666
  }
14667
+ /** The lens of a light is holed or cracked and stays; `pierced`: a round went through it to the bulb, and the lamp goes dark. */
14668
+ crackLight(p, pierced) {
14669
+ const z = this.zone(p);
14670
+ if (!z) return;
14671
+ for (const L of z.lights) this.kit.crackLight(this.c, L, new this.THREE.Vector3(p.box.center[0] > 0 ? -1 : 1, 0, 0), pierced);
14672
+ }
14434
14673
  /** The lens of a light breaks and its unit stays (damaged) or goes (destroyed). */
14435
14674
  breakLight(p, gone) {
14436
14675
  const z = this.zone(p);
14437
14676
  if (!z) return;
14438
14677
  for (const L of z.lights) {
14678
+ L.lensMaterial = p.material;
14439
14679
  this.kit.breakLight(this.c, L, new this.THREE.Vector3(p.box.center[0] > 0 ? -1 : 1, 0, 0));
14440
14680
  if (gone) {
14441
14681
  if (L.group) L.group.visible = false;
@@ -15081,6 +15321,7 @@ var GameVehicles = (() => {
15081
15321
  wear(p, amount, out);
15082
15322
  syncZone(p);
15083
15323
  if (p.hp <= 0) lose(p, "broken", out, cause);
15324
+ else if (p.def.role === "light") vis?.crackLight(p.def, dtype === "ballistic");
15084
15325
  if (p.def.role === "tyre" && !p.flat && p.hp < p.def.hp * 0.6 && !p.lost) {
15085
15326
  p.flat = true;
15086
15327
  vis?.tyreFlat(p.def, 0.24);
@@ -15893,7 +16134,7 @@ var GameVehicles = (() => {
15893
16134
  if (!list2.length) return 1;
15894
16135
  return Math.min(...list2.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
15895
16136
  };
15896
- 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) }));
16137
+ 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), rim: p.lost }));
15897
16138
  const status = computeStatus();
15898
16139
  const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
15899
16140
  const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;