@adatechnology/game-floorplan-three 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11268,7 +11268,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11268
11268
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
11269
11269
  band.c[L] = (lo + hi) / 2;
11270
11270
  band.s[L] = Math.max(hi - lo, 0.01);
11271
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
11271
+ if (inp.style === "snap") {
11272
+ for (const b of bsp(band, Math.max(3, Math.min(6, inp.maxFragments - out.length)), [0, 1, 2], inp.rnd)) out.push({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] });
11273
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
11272
11274
  return out;
11273
11275
  };
11274
11276
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -11299,6 +11301,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11299
11301
  const t = sorted[0];
11300
11302
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
11301
11303
  if (style === "snap") {
11304
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
11302
11305
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
11303
11306
  for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
11304
11307
  return out;
@@ -11472,6 +11475,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11472
11475
  for (const x of spread(dims[u], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(v, dims[v] / 2 - 0.02, [[u, x], [thin, 0]]));
11473
11476
  return out;
11474
11477
  };
11478
+ var clipBarsToBox = (bars, u, v, center, size) => {
11479
+ const out = [];
11480
+ for (const [a, b] of bars) {
11481
+ const axis = a[u] !== b[u] ? u : v;
11482
+ const other = axis === u ? v : u;
11483
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
11484
+ const lo = Math.max(Math.min(a[axis], b[axis]), center[axis] - size[axis] / 2), hi = Math.min(Math.max(a[axis], b[axis]), center[axis] + size[axis] / 2);
11485
+ if (hi - lo < 1e-6) continue;
11486
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
11487
+ p[axis] = lo;
11488
+ q[axis] = hi;
11489
+ out.push([p, q]);
11490
+ }
11491
+ return out;
11492
+ };
11493
+ var bendBar = ([a, b], thin, bow, i) => {
11494
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
11495
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
11496
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
11497
+ return [[a, m], [m, b]];
11498
+ };
11475
11499
  var clipBarsToDisc = (bars, u, v, center, radius) => {
11476
11500
  const out = [];
11477
11501
  for (const [a, b] of bars) {
@@ -12392,14 +12416,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12392
12416
  smoke: "grey"
12393
12417
  },
12394
12418
  visual: {
12395
- charColor: 1709072
12419
+ charColor: 1709072,
12420
+ innerColor: 14730131
12396
12421
  },
12397
12422
  tags: [
12398
12423
  "wood",
12399
12424
  "organic",
12400
12425
  "flammable"
12401
12426
  ],
12402
- compressive: 1e5
12427
+ compressive: 1e5,
12428
+ hits: {
12429
+ blunt: {
12430
+ tear: "splinters",
12431
+ tearJoules: 60
12432
+ }
12433
+ }
12403
12434
  },
12404
12435
  {
12405
12436
  id: "wood_hard",
@@ -12429,14 +12460,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12429
12460
  smoke: "grey"
12430
12461
  },
12431
12462
  visual: {
12432
- charColor: 1709072
12463
+ charColor: 1709072,
12464
+ innerColor: 13213802
12433
12465
  },
12434
12466
  tags: [
12435
12467
  "wood",
12436
12468
  "organic",
12437
12469
  "flammable"
12438
12470
  ],
12439
- compressive: 15e4
12471
+ compressive: 15e4,
12472
+ hits: {
12473
+ blunt: {
12474
+ tear: "splinters",
12475
+ tearJoules: 150
12476
+ }
12477
+ }
12440
12478
  },
12441
12479
  {
12442
12480
  id: "plywood",
@@ -12466,14 +12504,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12466
12504
  smoke: "grey"
12467
12505
  },
12468
12506
  visual: {
12469
- charColor: 1709072
12507
+ charColor: 1709072,
12508
+ innerColor: 14862234
12470
12509
  },
12471
12510
  tags: [
12472
12511
  "wood",
12473
12512
  "organic",
12474
12513
  "flammable"
12475
12514
  ],
12476
- compressive: 6e4
12515
+ compressive: 6e4,
12516
+ hits: {
12517
+ ballistic: {
12518
+ tear: "splinters"
12519
+ },
12520
+ blunt: {
12521
+ tear: "splinters",
12522
+ tearJoules: 80
12523
+ }
12524
+ }
12477
12525
  },
