@adatechnology/game-destruction-three 0.1.0 → 0.1.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.js CHANGED
@@ -690,6 +690,8 @@ var MU = 0.7;
690
690
  var SLEEP_AFTER = 0.5;
691
691
  var HARD_LANDING = 3;
692
692
  var SLOP = 2e-3;
693
+ var NARROW = 0.3;
694
+ var TOPPLE = 3;
693
695
  var PieceSystem = class {
694
696
  constructor(THREE, o) {
695
697
  __publicField(this, "THREE", THREE);
@@ -821,6 +823,7 @@ var PieceSystem = class {
821
823
  let landPoint = [pos[0], pos[1], pos[2]];
822
824
  let contactedLast = false;
823
825
  let touched = false;
826
+ let unstable = false;
824
827
  for (let s = 0; s < steps; s++) {
825
828
  v[1] -= this.o.gravity * h;
826
829
  pos[0] += v[0] * h;
@@ -909,6 +912,25 @@ var PieceSystem = class {
909
912
  }
910
913
  }
911
914
  }
915
+ let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
916
+ for (const r of rs) {
917
+ x0 = Math.min(x0, r[0]);
918
+ x1 = Math.max(x1, r[0]);
919
+ z0 = Math.min(z0, r[2]);
920
+ z1 = Math.max(z1, r[2]);
921
+ low = Math.min(low, r[1]);
922
+ }
923
+ const height = -low;
924
+ const wx = x1 - x0;
925
+ const wz = z1 - z0;
926
+ unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
927
+ if (unstable) {
928
+ const dirX = x0 + x1 > 0 ? -1 : 1;
929
+ const dirZ = z0 + z1 > 0 ? -1 : 1;
930
+ const k = TOPPLE * Math.min(height, 1) * h;
931
+ if (wx <= wz) w[2] -= dirX * k;
932
+ else w[0] += dirZ * k;
933
+ }
912
934
  const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
913
935
  w[0] *= rd;
914
936
  w[1] *= rd;
@@ -926,7 +948,7 @@ var PieceSystem = class {
926
948
  b.lastHit = this.time;
927
949
  this.o.onHardLanding(b, landPoint, landing);
928
950
  }
929
- const still = touched && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
951
+ const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
930
952
  if (still) {
931
953
  b.still += dt;
932
954
  if (b.still >= SLEEP_AFTER) {
@@ -986,6 +1008,166 @@ var PieceSystem = class {
986
1008
  }
987
1009
  };
988
1010
 
1011
+ // src/spec.ts
1012
+ import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
1013
+ function partHintOf(value) {
1014
+ if (typeof value === "string") return value ? { id: value } : null;
1015
+ if (!value || typeof value !== "object") return null;
1016
+ const v = value;
1017
+ if (typeof v.id !== "string" || !v.id) return null;
1018
+ const str = (x) => typeof x === "string" && x ? x : void 0;
1019
+ return { id: v.id, material: str(v.material), parent: str(v.parent), kind: str(v.kind) };
1020
+ }
1021
+ var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
1022
+ function sanitizeId(name) {
1023
+ let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1024
+ if (id && /^[0-9]/.test(id)) id = `p_${id}`;
1025
+ return id;
1026
+ }
1027
+ function isMesh(o) {
1028
+ return o.isMesh === true;
1029
+ }
1030
+ function sizeOf(box) {
1031
+ return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
1032
+ }
1033
+ function specFromObject3D(THREE, root, options = {}) {
1034
+ const fallback = options.fallback ?? "plastic";
1035
+ const minSize = options.minSize ?? 0.015;
1036
+ root.updateMatrixWorld(true);
1037
+ const inv = root.matrixWorld.clone().invert();
1038
+ const entries = [];
1039
+ const byKey = /* @__PURE__ */ new Map();
1040
+ const visit = (o) => {
1041
+ if (!o.visible || o.userData?.destructionIgnore) return;
1042
+ if (isMesh(o) && o.geometry) {
1043
+ const geom = o.geometry;
1044
+ if (!geom.boundingBox) geom.computeBoundingBox();
1045
+ if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
1046
+ const box = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
1047
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
1048
+ const matName = mats[0]?.name ?? "";
1049
+ const ud = o.userData ?? {};
1050
+ const hint = partHintOf(ud.destructionPart);
1051
+ const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? hint?.material ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
1052
+ const explicit = hint?.id ?? null;
1053
+ const hollow = ud.hollow === true || /hollow/i.test(o.name);
1054
+ const fill = options.fillOf?.(o, material);
1055
+ const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
1056
+ const existing = explicit ? byKey.get(explicit) : void 0;
1057
+ if (existing) {
1058
+ const before = sizeOf(existing.box);
1059
+ const bv = before[0] * before[1] * before[2];
1060
+ const ns = sizeOf(box);
1061
+ existing.box.union(box);
1062
+ existing.meshes.push(o);
1063
+ if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
1064
+ existing.hollow = existing.hollow || hollow;
1065
+ if (fill !== void 0) existing.fill = fill;
1066
+ } else {
1067
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
1068
+ entries.push(e);
1069
+ if (explicit) byKey.set(explicit, e);
1070
+ }
1071
+ }
1072
+ }
1073
+ for (const c of o.children) visit(c);
1074
+ };
1075
+ visit(root);
1076
+ const eps = 5e-3;
1077
+ const overlapVolume = (a, b) => {
1078
+ const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
1079
+ const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
1080
+ const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
1081
+ return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
1082
+ };
1083
+ const largest = (e) => Math.max(...sizeOf(e.box));
1084
+ const kept = [];
1085
+ const tiny = [];
1086
+ for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
1087
+ for (const t of tiny) {
1088
+ let best = null;
1089
+ let bestV = 0;
1090
+ for (const k of kept) {
1091
+ const v = overlapVolume(t.box, k.box);
1092
+ if (v > bestV) {
1093
+ bestV = v;
1094
+ best = k;
1095
+ }
1096
+ }
1097
+ if (!best) {
1098
+ let bd = 0.05;
1099
+ for (const k of kept) {
1100
+ const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
1101
+ if (d < bd) {
1102
+ bd = d;
1103
+ best = k;
1104
+ }
1105
+ }
1106
+ }
1107
+ if (best) {
1108
+ best.meshes.push(...t.meshes);
1109
+ } else {
1110
+ kept.push(t);
1111
+ }
1112
+ }
1113
+ const used = /* @__PURE__ */ new Set();
1114
+ const parts = [];
1115
+ const meshesOf = {};
1116
+ let counter = 0;
1117
+ for (const e of kept) {
1118
+ counter++;
1119
+ let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
1120
+ let id = base;
1121
+ for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
1122
+ used.add(id);
1123
+ base = id;
1124
+ const size = sizeOf(e.box);
1125
+ const center = e.box.getCenter(new THREE.Vector3());
1126
+ const part = {
1127
+ id,
1128
+ material: e.material,
1129
+ shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
1130
+ position: [center.x, center.y, center.z]
1131
+ };
1132
+ const minSide = Math.min(...size);
1133
+ if (e.fill !== void 0) part.fill = e.fill;
1134
+ else if (e.hollow) part.fill = 0.05;
1135
+ if (minSide < 0.01 && METAL.test(e.material)) {
1136
+ part.fill = e.fill ?? 1;
1137
+ part.thickness = Math.max(minSide, 5e-4);
1138
+ }
1139
+ if (e.tags) part.tags = [...e.tags];
1140
+ const ao = options.anchorBelow ?? -1;
1141
+ if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
1142
+ parts.push(part);
1143
+ meshesOf[id] = e.meshes;
1144
+ }
1145
+ const spec = { parts };
1146
+ if (options.name !== void 0) spec.name = options.name;
1147
+ else if (root.name) spec.name = root.name;
1148
+ if (options.tags) spec.tags = [...options.tags];
1149
+ spec.joints = options.joints === false ? [] : inferJoints(parts);
1150
+ if (options.joints !== false) {
1151
+ const finalId = /* @__PURE__ */ new Map();
1152
+ kept.forEach((e, i) => finalId.set(e, parts[i].id));
1153
+ const idOf = /* @__PURE__ */ new Map();
1154
+ for (const e of kept) if (e.explicitId) idOf.set(e.explicitId, finalId.get(e));
1155
+ const joints = spec.joints;
1156
+ for (const e of kept) {
1157
+ if (!e.parent) continue;
1158
+ const id = finalId.get(e);
1159
+ const parent = idOf.get(e.parent) ?? (used.has(sanitizeId(e.parent)) ? sanitizeId(e.parent) : null);
1160
+ if (!parent || parent === id) continue;
1161
+ const at = joints.findIndex((j) => j.a === parent && j.b === id || j.a === id && j.b === parent);
1162
+ const kind = e.jointKind ?? (at >= 0 ? joints[at].kind : "resting");
1163
+ const joint = { id: `${parent}~${id}`, a: parent, b: id, kind };
1164
+ if (at >= 0) joints[at] = joint;
1165
+ else joints.push(joint);
1166
+ }
1167
+ }
1168
+ return { spec, parts: meshesOf };
1169
+ }
1170
+
989
1171
  // src/heat.ts
990
1172
  var STOPS = [
991
1173
  [450, 0],
@@ -1075,6 +1257,7 @@ var Visuals = class {
1075
1257
  __publicField(this, "box");
1076
1258
  /** Total live decals across meshes (kept in sync by add/remove). */
1077
1259
  __publicField(this, "decalCount", 0);
1260
+ __publicField(this, "tints", /* @__PURE__ */ new WeakMap());
1078
1261
  this.kit = new DecalKit(THREE);
1079
1262
  this.a = tmpV(THREE);
1080
1263
  this.b = tmpV(THREE);
@@ -1105,10 +1288,33 @@ var Visuals = class {
1105
1288
  vertexCount(mv) {
1106
1289
  return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
1107
1290
  }
1108
- /** Colour of the original material (before any char), `0xRRGGBB`, or -1. */
1291
+ /**
1292
+ * Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
1293
+ * and debris of such a mesh multiply it by `material.color`; without it they come out white.
1294
+ */
1295
+ vertexTint(mv) {
1296
+ if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
1297
+ let tint = null;
1298
+ const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
1299
+ const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
1300
+ if (attr && attr.count > 0 && attr.itemSize >= 3) {
1301
+ let r = 0, g = 0, b = 0;
1302
+ for (let i = 0; i < attr.count; i++) {
1303
+ r += attr.getX(i);
1304
+ g += attr.getY(i);
1305
+ b += attr.getZ(i);
1306
+ }
1307
+ tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
1308
+ }
1309
+ this.tints.set(mv.mesh, tint);
1310
+ return tint;
1311
+ }
1312
+ /** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
1109
1313
  baseColorHex(mv) {
1110
1314
  const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
1111
- return c ? c.getHex() : -1;
1315
+ if (!c) return -1;
1316
+ const tint = this.vertexTint(mv);
1317
+ return tint ? c.clone().multiply(tint).getHex() : c.getHex();
1112
1318
  }
1113
1319
  /** Is the original material transparent? */
1114
1320
  isTransparent(mv) {
@@ -1579,8 +1785,8 @@ var Renderer = class {
1579
1785
  } else {
1580
1786
  const leftovers = [];
1581
1787
  for (const m of meshesIn(root)) {
1582
- const ud = m.userData?.destructionPart;
1583
- if (typeof ud === "string" && partIds.has(ud)) add(ud, m);
1788
+ const ud = partHintOf(m.userData?.destructionPart)?.id;
1789
+ if (ud !== void 0 && partIds.has(ud)) add(ud, m);
1584
1790
  else if (partIds.has(m.name)) add(m.name, m);
1585
1791
  else leftovers.push(m);
1586
1792
  }
@@ -1890,7 +2096,10 @@ var Renderer = class {
1890
2096
  if (!mv) return null;
1891
2097
  const mat = materialsOf2(mv.mesh.material)[0];
1892
2098
  const c = mat?.color;
1893
- if (c && c.isColor) return c.getHex();
2099
+ if (c && c.isColor) {
2100
+ const tint = this.visuals.vertexTint(mv);
2101
+ return tint ? c.clone().multiply(tint).getHex() : c.getHex();
2102
+ }
1894
2103
  return def ? fallbackColor(def.id, def) : null;
1895
2104
  }
1896
2105
  debrisOptions(kind, objectId, partId) {
@@ -2334,139 +2543,6 @@ var Renderer = class {
2334
2543
  this.disposed = true;
2335
2544
  }
2336
2545
  };
2337
-
2338
- // src/spec.ts
2339
- import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
2340
- var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
2341
- function sanitizeId(name) {
2342
- let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2343
- if (id && /^[0-9]/.test(id)) id = `p_${id}`;
2344
- return id;
2345
- }
2346
- function isMesh(o) {
2347
- return o.isMesh === true;
2348
- }
2349
- function sizeOf(box) {
2350
- return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
2351
- }
2352
- function specFromObject3D(THREE, root, options = {}) {
2353
- const fallback = options.fallback ?? "plastic";
2354
- const minSize = options.minSize ?? 0.015;
2355
- root.updateMatrixWorld(true);
2356
- const inv = root.matrixWorld.clone().invert();
2357
- const entries = [];
2358
- const byKey = /* @__PURE__ */ new Map();
2359
- const visit = (o) => {
2360
- if (!o.visible || o.userData?.destructionIgnore) return;
2361
- if (isMesh(o) && o.geometry) {
2362
- const geom = o.geometry;
2363
- if (!geom.boundingBox) geom.computeBoundingBox();
2364
- if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
2365
- const box = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
2366
- const mats = Array.isArray(o.material) ? o.material : [o.material];
2367
- const matName = mats[0]?.name ?? "";
2368
- const ud = o.userData ?? {};
2369
- const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
2370
- const explicit = typeof ud.destructionPart === "string" && ud.destructionPart ? ud.destructionPart : null;
2371
- const hollow = ud.hollow === true || /hollow/i.test(o.name);
2372
- const fill = options.fillOf?.(o, material);
2373
- const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
2374
- const existing = explicit ? byKey.get(explicit) : void 0;
2375
- if (existing) {
2376
- const before = sizeOf(existing.box);
2377
- const bv = before[0] * before[1] * before[2];
2378
- const ns = sizeOf(box);
2379
- existing.box.union(box);
2380
- existing.meshes.push(o);
2381
- if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
2382
- existing.hollow = existing.hollow || hollow;
2383
- if (fill !== void 0) existing.fill = fill;
2384
- } else {
2385
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name };
2386
- entries.push(e);
2387
- if (explicit) byKey.set(explicit, e);
2388
- }
2389
- }
2390
- }
2391
- for (const c of o.children) visit(c);
2392
- };
2393
- visit(root);
2394
- const eps = 5e-3;
2395
- const overlapVolume = (a, b) => {
2396
- const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
2397
- const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
2398
- const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
2399
- return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
2400
- };
2401
- const largest = (e) => Math.max(...sizeOf(e.box));
2402
- const kept = [];
2403
- const tiny = [];
2404
- for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
2405
- for (const t of tiny) {
2406
- let best = null;
2407
- let bestV = 0;
2408
- for (const k of kept) {
2409
- const v = overlapVolume(t.box, k.box);
2410
- if (v > bestV) {
2411
- bestV = v;
2412
- best = k;
2413
- }
2414
- }
2415
- if (!best) {
2416
- let bd = 0.05;
2417
- for (const k of kept) {
2418
- const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
2419
- if (d < bd) {
2420
- bd = d;
2421
- best = k;
2422
- }
2423
- }
2424
- }
2425
- if (best) {
2426
- best.meshes.push(...t.meshes);
2427
- } else {
2428
- kept.push(t);
2429
- }
2430
- }
2431
- const used = /* @__PURE__ */ new Set();
2432
- const parts = [];
2433
- const meshesOf = {};
2434
- let counter = 0;
2435
- for (const e of kept) {
2436
- counter++;
2437
- let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
2438
- let id = base;
2439
- for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
2440
- used.add(id);
2441
- base = id;
2442
- const size = sizeOf(e.box);
2443
- const center = e.box.getCenter(new THREE.Vector3());
2444
- const part = {
2445
- id,
2446
- material: e.material,
2447
- shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
2448
- position: [center.x, center.y, center.z]
2449
- };
2450
- const minSide = Math.min(...size);
2451
- if (e.fill !== void 0) part.fill = e.fill;
2452
- else if (e.hollow) part.fill = 0.05;
2453
- if (minSide < 0.01 && METAL.test(e.material)) {
2454
- part.fill = e.fill ?? 1;
2455
- part.thickness = Math.max(minSide, 5e-4);
2456
- }
2457
- if (e.tags) part.tags = [...e.tags];
2458
- const ao = options.anchorBelow ?? -1;
2459
- if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
2460
- parts.push(part);
2461
- meshesOf[id] = e.meshes;
2462
- }
2463
- const spec = { parts };
2464
- if (options.name !== void 0) spec.name = options.name;
2465
- else if (root.name) spec.name = root.name;
2466
- if (options.tags) spec.tags = [...options.tags];
2467
- spec.joints = options.joints === false ? [] : inferJoints(parts);
2468
- return { spec, parts: meshesOf };
2469
- }
2470
2546
  export {
2471
2547
  createDestructionRenderer,
2472
2548
  heatColor,