12478
12526
  {
12479
12527
  id: "mdf",
@@ -12500,14 +12548,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12500
12548
  smoke: "grey"
12501
12549
  },
12502
12550
  visual: {
12503
- charColor: 1709072
12551
+ charColor: 1709072,
12552
+ innerColor: 12096362
12504
12553
  },
12505
12554
  tags: [
12506
12555
  "wood",
12507
12556
  "organic",
12508
12557
  "flammable"
12509
12558
  ],
12510
- compressive: 3e4
12559
+ compressive: 3e4,
12560
+ hits: {
12561
+ ballistic: {
12562
+ tear: "lumps",
12563
+ tearJoules: 120
12564
+ },
12565
+ blunt: {
12566
+ tear: "lumps",
12567
+ tearJoules: 20
12568
+ }
12569
+ }
12511
12570
  },
12512
12571
  {
12513
12572
  id: "glass",
@@ -12621,6 +12680,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12621
12680
  damageScale: {
12622
12681
  explosive: 1.2
12623
12682
  },
12683
+ hits: {
12684
+ ballistic: {
12685
+ unitJoules: 500,
12686
+ floor: 900,
12687
+ crater: true
12688
+ },
12689
+ blunt: {
12690
+ unitJoules: 1500,
12691
+ floor: 60
12692
+ }
12693
+ },
12624
12694
  hitDebris: "chips",
12625
12695
  breakDebris: "dust",
12626
12696
  thermal: {
@@ -12989,7 +13059,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12989
13059
  smoke: "white"
12990
13060
  },
12991
13061
  visual: {
12992
- charColor: 1709072
13062
+ charColor: 1709072,
13063
+ innerColor: 14207126
12993
13064
  },
12994
13065
  tags: [
12995
13066
  "plant",
@@ -12997,7 +13068,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12997
13068
  "organic",
12998
13069
  "flammable"
12999
13070
  ],
13000
- compressive: 6e4
13071
+ compressive: 6e4,
13072
+ hits: {
13073
+ blunt: {
13074
+ tear: "none"
13075
+ }
13076
+ }
13001
13077
  },
13002
13078
  {
13003
13079
  id: "cactus",
@@ -13112,6 +13188,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13112
13188
  "masonry"
13113
13189
  ],
13114
13190
  compressive: 2e4
13191
+ },
13192
+ {
13193
+ id: "asphalt",
13194
+ density: 2350,
13195
+ hpPerKg: 10,
13196
+ penetration: 14,
13197
+ penetrationClass: "concrete",
13198
+ penetrationScale: 0.8,
13199
+ ductility: 0.1,
13200
+ fracture: "crumble",
13201
+ fragmentSize: [
13202
+ 0.03,
13203
+ 0.15
13204
+ ],
13205
+ hitDebris: "chips",
13206
+ breakDebris: "dust",
13207
+ thermal: {
13208
+ conductivity: 0.1,
13209
+ heatCapacity: 2,
13210
+ ignitionC: 400,
13211
+ burnRate: 2e-3,
13212
+ smoke: "black"
13213
+ },
13214
+ tags: [
13215
+ "masonry",
13216
+ "brittle"
13217
+ ],
13218
+ compressive: 3e4
13219
+ },
13220
+ {
13221
+ id: "sand",
13222
+ density: 1600,
13223
+ hpPerKg: 2,
13224
+ penetration: 6,
13225
+ ductility: 0.4,
13226
+ fracture: "crumble",
13227
+ fragmentSize: [
13228
+ 0.01,
13229
+ 0.04
13230
+ ],
13231
+ hitDebris: "dust",
13232
+ breakDebris: "dust",
13233
+ thermal: {
13234
+ conductivity: 0.1,
13235
+ heatCapacity: 2
13236
+ },
13237
+ tags: [
13238
+ "soil"
13239
+ ],
13240
+ compressive: 1e4
13241
+ },
13242
+ {
13243
+ id: "gravel",
13244
+ density: 1800,
13245
+ hpPerKg: 3,
13246
+ penetration: 8,
13247
+ ductility: 0.3,
13248
+ fracture: "crumble",
13249
+ fragmentSize: [
13250
+ 0.02,
13251
+ 0.06
13252
+ ],
13253
+ hitDebris: "chips",
13254
+ breakDebris: "dust",
13255
+ thermal: {
13256
+ conductivity: 0.1,
13257
+ heatCapacity: 2
13258
+ },
13259
+ tags: [
13260
+ "soil",
13261
+ "masonry"
13262
+ ],
13263
+ compressive: 2e4
13264
+ },
13265
+ {
13266
+ id: "turf",
13267
+ density: 900,
13268
+ hpPerKg: 3,
13269
+ penetration: 4,
13270
+ ductility: 0.5,
13271
+ fracture: "crumble",
13272
+ fragmentSize: [
13273
+ 0.03,
13274
+ 0.1
13275
+ ],
13276
+ hitDebris: "dust",
13277
+ breakDebris: "dust",
13278
+ thermal: {
13279
+ conductivity: 0.1,
13280
+ heatCapacity: 1.5,
13281
+ ignitionC: 250,
13282
+ burnRate: 0.03,
13283
+ smoke: "white"
13284
+ },
13285
+ tags: [
13286
+ "soil",
13287
+ "organic"
13288
+ ],
13289
+ compressive: 15e3
13290
+ },
13291
+ {
13292
+ id: "mortar",
13293
+ density: 1900,
13294
+ hpPerKg: 10,
13295
+ penetration: 12,
13296
+ penetrationClass: "concrete",
13297
+ penetrationScale: 1.2,
13298
+ ductility: 0.03,
13299
+ fracture: "crumble",
13300
+ fragmentSize: [
13301
+ 0.02,
13302
+ 0.08
13303
+ ],
13304
+ hitDebris: "chips",
13305
+ breakDebris: "dust",
13306
+ thermal: {
13307
+ conductivity: 0.15,
13308
+ heatCapacity: 2.5
13309
+ },
13310
+ tags: [
13311
+ "masonry",
13312
+ "brittle"
13313
+ ],
13314
+ compressive: 1e5
13115
13315
  }
13116
13316
  ];
13117
13317
  var material_hints_default = {
@@ -13285,6 +13485,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13285
13485
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
13286
13486
  };
13287
13487
  var THIN_SHEET_DISHING = 5e-3;
13488
+ var SWEEP_BACK = 0.01;
13489
+ function sweepOrigins(box3, d, spacing) {
13490
+ const origins = [];
13491
+ const line = (from, to) => {
13492
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
13493
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
13494
+ };
13495
+ for (const axis of [0, 1, 2]) {
13496
+ if (Math.abs(d[axis]) < 1e-3) continue;
13497
+ const lead = d[axis] > 0 ? box3.max[axis] - SWEEP_BACK : box3.min[axis] + SWEEP_BACK;
13498
+ const u = (axis + 1) % 3;
13499
+ const v = (axis + 2) % 3;
13500
+ for (const a of line(box3.min[u], box3.max[u])) {
13501
+ for (const b of line(box3.min[v], box3.max[v])) {
13502
+ const point = [0, 0, 0];
13503
+ point[axis] = lead;
13504
+ point[u] = a;
13505
+ point[v] = b;
13506
+ origins.push(point);
13507
+ }
13508
+ }
13509
+ }
13510
+ return origins;
13511
+ }
13512
+ var THIN_SHEET_CRUSH = 5e-4;
13513
+ var THIN_SHEET_CONTACT = 0.025;
13288
13514
  var DMG_PER_J = 0.02;
13289
13515
  var BLAST_COUPLING = 0.1;
13290
13516
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -13301,11 +13527,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13301
13527
  var SEVER_BAND = 0.1;
13302
13528
  var SEVER_ASPECT = 3;
13303
13529
  var SEVER_BALLISTIC = 2;
13530
+ var TEAR_JOULES = 150;
13531
+ var tears = 0;
13304
13532
  var SUPPORT_TOLERANCE = 0.05;
13305
13533
  var UNIT_FRAGMENT_FACTOR = 4;
13306
13534
  var BREACH_GRID_CAP = 4096;
13307
13535
  var CHIP_ENERGY = 200;
13308
13536
  var CHIP_SPLASH = 0.5;
13537
+ var BAR_BOW = 0.08;
13309
13538
  var CUT_SLICE = 1;
13310
13539
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13311
13540
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -13315,8 +13544,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13315
13544
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
13316
13545
  };
13317
13546
  var capacityScale = (p) => {
13318
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
13319
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
13547
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
13548
+ if (p.st.stripped) return residual;
13549
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
13550
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
13551
+ };
13552
+ var sectionLoss = (p) => {
13553
+ const cells = p.st.chipped;
13554
+ if (!cells?.length) return 0;
13555
+ const [u, v] = planeAxes(p.thin);
13556
+ const along = p.dims[u] >= p.dims[v] ? u : v;
13557
+ const across = along === u ? v : u;
13558
+ let worst = 0;
13559
+ for (const c of cells) {
13560
+ let gone = 0;
13561
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
13562
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
13563
+ }
13564
+ return Math.min(1, worst);
13320
13565
  };
13321
13566
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
13322
13567
  var clone = (v) => structuredClone(v);
@@ -14497,6 +14742,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14497
14742
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
14498
14743
  setStage(o, p, 1, {}, src);
14499
14744
  };
14745
+ const crater = (o, p, wp, normal, radius, src) => {
14746
+ markDamaged(p);
14747
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
14748
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
14749
+ setStage(o, p, 1, {}, src);
14750
+ };
14500
14751
  const insideOpening = (o, p, objectPoint) => {
14501
14752
  const op = p.st.opening;
14502
14753
  if (!op) return false;
@@ -14573,9 +14824,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14573
14824
  const lp = toLocal(p.pos, p.q, objectPoint);
14574
14825
  return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
14575
14826
  };
14576
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
14827
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
14577
14828
  var _a, _b;
14578
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14829
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14830
+ const rule = p.mat.hits?.[kind];
14831
+ const perKg = rule?.unitJoules ?? chipEnergy;
14832
+ const energy = hitEnergy - (rule?.floor ?? 0);
14833
+ if (perKg <= 0 || energy <= 0) return;
14579
14834
  const [u, v] = planeAxes(p.thin);
14580
14835
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14581
14836
  const grid = gridOf(p);
@@ -14589,7 +14844,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14589
14844
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
14590
14845
  const total = (worn[i] ?? 0) + energy * share;
14591
14846
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
14592
- if (total < chipEnergy * mass) {
14847
+ if (total < perKg * mass) {
14593
14848
  worn[i] = total;
14594
14849
  continue;
14595
14850
  }
@@ -14614,7 +14869,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14614
14869
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
14615
14870
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
14616
14871
  }
14872
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
14617
14873
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
14874
+ if (p.structural && !isLost(p)) {
14875
+ solveStructure(o);
14876
+ collapseCheck(o, src);
14877
+ }
14878
+ };
14879
+ const exposeRebar = (o, p, src) => {
14880
+ const r = p.spec.reinforcement;
14881
+ if (!r) return;
14882
+ const [u, v] = planeAxes(p.thin);
14883
+ const all = rebarBars(p.dims, p.thin, r.spacing);
14884
+ let bars;
14885
+ if (p.st.stripped) bars = all;
14886
+ else {
14887
+ bars = [];
14888
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
14889
+ const op = p.st.opening;
14890
+ if (op) {
14891
+ const lc = toLocal(p.pos, p.q, op.center);
14892
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
14893
+ }
14894
+ }
14895
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
14896
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
14897
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
14618
14898
  };
14619
14899
  const openBreach = (o, p, center, total, energy, dir, src) => {
14620
14900
  const [u, v] = planeAxes(p.thin);
@@ -14642,10 +14922,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14642
14922
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
14643
14923
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
14644
14924
  const reinforcement = p.spec.reinforcement;
14645
- if (reinforcement) {
14646
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
14647
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
14648
- }
14925
+ if (reinforcement) exposeRebar(o, p, src);
14649
14926
  if (p.structural) solveStructure(o);
14650
14927
  };
14651
14928
  const stripPart = (o, p, reinforcement, src) => {
@@ -14657,8 +14934,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14657
14934
  p.st.burning = false;
14658
14935
  p.rt.jet = false;
14659
14936
  markDamaged(p);
14660
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
14661
14937
  solveStructure(o);
14938
+ exposeRebar(o, p, src);
14662
14939
  collapseCheck(o, src);
14663
14940
  };
14664
14941
  const unitFragmentPlan = (p, cap) => {
@@ -14712,7 +14989,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14712
14989
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
14713
14990
  const hitLocal = toLocal(c, q, hitW);
14714
14991
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
14715
- const axis = style === "splinter" ? severAxis(p) : -1;
14992
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
14716
14993
  const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
14717
14994
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
14718
14995
  style,
@@ -14784,7 +15061,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14784
15061
  };
14785
15062
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
14786
15063
  const severAxis = (p) => {
14787
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
15064
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
14788
15065
  const d = p.dims;
14789
15066
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
14790
15067
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -14845,6 +15122,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14845
15122
  }
14846
15123
  }
14847
15124
  };
15125
+ const tearOf = (mat, kind) => {
15126
+ const h = mat.hits?.[kind];
15127
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
15128
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
15129
+ };
15130
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
15131
+ const { tear, joules } = tearOf(p.mat, kind);
15132
+ if (tear === "none") return;
15133
+ const shot = kind === "ballistic";
15134
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
15135
+ if (n === 0) return;
15136
+ const fibre = tear === "lumps";
15137
+ const { q } = partXf(o, p);
15138
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
15139
+ const fragments = [];
15140
+ for (let i = 0; i < n; i++) {
15141
+ const back = !exit || i % 3 === 2;
15142
+ const len = scale * (fibre ? 1.2 + 1.2 * rnd() : 3 + 4 * rnd()), w = scale * (0.8 + 0.8 * rnd()), t = scale * (fibre ? 0.7 + 0.6 * rnd() : 0.4 + 0.4 * rnd());
15143
+ const size = [w, w, w];
15144
+ size[along] = len;
15145
+ size[(along + 1) % 3] = w;
15146
+ size[(along + 2) % 3] = t;
15147
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
15148
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
15149
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
15150
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
15151
+ fragments.push({
15152
+ id: `${p.spec.id}#t${tears++}-${i}`,
15153
+ part: p.spec.id,
15154
+ material: p.spec.material,
15155
+ shape: fibre ? "chunk" : "splinter",
15156
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
15157
+ size,
15158
+ rotation: q,
15159
+ velocity: v,
15160
+ inner: true,
15161
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
15162
+ mass
15163
+ });
15164
+ }
15165
+ const patch = [scale * 4, scale * 4, scale * 4];
15166
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
15167
+ };
14848
15168
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
14849
15169
  const mat = p.mat;
14850
15170
  const piercing = round3.piercing ?? 1;
@@ -14873,6 +15193,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14873
15193
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
14874
15194
  }
14875
15195
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
15196
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
14876
15197
  const brittle = mat.ductility < 0.3;
14877
15198
  const ratio = Ep / Math.max(thr, 1e-6);
14878
15199
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -14906,8 +15227,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14906
15227
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
14907
15228
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
14908
15229
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
14909
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
14910
- chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
15230
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
15231
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15232
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
15233
+ chipUnits(o, p, "ballistic", entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
14911
15234
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
14912
15235
  };
14913
15236
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -14915,14 +15238,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14915
15238
  const thrMm = mat.penetration * p.thickness * 1e3;
14916
15239
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
14917
15240
  const Ec = E * fragile;
15241
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
15242
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
15243
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
14918
15244
  if (E > 0) {
14919
15245
  if (kind === "blunt") {
14920
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
15246
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(patch, 0.015), clamp2(E * 1e-4 * fold, 1e-3, sheet ? Math.min(0.06, Math.min(...p.dims) * 0.45) : 0.06), src);
14921
15247
  else if (mat.ductility < 0.3) {
14922
15248
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
14923
15249
  } else if (E >= thrMm * 0.2) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 6e-5, 1e-3, 0.04), src);
14924
15250
  } else if (mat.fracture === "tear") {
14925
15251
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
15252
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
15253
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
15254
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
14926
15255
  } else if (mat.ductility >= 0.5) {
14927
15256
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
14928
15257
  }
@@ -14935,7 +15264,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14935
15264
  const scale = mat.damageScale?.[kind] ?? 1;
14936
15265
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
14937
15266
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
14938
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
15267
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
15268
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
14939
15269
  return done;
14940
15270
  };
14941
15271
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -15514,6 +15844,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15514
15844
  if (p.rt.stressT <= 0) {
15515
15845
  p.rt.stressT = 0.5;
15516
15846
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
15847
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
15517
15848
  }
15518
15849
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
15519
15850
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -15730,6 +16061,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15730
16061
  const r = raycastInternal(origin, direction, maxDistance);
15731
16062
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
15732
16063
  },
16064
+ sweepBox(box3, direction, distance2, options2 = {}) {
16065
+ const d = vec.normalize(direction);
16066
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
16067
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
16068
+ let best = null;
16069
+ for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
16070
+ const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
16071
+ if (!r || best && r.distance >= best.distance) continue;
16072
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
16073
+ }
16074
+ return best;
16075
+ },
15733
16076
  shoot(origin, direction, energy, caliber, source, round3) {
15734
16077
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
15735
16078
  return withCollapses(run(() => {
@@ -18295,7 +18638,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18295
18638
  }
18296
18639
  return spec;
18297
18640
  };
18298
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
18641
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
18642
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
18643
+ var caged = (spec, part, rebar) => {
18644
+ if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
18645
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
18646
+ if (!size) return spec;
18647
+ const thin = Math.min(...size);
18648
+ return {
18649
+ ...spec,
18650
+ reinforcement: { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
18651
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
18652
+ };
18653
+ };
18654
+ var partSpecOf = (part, capacity, detailed, rebar) => {
18655
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
18656
+ const spec = boxSpec(part, capacity);
18657
+ return detailed ? caged(spec, part, rebar) : spec;
18658
+ };
18299
18659
  function toDestructibleSpec(structure, options = {}) {
18300
18660
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
18301
18661
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -18312,7 +18672,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18312
18672
  const sizing = { safety, redundancy, safetyOthers };
18313
18673
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
18314
18674
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
18315
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
18675
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
18316
18676
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
18317
18677
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
18318
18678
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -19192,7 +19552,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19192
19552
  if (!pc || pc.unit) return sp;
19193
19553
  if (pc.role !== "wall" && pc.role !== "partition") {
19194
19554
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
19195
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
19555
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
19556
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
19557
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
19558
+ }
19559
+ return out;
19196
19560
  }
19197
19561
  if (!pc.assembly) return sp;
19198
19562
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -19288,29 +19652,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19288
19652
  }
19289
19653
  return list2;
19290
19654
  }
19655
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
19656
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
19657
+ function hostOf(partId) {
19658
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
19659
+ const part = structure.byId.get(partId);
19660
+ if (!part) return null;
19661
+ for (const q of structure.parts) {
19662
+ if (q.id !== partId && same(q.aabb, part.aabb) && Math.abs(q.y1 - part.y0) < 0.01 && structure.meshesOf(q.id).some((it) => it.mesh)) return q.id;
19663
+ }
19664
+ return null;
19665
+ }
19666
+ const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
19291
19667
  function draw2(partId, change) {
19292
19668
  if (lod !== 0) return false;
19293
- const part = structure.byId.get(partId);
19294
- const spec = opts.spec()?.parts?.find((q) => q.id === partId);
19295
- if (!part || !spec || !spec.units) return false;
19296
- let view = views.get(partId);
19669
+ const hostId = hostOf(partId);
19670
+ if (!hostId || !specOf(partId)?.units) return false;
19671
+ let view = views.get(hostId);
19297
19672
  if (!view) {
19298
- const items = structure.meshesOf(partId).filter((it) => it.mesh);
19673
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
19299
19674
  if (!items.length) return false;
19300
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
19675
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19676
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
19301
19677
  }
19678
+ if (!view.chips.has(partId)) return false;
19302
19679
  if (change.opening) {
19303
- if (view.opening) return false;
19304
- view.opening = { center: local(change.opening.center), radius: change.opening.radius };
19680
+ if (view.openings.has(partId)) return false;
19681
+ view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19682
+ if (partId === hostId) view.opening = view.openings.get(partId);
19305
19683
  }
19306
- if (change.chip) view.chips.push(local(change.chip));
19307
- const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
19684
+ if (change.chip) view.chips.get(partId).push(local(change.chip));
19685
+ const list2 = [];
19686
+ for (const [id, chips] of view.chips) list2.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
19308
19687
  if (view.mesh) {
19309
19688
  view.mesh.removeFromParent();
19310
19689
  view.mesh.dispose();
19311
19690
  }
19312
19691
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19313
- mesh2.name = "breach:" + partId;
19692
+ mesh2.name = "breach:" + hostId;
19314
19693
  list2.forEach((u, i) => {
19315
19694
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19316
19695
  mesh2.setMatrixAt(i, tmp.m);
@@ -19325,7 +19704,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19325
19704
  if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19326
19705
  view.mesh = mesh2;
19327
19706
  view.units = list2.length;
19328
- views.set(partId, view);
19707
+ views.set(hostId, view);
19329
19708
  invalidate();
19330
19709
  invalidateShadow();
19331
19710
  return true;
@@ -21495,6 +21874,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21495
21874
  return {
21496
21875
  id: FLOORPLAN_AREA_ID,
21497
21876
  name: text.name,
21877
+ ground: { surface: "lawn" },
21498
21878
  group: text.group,
21499
21879
  get houses() {
21500
21880
  return placed;
@@ -22779,6 +23159,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22779
23159
 
22780
23160
  // src/furniture/spec.ts
22781
23161
  var WOOD_THIN = 0.035;
23162
+ var BOARD_TOP_THICK = 0.07;
23163
+ var BOARD_TOP_WIDTH = 0.5;
23164
+ var BOARD_WIDTH = 0.16;
22782
23165
  var SHELF_SPAN = 1.5;
22783
23166
  var SHELVING = /shelf|bookcase|rack/;
22784
23167
  function carcass(parts, drawn) {
@@ -22793,6 +23176,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22793
23176
  } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
22794
23177
  }
22795
23178
  }
23179
+ function splitTopsIntoBoards(drawn, materialOf2, carcassed) {
23180
+ const tops = /* @__PURE__ */ new Map();
23181
+ if (carcassed) return tops;
23182
+ for (let i = drawn.length - 1; i >= 0; i--) {
23183
+ const p = drawn[i];
23184
+ if (p.key !== "wood" || materialOf2.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
23185
+ const [sx, sy, sz] = p.shape.size;
23186
+ const across = sx < sz ? 0 : 2;
23187
+ const width = Math.min(sx, sz);
23188
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
23189
+ const count = Math.round(width / BOARD_WIDTH);
23190
+ const boards = [];
23191
+ for (let b = 0; b < count; b++) {
23192
+ const size = [sx, sy, sz];
23193
+ size[across] = width / count;
23194
+ const position = [...p.position];
23195
+ position[across] += (b + 0.5) * size[across] - width / 2;
23196
+ const min = [...p.min];
23197
+ const max = [...p.max];
23198
+ min[across] = position[across] - size[across] / 2;
23199
+ max[across] = position[across] + size[across] / 2;
23200
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min, max, color: shadeBoard(p.color, b) });
23201
+ }
23202
+ drawn.splice(i, 1, ...boards);
23203
+ tops.set(p.id, boards);
23204
+ }
23205
+ return tops;
23206
+ }
23207
+ function shadeBoard(color2, board) {
23208
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
23209
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
23210
+ return channel(16) | channel(8) | channel(0);
23211
+ }
22796
23212
  var FLOOR_TOUCH = 5e-3;
22797
23213
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
22798
23214
  var BASE_MATERIAL = {
@@ -22870,10 +23286,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22870
23286
  };
22871
23287
  run(A);
22872
23288
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
23289
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf2, Boolean(opts.carcass));
22873
23290
  const parts = drawn.map((p) => {
22874
23291
  const thin = Math.min(...p.shape.kind === "box" ? p.shape.size : p.shape.kind === "cylinder" ? [p.shape.radius * 2, p.shape.height] : [p.shape.radius * 2]);
22875
23292
  let material = materialOf2[p.key] ?? "plastic";
22876
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
23293
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
23294
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
22877
23295
  const part = { id: p.id, material, shape: p.shape, position: p.position };
22878
23296
  if (p.rotation) part.rotation = p.rotation;
22879
23297
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -22908,6 +23326,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22908
23326
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
22909
23327
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
22910
23328
  }
23329
+ for (const boards of boardsOf.values()) {
23330
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
23331
+ const mid = boards[boards.length >> 1];
23332
+ const min = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
23333
+ const max = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
23334
+ if ((max[0] - min[0]) * (max[1] - min[1]) >= 0.6 * footprint && mid.max[1] >= 0.2) top = { part: mid.id, y: mid.max[1], parts: boards.map((b) => b.id) };
23335
+ }
22911
23336
  if (opts.carcass) carcass(parts, byId);
22912
23337
  const spec = { name, parts, joints, tags: ["furniture"] };
22913
23338
  const colors = new Map(drawn.map((p) => [p.id, p.color]));