@adatechnology/game-test-room 0.16.0 → 0.17.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.
@@ -855,7 +855,9 @@ var GameTestRoom = (() => {
855
855
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
856
856
  band.c[L] = (lo + hi) / 2;
857
857
  band.s[L] = Math.max(hi - lo, 0.01);
858
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
858
+ if (inp.style === "snap") {
859
+ 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] });
860
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
859
861
  return out;
860
862
  };
861
863
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -886,6 +888,7 @@ var GameTestRoom = (() => {
886
888
  const t = sorted[0];
887
889
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
888
890
  if (style === "snap") {
891
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
889
892
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd2() * 3));
890
893
  for (const b of bsp(root, n2, [0, 1, 2], rnd2)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
891
894
  return out;
@@ -1059,6 +1062,27 @@ var GameTestRoom = (() => {
1059
1062
  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]]));
1060
1063
  return out;
1061
1064
  };
1065
+ var clipBarsToBox = (bars, u, v, center, size) => {
1066
+ const out = [];
1067
+ for (const [a, b] of bars) {
1068
+ const axis2 = a[u] !== b[u] ? u : v;
1069
+ const other = axis2 === u ? v : u;
1070
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
1071
+ const lo = Math.max(Math.min(a[axis2], b[axis2]), center[axis2] - size[axis2] / 2), hi = Math.min(Math.max(a[axis2], b[axis2]), center[axis2] + size[axis2] / 2);
1072
+ if (hi - lo < 1e-6) continue;
1073
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
1074
+ p[axis2] = lo;
1075
+ q[axis2] = hi;
1076
+ out.push([p, q]);
1077
+ }
1078
+ return out;
1079
+ };
1080
+ var bendBar = ([a, b], thin, bow, i) => {
1081
+ const length3 = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
1082
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
1083
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length3;
1084
+ return [[a, m], [m, b]];
1085
+ };
1062
1086
  var clipBarsToDisc = (bars, u, v, center, radius) => {
1063
1087
  const out = [];
1064
1088
  for (const [a, b] of bars) {
@@ -1969,14 +1993,21 @@ var GameTestRoom = (() => {
1969
1993
  smoke: "grey"
1970
1994
  },
1971
1995
  visual: {
1972
- charColor: 1709072
1996
+ charColor: 1709072,
1997
+ innerColor: 14730131
1973
1998
  },
1974
1999
  tags: [
1975
2000
  "wood",
1976
2001
  "organic",
1977
2002
  "flammable"
1978
2003
  ],
1979
- compressive: 1e5
2004
+ compressive: 1e5,
2005
+ hits: {
2006
+ blunt: {
2007
+ tear: "splinters",
2008
+ tearJoules: 60
2009
+ }
2010
+ }
1980
2011
  },
1981
2012
  {
1982
2013
  id: "wood_hard",
@@ -2006,14 +2037,21 @@ var GameTestRoom = (() => {
2006
2037
  smoke: "grey"
2007
2038
  },
2008
2039
  visual: {
2009
- charColor: 1709072
2040
+ charColor: 1709072,
2041
+ innerColor: 13213802
2010
2042
  },
2011
2043
  tags: [
2012
2044
  "wood",
2013
2045
  "organic",
2014
2046
  "flammable"
2015
2047
  ],
2016
- compressive: 15e4
2048
+ compressive: 15e4,
2049
+ hits: {
2050
+ blunt: {
2051
+ tear: "splinters",
2052
+ tearJoules: 150
2053
+ }
2054
+ }
2017
2055
  },
2018
2056
  {
2019
2057
  id: "plywood",
@@ -2043,14 +2081,24 @@ var GameTestRoom = (() => {
2043
2081
  smoke: "grey"
2044
2082
  },
2045
2083
  visual: {
2046
- charColor: 1709072
2084
+ charColor: 1709072,
2085
+ innerColor: 14862234
2047
2086
  },
2048
2087
  tags: [
2049
2088
  "wood",
2050
2089
  "organic",
2051
2090
  "flammable"
2052
2091
  ],
2053
- compressive: 6e4
2092
+ compressive: 6e4,
2093
+ hits: {
2094
+ ballistic: {
2095
+ tear: "splinters"
2096
+ },
2097
+ blunt: {
2098
+ tear: "splinters",
2099
+ tearJoules: 80
2100
+ }
2101
+ }
2054
2102
  },
2055
2103
  {
2056
2104
  id: "mdf",
@@ -2077,14 +2125,25 @@ var GameTestRoom = (() => {
2077
2125
  smoke: "grey"
2078
2126
  },
2079
2127
  visual: {
2080
- charColor: 1709072
2128
+ charColor: 1709072,
2129
+ innerColor: 12096362
2081
2130
  },
2082
2131
  tags: [
2083
2132
  "wood",
2084
2133
  "organic",
2085
2134
  "flammable"
2086
2135
  ],
2087
- compressive: 3e4
2136
+ compressive: 3e4,
2137
+ hits: {
2138
+ ballistic: {
2139
+ tear: "lumps",
2140
+ tearJoules: 120
2141
+ },
2142
+ blunt: {
2143
+ tear: "lumps",
2144
+ tearJoules: 20
2145
+ }
2146
+ }
2088
2147
  },
2089
2148
  {
2090
2149
  id: "glass",
@@ -2198,6 +2257,17 @@ var GameTestRoom = (() => {
2198
2257
  damageScale: {
2199
2258
  explosive: 1.2
2200
2259
  },
2260
+ hits: {
2261
+ ballistic: {
2262
+ unitJoules: 500,
2263
+ floor: 900,
2264
+ crater: true
2265
+ },
2266
+ blunt: {
2267
+ unitJoules: 1500,
2268
+ floor: 60
2269
+ }
2270
+ },
2201
2271
  hitDebris: "chips",
2202
2272
  breakDebris: "dust",
2203
2273
  thermal: {
@@ -2566,7 +2636,8 @@ var GameTestRoom = (() => {
2566
2636
  smoke: "white"
2567
2637
  },
2568
2638
  visual: {
2569
- charColor: 1709072
2639
+ charColor: 1709072,
2640
+ innerColor: 14207126
2570
2641
  },
2571
2642
  tags: [
2572
2643
  "plant",
@@ -2574,7 +2645,12 @@ var GameTestRoom = (() => {
2574
2645
  "organic",
2575
2646
  "flammable"
2576
2647
  ],
2577
- compressive: 6e4
2648
+ compressive: 6e4,
2649
+ hits: {
2650
+ blunt: {
2651
+ tear: "none"
2652
+ }
2653
+ }
2578
2654
  },
2579
2655
  {
2580
2656
  id: "cactus",
@@ -2689,6 +2765,130 @@ var GameTestRoom = (() => {
2689
2765
  "masonry"
2690
2766
  ],
2691
2767
  compressive: 2e4
2768
+ },
2769
+ {
2770
+ id: "asphalt",
2771
+ density: 2350,
2772
+ hpPerKg: 10,
2773
+ penetration: 14,
2774
+ penetrationClass: "concrete",
2775
+ penetrationScale: 0.8,
2776
+ ductility: 0.1,
2777
+ fracture: "crumble",
2778
+ fragmentSize: [
2779
+ 0.03,
2780
+ 0.15
2781
+ ],
2782
+ hitDebris: "chips",
2783
+ breakDebris: "dust",
2784
+ thermal: {
2785
+ conductivity: 0.1,
2786
+ heatCapacity: 2,
2787
+ ignitionC: 400,
2788
+ burnRate: 2e-3,
2789
+ smoke: "black"
2790
+ },
2791
+ tags: [
2792
+ "masonry",
2793
+ "brittle"
2794
+ ],
2795
+ compressive: 3e4
2796
+ },
2797
+ {
2798
+ id: "sand",
2799
+ density: 1600,
2800
+ hpPerKg: 2,
2801
+ penetration: 6,
2802
+ ductility: 0.4,
2803
+ fracture: "crumble",
2804
+ fragmentSize: [
2805
+ 0.01,
2806
+ 0.04
2807
+ ],
2808
+ hitDebris: "dust",
2809
+ breakDebris: "dust",
2810
+ thermal: {
2811
+ conductivity: 0.1,
2812
+ heatCapacity: 2
2813
+ },
2814
+ tags: [
2815
+ "soil"
2816
+ ],
2817
+ compressive: 1e4
2818
+ },
2819
+ {
2820
+ id: "gravel",
2821
+ density: 1800,
2822
+ hpPerKg: 3,
2823
+ penetration: 8,
2824
+ ductility: 0.3,
2825
+ fracture: "crumble",
2826
+ fragmentSize: [
2827
+ 0.02,
2828
+ 0.06
2829
+ ],
2830
+ hitDebris: "chips",
2831
+ breakDebris: "dust",
2832
+ thermal: {
2833
+ conductivity: 0.1,
2834
+ heatCapacity: 2
2835
+ },
2836
+ tags: [
2837
+ "soil",
2838
+ "masonry"
2839
+ ],
2840
+ compressive: 2e4
2841
+ },
2842
+ {
2843
+ id: "turf",
2844
+ density: 900,
2845
+ hpPerKg: 3,
2846
+ penetration: 4,
2847
+ ductility: 0.5,
2848
+ fracture: "crumble",
2849
+ fragmentSize: [
2850
+ 0.03,
2851
+ 0.1
2852
+ ],
2853
+ hitDebris: "dust",
2854
+ breakDebris: "dust",
2855
+ thermal: {
2856
+ conductivity: 0.1,
2857
+ heatCapacity: 1.5,
2858
+ ignitionC: 250,
2859
+ burnRate: 0.03,
2860
+ smoke: "white"
2861
+ },
2862
+ tags: [
2863
+ "soil",
2864
+ "organic"
2865
+ ],
2866
+ compressive: 15e3
2867
+ },
2868
+ {
2869
+ id: "mortar",
2870
+ density: 1900,
2871
+ hpPerKg: 10,
2872
+ penetration: 12,
2873
+ penetrationClass: "concrete",
2874
+ penetrationScale: 1.2,
2875
+ ductility: 0.03,
2876
+ fracture: "crumble",
2877
+ fragmentSize: [
2878
+ 0.02,
2879
+ 0.08
2880
+ ],
2881
+ hitDebris: "chips",
2882
+ breakDebris: "dust",
2883
+ thermal: {
2884
+ conductivity: 0.15,
2885
+ heatCapacity: 2.5
2886
+ },
2887
+ tags: [
2888
+ "masonry",
2889
+ "brittle"
2890
+ ],
2891
+ compressive: 1e5
2692
2892
  }
2693
2893
  ];
2694
2894
  var material_hints_default = {
@@ -2867,6 +3067,32 @@ var GameTestRoom = (() => {
2867
3067
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
2868
3068
  };
2869
3069
  var THIN_SHEET_DISHING = 5e-3;
3070
+ var SWEEP_BACK = 0.01;
3071
+ function sweepOrigins(box2, d, spacing) {
3072
+ const origins = [];
3073
+ const line = (from, to) => {
3074
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
3075
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
3076
+ };
3077
+ for (const axis2 of [0, 1, 2]) {
3078
+ if (Math.abs(d[axis2]) < 1e-3) continue;
3079
+ const lead = d[axis2] > 0 ? box2.max[axis2] - SWEEP_BACK : box2.min[axis2] + SWEEP_BACK;
3080
+ const u = (axis2 + 1) % 3;
3081
+ const v = (axis2 + 2) % 3;
3082
+ for (const a of line(box2.min[u], box2.max[u])) {
3083
+ for (const b of line(box2.min[v], box2.max[v])) {
3084
+ const point = [0, 0, 0];
3085
+ point[axis2] = lead;
3086
+ point[u] = a;
3087
+ point[v] = b;
3088
+ origins.push(point);
3089
+ }
3090
+ }
3091
+ }
3092
+ return origins;
3093
+ }
3094
+ var THIN_SHEET_CRUSH = 5e-4;
3095
+ var THIN_SHEET_CONTACT = 0.025;
2870
3096
  var DMG_PER_J = 0.02;
2871
3097
  var BLAST_COUPLING = 0.1;
2872
3098
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2883,11 +3109,14 @@ var GameTestRoom = (() => {
2883
3109
  var SEVER_BAND = 0.1;
2884
3110
  var SEVER_ASPECT = 3;
2885
3111
  var SEVER_BALLISTIC = 2;
3112
+ var TEAR_JOULES = 150;
3113
+ var tears = 0;
2886
3114
  var SUPPORT_TOLERANCE = 0.05;
2887
3115
  var UNIT_FRAGMENT_FACTOR = 4;
2888
3116
  var BREACH_GRID_CAP = 4096;
2889
3117
  var CHIP_ENERGY = 200;
2890
3118
  var CHIP_SPLASH = 0.5;
3119
+ var BAR_BOW = 0.08;
2891
3120
  var CUT_SLICE = 1;
2892
3121
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2893
3122
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2897,8 +3126,24 @@ var GameTestRoom = (() => {
2897
3126
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2898
3127
  };
2899
3128
  var capacityScale = (p) => {
2900
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2901
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
3129
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
3130
+ if (p.st.stripped) return residual;
3131
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
3132
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
3133
+ };
3134
+ var sectionLoss = (p) => {
3135
+ const cells = p.st.chipped;
3136
+ if (!cells?.length) return 0;
3137
+ const [u, v] = planeAxes(p.thin);
3138
+ const along = p.dims[u] >= p.dims[v] ? u : v;
3139
+ const across = along === u ? v : u;
3140
+ let worst = 0;
3141
+ for (const c of cells) {
3142
+ let gone = 0;
3143
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
3144
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
3145
+ }
3146
+ return Math.min(1, worst);
2902
3147
  };
2903
3148
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2904
3149
  var clone = (v) => structuredClone(v);
@@ -4079,6 +4324,12 @@ var GameTestRoom = (() => {
4079
4324
  debris2(o, p, layer2.mat.hitDebris, wp, normal, 5, 2, src);
4080
4325
  setStage(o, p, 1, {}, src);
4081
4326
  };
4327
+ const crater = (o, p, wp, normal, radius, src) => {
4328
+ markDamaged(p);
4329
+ emit2({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
4330
+ debris2(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
4331
+ setStage(o, p, 1, {}, src);
4332
+ };
4082
4333
  const insideOpening = (o, p, objectPoint) => {
4083
4334
  const op = p.st.opening;
4084
4335
  if (!op) return false;
@@ -4155,9 +4406,13 @@ var GameTestRoom = (() => {
4155
4406
  const lp = toLocal(p.pos, p.q, objectPoint);
4156
4407
  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);
4157
4408
  };
4158
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4409
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4159
4410
  var _a, _b;
4160
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4411
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4412
+ const rule = p.mat.hits?.[kind];
4413
+ const perKg = rule?.unitJoules ?? chipEnergy;
4414
+ const energy = hitEnergy - (rule?.floor ?? 0);
4415
+ if (perKg <= 0 || energy <= 0) return;
4161
4416
  const [u, v] = planeAxes(p.thin);
4162
4417
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4163
4418
  const grid = gridOf(p);
@@ -4171,7 +4426,7 @@ var GameTestRoom = (() => {
4171
4426
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4172
4427
  const total = (worn[i] ?? 0) + energy * share;
4173
4428
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4174
- if (total < chipEnergy * mass) {
4429
+ if (total < perKg * mass) {
4175
4430
  worn[i] = total;
4176
4431
  continue;
4177
4432
  }
@@ -4196,7 +4451,32 @@ var GameTestRoom = (() => {
4196
4451
  emit2({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4197
4452
  debris2(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4198
4453
  }
4454
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
4199
4455
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4456
+ if (p.structural && !isLost(p)) {
4457
+ solveStructure(o);
4458
+ collapseCheck(o, src);
4459
+ }
4460
+ };
4461
+ const exposeRebar = (o, p, src) => {
4462
+ const r = p.spec.reinforcement;
4463
+ if (!r) return;
4464
+ const [u, v] = planeAxes(p.thin);
4465
+ const all = rebarBars(p.dims, p.thin, r.spacing);
4466
+ let bars;
4467
+ if (p.st.stripped) bars = all;
4468
+ else {
4469
+ bars = [];
4470
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
4471
+ const op = p.st.opening;
4472
+ if (op) {
4473
+ const lc = toLocal(p.pos, p.q, op.center);
4474
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
4475
+ }
4476
+ }
4477
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
4478
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
4479
+ emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4200
4480
  };
4201
4481
  const openBreach = (o, p, center, total, energy, dir, src) => {
4202
4482
  const [u, v] = planeAxes(p.thin);
@@ -4224,10 +4504,7 @@ var GameTestRoom = (() => {
4224
4504
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4225
4505
  debris2(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4226
4506
  const reinforcement = p.spec.reinforcement;
4227
- if (reinforcement) {
4228
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
4229
- if (bars.length > 0) emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars });
4230
- }
4507
+ if (reinforcement) exposeRebar(o, p, src);
4231
4508
  if (p.structural) solveStructure(o);
4232
4509
  };
4233
4510
  const stripPart = (o, p, reinforcement, src) => {
@@ -4239,8 +4516,8 @@ var GameTestRoom = (() => {
4239
4516
  p.st.burning = false;
4240
4517
  p.rt.jet = false;
4241
4518
  markDamaged(p);
4242
- emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4243
4519
  solveStructure(o);
4520
+ exposeRebar(o, p, src);
4244
4521
  collapseCheck(o, src);
4245
4522
  };
4246
4523
  const unitFragmentPlan = (p, cap) => {
@@ -4294,7 +4571,7 @@ var GameTestRoom = (() => {
4294
4571
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4295
4572
  const hitLocal = toLocal(c, q, hitW);
4296
4573
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4297
- const axis2 = style === "splinter" ? severAxis(p) : -1;
4574
+ const axis2 = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4298
4575
  const cutAt = axis2 < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis2] : void 0);
4299
4576
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4300
4577
  style,
@@ -4366,7 +4643,7 @@ var GameTestRoom = (() => {
4366
4643
  };
4367
4644
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4368
4645
  const severAxis = (p) => {
4369
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4646
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
4370
4647
  const d = p.dims;
4371
4648
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4372
4649
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -4427,6 +4704,49 @@ var GameTestRoom = (() => {
4427
4704
  }
4428
4705
  }
4429
4706
  };
4707
+ const tearOf = (mat, kind) => {
4708
+ const h = mat.hits?.[kind];
4709
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
4710
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
4711
+ };
4712
+ const tearAt = (o, p, kind, entry, exit, d, E, scale2, src) => {
4713
+ const { tear, joules } = tearOf(p.mat, kind);
4714
+ if (tear === "none") return;
4715
+ const shot = kind === "ballistic";
4716
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
4717
+ if (n === 0) return;
4718
+ const fibre = tear === "lumps";
4719
+ const { q } = partXf(o, p);
4720
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
4721
+ const fragments = [];
4722
+ for (let i = 0; i < n; i++) {
4723
+ const back = !exit || i % 3 === 2;
4724
+ const len = scale2 * (fibre ? 1.2 + 1.2 * rnd2() : 3 + 4 * rnd2()), w = scale2 * (0.8 + 0.8 * rnd2()), t = scale2 * (fibre ? 0.7 + 0.6 * rnd2() : 0.4 + 0.4 * rnd2());
4725
+ const size = [w, w, w];
4726
+ size[along] = len;
4727
+ size[(along + 1) % 3] = w;
4728
+ size[(along + 2) % 3] = t;
4729
+ const spread3 = [(rnd2() - 0.5) * 0.8, (rnd2() - 0.2) * 0.8, (rnd2() - 0.5) * 0.8];
4730
+ const speed = shot ? back ? 1.5 + 2 * rnd2() : 2 + 4 * rnd2() : 0.8 + 1.2 * rnd2();
4731
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
4732
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
4733
+ fragments.push({
4734
+ id: `${p.spec.id}#t${tears++}-${i}`,
4735
+ part: p.spec.id,
4736
+ material: p.spec.material,
4737
+ shape: fibre ? "chunk" : "splinter",
4738
+ center: back || !exit ? vec.madd(entry, d, -0.01) : vec.madd(exit, d, 0.01),
4739
+ size,
4740
+ rotation: q,
4741
+ velocity: v,
4742
+ inner: true,
4743
+ angularVelocity: [(rnd2() - 0.5) * 30, (rnd2() - 0.5) * 30, (rnd2() - 0.5) * 30],
4744
+ mass
4745
+ });
4746
+ }
4747
+ const patch = [scale2 * 4, scale2 * 4, scale2 * 4];
4748
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4749
+ };
4430
4750
  const ballistic2 = (o, p, entry, n, d, E, caliber, pathLen, src, round2 = {}) => {
4431
4751
  const mat = p.mat;
4432
4752
  const piercing = round2.piercing ?? 1;
@@ -4455,6 +4775,7 @@ var GameTestRoom = (() => {
4455
4775
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
4456
4776
  }
4457
4777
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
4778
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
4458
4779
  const brittle = mat.ductility < 0.3;
4459
4780
  const ratio = Ep / Math.max(thr, 1e-6);
4460
4781
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -4488,8 +4809,10 @@ var GameTestRoom = (() => {
4488
4809
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4489
4810
  debris2(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4490
4811
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4491
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale2, "ballistic", entry, d, E * 0.1, 12, src);
4492
- chipUnits(o, p, entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4812
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4813
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale2, "ballistic", entry, d, absorbed * 0.1, 12, src);
4814
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4815
+ chipUnits(o, p, "ballistic", entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4493
4816
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4494
4817
  };
4495
4818
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4497,14 +4820,20 @@ var GameTestRoom = (() => {
4497
4820
  const thrMm = mat.penetration * p.thickness * 1e3;
4498
4821
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
4499
4822
  const Ec = E * fragile;
4823
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
4824
+ const fold2 = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
4825
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
4500
4826
  if (E > 0) {
4501
4827
  if (kind === "blunt") {
4502
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
4828
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(patch, 0.015), clamp2(E * 1e-4 * fold2, 1e-3, sheet ? Math.min(0.06, Math.min(...p.dims) * 0.45) : 0.06), src);
4503
4829
  else if (mat.ductility < 0.3) {
4504
4830
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
4505
4831
  } 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);
4506
4832
  } else if (mat.fracture === "tear") {
4507
4833
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
4834
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
4835
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
4836
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold2, 5e-4, 0.02), src);
4508
4837
  } else if (mat.ductility >= 0.5) {
4509
4838
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
4510
4839
  }
@@ -4517,7 +4846,8 @@ var GameTestRoom = (() => {
4517
4846
  const scale2 = mat.damageScale?.[kind] ?? 1;
4518
4847
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale2 * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4519
4848
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
4520
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
4849
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
4850
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
4521
4851
  return done;
4522
4852
  };
4523
4853
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -5096,6 +5426,7 @@ var GameTestRoom = (() => {
5096
5426
  if (p.rt.stressT <= 0) {
5097
5427
  p.rt.stressT = 0.5;
5098
5428
  emit2({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
5429
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
5099
5430
  }
5100
5431
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
5101
5432
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -5312,6 +5643,18 @@ var GameTestRoom = (() => {
5312
5643
  const r = raycastInternal(origin, direction, maxDistance);
5313
5644
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
5314
5645
  },
5646
+ sweepBox(box2, direction, distance4, options2 = {}) {
5647
+ const d = vec.normalize(direction);
5648
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5649
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5650
+ let best = null;
5651
+ for (const origin of sweepOrigins(box2, d, options2.spacing ?? 0.2)) {
5652
+ const r = raycastInternal(origin, d, distance4 + SWEEP_BACK, ignore);
5653
+ if (!r || best && r.distance >= best.distance) continue;
5654
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
5655
+ }
5656
+ return best;
5657
+ },
5315
5658
  shoot(origin, direction, energy, caliber, source, round2) {
5316
5659
  const extras = typeof round2 === "number" ? { piercing: round2 } : round2 ?? {};
5317
5660
  return withCollapses(run(() => {
@@ -5614,7 +5957,12 @@ var GameTestRoom = (() => {
5614
5957
  cactus: "cactus",
5615
5958
  flesh_fruit: "fruit",
5616
5959
  wax: "wax",
5617
- soil: "soil"
5960
+ soil: "soil",
5961
+ asphalt: "asphalt",
5962
+ sand: "sand",
5963
+ gravel: "gravel",
5964
+ turf: "turf",
5965
+ mortar: "mortar"
5618
5966
  },
5619
5967
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5620
5968
  jointKinds: {
@@ -5705,7 +6053,12 @@ var GameTestRoom = (() => {
5705
6053
  cactus: "cacto",
5706
6054
  flesh_fruit: "fruta",
5707
6055
  wax: "cera",
5708
- soil: "terra"
6056
+ soil: "terra",
6057
+ asphalt: "asfalto",
6058
+ sand: "areia",
6059
+ gravel: "brita",
6060
+ turf: "grama",
6061
+ mortar: "argamassa"
5709
6062
  },
5710
6063
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5711
6064
  jointKinds: {
@@ -14499,77 +14852,6 @@ var GameTestRoom = (() => {
14499
14852
  const key = event.stage !== void 0 ? String(event.stage) : event.material;
14500
14853
  return key === void 0 ? void 0 : entry[key];
14501
14854
  }
14502
- var DEFAULT_SIZE2 = 60;
14503
- var DEFAULT_SEGMENTS = 180;
14504
- var MAX_DEPTH = 3;
14505
- var RIM_REACH = 1.45;
14506
- var REACH_FACTOR = 1.5;
14507
- function createGroundField(options = {}) {
14508
- const size = options.size ?? DEFAULT_SIZE2;
14509
- const segments = options.segments ?? DEFAULT_SEGMENTS;
14510
- const stride = segments + 1;
14511
- const cellSize = size / segments;
14512
- const origin = -size / 2;
14513
- const heights = new Float32Array(stride * stride);
14514
- let version = 0;
14515
- function heightAt(x, z) {
14516
- const fx = (x - origin) / cellSize;
14517
- const fz = (z - origin) / cellSize;
14518
- if (fx < 0 || fz < 0 || fx >= segments || fz >= segments) return 0;
14519
- const column = fx | 0;
14520
- const row = fz | 0;
14521
- const tx = fx - column;
14522
- const tz = fz - row;
14523
- const index = row * stride + column;
14524
- return (heights[index] * (1 - tx) + heights[index + 1] * tx) * (1 - tz) + (heights[index + stride] * (1 - tx) + heights[index + stride + 1] * tx) * tz;
14525
- }
14526
- function dent(random, x, z, radius, depth) {
14527
- const reach = radius * REACH_FACTOR;
14528
- const column0 = Math.max(0, Math.floor((x - reach - origin) / cellSize));
14529
- const column1 = Math.min(segments, Math.ceil((x + reach - origin) / cellSize));
14530
- const row0 = Math.max(0, Math.floor((z - reach - origin) / cellSize));
14531
- const row1 = Math.min(segments, Math.ceil((z + reach - origin) / cellSize));
14532
- const phaseA = random.next() * 6;
14533
- const phaseB = random.next() * 6;
14534
- for (let row = row0; row <= row1; row++) {
14535
- for (let column = column0; column <= column1; column++) {
14536
- const px = origin + column * cellSize;
14537
- const pz = origin + row * cellSize;
14538
- const distance4 = Math.hypot(px - x, pz - z);
14539
- const angle = Math.atan2(pz - z, px - x);
14540
- const q = distance4 / (radius * (1 + 0.13 * Math.sin(angle * 5 + phaseA) + 0.07 * Math.sin(angle * 11 + phaseB)));
14541
- let change = 0;
14542
- if (q < 1) change = -depth * (1 - q * q) * (0.85 + random.next() * 0.3);
14543
- else if (q < RIM_REACH) {
14544
- const k = (q - 1) / 0.45;
14545
- change = depth * 0.3 * Math.sin(k * Math.PI) * (1 - k * 0.3) * (0.6 + random.next() * 0.8);
14546
- }
14547
- if (!change) continue;
14548
- const index = row * stride + column;
14549
- heights[index] = Math.max(-MAX_DEPTH, heights[index] + change);
14550
- }
14551
- }
14552
- version++;
14553
- }
14554
- function reset() {
14555
- heights.fill(0);
14556
- version++;
14557
- }
14558
- return {
14559
- size,
14560
- segments,
14561
- stride,
14562
- cellSize,
14563
- origin,
14564
- heights,
14565
- get version() {
14566
- return version;
14567
- },
14568
- heightAt,
14569
- dent,
14570
- reset
14571
- };
14572
- }
14573
14855
 
14574
14856
  // ../game-vfx-three/dist/index.js
14575
14857
  var GLOW_VERTEX = `attribute vec4 aP;attribute vec4 aC;varying vec2 vU;varying vec4 vC;
@@ -15854,40 +16136,47 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15854
16136
  };
15855
16137
  }
15856
16138
  var LIGHT_BUDGET = EVENT_BUDGET.blastLights + EVENT_BUDGET.fireLights;
15857
- var DEFAULT_TEXTURE_METRES = 17;
15858
- function createGroundPatch(THREE, options) {
15859
- const field = options.field;
15860
- const metres = options.textureMetres ?? DEFAULT_TEXTURE_METRES;
15861
- const geometry = new THREE.PlaneGeometry(field.size, field.size, field.segments, field.segments).rotateX(-Math.PI / 2);
15862
- const position = geometry.attributes.position;
15863
- const uv = geometry.attributes.uv;
15864
- for (let index = 0; index < position.count; index++) uv.setXY(index, position.getX(index) / metres, position.getZ(index) / metres);
15865
- const material = options.material ?? new THREE.MeshStandardMaterial({ color: 2829359, roughness: 0.93, flatShading: true });
15866
- const mesh2 = new THREE.Mesh(geometry, material);
15867
- mesh2.receiveShadow = true;
15868
- let version = -1;
15869
- function sync() {
15870
- if (field.version === version) return false;
15871
- version = field.version;
15872
- for (let index = 0; index < position.count; index++) position.setY(index, field.heights[index]);
15873
- position.needsUpdate = true;
15874
- geometry.computeBoundingSphere();
15875
- return true;
15876
- }
15877
- return {
15878
- mesh: mesh2,
15879
- sync,
15880
- dispose() {
15881
- geometry.dispose();
15882
- if (!options.material) material.dispose();
15883
- }
15884
- };
15885
- }
15886
16139
 
15887
16140
  // ../game-destruction-three/dist/index.js
15888
16141
  var __defProp3 = Object.defineProperty;
15889
16142
  var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
15890
16143
  var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
16144
+ var SLICE_LENGTH = 0.12;
16145
+ var MAX_SLICES = 12;
16146
+ var SNUG_RATIO = 1.1;
16147
+ function pushFittedBoxes(local2, matrix, out) {
16148
+ const e = matrix.elements;
16149
+ const sizes = [0, 1, 2].map((k) => k === 0 ? local2.max.x - local2.min.x : k === 1 ? local2.max.y - local2.min.y : local2.max.z - local2.min.z);
16150
+ const lengths = [0, 1, 2].map((k) => sizes[k] * Math.hypot(e[k * 4], e[k * 4 + 1], e[k * 4 + 2]));
16151
+ const longest = lengths.indexOf(Math.max(...lengths));
16152
+ const whole = worldBoxOf(local2, matrix, longest, 0, 1);
16153
+ const span = (whole[3] - whole[0]) * (whole[4] - whole[1]) * (whole[5] - whole[2]);
16154
+ const volume = lengths[0] * lengths[1] * lengths[2];
16155
+ const slices = span <= volume * SNUG_RATIO ? 1 : Math.min(MAX_SLICES, Math.ceil(lengths[longest] / SLICE_LENGTH));
16156
+ if (slices <= 1) {
16157
+ out.push(...whole);
16158
+ return;
16159
+ }
16160
+ for (let i = 0; i < slices; i++) out.push(...worldBoxOf(local2, matrix, longest, i / slices, (i + 1) / slices));
16161
+ }
16162
+ function worldBoxOf(local2, matrix, axis2, from, to) {
16163
+ const e = matrix.elements;
16164
+ const low = [local2.min.x, local2.min.y, local2.min.z];
16165
+ const high = [local2.max.x, local2.max.y, local2.max.z];
16166
+ const start = low[axis2];
16167
+ const size = high[axis2] - start;
16168
+ low[axis2] = start + size * from;
16169
+ high[axis2] = start + size * to;
16170
+ const box2 = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
16171
+ for (const x of [low[0], high[0]]) for (const y of [low[1], high[1]]) for (const z of [low[2], high[2]]) {
16172
+ for (let k = 0; k < 3; k++) {
16173
+ const v = e[k] * x + e[k + 4] * y + e[k + 8] * z + e[k + 12];
16174
+ box2[k] = Math.min(box2[k], v);
16175
+ box2[k + 3] = Math.max(box2[k + 3], v);
16176
+ }
16177
+ }
16178
+ return box2;
16179
+ }
15891
16180
  function createRng3(seed) {
15892
16181
  let a = seed >>> 0;
15893
16182
  return () => {
@@ -15926,12 +16215,19 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15926
16215
  var SHEET_MAX = 4e-3;
15927
16216
  var SHEET_AIR = 2.5;
15928
16217
  var SHEET_SWAY = 3;
16218
+ var BIG_LENGTH = 0.2;
16219
+ var BIG_WIDTH = 0.03;
16220
+ var isBig = (size) => {
16221
+ const [, middle, longest] = [...size].sort((a, b) => a - b);
16222
+ return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
16223
+ };
15929
16224
  var FragmentSystem = class {
15930
16225
  constructor(THREE, o) {
15931
16226
  __publicField2(this, "THREE", THREE);
15932
16227
  __publicField2(this, "o", o);
15933
16228
  __publicField2(this, "list", []);
15934
16229
  __publicField2(this, "box");
16230
+ __publicField2(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
15935
16231
  __publicField2(this, "shardGeos", []);
15936
16232
  __publicField2(this, "materials", /* @__PURE__ */ new Map());
15937
16233
  __publicField2(this, "q");
@@ -15940,6 +16236,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
15940
16236
  __publicField2(this, "v");
15941
16237
  __publicField2(this, "w");
15942
16238
  __publicField2(this, "rng", createRng3(1374496523));
16239
+ __publicField2(this, "tick", 0);
15943
16240
  __publicField2(this, "markGeometry", null);
15944
16241
  __publicField2(this, "markMaterial", null);
15945
16242
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -16007,8 +16304,28 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16007
16304
  mesh2.add(disc);
16008
16305
  }
16009
16306
  }
16010
- /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
16307
+ /**
16308
+ * Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget: over it the oldest fragment still showing fades out to make room, and a burst
16309
+ * bigger than the budget (every live fragment was made in this same frame) does not make the rest at all, so a blast never builds a thousand meshes only to fade them
16310
+ * out. Returns null when the fragment was not made.
16311
+ */
16011
16312
  spawn(f, material, home, maxAllowed) {
16313
+ if (this.list.length >= maxAllowed) {
16314
+ let alive = 0;
16315
+ let oldest;
16316
+ let oldestSmall;
16317
+ for (const l of this.list) {
16318
+ if (l.dying) continue;
16319
+ alive++;
16320
+ oldest ?? (oldest = l);
16321
+ if (!l.body) oldestSmall ?? (oldestSmall = l);
16322
+ }
16323
+ oldest = oldestSmall ?? oldest;
16324
+ if (alive >= maxAllowed) {
16325
+ if (!oldest || oldest.born === this.tick) return null;
16326
+ oldest.dying = true;
16327
+ }
16328
+ }
16012
16329
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
16013
16330
  const mesh2 = new this.THREE.Mesh(geo, material);
16014
16331
  mesh2.position.set(f.center[0], f.center[1], f.center[2]);
@@ -16032,19 +16349,42 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16032
16349
  rest: 0,
16033
16350
  fade: 1,
16034
16351
  dying: false,
16035
- air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
16352
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
16353
+ body: null,
16354
+ born: this.tick
16036
16355
  };
16037
- this.list.push(live);
16038
- let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
16039
- for (let i = 0; i < this.list.length && alive > maxAllowed; i++) {
16040
- if (!this.list[i].dying) {
16041
- this.list[i].dying = true;
16042
- alive--;
16043
- }
16356
+ if (f.shape !== "shard" && isBig(f.size)) {
16357
+ live.body = {
16358
+ mass: f.mass,
16359
+ push: (x, z) => {
16360
+ live.vel.x = x;
16361
+ live.vel.z = z;
16362
+ live.vel.y = Math.max(live.vel.y, 0.3);
16363
+ live.asleep = false;
16364
+ live.rest = 0;
16365
+ }
16366
+ };
16044
16367
  }
16368
+ this.list.push(live);
16045
16369
  this.addMarks(mesh2, f);
16046
16370
  return mesh2;
16047
16371
  }
16372
+ /** The boxes of the big fragments around (x, z). */
16373
+ solidsNear(x, z, radius, found) {
16374
+ const boxes = [];
16375
+ for (const f of this.list) {
16376
+ if (!f.body || f.dying) continue;
16377
+ const reach = f.half.length() + radius;
16378
+ if (Math.abs(f.mesh.position.x - x) > reach || Math.abs(f.mesh.position.z - z) > reach) continue;
16379
+ f.mesh.updateMatrixWorld();
16380
+ boxes.length = 0;
16381
+ pushFittedBoxes(this.unitBox, f.mesh.matrixWorld, boxes);
16382
+ for (let i = 0; i < boxes.length; i += 6) {
16383
+ if (x < boxes[i] - radius || x > boxes[i + 3] + radius || z < boxes[i + 2] - radius || z > boxes[i + 5] + radius) continue;
16384
+ found(f.home, [boxes[i], boxes[i + 1], boxes[i + 2]], [boxes[i + 3], boxes[i + 4], boxes[i + 5]], f.body);
16385
+ }
16386
+ }
16387
+ }
16048
16388
  /** Add velocity to every fragment within `radius` of a point (blasts). */
16049
16389
  blast(point, radius, speed) {
16050
16390
  for (const f of this.list) {
@@ -16060,7 +16400,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16060
16400
  f.rest = 0;
16061
16401
  }
16062
16402
  }
16403
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
16404
+ wakeRegion(x, z, radius) {
16405
+ for (const f of this.list) {
16406
+ if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
16407
+ f.asleep = false;
16408
+ f.rest = 0;
16409
+ }
16410
+ }
16063
16411
  update(dt) {
16412
+ this.tick++;
16064
16413
  const g = this.o.gravity;
16065
16414
  for (let i = this.list.length - 1; i >= 0; i--) {
16066
16415
  const f = this.list[i];
@@ -16075,7 +16424,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16075
16424
  }
16076
16425
  if (f.asleep) {
16077
16426
  f.rest += dt;
16078
- if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
16427
+ if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
16079
16428
  continue;
16080
16429
  }
16081
16430
  this.step(f, dt, g);
@@ -16149,7 +16498,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16149
16498
  let small = 0;
16150
16499
  for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
16151
16500
  let axis2 = small;
16152
- if (Math.abs(up[small]) < 0.35) {
16501
+ if (Math.abs(up[small]) < 0.35 && !f.body) {
16153
16502
  axis2 = 0;
16154
16503
  for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis2])) axis2 = i;
16155
16504
  }
@@ -16271,6 +16620,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16271
16620
  var SLOP = 2e-3;
16272
16621
  var NARROW = 0.3;
16273
16622
  var TOPPLE = 3;
16623
+ var TIP_ACCELERATION = 30;
16624
+ var TIP_LYING = 0.25;
16625
+ var TIP_SECONDS = 3;
16626
+ var STEP = 0.03;
16627
+ var boxTopAt = (s, x, z) => {
16628
+ const e = s.e;
16629
+ const dx = x - s.c[0], dz = z - s.c[2];
16630
+ let t0 = -Infinity, t1 = Infinity;
16631
+ for (let k = 0; k < 3; k++) {
16632
+ const p = e[k * 4] * dx + e[k * 4 + 2] * dz;
16633
+ const d = e[k * 4 + 1];
16634
+ const h = s.half[k];
16635
+ if (Math.abs(d) < 1e-9) {
16636
+ if (Math.abs(p) > h) return -Infinity;
16637
+ continue;
16638
+ }
16639
+ let a = (-h - p) / d, b = (h - p) / d;
16640
+ if (a > b) [a, b] = [b, a];
16641
+ t0 = Math.max(t0, a);
16642
+ t1 = Math.min(t1, b);
16643
+ if (t0 > t1) return -Infinity;
16644
+ }
16645
+ return s.c[1] + t1;
16646
+ };
16274
16647
  var PieceSystem = class {
16275
16648
  constructor(THREE, o) {
16276
16649
  __publicField2(this, "THREE", THREE);
@@ -16282,6 +16655,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16282
16655
  __publicField2(this, "dq");
16283
16656
  __publicField2(this, "m");
16284
16657
  __publicField2(this, "ax");
16658
+ __publicField2(this, "solids", []);
16285
16659
  this.p = new THREE.Vector3();
16286
16660
  this.q = new THREE.Quaternion();
16287
16661
  this.dq = new THREE.Quaternion();
@@ -16291,6 +16665,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16291
16665
  get(objectId) {
16292
16666
  return this.list.find((b) => b.objectId === objectId) ?? null;
16293
16667
  }
16668
+ /** Sets a piece falling over, about the world axes x and z (a unit axis): its feet are thinner than the box that stands for it, so the box alone would hold it up. */
16669
+ tip(body, x, z) {
16670
+ body.tipping = { x, z, seconds: TIP_SECONDS };
16671
+ body.sleeping = false;
16672
+ }
16294
16673
  create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
16295
16674
  const existing = this.get(objectId);
16296
16675
  if (existing) {
@@ -16304,6 +16683,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16304
16683
  }
16305
16684
  const body = {
16306
16685
  objectId,
16686
+ tipping: null,
16307
16687
  root,
16308
16688
  velocity: velocity ? [...velocity] : [0, 0, 0],
16309
16689
  angularVelocity: angular ? [...angular] : [0, 0, 0],
@@ -16373,8 +16753,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16373
16753
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
16374
16754
  return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
16375
16755
  }
16756
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
16757
+ wakeRegion(x, z, radius) {
16758
+ for (const b of this.list) {
16759
+ if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
16760
+ b.sleeping = false;
16761
+ b.still = 0;
16762
+ }
16763
+ }
16376
16764
  update(dt) {
16377
16765
  this.time += dt;
16766
+ this.solids = this.list.map((body) => {
16767
+ const c = this.centerOf(body, new this.THREE.Vector3());
16768
+ const h = body.halfExtents;
16769
+ return { body, c: [c.x, c.y, c.z], e: [...new this.THREE.Matrix4().makeRotationFromQuaternion(body.root.quaternion).elements], half: h, r: Math.hypot(h[0], h[1], h[2]) };
16770
+ });
16378
16771
  for (const b of this.list) {
16379
16772
  if (b.held) {
16380
16773
  b.velocity = [0, 0, 0];
@@ -16388,6 +16781,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16388
16781
  }
16389
16782
  this.separate(dt);
16390
16783
  }
16784
+ /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
16785
+ supportAt(self, x, y, z, step) {
16786
+ let top = this.o.groundAt(x, z);
16787
+ for (const s of this.solids) {
16788
+ if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
16789
+ const t = boxTopAt(s, x, z);
16790
+ if (t > top && t <= y + step) top = t;
16791
+ }
16792
+ const fixed2 = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
16793
+ return Math.max(top, fixed2);
16794
+ }
16391
16795
  integrate(b, dt) {
16392
16796
  const c = this.centerOf(b, this.p);
16393
16797
  const pos = [c.x, c.y, c.z];
@@ -16426,8 +16830,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16426
16830
  const sy = i & 2 ? hy : -hy;
16427
16831
  const sz = i & 4 ? hz : -hz;
16428
16832
  const r = [e[0] * sx + e[4] * sy + e[8] * sz, e[1] * sx + e[5] * sy + e[9] * sz, e[2] * sx + e[6] * sy + e[10] * sz];
16429
- const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
16430
- const pen = gy - (pos[1] + r[1]);
16833
+ const cy = pos[1] + r[1];
16834
+ const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
16835
+ const pen = gy - cy;
16431
16836
  if (pen > -SLOP) {
16432
16837
  rs.push(r);
16433
16838
  if (pen > maxPen) maxPen = pen;
@@ -16450,7 +16855,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16450
16855
  }
16451
16856
  const lx = pos[0] + sx / rs.length;
16452
16857
  const lz = pos[2] + sz / rs.length;
16453
- landPoint = [lx, this.o.groundAt(lx, lz), lz];
16858
+ landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
16454
16859
  }
16455
16860
  for (let it = 0; it < 4; it++) {
16456
16861
  for (const r of rs) {
@@ -16503,6 +16908,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16503
16908
  const wx = x1 - x0;
16504
16909
  const wz = z1 - z0;
16505
16910
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
16911
+ if (b.tipping) {
16912
+ b.tipping.seconds -= h;
16913
+ if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
16914
+ else {
16915
+ w[0] += b.tipping.x * TIP_ACCELERATION * h;
16916
+ w[2] += b.tipping.z * TIP_ACCELERATION * h;
16917
+ unstable = true;
16918
+ }
16919
+ }
16506
16920
  if (unstable) {
16507
16921
  const dirX = x0 + x1 > 0 ? -1 : 1;
16508
16922
  const dirZ = z0 + z1 > 0 ? -1 : 1;
@@ -16539,6 +16953,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16539
16953
  b.still = 0;
16540
16954
  }
16541
16955
  }
16956
+ /** Half the height of the box of a piece as it is turned now. */
16957
+ halfHeight(b) {
16958
+ const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
16959
+ return Math.abs(e[1]) * b.halfExtents[0] + Math.abs(e[5]) * b.halfExtents[1] + Math.abs(e[9]) * b.halfExtents[2];
16960
+ }
16542
16961
  /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
16543
16962
  separate(dt) {
16544
16963
  const n = this.list.length;
@@ -16557,6 +16976,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16557
16976
  this.centerOf(a, ca);
16558
16977
  this.centerOf(b, cb);
16559
16978
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
16979
+ if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
16560
16980
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
16561
16981
  let dx = cb.x - ca.x;
16562
16982
  let dz = cb.z - ca.z;
@@ -16712,6 +17132,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16712
17132
  const fill = options.fillOf?.(o, material);
16713
17133
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
16714
17134
  const contains = containsOf(ud.destructionContains);
17135
+ const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
16715
17136
  const existing = explicit ? byKey.get(explicit) : void 0;
16716
17137
  if (existing) {
16717
17138
  const before = sizeOf(existing.box);
@@ -16724,8 +17145,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16724
17145
  existing.skin = existing.skin && skin;
16725
17146
  if (fill !== void 0) existing.fill = fill;
16726
17147
  if (contains) existing.contains = contains;
17148
+ if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
16727
17149
  } else {
16728
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box: box2, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
17150
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box: box2, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
16729
17151
  entries.push(e);
16730
17152
  if (explicit) byKey.set(explicit, e);
16731
17153
  }
@@ -16793,7 +17215,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16793
17215
  const minSide = Math.min(...size);
16794
17216
  if (e.fill !== void 0) part.fill = e.fill;
16795
17217
  else if (e.hollow) part.fill = 0.05;
16796
- if (minSide < 0.01 && METAL.test(e.material)) {
17218
+ if (e.thickness !== void 0) part.thickness = e.thickness;
17219
+ else if (minSide < 0.01 && METAL.test(e.material)) {
16797
17220
  part.fill = e.fill ?? 1;
16798
17221
  part.thickness = Math.max(minSide, 5e-4);
16799
17222
  }
@@ -16880,9 +17303,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
16880
17303
  const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
16881
17304
  ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
16882
17305
  triangle.set(corners[0], corners[1], corners[2]);
17306
+ if (triangle.getArea() <= 1e-14) continue;
16883
17307
  triangle.closestPointToPoint(local2, closest);
16884
17308
  const distance4 = closest.distanceToSquared(local2);
16885
- if (distance4 >= nearest) continue;
17309
+ if (!(distance4 < nearest)) continue;
16886
17310
  nearest = distance4;
16887
17311
  nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
16888
17312
  triangle.getBarycoord(closest, weights);
@@ -17058,6 +17482,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17058
17482
  return out;
17059
17483
  }
17060
17484
  /** Box of the part in world space. */
17485
+ /** Appends the boxes that follow the mesh in the world (`pushFittedBoxes`) to `out`. */
17486
+ fittedBoxes(mv, out) {
17487
+ const box2 = new this.THREE.Box3();
17488
+ if (mv.host) this.localBox(mv, box2);
17489
+ else {
17490
+ const geometry = mv.mesh.geometry;
17491
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
17492
+ if (geometry.boundingBox) box2.copy(geometry.boundingBox);
17493
+ else box2.makeEmpty();
17494
+ }
17495
+ if (box2.isEmpty()) return;
17496
+ mv.mesh.updateWorldMatrix(true, false);
17497
+ pushFittedBoxes(box2, mv.mesh.matrixWorld, out);
17498
+ }
17061
17499
  worldBox(mv, out) {
17062
17500
  if (!mv.host) return out.setFromObject(mv.mesh);
17063
17501
  this.localBox(mv, out);
@@ -17804,6 +18242,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17804
18242
  var IMPACT_TAGS = ["metal", "glass", "wood"];
17805
18243
  var GLOW_FROM_C = 430;
17806
18244
  var QUEUE_CAP = 4e3;
18245
+ var STATIC_REFRESH = 0.5;
18246
+ var STATIC_SPAN = 3;
18247
+ var FEET_BAND = 0.03;
18248
+ var TIP_MIN_HEIGHT = 0.3;
18249
+ var TIP_NARROW = 0.3;
17807
18250
  var FIRE_CEILING_SECONDS = 3600;
17808
18251
  var GENERIC_REFERENCE_RADIUS = 15;
17809
18252
  var PRIMED_FIRE_RADIUS = 0.2;
@@ -17848,6 +18291,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17848
18291
  if (def.fracture === "tear") return "impact_soft";
17849
18292
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
17850
18293
  }
18294
+ var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
17851
18295
  function fractureKey(def, style) {
17852
18296
  switch (style) {
17853
18297
  case "shatter":
@@ -17889,6 +18333,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17889
18333
  __publicField2(this, "autoSplit");
17890
18334
  __publicField2(this, "maxFragments");
17891
18335
  __publicField2(this, "bounds", /* @__PURE__ */ new Map());
18336
+ __publicField2(this, "staticBoxes", /* @__PURE__ */ new Map());
18337
+ __publicField2(this, "staticDirty", /* @__PURE__ */ new Set());
18338
+ __publicField2(this, "staticAll", true);
18339
+ __publicField2(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
18340
+ __publicField2(this, "staticAt", 0);
17892
18341
  __publicField2(this, "queue", []);
17893
18342
  __publicField2(this, "unsubscribe");
17894
18343
  __publicField2(this, "visuals");
@@ -17946,6 +18395,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17946
18395
  this.cages = new RebarCages(THREE, o.scene);
17947
18396
  this.pieceSys = new PieceSystem(THREE, {
17948
18397
  groundAt: this.groundAt,
18398
+ staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
17949
18399
  gravity: this.gravity,
17950
18400
  scene: o.scene,
17951
18401
  onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
@@ -17958,6 +18408,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
17958
18408
  // Binding
17959
18409
  // ----------------------------------------------------------------------------------------------------------------------------------
17960
18410
  bind(objectId, root, options = {}) {
18411
+ this.staticDirty.add(objectId);
17961
18412
  if (this.bounds.has(objectId)) this.unbind(objectId);
17962
18413
  const state = this.world.state(objectId);
17963
18414
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
@@ -18077,6 +18528,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18077
18528
  }
18078
18529
  }
18079
18530
  unbind(objectId) {
18531
+ this.staticDirty.add(objectId);
18080
18532
  const b = this.bounds.get(objectId);
18081
18533
  if (!b) return;
18082
18534
  this.restore(objectId);
@@ -18116,6 +18568,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18116
18568
  return m > 0 ? m : 5;
18117
18569
  }
18118
18570
  makePiece(objectId, options = {}) {
18571
+ this.staticDirty.add(objectId);
18119
18572
  const b = this.bounds.get(objectId);
18120
18573
  if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
18121
18574
  if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
@@ -18228,7 +18681,100 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18228
18681
  // ----------------------------------------------------------------------------------------------------------------------------------
18229
18682
  // Events
18230
18683
  // ----------------------------------------------------------------------------------------------------------------------------------
18684
+ /**
18685
+ * The highest top of a bound object that is not a piece under (x, z) a corner at height `y` can land on (at most `step` above it). The world boxes
18686
+ * of their drawn meshes are kept until an event or a moved root changes them; parts drawn inside a merged mesh and meshes wider than `STATIC_SPAN`
18687
+ * are left out (walls, floors, roofs: the terrain under them is the host's `groundAt`).
18688
+ */
18689
+ staticTopAt(x, z, y, step) {
18690
+ let top = -Infinity;
18691
+ for (const a of this.staticNow().values()) {
18692
+ if (a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
18693
+ for (let i = 6; i < a.length; i += 6) {
18694
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
18695
+ const t = a[i + 4];
18696
+ if (t > top && t <= y + step) top = t;
18697
+ }
18698
+ }
18699
+ return top;
18700
+ }
18701
+ /** The boxes of the fixed objects (per object: the box around them all, then each one), each object measured again when an event touched it (all of them every `STATIC_REFRESH` s). */
18702
+ staticNow() {
18703
+ if (this.time - this.staticAt > STATIC_REFRESH) {
18704
+ this.staticAt = this.time;
18705
+ this.staticAll = true;
18706
+ }
18707
+ if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
18708
+ const box2 = new this.THREE.Box3();
18709
+ const measure = (b) => {
18710
+ if (b.piece || this.pieceSys.get(b.id)) {
18711
+ this.staticBoxes.delete(b.id);
18712
+ return;
18713
+ }
18714
+ const out = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
18715
+ for (const mv of this.meshesOf(b)) {
18716
+ if (mv.host || mv.loose || !this.visuals.shown(mv)) continue;
18717
+ this.visuals.worldBox(mv, box2);
18718
+ if (box2.isEmpty() || box2.max.x - box2.min.x > STATIC_SPAN || box2.max.z - box2.min.z > STATIC_SPAN) continue;
18719
+ this.visuals.fittedBoxes(mv, out);
18720
+ for (let k = 0; k < 3; k++) {
18721
+ out[k] = Math.min(out[k], box2.min.getComponent(k));
18722
+ out[k + 3] = Math.max(out[k + 3], box2.max.getComponent(k));
18723
+ }
18724
+ }
18725
+ this.staticBoxes.set(b.id, out.length > 6 ? out : []);
18726
+ };
18727
+ if (this.staticAll) {
18728
+ this.staticBoxes.clear();
18729
+ for (const b of this.bounds.values()) measure(b);
18730
+ } else {
18731
+ for (const id of this.staticDirty) {
18732
+ const b = this.bounds.get(id);
18733
+ if (b) measure(b);
18734
+ else this.staticBoxes.delete(id);
18735
+ }
18736
+ }
18737
+ this.staticAll = false;
18738
+ this.staticDirty.clear();
18739
+ return this.staticBoxes;
18740
+ }
18741
+ /** The boxes of a piece's drawn meshes, measured once per frame. */
18742
+ pieceBoxesOf(pc) {
18743
+ const cached = this.pieceBoxes.get(pc);
18744
+ if (cached && cached.at === this.time) return cached.boxes;
18745
+ const boxes = [];
18746
+ const b = this.bounds.get(pc.objectId);
18747
+ if (b) {
18748
+ for (const mv of this.meshesOf(b)) {
18749
+ if (this.visuals.shown(mv)) this.visuals.fittedBoxes(mv, boxes);
18750
+ }
18751
+ }
18752
+ this.pieceBoxes.set(pc, { at: this.time, boxes });
18753
+ return boxes;
18754
+ }
18755
+ solidsNear(x, z, radius) {
18756
+ const found = [];
18757
+ const scan = (a, id, piece, from) => {
18758
+ if (from > 0 && (a.length === 0 || x < a[0] - radius || x > a[3] + radius || z < a[2] - radius || z > a[5] + radius)) return;
18759
+ for (let i = from; i < a.length; i += 6) {
18760
+ if (x < a[i] - radius || x > a[i + 3] + radius || z < a[i + 2] - radius || z > a[i + 5] + radius) continue;
18761
+ found.push({ objectId: id, min: [a[i], a[i + 1], a[i + 2]], max: [a[i + 3], a[i + 4], a[i + 5]], piece });
18762
+ }
18763
+ };
18764
+ for (const [id, a] of this.staticNow()) scan(a, id, null, 6);
18765
+ const c = this.v1;
18766
+ for (const pc of this.pieceSys.list) {
18767
+ if (pc.held) continue;
18768
+ this.pieceSys.centerOf(pc, c);
18769
+ const reach = Math.hypot(pc.halfExtents[0], pc.halfExtents[1], pc.halfExtents[2]) + radius;
18770
+ if (Math.abs(c.x - x) > reach || Math.abs(c.z - z) > reach) continue;
18771
+ scan(this.pieceBoxesOf(pc), pc.objectId, pc, 0);
18772
+ }
18773
+ this.fragments.solidsNear(x, z, radius, (home, min2, max2, loose) => found.push({ objectId: home, min: min2, max: max2, piece: null, loose }));
18774
+ return found;
18775
+ }
18231
18776
  apply(ev) {
18777
+ this.staticDirty.add(ev.object);
18232
18778
  switch (ev.type) {
18233
18779
  case "damage":
18234
18780
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -18565,7 +19111,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18565
19111
  const def = pv ? this.world.materials.get(pv.material) : void 0;
18566
19112
  const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
18567
19113
  const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
18568
- for (const fr of ev.fragments) this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fallbackColor(fr.material, def), false), ev.object, maxAlive);
19114
+ for (const fr of ev.fragments) {
19115
+ const fdef = this.world.materials.get(fr.material);
19116
+ const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
19117
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone2, false), ev.object, maxAlive);
19118
+ }
18569
19119
  if (!this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.center)) this.vfx.burst("dust", ev.center, [0, 1, 0], 6, 0.8, { color });
18570
19120
  this.sound(impactKey(def), ev.center, 0.7);
18571
19121
  }
@@ -18646,6 +19196,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18646
19196
  this.pieceSys.setCenter(old, center);
18647
19197
  old.mass = Math.max(0.05, this.massOf(objectId));
18648
19198
  old.sleeping = false;
19199
+ } else if (kitOriginOf(b.root)?.kind === "furniture") {
19200
+ this.makePiece(objectId);
19201
+ this.tipIfTopHeavy(b);
18649
19202
  }
18650
19203
  }
18651
19204
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -18692,8 +19245,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18692
19245
  }
18693
19246
  const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
18694
19247
  nb.piece = piece;
19248
+ if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
18695
19249
  this.o.onPiece?.(piece);
18696
19250
  }
19251
+ /**
19252
+ * A piece of furniture standing on feet too close for its height (a board on two legs in a line, a lone leg) does not stay up: the box of a piece hides that its weight is up
19253
+ * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
19254
+ */
19255
+ tipIfTopHeavy(b) {
19256
+ const piece = b.piece;
19257
+ if (!piece) return;
19258
+ const box2 = new this.THREE.Box3();
19259
+ const boxes = this.meshesOf(b).filter((mv) => this.visuals.shown(mv)).map((mv) => this.visuals.worldBox(mv, new this.THREE.Box3()));
19260
+ if (boxes.length === 0) return;
19261
+ for (const one of boxes) box2.union(one);
19262
+ const height = box2.max.y - box2.min.y;
19263
+ const feet = new this.THREE.Box3();
19264
+ for (const one of boxes) if (one.min.y < box2.min.y + FEET_BAND) feet.union(one);
19265
+ const spanX = feet.max.x - feet.min.x;
19266
+ const spanZ = feet.max.z - feet.min.z;
19267
+ if (height < TIP_MIN_HEIGHT || Math.min(spanX, spanZ) >= TIP_NARROW * height) return;
19268
+ const across = spanX <= spanZ ? 0 : 2;
19269
+ const lean = across === 0 ? box2.min.x + box2.max.x - feet.min.x - feet.max.x : box2.min.z + box2.max.z - feet.min.z - feet.max.z;
19270
+ const away = lean < 0 ? -1 : 1;
19271
+ if (across === 0) this.pieceSys.tip(piece, 0, -away);
19272
+ else this.pieceSys.tip(piece, away, 0);
19273
+ }
18697
19274
  onExplode(ev, options = {}) {
18698
19275
  const p = ev.point;
18699
19276
  if (options.visual !== false) this.playExplosion(ev, options);
@@ -18944,6 +19521,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
18944
19521
  this.vfx.sprites().clear();
18945
19522
  this.ownVfx?.world.clear();
18946
19523
  }
19524
+ wakeRegion(x, z, radius) {
19525
+ this.fragments.wakeRegion(x, z, radius);
19526
+ this.pieceSys.wakeRegion(x, z, radius);
19527
+ }
18947
19528
  stats() {
18948
19529
  return { fragments: this.fragments.count, particles: this.vfx.sprites().live + this.ownedVfxParticles(), pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
18949
19530
  }
@@ -19054,7 +19635,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
19054
19635
  volume: 0,
19055
19636
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
19056
19637
  };
19057
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
19638
+ var drag = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
19639
+ var DRAG_SOUNDS = [
19640
+ drag("drag_wood", [sceneNoise({ type: "bandpass", f: 380, q: 1.4, d: 0.28, vol: 0.5 }), sceneNoise({ type: "bandpass", f: [900, 1300], q: 3, d: 0.08, vol: 0.14 })]),
19641
+ drag("drag_metal", [sceneNoise({ type: "bandpass", f: 1500, q: 4, d: 0.26, vol: 0.32 }), sceneTone({ f: [880, 1240], f2: 760, d: 0.18, vol: 0.05 })]),
19642
+ drag("drag_glass", [sceneNoise({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
19643
+ drag("drag_masonry", [sceneNoise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
19644
+ drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
19645
+ ];
19646
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
19058
19647
  var DESTRUCTION_SOUND_KEYS = {
19059
19648
  art_scene: {
19060
19649
  impact_metal: { sound: "clang" },
@@ -19062,6 +19651,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
19062
19651
  impact_glass: { sound: "ping" },
19063
19652
  impact_masonry: { sound: "chip" },
19064
19653
  impact_soft: { sound: "thud" },
19654
+ drag_wood: { sound: "drag_wood" },
19655
+ drag_metal: { sound: "drag_metal" },
19656
+ drag_glass: { sound: "drag_glass" },
19657
+ drag_masonry: { sound: "drag_masonry" },
19658
+ drag_soft: { sound: "drag_soft" },
19065
19659
  shatter_glass: { sound: "glass_shatter" },
19066
19660
  crumble: { sound: "crumble" },
19067
19661
  splinter: { sound: "crack" },
@@ -32987,9 +33581,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
32987
33581
  const speed = load && load.speed !== cal.muzzleSpeed ? weaponSpeed(a, cal) * (load.speed / cal.muzzleSpeed) : weaponSpeed(a, cal);
32988
33582
  const grams = load ? load.grams : cal.bulletGrams;
32989
33583
  const energy = 0.5 * (grams / 1e3) * speed * speed;
32990
- const drag = cal.dragPer100m ?? 0.1;
33584
+ const drag2 = cal.dragPer100m ?? 0.1;
32991
33585
  const energyAt = (distance22) => {
32992
- const f = Math.pow(1 - drag, Math.max(0, distance22) / 100);
33586
+ const f = Math.pow(1 - drag2, Math.max(0, distance22) / 100);
32993
33587
  return energy * f * f;
32994
33588
  };
32995
33589
  const bloomNow = this.extraAt(t);
@@ -35734,10 +36328,10 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
35734
36328
  previous[1] = position[offset + 1];
35735
36329
  previous[2] = position[offset + 2];
35736
36330
  velocity[offset + 1] -= GRAVITY23 * dt;
35737
- const drag = 1 - dt * AIR_DRAG;
35738
- velocity[offset] *= drag;
35739
- velocity[offset + 1] *= drag;
35740
- velocity[offset + 2] *= drag;
36331
+ const drag2 = 1 - dt * AIR_DRAG;
36332
+ velocity[offset] *= drag2;
36333
+ velocity[offset + 1] *= drag2;
36334
+ velocity[offset + 2] *= drag2;
35741
36335
  point[0] = position[offset] + velocity[offset] * dt;
35742
36336
  point[1] = position[offset + 1] + velocity[offset + 1] * dt;
35743
36337
  point[2] = position[offset + 2] + velocity[offset + 2] * dt;
@@ -35966,12 +36560,12 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
35966
36560
  }
35967
36561
  function updateDrags(drags, context, layout, launch, dt) {
35968
36562
  const random = context.random;
35969
- for (const drag of drags) {
35970
- if (drag.done) continue;
35971
- drag.elapsed += dt;
35972
- const progress = Math.min(1, drag.elapsed / drag.duration);
35973
- const here = drag.path(progress);
35974
- const ahead = drag.path(Math.min(1, progress + 0.01));
36563
+ for (const drag2 of drags) {
36564
+ if (drag2.done) continue;
36565
+ drag2.elapsed += dt;
36566
+ const progress = Math.min(1, drag2.elapsed / drag2.duration);
36567
+ const here = drag2.path(progress);
36568
+ const ahead = drag2.path(Math.min(1, progress + 0.01));
35975
36569
  let directionColumn = ahead.column - here.column;
35976
36570
  let directionRow = ahead.row - here.row;
35977
36571
  const length22 = Math.hypot(directionColumn, directionRow) || 1;
@@ -35980,24 +36574,24 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
35980
36574
  const sideColumn = -directionRow;
35981
36575
  const sideRow = directionColumn;
35982
36576
  const load = Math.pow(1 - progress, 1.3);
35983
- for (let offset = -drag.width; offset <= drag.width; offset += 0.5) {
35984
- const across = offset / drag.width;
35985
- const stripe = drag.stripes[Math.round((offset + drag.width) * 2)];
36577
+ for (let offset = -drag2.width; offset <= drag2.width; offset += 0.5) {
36578
+ const across = offset / drag2.width;
36579
+ const stripe = drag2.stripes[Math.round((offset + drag2.width) * 2)];
35986
36580
  const edge = 1 - across * across;
35987
- const amount = drag.amount * load * (0.25 + 0.75 * stripe) * edge;
36581
+ const amount = drag2.amount * load * (0.25 + 0.75 * stripe) * edge;
35988
36582
  if (load < 0.45 && random() > load * 2.2) continue;
35989
36583
  depositCell(context, Math.round(here.row + sideRow * offset), Math.round(here.column + sideColumn * offset), amount * dt * 30);
35990
36584
  }
35991
- if (drag.hand && random() < dt * 14 * load + 0.02) {
36585
+ if (drag2.hand && random() < dt * 14 * load + 0.02) {
35992
36586
  const world = layout.cellToWorld(here.row, here.column);
35993
- const origin = [world[0] + (random() - 0.5) * 0.06, drag.hand.height + random() * 0.3, world[2] + (random() - 0.5) * 0.06];
35994
- const velocity = [directionColumn * drag.hand.speed, 0 * drag.hand.speed, directionRow * drag.hand.speed];
36587
+ const origin = [world[0] + (random() - 0.5) * 0.06, drag2.hand.height + random() * 0.3, world[2] + (random() - 0.5) * 0.06];
36588
+ const velocity = [directionColumn * drag2.hand.speed, 0 * drag2.hand.speed, directionRow * drag2.hand.speed];
35995
36589
  velocity[0] += (random() - 0.5) * 0.3;
35996
36590
  velocity[1] += 0.2;
35997
36591
  velocity[2] += (random() - 0.5) * 0.3;
35998
36592
  launch(origin, velocity, 2 + random() * 2.2);
35999
36593
  }
36000
- if (progress >= 1) drag.done = true;
36594
+ if (progress >= 1) drag2.done = true;
36001
36595
  }
36002
36596
  }
36003
36597
  var DEFAULT_STEP_RATE = 180;
@@ -36676,6 +37270,12 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
36676
37270
  return count;
36677
37271
  };
36678
37272
  var INSET = 0.985;
37273
+ var narrowMouth = (state) => {
37274
+ const shape = state.shape;
37275
+ if (!shape || state.lipRadius === void 0) return false;
37276
+ const inner = shape.kind === "cylinder" ? shape.radius : Math.min(shape.size[0], shape.size[2]) / 2;
37277
+ return state.lipRadius < inner * 0.5;
37278
+ };
36679
37279
  var LiquidBody = class {
36680
37280
  constructor(THREE, containerId, object, offset) {
36681
37281
  __publicField5(this, "THREE", THREE);
@@ -36738,7 +37338,7 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
36738
37338
  this.vertices = 0;
36739
37339
  return;
36740
37340
  }
36741
- if (state.closed && this.isShellOpaque()) {
37341
+ if ((state.closed || narrowMouth(state)) && this.isShellOpaque()) {
36742
37342
  if (this.mesh) this.mesh.visible = false;
36743
37343
  this.vertices = 0;
36744
37344
  return;
@@ -36768,12 +37368,15 @@ ${problems.map((problem) => ` - ${problem}`).join("\n")}`);
36768
37368
  }
36769
37369
  } else this.steamAcc = 0;
36770
37370
  }
36771
- /** Every mesh of the bound object (the liquid itself aside) is drawn with an opaque material. */
37371
+ /**
37372
+ * Every mesh of the bound object (the liquid itself aside) is drawn with an opaque material. The marks drawn on its surface (bullet holes, scorches: the
37373
+ * `userData.destructionDebris` meshes of `game-destruction-three`) are see-through discs on the shell, not windows into it.
37374
+ */
36772
37375
  isShellOpaque() {
36773
37376
  let opaque = true;
36774
37377
  this.object.traverse((child) => {
36775
37378
  const mesh2 = child;
36776
- if (!opaque || !mesh2.isMesh || child === this.mesh) return;
37379
+ if (!opaque || !mesh2.isMesh || child === this.mesh || child.userData?.destructionDebris === true) return;
36777
37380
  const materials = Array.isArray(mesh2.material) ? mesh2.material : [mesh2.material];
36778
37381
  if (materials.some((material) => material.transparent || material.opacity < 1)) opaque = false;
36779
37382
  });
@@ -37815,7 +38418,7 @@ void main(){
37815
38418
  this.film.update({ dt, dryingSeconds });
37816
38419
  const bounds = this.film.simulator.bounds;
37817
38420
  const launched = this.film.drops.launched;
37818
- const busy = launched !== this.launched || this.film.drip !== void 0 || this.film.sources.length > 0 || this.film.drags.some((drag) => !drag.done) || this.dropsInFlight();
38421
+ const busy = launched !== this.launched || this.film.drip !== void 0 || this.film.sources.length > 0 || this.film.drags.some((drag2) => !drag2.done) || this.dropsInFlight();
37819
38422
  this.launched = launched;
37820
38423
  const current = bounds.lastRow < 0 ? null : { firstRow: bounds.firstRow, lastRow: bounds.lastRow, firstColumn: bounds.firstColumn, lastColumn: bounds.lastColumn };
37821
38424
  const rect = this.union(this.previous, current);
@@ -38384,14 +38987,6 @@ void main(){
38384
38987
  sc.far = 40;
38385
38988
  sun.shadow.bias = -6e-4;
38386
38989
  group.add(sun);
38387
- const floor2 = new THREE.Mesh(new THREE.PlaneGeometry(size * 2, size * 2), new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }));
38388
- floor2.rotation.x = -Math.PI / 2;
38389
- floor2.receiveShadow = true;
38390
- floor2.name = "floor";
38391
- group.add(floor2);
38392
- const grid = new THREE.GridHelper(size * 2, size * 2, 5000274, 7829371);
38393
- grid.position.y = 4e-3;
38394
- group.add(grid);
38395
38990
  const sceneryObjects = resolveScenery(scenery);
38396
38991
  const wallObject = sceneryObjects.find((o) => o.id === WALL_ID);
38397
38992
  const wallZ = wallObject ? wallObject.position[2] : DEFAULT_WALL_Z;
@@ -38430,16 +39025,23 @@ void main(){
38430
39025
  const surfaces = [{ id: "floor", point: [0, 0, 0], normal: [0, 1, 0], halfSize: [size, size], porosity: 0.05 }];
38431
39026
  for (const o of sceneryObjects) {
38432
39027
  if (!o.top) continue;
38433
- const b = specBounds(THREE, o.spec, o.position, o.rotationY, { onlyPart: o.top.part });
38434
- if (!b) continue;
39028
+ const boards = (o.top.parts ?? []).flatMap((part) => {
39029
+ const f = specBounds(THREE, o.spec, o.position, o.rotationY, { onlyPart: part });
39030
+ return f ? [{ part, minX: f.minX, maxX: f.maxX, minZ: f.minZ, maxZ: f.maxZ }] : [];
39031
+ });
39032
+ const whole = specBounds(THREE, o.spec, o.position, o.rotationY, { onlyPart: o.top.part });
39033
+ if (!whole) continue;
39034
+ const b = boards.length === 0 ? whole : { minX: Math.min(...boards.map((f) => f.minX)), maxX: Math.max(...boards.map((f) => f.maxX)), minZ: Math.min(...boards.map((f) => f.minZ)), maxZ: Math.max(...boards.map((f) => f.maxZ)) };
38435
39035
  const top = o.position[1] + o.top.y;
38436
- tops.push({ id: o.id, topPart: o.top.part, minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ, top });
39036
+ tops.push({ id: o.id, topPart: o.top.part, minX: b.minX, maxX: b.maxX, minZ: b.minZ, maxZ: b.maxZ, top, ...boards.length > 0 ? { boards } : {} });
38437
39037
  surfaces.push({ id: o.id, point: [(b.minX + b.maxX) / 2, top, (b.minZ + b.maxZ) / 2], normal: [0, 1, 0], halfSize: [(b.maxX - b.minX) / 2, (b.maxZ - b.minZ) / 2], porosity: 0.3 });
38438
39038
  }
38439
39039
  const colliders = [];
39040
+ const colliderIds = [];
38440
39041
  for (const o of sceneryObjects) {
38441
39042
  const b = specBounds(THREE, o.spec, o.position, o.rotationY, { minHeight: 0.1 });
38442
39043
  if (!b) continue;
39044
+ colliderIds.push(o.id);
38443
39045
  colliders.push(o.id === WALL_ID ? [-size, b.minZ - 1, size, b.maxZ + 0.1] : [b.minX, b.minZ, b.maxX, b.maxZ]);
38444
39046
  }
38445
39047
  let tapSpout = null;
@@ -38480,6 +39082,7 @@ void main(){
38480
39082
  }
38481
39083
  tapSpout = [cx + 0.3, y + 0.5, cz];
38482
39084
  colliders.push([postX - 0.05, cz - 0.1, postX + 0.05, cz + 0.1]);
39085
+ colliderIds.push(null);
38483
39086
  }
38484
39087
  }
38485
39088
  return {
@@ -38487,6 +39090,7 @@ void main(){
38487
39090
  objects: sceneryObjects,
38488
39091
  surfaces,
38489
39092
  colliders,
39093
+ colliderIds,
38490
39094
  playerStart,
38491
39095
  wallZ,
38492
39096
  tops,
@@ -40132,13 +40736,19 @@ void main(){
40132
40736
  // src/player.ts
40133
40737
  var EYE = 1.7;
40134
40738
  var PLAYER_RADIUS = 0.3;
40739
+ var FOOT = 0.5;
40135
40740
  var WALK = 3.4;
40136
40741
  var CROUCH_DROP = 0.55;
40137
40742
  var CROUCH_WALK = 1.7;
40138
40743
  var CROUCH_RATE = 9;
40744
+ var BEND_RATE = 7;
40745
+ var CLEAR_SHARES = [1, 0.75, 0.5, 0.25];
40746
+ var CROUCH_TAP = 0.25;
40139
40747
  var RUN = 6.2;
40140
40748
  var JUMP = 4.6;
40141
40749
  var GRAVITY4 = 13;
40750
+ var FLOOR_CLIMB_SPEED = 3;
40751
+ var FLOOR_FALL_SPEED = 6;
40142
40752
  var LOOK = 22e-4;
40143
40753
  function createPlayer(THREE, o) {
40144
40754
  const cam = o.camera;
@@ -40147,6 +40757,8 @@ void main(){
40147
40757
  let y = 0;
40148
40758
  let eye = EYE;
40149
40759
  let crouch = 0;
40760
+ let bend = 0;
40761
+ let bendTarget = 0;
40150
40762
  let vy2 = 0;
40151
40763
  let walked = 0;
40152
40764
  let running = false;
@@ -40155,33 +40767,79 @@ void main(){
40155
40767
  const keys = /* @__PURE__ */ new Set();
40156
40768
  const crouchKeys = o.crouchKeys ?? ["KeyC"];
40157
40769
  const isCrouchHeld = () => crouchKeys.some((code) => keys.has(code));
40770
+ let crouchLatched = false;
40771
+ let crouchPress = null;
40772
+ const isCrouched = () => crouchLatched || isCrouchHeld();
40773
+ const pressCrouch = (code) => {
40774
+ if (crouchKeys.includes(code) && !isCrouchHeld()) crouchPress = { held: 0, latched: crouchLatched };
40775
+ };
40776
+ const releaseCrouch = (code) => {
40777
+ if (!crouchKeys.includes(code) || !crouchPress || isCrouchHeld()) return;
40778
+ crouchLatched = crouchPress.held < CROUCH_TAP ? !crouchPress.latched : false;
40779
+ crouchPress = null;
40780
+ };
40158
40781
  const euler = new THREE.Euler(0, 0, 0, "YXZ");
40782
+ const floorHere = () => o.floorAt?.(px.x, px.z) ?? 0;
40783
+ let floorLevel = 0;
40784
+ const followFloor = (dt) => {
40785
+ const gap = floorHere() - floorLevel;
40786
+ const reach = (gap > 0 ? FLOOR_CLIMB_SPEED : FLOOR_FALL_SPEED) * dt;
40787
+ floorLevel += Math.max(-reach, Math.min(reach, gap));
40788
+ };
40159
40789
  const apply = () => {
40160
40790
  euler.set(pitch, yaw, 0, "YXZ");
40161
40791
  cam.quaternion.setFromEuler(euler);
40162
- cam.position.set(px.x, eye + y - CROUCH_DROP * crouch, px.z);
40792
+ cam.position.set(px.x, eye + floorLevel + y - CROUCH_DROP * crouch - bend, px.z);
40163
40793
  cam.updateMatrixWorld(true);
40164
40794
  };
40165
40795
  apply();
40166
- const blocked = (x, z) => {
40167
- if (Math.abs(x) > o.limit - PLAYER_RADIUS || Math.abs(z) > o.limit - PLAYER_RADIUS) return true;
40168
- for (const c of o.colliders) if (x > c[0] - PLAYER_RADIUS && x < c[2] + PLAYER_RADIUS && z > c[1] - PLAYER_RADIUS && z < c[3] + PLAYER_RADIUS) return true;
40796
+ const outside = (x, z) => Math.abs(x) > o.limit - PLAYER_RADIUS || Math.abs(z) > o.limit - PLAYER_RADIUS;
40797
+ const blocked = (x, z, radius = PLAYER_RADIUS) => {
40798
+ if (outside(x, z)) return true;
40799
+ for (const c of o.colliders) if (x > c[0] - radius && x < c[2] + radius && z > c[1] - radius && z < c[3] + radius) return true;
40169
40800
  if (o.blockers) {
40170
- for (const blocked2 of o.blockers) if (blocked2(x, z, PLAYER_RADIUS)) return true;
40801
+ for (const isBlocked2 of o.blockers) if (isBlocked2(x, z, radius)) return true;
40171
40802
  }
40172
- return false;
40803
+ return o.footing ? o.footing(x, z, radius, y).blocked : false;
40804
+ };
40805
+ const clearRadius = () => {
40806
+ for (const share of CLEAR_SHARES) if (!blocked(px.x, px.z, PLAYER_RADIUS * share)) return PLAYER_RADIUS * share;
40807
+ return null;
40173
40808
  };
40809
+ const wish = { x: 0, z: 0 };
40174
40810
  const move = (dx, dz) => {
40175
- if (!blocked(px.x + dx, px.z)) px.x += dx;
40176
- if (!blocked(px.x, px.z + dz)) px.z += dz;
40811
+ wish.x += dx;
40812
+ wish.z += dz;
40813
+ const radius = clearRadius();
40814
+ const free = (x, z) => radius === null ? !outside(x, z) : !blocked(x, z, radius);
40815
+ const steps = Math.max(1, Math.ceil(Math.hypot(dx, dz) / (PLAYER_RADIUS / 3)));
40816
+ const sx = dx / steps;
40817
+ const sz = dz / steps;
40818
+ for (let i = 0; i < steps; i++) {
40819
+ if (free(px.x + sx, px.z)) px.x += sx;
40820
+ if (free(px.x, px.z + sz)) px.z += sz;
40821
+ }
40822
+ };
40823
+ const walkKeys = () => {
40824
+ const f = (keys.has("KeyW") || keys.has("ArrowUp") ? 1 : 0) - (keys.has("KeyS") || keys.has("ArrowDown") ? 1 : 0);
40825
+ const r = (keys.has("KeyD") || keys.has("ArrowRight") ? 1 : 0) - (keys.has("KeyA") || keys.has("ArrowLeft") ? 1 : 0);
40826
+ if (!f && !r) return [0, 0];
40827
+ const len = Math.hypot(f, r);
40828
+ return [(-Math.sin(yaw) * f + Math.cos(yaw) * r) / len, (-Math.cos(yaw) * f - Math.sin(yaw) * r) / len];
40177
40829
  };
40178
40830
  let dragging = false;
40179
40831
  let moved = 0;
40180
40832
  let downAt = 0;
40181
40833
  let useStarted = false;
40182
40834
  let holdTimer = null;
40835
+ let walkHeld = false;
40836
+ let lookLimit = null;
40183
40837
  const look = (dx, dy) => {
40184
40838
  yaw -= dx * LOOK;
40839
+ if (lookLimit) {
40840
+ const off = Math.atan2(Math.sin(yaw - lookLimit.yaw), Math.cos(yaw - lookLimit.yaw));
40841
+ yaw = lookLimit.yaw + Math.max(-lookLimit.range, Math.min(lookLimit.range, off));
40842
+ }
40185
40843
  pitch = Math.max(-1.5, Math.min(1.5, pitch - dy * LOOK));
40186
40844
  };
40187
40845
  const onLockChange = () => {
@@ -40273,16 +40931,23 @@ void main(){
40273
40931
  if (isControlHeld || e.altKey && locked) {
40274
40932
  if (!/^F\d+$/.test(e.code)) e.preventDefault();
40275
40933
  }
40276
- if (!e.repeat) keys.add(e.code);
40934
+ if (!e.repeat) {
40935
+ pressCrouch(e.code);
40936
+ keys.add(e.code);
40937
+ }
40277
40938
  o.hooks.key(e.code, true, e);
40278
40939
  };
40279
40940
  const onKeyUp = (e) => {
40280
40941
  keys.delete(e.code);
40942
+ releaseCrouch(e.code);
40281
40943
  if (typing(e)) return;
40282
40944
  o.hooks.key(e.code, false, e);
40283
40945
  };
40284
40946
  const onContext = (e) => e.preventDefault();
40285
- const onBlur = () => keys.clear();
40947
+ const onBlur = () => {
40948
+ keys.clear();
40949
+ crouchPress = null;
40950
+ };
40286
40951
  const onBeforeUnload = (e) => {
40287
40952
  if (!locked && !keys.has("ControlLeft") && !keys.has("ControlRight")) return;
40288
40953
  e.preventDefault();
@@ -40299,6 +40964,21 @@ void main(){
40299
40964
  window.addEventListener("blur", onBlur);
40300
40965
  window.addEventListener("beforeunload", onBeforeUnload);
40301
40966
  return {
40967
+ get walkIntent() {
40968
+ return walkKeys();
40969
+ },
40970
+ canStand(x, z) {
40971
+ return !blocked(x, z);
40972
+ },
40973
+ setWalkHeld(held) {
40974
+ walkHeld = held;
40975
+ },
40976
+ setBend(metres) {
40977
+ bendTarget = metres;
40978
+ },
40979
+ setLookLimit(limit) {
40980
+ lookLimit = limit;
40981
+ },
40302
40982
  get yaw() {
40303
40983
  return yaw;
40304
40984
  },
@@ -40315,10 +40995,10 @@ void main(){
40315
40995
  return px.z;
40316
40996
  },
40317
40997
  get feet() {
40318
- return y;
40998
+ return floorLevel + y;
40319
40999
  },
40320
41000
  get position() {
40321
- return [px.x, y, px.z];
41001
+ return [px.x, floorLevel + y, px.z];
40322
41002
  },
40323
41003
  get speed() {
40324
41004
  return walked;
@@ -40327,49 +41007,43 @@ void main(){
40327
41007
  return running;
40328
41008
  },
40329
41009
  get crouching() {
40330
- return isCrouchHeld();
41010
+ return isCrouched();
40331
41011
  },
40332
41012
  keys,
40333
41013
  update(dt) {
40334
- const crouched = isCrouchHeld();
41014
+ if (crouchPress) crouchPress.held += dt;
41015
+ const crouched = isCrouched();
40335
41016
  crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
40336
41017
  if (!crouched && crouch < 1e-3) crouch = 0;
40337
- const run = !crouched && (keys.has("ShiftLeft") || keys.has("ShiftRight"));
41018
+ bend += (bendTarget - bend) * Math.min(1, dt * BEND_RATE);
41019
+ const run = !crouched && !walkHeld && (keys.has("ShiftLeft") || keys.has("ShiftRight"));
40338
41020
  const fromX = px.x;
40339
41021
  const fromZ = px.z;
40340
41022
  const speed = crouched ? CROUCH_WALK : run ? RUN : WALK;
40341
- let f = 0;
40342
- let r = 0;
40343
- if (keys.has("KeyW") || keys.has("ArrowUp")) f += 1;
40344
- if (keys.has("KeyS") || keys.has("ArrowDown")) f -= 1;
40345
- if (keys.has("KeyD") || keys.has("ArrowRight")) r += 1;
40346
- if (keys.has("KeyA") || keys.has("ArrowLeft")) r -= 1;
40347
- if ((f || r || keys.has("Space")) && eye !== EYE) eye = EYE;
40348
- if (f || r) {
40349
- const len = Math.hypot(f, r);
40350
- const fx = -Math.sin(yaw);
40351
- const fz = -Math.cos(yaw);
40352
- const rx = Math.cos(yaw);
40353
- const rz = -Math.sin(yaw);
40354
- move((fx * f + rx * r) / len * speed * dt, (fz * f + rz * r) / len * speed * dt);
40355
- }
41023
+ const [walkX, walkZ] = walkKeys();
41024
+ if ((walkX || walkZ || keys.has("Space")) && eye !== EYE) eye = EYE;
41025
+ if ((walkX || walkZ) && !walkHeld) move(walkX * speed * dt, walkZ * speed * dt);
40356
41026
  walked = dt > 0 ? Math.hypot(px.x - fromX, px.z - fromZ) / dt : 0;
40357
41027
  running = run && walked > 0;
40358
- if (y <= 0 && keys.has("Space")) vy2 = JUMP;
40359
- if (y > 0 || vy2 > 0) {
41028
+ const floor2 = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
41029
+ if (y < floor2 && vy2 <= 0) y = floor2;
41030
+ if (y <= floor2 && keys.has("Space") && !walkHeld) vy2 = JUMP;
41031
+ if (y > floor2 || vy2 > 0) {
40360
41032
  vy2 -= GRAVITY4 * dt;
40361
41033
  y += vy2 * dt;
40362
- if (y <= 0) {
40363
- y = 0;
41034
+ if (y <= floor2) {
41035
+ y = floor2;
40364
41036
  vy2 = 0;
40365
41037
  }
40366
41038
  }
41039
+ followFloor(dt);
40367
41040
  apply();
40368
41041
  },
40369
41042
  setPose(position, yw, pt2) {
40370
41043
  px.x = position[0];
40371
41044
  px.z = position[2];
40372
- eye = position[1];
41045
+ floorLevel = floorHere();
41046
+ eye = position[1] - floorLevel;
40373
41047
  y = 0;
40374
41048
  vy2 = 0;
40375
41049
  yaw = yw;
@@ -40379,7 +41053,8 @@ void main(){
40379
41053
  look(position, target) {
40380
41054
  px.x = position[0];
40381
41055
  px.z = position[2];
40382
- eye = position[1];
41056
+ floorLevel = floorHere();
41057
+ eye = position[1] - floorLevel;
40383
41058
  y = 0;
40384
41059
  vy2 = 0;
40385
41060
  const dx = target[0] - position[0];
@@ -40398,6 +41073,12 @@ void main(){
40398
41073
  move(dx, dz);
40399
41074
  apply();
40400
41075
  },
41076
+ takeWish() {
41077
+ const out = [wish.x, wish.z];
41078
+ wish.x = 0;
41079
+ wish.z = 0;
41080
+ return out;
41081
+ },
40401
41082
  walk(forward, right) {
40402
41083
  move(-Math.sin(yaw) * forward + Math.cos(yaw) * right, -Math.cos(yaw) * forward - Math.sin(yaw) * right);
40403
41084
  apply();
@@ -40419,6 +41100,1882 @@ void main(){
40419
41100
  };
40420
41101
  }
40421
41102
 
41103
+ // ../game-player/dist/index.js
41104
+ var FURNITURE_PUSH_DEFAULTS = {
41105
+ forceN: 500,
41106
+ gravity: 9.81,
41107
+ frictionByMaterial: { wood_soft: 0.4, wood_hard: 0.4, mdf: 0.4, plywood: 0.4, steel: 0.35, aluminium: 0.35, cast_iron: 0.4, copper: 0.35, plastic: 0.35, rubber: 0.8, fabric: 0.45, foam: 0.5, ceramic: 0.5, glass: 0.3, tempered_glass: 0.3, brick: 0.6, concrete: 0.65 },
41108
+ defaultFriction: 0.45,
41109
+ gripSeconds: { base: 0.4, perKg: 0.015, max: 2.5 },
41110
+ speed: { max: 1.8, min: 0.35 },
41111
+ tapSeconds: 0.2,
41112
+ strainHoldSeconds: 0.25
41113
+ };
41114
+ var frictionOf = (target, rules) => (target.material !== void 0 ? rules.frictionByMaterial[target.material] : void 0) ?? rules.defaultFriction;
41115
+ var loadShare = (target, rules) => frictionOf(target, rules) * target.massKg * rules.gravity / rules.forceN;
41116
+ var gripSecondsFor = (massKg, rules = FURNITURE_PUSH_DEFAULTS) => Math.min(rules.gripSeconds.max, rules.gripSeconds.base + rules.gripSeconds.perKg * Math.max(0, massKg));
41117
+ var canPush = (target, rules = FURNITURE_PUSH_DEFAULTS) => loadShare(target, rules) <= 1;
41118
+ var pushSpeedFor = (target, rules = FURNITURE_PUSH_DEFAULTS) => {
41119
+ const share = loadShare(target, rules);
41120
+ return share > 1 ? 0 : rules.speed.max + (rules.speed.min - rules.speed.max) * share;
41121
+ };
41122
+ var STILL = { dx: 0, dz: 0, engaged: false, hitObstacle: false };
41123
+ function createFurniturePush(overrides = {}) {
41124
+ const rules = { ...FURNITURE_PUSH_DEFAULTS, ...overrides };
41125
+ let phase = "idle";
41126
+ let target = { massKg: 0 };
41127
+ let held = 0;
41128
+ let gripSeconds = 1;
41129
+ let speed = 0;
41130
+ let block = null;
41131
+ let refused = 0;
41132
+ let wasRefused = false;
41133
+ const walkWith = (dt, input) => {
41134
+ refused = Math.max(0, refused - dt);
41135
+ if (block === "too_heavy") return STILL;
41136
+ const [x, z] = input.walk;
41137
+ if (x === 0 && z === 0 || dt <= 0) {
41138
+ if (refused === 0) {
41139
+ phase = "push";
41140
+ block = null;
41141
+ wasRefused = false;
41142
+ }
41143
+ return STILL;
41144
+ }
41145
+ const dx = x * speed * dt;
41146
+ const dz = z * speed * dt;
41147
+ if (input.isFree(dx, dz)) {
41148
+ if (refused === 0) {
41149
+ phase = "push";
41150
+ block = null;
41151
+ }
41152
+ wasRefused = false;
41153
+ return { dx, dz, engaged: false, hitObstacle: false };
41154
+ }
41155
+ const hitObstacle = !wasRefused;
41156
+ wasRefused = true;
41157
+ refused = rules.strainHoldSeconds;
41158
+ phase = "strain";
41159
+ block = "obstacle";
41160
+ return { dx: 0, dz: 0, engaged: false, hitObstacle };
41161
+ };
41162
+ return {
41163
+ get phase() {
41164
+ return phase;
41165
+ },
41166
+ get progress() {
41167
+ return phase === "idle" ? 0 : Math.min(1, held / gripSeconds);
41168
+ },
41169
+ get block() {
41170
+ return block;
41171
+ },
41172
+ get heldSeconds() {
41173
+ return held;
41174
+ },
41175
+ get isTap() {
41176
+ return phase === "grip" && held < rules.tapSeconds;
41177
+ },
41178
+ begin(next) {
41179
+ target = next;
41180
+ phase = "grip";
41181
+ held = 0;
41182
+ block = null;
41183
+ refused = 0;
41184
+ wasRefused = false;
41185
+ gripSeconds = gripSecondsFor(next.massKg, rules);
41186
+ speed = pushSpeedFor(next, rules);
41187
+ },
41188
+ release() {
41189
+ phase = "idle";
41190
+ held = 0;
41191
+ block = null;
41192
+ },
41193
+ update(dt, input) {
41194
+ if (phase === "idle") return STILL;
41195
+ held += dt;
41196
+ if (phase !== "grip") return walkWith(dt, input);
41197
+ if (held < gripSeconds) return STILL;
41198
+ if (canPush(target, rules)) phase = "push";
41199
+ else {
41200
+ phase = "strain";
41201
+ block = "too_heavy";
41202
+ }
41203
+ return { ...STILL, engaged: true };
41204
+ }
41205
+ };
41206
+ }
41207
+ var FRAME = 1 / 30;
41208
+
41209
+ // src/furniture-push.ts
41210
+ var PUSH_REACH = 1.1;
41211
+ var STANDOFF = 0.58;
41212
+ var LOW_TOP = 0.95;
41213
+ var LOW_STANDOFF = 0.42;
41214
+ var LOW_HAND_ON_TOP = 0.07;
41215
+ var LOW_EYE_OVER_TOP = 0.62;
41216
+ var MAX_BEND = 0.8;
41217
+ var STANDING_EYE = 1.7;
41218
+ var APPROACH_SPEED = 1.2;
41219
+ var MAX_BASE_HEIGHT = 0.12;
41220
+ var FLOOR_CLEARANCE = 0.06;
41221
+ var SWEEP_SKIN = 0.015;
41222
+ var HAND_SPREAD = 0.2;
41223
+ var HAND_TOP = 1.35;
41224
+ var HAND_UNDER_TOP = 0.05;
41225
+ var LOOK_RANGE = 0.9;
41226
+ var DRAG_GRAIN_METRES = 0.3;
41227
+ var RING_DELAY_S = 0.1;
41228
+ function contactOf(box2, player) {
41229
+ const x = Math.max(box2.min[0] ?? 0, Math.min(box2.max[0] ?? 0, player.x));
41230
+ const z = Math.max(box2.min[2] ?? 0, Math.min(box2.max[2] ?? 0, player.z));
41231
+ const dx = x - player.x;
41232
+ const dz = z - player.z;
41233
+ const distance4 = Math.hypot(dx, dz);
41234
+ if (distance4 < 1e-3) return { contact: [x, z], into: [-Math.sin(player.yaw), -Math.cos(player.yaw)], distance: 0 };
41235
+ return { contact: [x, z], into: [dx / distance4, dz / distance4], distance: distance4 };
41236
+ }
41237
+ function createRoomPush(options) {
41238
+ const { THREE, player, world, arena, blockers } = options;
41239
+ const control = createFurniturePush(options.rules);
41240
+ const box3 = new THREE.Box3();
41241
+ let leaning = null;
41242
+ let scriptedWalk = null;
41243
+ let dragged = 0;
41244
+ const event = (type, lean) => options.onEvent({ type, objectId: lean.target.objectId, name: lean.target.name, massKg: lean.massKg });
41245
+ const standsClear = (lean, dx, dz) => {
41246
+ const minX = lean.min[0] + dx;
41247
+ const maxX = lean.max[0] + dx;
41248
+ const minZ = lean.min[2] + dz;
41249
+ const maxZ = lean.max[2] + dz;
41250
+ if (Math.max(Math.abs(minX), Math.abs(maxX)) > arena.limit || Math.max(Math.abs(minZ), Math.abs(maxZ)) > arena.limit) return false;
41251
+ for (const c of arena.colliders) {
41252
+ if (lean.max[0] > c[0] && lean.min[0] < c[2] && lean.max[2] > c[1] && lean.min[2] < c[3]) continue;
41253
+ if (maxX > c[0] && minX < c[2] && maxZ > c[1] && minZ < c[3]) return false;
41254
+ }
41255
+ if (blockers.size === 0) return true;
41256
+ const midX = (minX + maxX) / 2;
41257
+ const midZ = (minZ + maxZ) / 2;
41258
+ for (const [x, z] of [[minX, minZ], [maxX, minZ], [minX, maxZ], [maxX, maxZ], [midX, minZ], [midX, maxZ], [minX, midZ], [maxX, midZ]]) {
41259
+ for (const isBlocked2 of blockers) if (isBlocked2(x, z, 0)) return false;
41260
+ }
41261
+ return true;
41262
+ };
41263
+ const isFree = (lean, dx, dz) => {
41264
+ if (!standsClear(lean, dx, dz) || !player.canStand(player.x + dx, player.z + dz)) return false;
41265
+ const distance4 = Math.hypot(dx, dz);
41266
+ const box2 = { min: [lean.min[0], lean.min[1] + FLOOR_CLEARANCE, lean.min[2]], max: [lean.max[0], lean.max[1], lean.max[2]] };
41267
+ return world.sweepBox(box2, [dx / distance4, 0, dz / distance4], distance4 + SWEEP_SKIN, { ignore: lean.target.objectId }) === null;
41268
+ };
41269
+ const release = () => {
41270
+ const lean = leaning;
41271
+ if (!lean) return;
41272
+ leaning = null;
41273
+ if (lean.piece) {
41274
+ lean.piece.held = false;
41275
+ lean.piece.sleeping = false;
41276
+ lean.piece.velocity = [0, 0, 0];
41277
+ }
41278
+ control.release();
41279
+ player.setWalkHeld(false);
41280
+ player.setBend(0);
41281
+ player.setLookLimit(null);
41282
+ options.ring(null);
41283
+ event("release", lean);
41284
+ };
41285
+ const approach = (lean, dt) => {
41286
+ const off = Math.hypot(lean.contact[0] - player.x, lean.contact[1] - player.z) - (lean.isLow ? LOW_STANDOFF : STANDOFF);
41287
+ const move = Math.sign(off) * Math.min(Math.abs(off), APPROACH_SPEED * dt);
41288
+ if (move !== 0) player.moveBy(lean.into[0] * move, lean.into[1] * move);
41289
+ };
41290
+ const moveWith = (lean, dx, dz) => {
41291
+ lean.target.root.position.x += dx;
41292
+ lean.target.root.position.z += dz;
41293
+ lean.target.root.updateMatrixWorld(true);
41294
+ player.moveBy(dx, dz);
41295
+ lean.min[0] += dx;
41296
+ lean.max[0] += dx;
41297
+ lean.min[2] += dz;
41298
+ lean.max[2] += dz;
41299
+ lean.contact[0] += dx;
41300
+ lean.contact[1] += dz;
41301
+ dragged += Math.hypot(dx, dz);
41302
+ if (dragged < DRAG_GRAIN_METRES) return;
41303
+ dragged = 0;
41304
+ options.dragSound(lean.target.objectId, [lean.contact[0], lean.min[1], lean.contact[1]], 0.3 + 0.5 * Math.min(1, lean.massKg / 100));
41305
+ };
41306
+ return {
41307
+ get active() {
41308
+ return leaning !== null;
41309
+ },
41310
+ get state() {
41311
+ return leaning ? { objectId: leaning.target.objectId, phase: control.phase, progress: control.progress, block: control.block, massKg: leaning.massKg } : null;
41312
+ },
41313
+ get isBent() {
41314
+ return leaning?.isLow ?? false;
41315
+ },
41316
+ get avatarState() {
41317
+ return leaning && control.phase !== "idle" ? { phase: control.phase, progress: control.progress } : null;
41318
+ },
41319
+ begin() {
41320
+ if (leaning) return true;
41321
+ const target = options.pick();
41322
+ if (!target || !world.has(target.objectId)) return false;
41323
+ box3.setFromObject(target.root);
41324
+ if (box3.min.y > player.feet + MAX_BASE_HEIGHT) return false;
41325
+ const min2 = [box3.min.x, box3.min.y, box3.min.z];
41326
+ const max2 = [box3.max.x, box3.max.y, box3.max.z];
41327
+ const { contact, into, distance: distance4 } = contactOf({ min: min2, max: max2 }, player);
41328
+ if (distance4 > PUSH_REACH) return false;
41329
+ const massKg = options.massOf(target.objectId);
41330
+ const material = options.materialOf(target.objectId);
41331
+ const isLow = max2[1] - player.feet < LOW_TOP;
41332
+ const side = new THREE.Vector3(-into[1], 0, into[0]);
41333
+ const way = new THREE.Vector3(into[0], 0, into[1]);
41334
+ const up = new THREE.Vector3(0, 1, 0);
41335
+ const basis = isLow ? new THREE.Matrix4().makeBasis(way, up, side) : new THREE.Matrix4().makeBasis(up, way.clone().negate(), side);
41336
+ const palm = new THREE.Quaternion().setFromRotationMatrix(basis);
41337
+ leaning = { target, massKg, piece: target.piece, min: min2, max: max2, contact, into, isLow, palm: [palm.x, palm.y, palm.z, palm.w] };
41338
+ if (target.piece) target.piece.held = true;
41339
+ control.begin(material === void 0 ? { massKg } : { massKg, material });
41340
+ dragged = 0;
41341
+ player.setWalkHeld(true);
41342
+ if (isLow) player.setBend(Math.min(MAX_BEND, Math.max(0, STANDING_EYE - (max2[1] - player.feet + LOW_EYE_OVER_TOP))));
41343
+ player.setLookLimit({ yaw: Math.atan2(-into[0], -into[1]), range: LOOK_RANGE });
41344
+ event("grip", leaning);
41345
+ return true;
41346
+ },
41347
+ end() {
41348
+ if (!leaning) return false;
41349
+ const tap = control.isTap;
41350
+ release();
41351
+ return tap;
41352
+ },
41353
+ setWalk(walk) {
41354
+ scriptedWalk = walk;
41355
+ },
41356
+ step(dt) {
41357
+ const lean = leaning;
41358
+ if (!lean) return;
41359
+ if (!world.has(lean.target.objectId)) {
41360
+ release();
41361
+ return;
41362
+ }
41363
+ const gripping = control.phase === "grip";
41364
+ if (gripping) approach(lean, dt);
41365
+ const result2 = control.update(dt, { walk: scriptedWalk ?? player.walkIntent, isFree: (dx, dz) => isFree(lean, dx, dz) });
41366
+ if (result2.engaged) {
41367
+ if (control.block !== "too_heavy") {
41368
+ lean.piece ?? (lean.piece = options.makePiece(lean.target.objectId));
41369
+ lean.piece.held = true;
41370
+ }
41371
+ event(control.block === "too_heavy" ? "too_heavy" : "engaged", lean);
41372
+ }
41373
+ if (result2.hitObstacle) {
41374
+ options.hitSound(lean.target.objectId, [lean.contact[0], lean.min[1] + 0.3, lean.contact[1]]);
41375
+ event("obstacle", lean);
41376
+ }
41377
+ if (result2.dx !== 0 || result2.dz !== 0) moveWith(lean, result2.dx, result2.dz);
41378
+ options.ring(control.phase === "grip" && control.heldSeconds >= RING_DELAY_S ? control.progress : null);
41379
+ },
41380
+ handTargets() {
41381
+ const lean = leaning;
41382
+ if (!lean) return null;
41383
+ const strain = control.phase === "strain";
41384
+ const y = lean.isLow ? lean.max[1] + 0.01 : Math.min(HAND_TOP, lean.max[1] - HAND_UNDER_TOP);
41385
+ const shake = strain ? Math.sin(performance.now() * 0.045) * 6e-3 : 0;
41386
+ const inset = lean.isLow ? -LOW_HAND_ON_TOP + shake : (strain ? 0.05 : 0.02) + shake;
41387
+ const px = lean.contact[0] - lean.into[0] * inset;
41388
+ const pz = lean.contact[1] - lean.into[1] * inset;
41389
+ const sideX = -lean.into[1] * HAND_SPREAD;
41390
+ const sideZ = lean.into[0] * HAND_SPREAD;
41391
+ const grip = 0.05 + 0.2 * control.progress;
41392
+ return {
41393
+ R: { position: [px + sideX, y, pz + sideZ], quaternion: lean.palm },
41394
+ L: { position: [px - sideX, y, pz - sideZ], quaternion: lean.palm },
41395
+ grip,
41396
+ gripL: grip
41397
+ };
41398
+ }
41399
+ };
41400
+ }
41401
+
41402
+ // ../game-terrain/dist/index.js
41403
+ var TERRAIN_MATERIALS = [
41404
+ {
41405
+ id: "turf",
41406
+ debris: "clods",
41407
+ rimShare: 0.25,
41408
+ reposeDegrees: 55,
41409
+ color: [0.09, 0.2, 0.04],
41410
+ pattern: "grass",
41411
+ hits: {
41412
+ ballistic: { specificEnergy: 3e6, aspect: 0.6 },
41413
+ blunt: { specificEnergy: 2e6, aspect: 0.2, thresholdJoules: 60 },
41414
+ slash: { specificEnergy: 3e5, aspect: 0.5 },
41415
+ explosive: { specificEnergy: 3e6, aspect: 0.45 }
41416
+ },
41417
+ fire: { maxScorch: 1, scorchPerSecond: 0.45, flammable: true }
41418
+ },
41419
+ {
41420
+ id: "soil",
41421
+ debris: "clods",
41422
+ rimShare: 0.35,
41423
+ reposeDegrees: 40,
41424
+ color: [0.2, 0.12, 0.06],
41425
+ pattern: "soil",
41426
+ hits: {
41427
+ ballistic: { specificEnergy: 4e6, aspect: 0.6 },
41428
+ blunt: { specificEnergy: 2e6, aspect: 0.2, thresholdJoules: 60 },
41429
+ slash: { specificEnergy: 3e5, aspect: 0.5 },
41430
+ explosive: { specificEnergy: 4e6, aspect: 0.45 }
41431
+ },
41432
+ fire: { maxScorch: 0.6, scorchPerSecond: 0.15 }
41433
+ },
41434
+ {
41435
+ id: "sand",
41436
+ debris: "dust",
41437
+ rimShare: 0.5,
41438
+ reposeDegrees: 33,
41439
+ color: [0.62, 0.5, 0.3],
41440
+ pattern: "sand",
41441
+ hits: {
41442
+ ballistic: { specificEnergy: 25e5, aspect: 0.5 },
41443
+ blunt: { specificEnergy: 1e6, aspect: 0.2, thresholdJoules: 30 },
41444
+ slash: { specificEnergy: 15e4, aspect: 0.5 },
41445
+ explosive: { specificEnergy: 3e6, aspect: 0.35 }
41446
+ },
41447
+ fire: { maxScorch: 0.2, scorchPerSecond: 0.05 }
41448
+ },
41449
+ {
41450
+ id: "gravel",
41451
+ debris: "chips",
41452
+ rimShare: 0.3,
41453
+ reposeDegrees: 38,
41454
+ color: [0.27, 0.25, 0.22],
41455
+ pattern: "stone",
41456
+ hits: {
41457
+ ballistic: { specificEnergy: 8e6, aspect: 0.4 },
41458
+ blunt: { specificEnergy: 8e6, aspect: 0.2, thresholdJoules: 200 },
41459
+ slash: { specificEnergy: 3e7, aspect: 0.3, thresholdJoules: 400 },
41460
+ explosive: { specificEnergy: 1e7, aspect: 0.35 }
41461
+ },
41462
+ fire: { maxScorch: 0.3, scorchPerSecond: 0.06 }
41463
+ },
41464
+ {
41465
+ id: "asphalt",
41466
+ debris: "chips",
41467
+ color: [0.05, 0.05, 0.055],
41468
+ pattern: "asphalt",
41469
+ hits: {
41470
+ ballistic: { specificEnergy: 3e7, aspect: 0.2, minRadius: 0.03 },
41471
+ blunt: { specificEnergy: 1e8, aspect: 0.1, thresholdJoules: 400 },
41472
+ slash: { specificEnergy: 3e8, aspect: 0.2, thresholdJoules: 400 },
41473
+ explosive: { specificEnergy: 2e8, aspect: 0.12 }
41474
+ },
41475
+ fire: { maxScorch: 0.85, scorchPerSecond: 0.1, burnsAway: 5e-5 }
41476
+ },
41477
+ {
41478
+ id: "concrete",
41479
+ debris: "chips",
41480
+ color: [0.33, 0.33, 0.31],
41481
+ pattern: "concrete",
41482
+ hits: {
41483
+ ballistic: { specificEnergy: 15e7, aspect: 0.14, minRadius: 0.018 },
41484
+ blunt: { specificEnergy: 3e8, aspect: 0.08, thresholdJoules: 800 },
41485
+ slash: { specificEnergy: 1e9, aspect: 0.2, thresholdJoules: 800 },
41486
+ explosive: { specificEnergy: 27e8, aspect: 0.12 }
41487
+ },
41488
+ fire: { maxScorch: 0.45, scorchPerSecond: 0.08 }
41489
+ },
41490
+ {
41491
+ id: "mortar",
41492
+ debris: "chips",
41493
+ color: [0.42, 0.4, 0.36],
41494
+ pattern: "plain",
41495
+ hits: {
41496
+ ballistic: { specificEnergy: 4e7, aspect: 0.2 },
41497
+ blunt: { specificEnergy: 8e7, aspect: 0.1, thresholdJoules: 300 },
41498
+ slash: { specificEnergy: 3e8, aspect: 0.2, thresholdJoules: 300 },
41499
+ explosive: { specificEnergy: 8e8, aspect: 0.14 }
41500
+ },
41501
+ fire: { maxScorch: 0.45, scorchPerSecond: 0.08 }
41502
+ },
41503
+ {
41504
+ id: "ceramic",
41505
+ debris: "shards",
41506
+ color: [0.72, 0.7, 0.66],
41507
+ pattern: "tile",
41508
+ hits: {
41509
+ ballistic: { specificEnergy: 8e6, aspect: 0.1, minRadius: 0.05 },
41510
+ blunt: { specificEnergy: 1e7, aspect: 0.08, thresholdJoules: 80, minRadius: 0.06 },
41511
+ slash: { specificEnergy: 2e8, aspect: 0.1, thresholdJoules: 200, minRadius: 0.03 },
41512
+ explosive: { specificEnergy: 1e8, aspect: 0.03, minRadius: 0.12 }
41513
+ },
41514
+ fire: { maxScorch: 0.5, scorchPerSecond: 0.08 }
41515
+ },
41516
+ {
41517
+ id: "wood_hard",
41518
+ debris: "splinters",
41519
+ color: [0.3, 0.16, 0.07],
41520
+ pattern: "planks",
41521
+ hits: {
41522
+ ballistic: { specificEnergy: 25e6, aspect: 0.35, minRadius: 0.012 },
41523
+ blunt: { specificEnergy: 8e7, aspect: 0.1, thresholdJoules: 120 },
41524
+ slash: { specificEnergy: 15e6, aspect: 0.4, thresholdJoules: 15 },
41525
+ explosive: { specificEnergy: 2e8, aspect: 0.1, minRadius: 0.1 }
41526
+ },
41527
+ fire: { maxScorch: 1, scorchPerSecond: 0.2, burnsAway: 12e-4, flammable: true }
41528
+ },
41529
+ {
41530
+ id: "stone",
41531
+ debris: "chips",
41532
+ color: [0.23, 0.22, 0.2],
41533
+ pattern: "stone",
41534
+ hits: {
41535
+ ballistic: { specificEnergy: 25e7, aspect: 0.12, minRadius: 0.015 },
41536
+ blunt: { specificEnergy: 5e8, aspect: 0.08, thresholdJoules: 1500 },
41537
+ slash: { specificEnergy: 2e9, aspect: 0.2, thresholdJoules: 1500 },
41538
+ explosive: { specificEnergy: 4e9, aspect: 0.14 }
41539
+ },
41540
+ fire: { maxScorch: 0.4, scorchPerSecond: 0.06 }
41541
+ }
41542
+ ];
41543
+ var TERRAIN_PROFILES = [
41544
+ { id: "lawn", strata: [{ material: "turf", thickness: 0.05 }, { material: "soil", thickness: 1.5 }, { material: "stone", thickness: 0.5 }] },
41545
+ { id: "dirt", strata: [{ material: "soil", thickness: 1.8 }, { material: "stone", thickness: 0.5 }] },
41546
+ { id: "sand", strata: [{ material: "sand", thickness: 2.5 }] },
41547
+ { id: "road", strata: [{ material: "asphalt", thickness: 0.05 }, { material: "gravel", thickness: 0.25 }, { material: "soil", thickness: 1.5 }] },
41548
+ { id: "concrete_slab", strata: [{ material: "concrete", thickness: 0.12, reinforcement: { material: "steel", cover: 0.03, spacing: 0.15, resistance: 4 } }] },
41549
+ { id: "suspended_slab", drop: 3, strata: [{ material: "concrete", thickness: 0.12, reinforcement: { material: "steel", cover: 0.03, spacing: 0.15, resistance: 4 } }] },
41550
+ { id: "tile_floor", strata: [{ material: "ceramic", thickness: 8e-3 }, { material: "mortar", thickness: 0.03 }, { material: "concrete", thickness: 0.1, reinforcement: { material: "steel", cover: 0.03, spacing: 0.15, resistance: 4 } }] },
41551
+ { id: "wood_floor", strata: [{ material: "wood_hard", thickness: 0.022 }, { material: "mortar", thickness: 0.03 }, { material: "concrete", thickness: 0.1, reinforcement: { material: "steel", cover: 0.03, spacing: 0.15, resistance: 4 } }] },
41552
+ { id: "stone_paving", strata: [{ material: "stone", thickness: 0.08 }, { material: "gravel", thickness: 0.2 }, { material: "soil", thickness: 1.2 }] }
41553
+ ];
41554
+ var DEFAULT_BAR_YIELD_JOULES = 24e5;
41555
+ var DEFAULT_PROFILE = "concrete_slab";
41556
+ var REST_SPEED2 = 0.6;
41557
+ var RESTITUTION2 = 0.25;
41558
+ var FRICTION = 0.55;
41559
+ var MAX_BOUNCES = 2;
41560
+ var REST_GAP = 0.01;
41561
+ function createDebrisStore(max2) {
41562
+ const list = [];
41563
+ let serial = 0;
41564
+ function makeRoom() {
41565
+ if (list.length < max2()) return;
41566
+ let at3 = 0;
41567
+ let oldest = Infinity;
41568
+ let oldestResting = -1;
41569
+ let restingBorn = Infinity;
41570
+ list.forEach((piece, index) => {
41571
+ if (piece.born < oldest) {
41572
+ oldest = piece.born;
41573
+ at3 = index;
41574
+ }
41575
+ if (piece.atRest && piece.born < restingBorn) {
41576
+ restingBorn = piece.born;
41577
+ oldestResting = index;
41578
+ }
41579
+ });
41580
+ list.splice(oldestResting >= 0 ? oldestResting : at3, 1);
41581
+ }
41582
+ return {
41583
+ list,
41584
+ throwPiece(spec) {
41585
+ makeRoom();
41586
+ list.push({ ...spec, id: ++serial, born: serial, atRest: false, bounces: 0 });
41587
+ },
41588
+ update(dt, world) {
41589
+ for (let i = list.length - 1; i >= 0; i--) {
41590
+ const piece = list[i];
41591
+ if (piece.atRest) {
41592
+ const under = world.heightAt(piece.position[0], piece.position[2]) + piece.radius * 0.6;
41593
+ if (piece.position[1] > under + REST_GAP) piece.atRest = false;
41594
+ else if (piece.position[1] < under - REST_GAP) piece.position[1] = under;
41595
+ if (piece.atRest) continue;
41596
+ }
41597
+ const { position, velocity } = piece;
41598
+ velocity[1] -= world.gravity * dt;
41599
+ position[0] += velocity[0] * dt;
41600
+ position[1] += velocity[1] * dt;
41601
+ position[2] += velocity[2] * dt;
41602
+ const ground = world.heightAt(position[0], position[2]) + piece.radius * 0.6;
41603
+ if (position[1] > ground) continue;
41604
+ position[1] = ground;
41605
+ const speed = Math.hypot(velocity[0], velocity[1], velocity[2]);
41606
+ if (speed < REST_SPEED2 || piece.bounces >= MAX_BOUNCES) {
41607
+ piece.atRest = true;
41608
+ if (world.settle(piece)) list.splice(i, 1);
41609
+ continue;
41610
+ }
41611
+ piece.bounces++;
41612
+ velocity[1] = Math.abs(velocity[1]) * RESTITUTION2;
41613
+ velocity[0] *= FRICTION;
41614
+ velocity[2] *= FRICTION;
41615
+ }
41616
+ },
41617
+ clear() {
41618
+ list.length = 0;
41619
+ }
41620
+ };
41621
+ }
41622
+ var SAME_HOLE = 0.75;
41623
+ function createMarkStore2(budget) {
41624
+ const list = [];
41625
+ const areas = /* @__PURE__ */ new Map();
41626
+ let serial = 0;
41627
+ let version = 0;
41628
+ const areaKey = (x, z) => Math.floor(x / budget.markAreaSize) * 100003 + Math.floor(z / budget.markAreaSize);
41629
+ function detach(mark) {
41630
+ const key = areaKey(mark.x, mark.z);
41631
+ const area = areas.get(key);
41632
+ if (area) {
41633
+ const at22 = area.indexOf(mark);
41634
+ if (at22 >= 0) area.splice(at22, 1);
41635
+ if (area.length === 0) areas.delete(key);
41636
+ }
41637
+ const at3 = list.indexOf(mark);
41638
+ if (at3 >= 0) list.splice(at3, 1);
41639
+ version++;
41640
+ }
41641
+ function attach(mark) {
41642
+ const key = areaKey(mark.x, mark.z);
41643
+ const area = areas.get(key);
41644
+ if (area) area.push(mark);
41645
+ else areas.set(key, [mark]);
41646
+ list.push(mark);
41647
+ version++;
41648
+ }
41649
+ function mergeClosestPair(area) {
41650
+ let bestA = area[0];
41651
+ let bestB = area[1];
41652
+ let best = Infinity;
41653
+ for (let i = 0; i < area.length; i++) {
41654
+ for (let j = i + 1; j < area.length; j++) {
41655
+ const distance4 = Math.hypot(area[i].x - area[j].x, area[i].z - area[j].z);
41656
+ if (distance4 < best) {
41657
+ best = distance4;
41658
+ bestA = area[i];
41659
+ bestB = area[j];
41660
+ }
41661
+ }
41662
+ }
41663
+ const weight = bestA.radius ** 2 + bestB.radius ** 2;
41664
+ const shareB = weight > 0 ? bestB.radius ** 2 / weight : 0.5;
41665
+ detach(bestB);
41666
+ bestA.x += (bestB.x - bestA.x) * shareB;
41667
+ bestA.z += (bestB.z - bestA.z) * shareB;
41668
+ bestA.radius = Math.sqrt(weight);
41669
+ bestA.depth = Math.max(bestA.depth, bestB.depth);
41670
+ bestA.count += bestB.count;
41671
+ version++;
41672
+ }
41673
+ return {
41674
+ list,
41675
+ add(spec) {
41676
+ const mark = { ...spec, id: ++serial, count: 1, born: serial };
41677
+ attach(mark);
41678
+ const area = areas.get(areaKey(mark.x, mark.z));
41679
+ if (area.length > budget.maxMarksPerArea) mergeClosestPair(area);
41680
+ if (list.length > budget.maxMarks) detach(list.reduce((oldest, candidate) => candidate.born < oldest.born ? candidate : oldest));
41681
+ return mark;
41682
+ },
41683
+ near(x, z, radius) {
41684
+ let found;
41685
+ let best = Infinity;
41686
+ const reach = Math.ceil((radius + 0.5) / budget.markAreaSize);
41687
+ const baseX = Math.floor(x / budget.markAreaSize);
41688
+ const baseZ = Math.floor(z / budget.markAreaSize);
41689
+ for (let dx = -reach; dx <= reach; dx++) {
41690
+ for (let dz = -reach; dz <= reach; dz++) {
41691
+ for (const mark of areas.get((baseX + dx) * 100003 + baseZ + dz) ?? []) {
41692
+ const distance4 = Math.hypot(mark.x - x, mark.z - z);
41693
+ if (distance4 < best && distance4 < Math.max(mark.radius, radius) * SAME_HOLE) {
41694
+ best = distance4;
41695
+ found = mark;
41696
+ }
41697
+ }
41698
+ }
41699
+ }
41700
+ return found;
41701
+ },
41702
+ depthAt(x, z) {
41703
+ let depth = 0;
41704
+ for (const mark of areas.get(areaKey(x, z)) ?? []) {
41705
+ const distance4 = Math.hypot(mark.x - x, mark.z - z);
41706
+ if (distance4 < mark.radius) depth = Math.max(depth, mark.depth * (1 - (distance4 / mark.radius) ** 2));
41707
+ }
41708
+ return depth;
41709
+ },
41710
+ remove: detach,
41711
+ removeInside(x, z, radius) {
41712
+ for (const mark of [...list]) if (Math.hypot(mark.x - x, mark.z - z) + mark.radius * 0.5 < radius) detach(mark);
41713
+ },
41714
+ clear() {
41715
+ list.length = 0;
41716
+ areas.clear();
41717
+ version++;
41718
+ },
41719
+ load(marks) {
41720
+ this.clear();
41721
+ for (const mark of marks) {
41722
+ attach({ ...mark });
41723
+ serial = Math.max(serial, mark.id);
41724
+ }
41725
+ },
41726
+ get version() {
41727
+ return version;
41728
+ }
41729
+ };
41730
+ }
41731
+ function createRandom(seed) {
41732
+ let state = seed | 0;
41733
+ return () => {
41734
+ state = state + 1831565813 | 0;
41735
+ let mixed = Math.imul(state ^ state >>> 15, 1 | state);
41736
+ mixed = mixed + Math.imul(mixed ^ mixed >>> 7, 61 | mixed) ^ mixed;
41737
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
41738
+ };
41739
+ }
41740
+ var DEFAULT_TERRAIN_BUDGET = {
41741
+ markAreaSize: 2,
41742
+ maxMarksPerArea: 24,
41743
+ maxMarks: 800,
41744
+ maxDirtyChunksPerFrame: 6,
41745
+ maxDebris: 400,
41746
+ maxDebrisPerBlow: 36,
41747
+ maxSlideCellsPerFrame: 2500,
41748
+ maxBurningCells: 300
41749
+ };
41750
+ var GRAVITY5 = 9.81;
41751
+ var FINE_CELLS = 1.5;
41752
+ var FINE_DEPTH_CELLS = 0.4;
41753
+ var RIGID_WIDENING = 2.5;
41754
+ var COLLAPSE_DEPTH_SHARE = 0.75;
41755
+ var COLLAPSE_SPREAD_BEND = 0.5;
41756
+ var RIM_REACH = 1.45;
41757
+ var FLAME_MINIMUM = 200;
41758
+ var FLAME_REACH = 0.6;
41759
+ var BLAST_HEIGHT_REACH = 0.35;
41760
+ var THROW_SHARE = 0.02;
41761
+ var FIRE_CELL = 0.5;
41762
+ var FIRE_SECONDS = 7;
41763
+ var FIRE_SPREAD_PER_SECOND = 0.35;
41764
+ var SLIDE_SHARE = 0.4;
41765
+ var SLIDE_EPSILON = 2e-3;
41766
+ var PIECE_RADIUS = { clods: 0.045, chips: 0.022, shards: 0.028, splinters: 0.02, dust: 0.012 };
41767
+ var MAX_PIECE_SPEED = { explosive: 14, ballistic: 5, blunt: 2.5, slash: 1.5 };
41768
+ var clampIndex = (value, max2) => Math.max(0, Math.min(max2, value));
41769
+ var sparse = (array) => {
41770
+ const entries = [];
41771
+ array.forEach((value, index) => {
41772
+ if (value !== 0) entries.push([index, value]);
41773
+ });
41774
+ return entries;
41775
+ };
41776
+ var fillSparse = (array, entries) => {
41777
+ array.fill(0);
41778
+ for (const [index, value] of entries) array[index] = value;
41779
+ };
41780
+ function stratumAtDepth(profile, depth) {
41781
+ let top = 0;
41782
+ for (let index2 = 0; index2 < profile.strata.length; index2++) {
41783
+ const stratum2 = profile.strata[index2];
41784
+ if (depth < top + stratum2.thickness - 1e-6) return { index: index2, stratum: stratum2, top };
41785
+ top += stratum2.thickness;
41786
+ }
41787
+ const index = profile.strata.length - 1;
41788
+ const stratum = profile.strata[index];
41789
+ return { index, stratum, top: top - stratum.thickness };
41790
+ }
41791
+ var totalThickness = (profile) => profile.strata.reduce((sum2, stratum) => sum2 + stratum.thickness, 0);
41792
+ function createTerrain(options) {
41793
+ const [x0, z0, x1, z1] = options.bounds;
41794
+ const cellSize = options.cellSize ?? 0.1;
41795
+ const chunkCells = options.chunkCells ?? 32;
41796
+ const columns = Math.max(1, Math.ceil((x1 - x0) / cellSize));
41797
+ const rows = Math.max(1, Math.ceil((z1 - z0) / cellSize));
41798
+ const stride = columns + 1;
41799
+ const vertexCount = stride * (rows + 1);
41800
+ const chunksX = Math.ceil(columns / chunkCells);
41801
+ const chunksZ = Math.ceil(rows / chunkCells);
41802
+ const budget = { ...DEFAULT_TERRAIN_BUDGET, ...options.budget };
41803
+ const gravity = options.gravity ?? GRAVITY5;
41804
+ const materials = new Map([...TERRAIN_MATERIALS, ...options.materials ?? []].map((material) => [material.id, material]));
41805
+ const profileMap = new Map([...TERRAIN_PROFILES, ...options.profiles ?? []].map((profile) => [profile.id, profile]));
41806
+ const profileList = [...profileMap.values()];
41807
+ for (const profile of profileList) {
41808
+ for (const stratum of profile.strata) if (!materials.has(stratum.material)) throw new Error(`game-terrain: profile "${profile.id}" uses material "${stratum.material}", which has no ground behaviour`);
41809
+ }
41810
+ const indexOfProfile = new Map(profileList.map((profile, index) => [profile.id, index]));
41811
+ const maxDepthOf = profileList.map(totalThickness);
41812
+ const base = new Float32Array(vertexCount);
41813
+ if (options.baseHeight) {
41814
+ for (let row = 0; row <= rows; row++) for (let column = 0; column <= columns; column++) base[row * stride + column] = options.baseHeight(x0 + column * cellSize, z0 + row * cellSize);
41815
+ }
41816
+ const dug = new Float32Array(vertexCount);
41817
+ const loose = new Float32Array(vertexCount);
41818
+ const scorch = new Float32Array(vertexCount);
41819
+ const barBend = new Float32Array(vertexCount);
41820
+ const profileIndex = new Uint8Array(vertexCount);
41821
+ const touched = new Uint8Array(chunksX * chunksZ);
41822
+ const mixed = new Uint8Array(chunksX * chunksZ);
41823
+ const dirty = /* @__PURE__ */ new Set();
41824
+ const marks = createMarkStore2(budget);
41825
+ const debrisStore = createDebrisStore(() => budget.maxDebris);
41826
+ const sliding = /* @__PURE__ */ new Set();
41827
+ const burning = /* @__PURE__ */ new Map();
41828
+ const random = createRandom(options.seed ?? 1);
41829
+ let currentLayout = options.layout ?? { surface: DEFAULT_PROFILE };
41830
+ const deformListeners = /* @__PURE__ */ new Set();
41831
+ const emitDeform = (x, z, radius) => {
41832
+ for (const listener of deformListeners) listener({ x, z, radius });
41833
+ };
41834
+ const chunkOfVertex = (column, row) => Math.min(chunksZ - 1, Math.floor(row / chunkCells)) * chunksX + Math.min(chunksX - 1, Math.floor(column / chunkCells));
41835
+ function touchVertex(column, row) {
41836
+ const chunk2 = chunkOfVertex(column, row);
41837
+ touched[chunk2] = 1;
41838
+ dirty.add(chunk2);
41839
+ if (column % chunkCells === 0 && column > 0) markChunk(chunk2 - 1);
41840
+ if (row % chunkCells === 0 && row > 0) markChunk(chunk2 - chunksX);
41841
+ if (column % chunkCells === 0 && row % chunkCells === 0 && column > 0 && row > 0) markChunk(chunk2 - chunksX - 1);
41842
+ }
41843
+ function markChunk(chunk2) {
41844
+ touched[chunk2] = 1;
41845
+ dirty.add(chunk2);
41846
+ }
41847
+ const profileId = (id) => indexOfProfile.get(id) ?? indexOfProfile.get(DEFAULT_PROFILE) ?? 0;
41848
+ function applyLayout(layout) {
41849
+ currentLayout = layout;
41850
+ profileIndex.fill(profileId(layout.surface));
41851
+ for (const zone of layout.zones ?? []) {
41852
+ const index = profileId(zone.surface);
41853
+ const [zx0, zz0, zx1, zz1] = zone.rect;
41854
+ const column0 = clampIndex(Math.round((zx0 - x0) / cellSize), columns);
41855
+ const column1 = clampIndex(Math.round((zx1 - x0) / cellSize), columns);
41856
+ const row0 = clampIndex(Math.round((zz0 - z0) / cellSize), rows);
41857
+ const row1 = clampIndex(Math.round((zz1 - z0) / cellSize), rows);
41858
+ for (let row = row0; row <= row1; row++) profileIndex.fill(index, row * stride + column0, row * stride + column1 + 1);
41859
+ }
41860
+ for (let chunkZ = 0; chunkZ < chunksZ; chunkZ++) {
41861
+ for (let chunkX = 0; chunkX < chunksX; chunkX++) {
41862
+ const chunk2 = chunkZ * chunksX + chunkX;
41863
+ const first = profileIndex[chunkZ * chunkCells * stride + chunkX * chunkCells];
41864
+ let isMixed = 0;
41865
+ const lastRow = Math.min(rows, (chunkZ + 1) * chunkCells);
41866
+ const lastColumn = Math.min(columns, (chunkX + 1) * chunkCells);
41867
+ for (let row = chunkZ * chunkCells; row <= lastRow && !isMixed; row++) {
41868
+ for (let column = chunkX * chunkCells; column <= lastColumn; column++) {
41869
+ if (profileIndex[row * stride + column] !== first) {
41870
+ isMixed = 1;
41871
+ break;
41872
+ }
41873
+ }
41874
+ }
41875
+ mixed[chunk2] = isMixed;
41876
+ dirty.add(chunk2);
41877
+ }
41878
+ }
41879
+ }
41880
+ const vertexHeight = (column, row) => {
41881
+ const index = row * stride + column;
41882
+ return base[index] - dug[index] + loose[index];
41883
+ };
41884
+ const contains = (x, z) => x >= x0 && x <= x1 && z >= z0 && z <= z1;
41885
+ function heightAt(x, z) {
41886
+ if (!contains(x, z)) return options.baseHeight?.(x, z) ?? 0;
41887
+ const fx = Math.max(0, Math.min(columns - 1e-6, (x - x0) / cellSize));
41888
+ const fz = Math.max(0, Math.min(rows - 1e-6, (z - z0) / cellSize));
41889
+ const column = fx | 0;
41890
+ const row = fz | 0;
41891
+ const tx = fx - column;
41892
+ const tz = fz - row;
41893
+ const index = row * stride + column;
41894
+ const h = (i) => base[i] - dug[i] + loose[i];
41895
+ return (h(index) * (1 - tx) + h(index + 1) * tx) * (1 - tz) + (h(index + stride) * (1 - tx) + h(index + stride + 1) * tx) * tz;
41896
+ }
41897
+ const vertexOf = (x, z) => {
41898
+ const column = clampIndex(Math.round((x - x0) / cellSize), columns);
41899
+ const row = clampIndex(Math.round((z - z0) / cellSize), rows);
41900
+ return { column, row, index: row * stride + column };
41901
+ };
41902
+ function vertexMaterial(column, row) {
41903
+ const index = row * stride + column;
41904
+ return materials.get(stratumAtDepth(profileList[profileIndex[index]], dug[index]).stratum.material);
41905
+ }
41906
+ function columnAt(x, z) {
41907
+ const { index } = vertexOf(x, z);
41908
+ const which = profileIndex[index];
41909
+ const profile = profileList[which];
41910
+ const depth = Math.min(maxDepthOf[which], dug[index] + marks.depthAt(x, z));
41911
+ const layer2 = stratumAtDepth(profile, depth);
41912
+ return { profile: profile.id, material: layer2.stratum.material, stratum: layer2.index, depth, remaining: maxDepthOf[which] - depth, scorch: scorch[index], loose: loose[index] };
41913
+ }
41914
+ function removeFromVertex(index, amount) {
41915
+ const fromLoose = Math.min(loose[index], amount);
41916
+ loose[index] = loose[index] - fromLoose;
41917
+ const fromColumn = Math.min(maxDepthOf[profileIndex[index]] - dug[index], amount - fromLoose);
41918
+ if (fromColumn > 0) dug[index] = dug[index] + fromColumn;
41919
+ return fromLoose + Math.max(0, fromColumn);
41920
+ }
41921
+ function addToVertex(index, amount, fillsColumn) {
41922
+ const filled = fillsColumn ? Math.min(dug[index], amount) : 0;
41923
+ dug[index] = dug[index] - filled;
41924
+ loose[index] = loose[index] + amount - filled;
41925
+ }
41926
+ const isSoftExposed = (index) => materials.get(stratumAtDepth(profileList[profileIndex[index]], dug[index]).stratum.material).reposeDegrees !== void 0;
41927
+ function stampBowl(x, z, radius, depth, rimVolume) {
41928
+ const reach = radius * RIM_REACH;
41929
+ const column0 = clampIndex(Math.floor((x - reach - x0) / cellSize), columns);
41930
+ const column1 = clampIndex(Math.ceil((x + reach - x0) / cellSize), columns);
41931
+ const row0 = clampIndex(Math.floor((z - reach - z0) / cellSize), rows);
41932
+ const row1 = clampIndex(Math.ceil((z + reach - z0) / cellSize), rows);
41933
+ const phaseA = random() * 6.283;
41934
+ const phaseB = random() * 6.283;
41935
+ const shapeOf2 = (px, pz) => {
41936
+ const angle = Math.atan2(pz - z, px - x);
41937
+ return Math.hypot(px - x, pz - z) / (radius * (1 + 0.12 * Math.sin(angle * 5 + phaseA) + 0.06 * Math.sin(angle * 11 + phaseB)));
41938
+ };
41939
+ let rimWeight = 0;
41940
+ if (rimVolume > 0) {
41941
+ for (let row = row0; row <= row1; row++) {
41942
+ for (let column = column0; column <= column1; column++) {
41943
+ const q = shapeOf2(x0 + column * cellSize, z0 + row * cellSize);
41944
+ if (q >= 1 && q < RIM_REACH) rimWeight += Math.sin((q - 1) / (RIM_REACH - 1) * Math.PI);
41945
+ }
41946
+ }
41947
+ }
41948
+ const cellArea = cellSize * cellSize;
41949
+ for (let row = row0; row <= row1; row++) {
41950
+ for (let column = column0; column <= column1; column++) {
41951
+ const q = shapeOf2(x0 + column * cellSize, z0 + row * cellSize);
41952
+ const index = row * stride + column;
41953
+ if (q < 1) {
41954
+ removeFromVertex(index, depth * (1 - q * q) * (0.92 + random() * 0.16));
41955
+ sliding.add(index);
41956
+ touchVertex(column, row);
41957
+ } else if (q < RIM_REACH && rimWeight > 0) {
41958
+ loose[index] = loose[index] + rimVolume * Math.sin((q - 1) / (RIM_REACH - 1) * Math.PI) / (rimWeight * cellArea);
41959
+ sliding.add(index);
41960
+ touchVertex(column, row);
41961
+ }
41962
+ }
41963
+ }
41964
+ marks.removeInside(x, z, radius);
41965
+ emitDeform(x, z, reach);
41966
+ }
41967
+ function throwPieces(spec) {
41968
+ const radius = PIECE_RADIUS[spec.material.debris];
41969
+ const pieceVolume = 4 / 3 * Math.PI * radius ** 3;
41970
+ const count = Math.min(spec.quota, Math.max(1, Math.round(spec.volume / pieceVolume)));
41971
+ const mass = Math.max(1e-3, spec.volume * 1500);
41972
+ const launch = Math.min(MAX_PIECE_SPEED[spec.blow], Math.sqrt(2 * spec.energy * THROW_SHARE / mass));
41973
+ for (let i = 0; i < count; i++) {
41974
+ const angle = random() * 6.283;
41975
+ const out = spec.bowlRadius * Math.sqrt(random()) * 0.8;
41976
+ const speed = launch * (0.5 + random() * 0.7);
41977
+ const spread2 = 0.35 + random() * 0.65;
41978
+ const backX = spec.direction ? -(spec.direction[0] ?? 0) * 0.4 : 0;
41979
+ const backZ = spec.direction ? -(spec.direction[2] ?? 0) * 0.4 : 0;
41980
+ debrisStore.throwPiece({
41981
+ material: spec.material.id,
41982
+ kind: spec.material.debris,
41983
+ radius: radius * (0.6 + random() * 0.9),
41984
+ position: [spec.x + Math.cos(angle) * out, spec.y + 0.03, spec.z + Math.sin(angle) * out],
41985
+ velocity: [(Math.cos(angle) * spread2 + backX) * speed, (0.6 + random() * 0.7) * speed, (Math.sin(angle) * spread2 + backZ) * speed],
41986
+ spin: [(random() - 0.5) * 14, (random() - 0.5) * 14, (random() - 0.5) * 14]
41987
+ });
41988
+ }
41989
+ }
41990
+ function strike(blow3, spec) {
41991
+ const { x, z } = spec;
41992
+ const start = columnAt(x, z);
41993
+ const profile = profileMap.get(start.profile);
41994
+ const removed = [];
41995
+ const point = [x, heightAt(x, z), z];
41996
+ let energy = spec.energy;
41997
+ let depth = start.depth;
41998
+ let radius = 0;
41999
+ let carved = 0;
42000
+ const firstMaterial = materials.get(start.material);
42001
+ if (start.loose <= 0) {
42002
+ for (; ; ) {
42003
+ const layer2 = stratumAtDepth(profile, depth);
42004
+ const bottom = layer2.top + layer2.stratum.thickness;
42005
+ const room = bottom - depth;
42006
+ if (room <= 1e-6) break;
42007
+ const material = materials.get(layer2.stratum.material);
42008
+ const response = material.hits[blow3];
42009
+ if (!response || energy < (response.thresholdJoules ?? 0)) break;
42010
+ const bars = layer2.stratum.reinforcement;
42011
+ const pastCover = bars !== void 0 && depth - layer2.top >= bars.cover - 1e-6;
42012
+ const specificEnergy = response.specificEnergy * (pastCover ? bars.resistance : 1);
42013
+ const limit = bars && !pastCover ? Math.min(room, layer2.top + bars.cover - depth) : room;
42014
+ const volume = energy / specificEnergy;
42015
+ const wanted = Math.cbrt(2 * volume / (Math.PI * response.aspect));
42016
+ const wantedDepth = wanted * response.aspect;
42017
+ const takenDepth = Math.min(wantedDepth, limit);
42018
+ const takenRadius = takenDepth < wantedDepth ? takenDepth / response.aspect : wanted;
42019
+ const takenVolume = Math.PI * takenRadius * takenRadius * takenDepth / 2;
42020
+ energy -= takenVolume * specificEnergy;
42021
+ depth += takenDepth;
42022
+ carved += takenDepth;
42023
+ radius = Math.max(radius, takenRadius, response.minRadius ?? 0, spec.contactRadius ?? 0);
42024
+ const last = removed[removed.length - 1];
42025
+ if (last?.material === material.id) last.volume += takenVolume;
42026
+ else removed.push({ material: material.id, volume: takenVolume });
42027
+ if (energy <= 1e-6 || takenDepth >= wantedDepth - 1e-9) break;
42028
+ }
42029
+ } else {
42030
+ const response = firstMaterial.hits[blow3];
42031
+ if (response && energy >= (response.thresholdJoules ?? 0)) {
42032
+ const volume = energy / response.specificEnergy;
42033
+ radius = Math.cbrt(2 * volume / (Math.PI * response.aspect));
42034
+ carved = radius * response.aspect;
42035
+ removed.push({ material: firstMaterial.id, volume });
42036
+ }
42037
+ }
42038
+ const collapsed = loadBars(blow3, spec.energy, x, z, profile);
42039
+ if (radius <= 0) return { point, radius: 0, depth: 0, totalDepth: columnAt(x, z).depth, removed: [], exposed: start.material, atLimit: start.remaining <= 1e-6, rebarExposed: rebarShowing(profile, start.depth), collapsed, fine: true };
42040
+ if (firstMaterial.reposeDegrees === void 0 && start.loose <= 0) radius *= 1 + RIGID_WIDENING * (start.depth / maxDepthOfProfile(profile));
42041
+ const volumeTotal = removed.reduce((sum2, entry) => sum2 + entry.volume, 0);
42042
+ const fine = radius < cellSize * FINE_CELLS;
42043
+ if (fine) depositMark({ x, z, radius, depth: carved, blow: blow3, profile, cap: maxDepthOfProfile(profile) });
42044
+ else {
42045
+ const rimShare = materials.get(removed[0].material).rimShare ?? 0;
42046
+ stampBowl(x, z, radius, carved, volumeTotal * rimShare);
42047
+ }
42048
+ let quota = budget.maxDebrisPerBlow;
42049
+ for (const entry of removed) {
42050
+ const material = materials.get(entry.material);
42051
+ const thrown = entry.volume * (1 - (fine ? 0 : material.rimShare ?? 0));
42052
+ if (thrown <= 0 || quota <= 0) continue;
42053
+ const share = Math.max(1, Math.round(quota * entry.volume / volumeTotal));
42054
+ throwPieces({ x, y: point[1], z, volume: thrown, material, energy: spec.energy * (entry.volume / volumeTotal), direction: spec.direction, quota: share, bowlRadius: radius, blow: blow3 });
42055
+ quota -= share;
42056
+ }
42057
+ const after2 = columnAt(x, z);
42058
+ return { point, radius, depth: carved, totalDepth: after2.depth, removed, exposed: after2.material, atLimit: after2.remaining <= 1e-4, rebarExposed: rebarShowing(profile, after2.depth), collapsed, fine };
42059
+ }
42060
+ function loadBars(blow3, energy, x, z, profile) {
42061
+ const bars = profile.strata.find((stratum) => stratum.reinforcement);
42062
+ if (!bars?.reinforcement) return false;
42063
+ const response = materials.get(bars.material).hits[blow3];
42064
+ if (!response || energy < (response.thresholdJoules ?? 0)) return false;
42065
+ const yieldJoules = bars.reinforcement.yieldJoules ?? DEFAULT_BAR_YIELD_JOULES;
42066
+ let cover = bars.reinforcement.cover;
42067
+ for (const stratum of profile.strata) {
42068
+ if (stratum === bars) break;
42069
+ cover += stratum.thickness;
42070
+ }
42071
+ const reach = Math.max(cellSize * FINE_CELLS, Math.cbrt(2 * energy / response.specificEnergy / (Math.PI * response.aspect)) * (1 + RIGID_WIDENING));
42072
+ const column0 = clampIndex(Math.floor((x - reach - x0) / cellSize), columns);
42073
+ const column1 = clampIndex(Math.ceil((x + reach - x0) / cellSize), columns);
42074
+ const row0 = clampIndex(Math.floor((z - reach - z0) / cellSize), rows);
42075
+ const row1 = clampIndex(Math.ceil((z + reach - z0) / cellSize), rows);
42076
+ const limit = maxDepthOfProfile(profile);
42077
+ let collapsed = false;
42078
+ for (let row = row0; row <= row1; row++) {
42079
+ for (let column = column0; column <= column1; column++) {
42080
+ const index = row * stride + column;
42081
+ if (dug[index] < cover) continue;
42082
+ const q = Math.hypot(x0 + column * cellSize - x, z0 + row * cellSize - z) / reach;
42083
+ if (q >= 1) continue;
42084
+ barBend[index] = barBend[index] + energy / yieldJoules * (1 - q * q);
42085
+ touchVertex(column, row);
42086
+ if (barBend[index] >= 1 && profile.drop !== void 0 && dug[index] >= limit * COLLAPSE_DEPTH_SHARE && dug[index] < limit + profile.drop) {
42087
+ dug[index] = limit + profile.drop;
42088
+ collapsed = true;
42089
+ }
42090
+ }
42091
+ }
42092
+ if (collapsed) emitDeform(x, z, reach);
42093
+ if (collapsed && profile.drop !== void 0) {
42094
+ for (let row = row0; row <= row1; row++) {
42095
+ for (let column = column0; column <= column1; column++) {
42096
+ const index = row * stride + column;
42097
+ if (barBend[index] >= COLLAPSE_SPREAD_BEND && dug[index] < limit + profile.drop) {
42098
+ dug[index] = limit + profile.drop;
42099
+ touchVertex(column, row);
42100
+ }
42101
+ }
42102
+ }
42103
+ }
42104
+ return collapsed;
42105
+ }
42106
+ const maxDepthOfProfile = (profile) => maxDepthOf[indexOfProfile.get(profile.id)];
42107
+ function rebarShowing(profile, depth) {
42108
+ const layer2 = stratumAtDepth(profile, depth);
42109
+ return layer2.stratum.reinforcement !== void 0 && depth - layer2.top >= layer2.stratum.reinforcement.cover - 1e-6;
42110
+ }
42111
+ function depositMark(spec) {
42112
+ const { x, z, radius, depth, blow: blow3, profile, cap } = spec;
42113
+ const dugHere = dug[vertexOf(x, z).index];
42114
+ const existing = marks.near(x, z, radius);
42115
+ let mark;
42116
+ if (existing) {
42117
+ existing.radius = Math.hypot(existing.radius, radius);
42118
+ existing.depth = Math.min(cap - dugHere, existing.depth + depth);
42119
+ existing.count++;
42120
+ existing.blow = blow3;
42121
+ mark = existing;
42122
+ } else {
42123
+ mark = marks.add({ x, z, radius, depth: Math.min(cap - dugHere, depth), blow: blow3, material: "" });
42124
+ }
42125
+ mark.material = stratumAtDepth(profile, dugHere + mark.depth - 1e-6).stratum.material;
42126
+ if (mark.radius >= cellSize * FINE_CELLS || mark.depth >= cellSize * FINE_DEPTH_CELLS) {
42127
+ const bowlRadius = Math.max(mark.radius, cellSize * FINE_CELLS);
42128
+ const bowlDepth = mark.depth;
42129
+ marks.remove(mark);
42130
+ stampBowl(mark.x, mark.z, bowlRadius, bowlDepth, 0);
42131
+ }
42132
+ }
42133
+ function burn(spec) {
42134
+ if (spec.temperature < FLAME_MINIMUM) return;
42135
+ const column0 = clampIndex(Math.floor((spec.x - spec.radius - x0) / cellSize), columns);
42136
+ const column1 = clampIndex(Math.ceil((spec.x + spec.radius - x0) / cellSize), columns);
42137
+ const row0 = clampIndex(Math.floor((spec.z - spec.radius - z0) / cellSize), rows);
42138
+ const row1 = clampIndex(Math.ceil((spec.z + spec.radius - z0) / cellSize), rows);
42139
+ for (let row = row0; row <= row1; row++) {
42140
+ for (let column = column0; column <= column1; column++) {
42141
+ if (Math.hypot(x0 + column * cellSize - spec.x, z0 + row * cellSize - spec.z) > spec.radius) continue;
42142
+ burnVertex(column, row, spec.seconds);
42143
+ }
42144
+ }
42145
+ }
42146
+ function burnVertex(column, row, seconds) {
42147
+ const index = row * stride + column;
42148
+ const material = vertexMaterial(column, row);
42149
+ const response = material.fire;
42150
+ if (!response) return;
42151
+ const before = scorch[index];
42152
+ scorch[index] = Math.max(before, Math.min(response.maxScorch, before + response.scorchPerSecond * seconds));
42153
+ if (response.burnsAway) removeFromVertex(index, response.burnsAway * seconds);
42154
+ if (scorch[index] !== before || response.burnsAway) touchVertex(column, row);
42155
+ if (response.flammable) ignite(x0 + column * cellSize, z0 + row * cellSize);
42156
+ }
42157
+ const fireKey = (x, z) => Math.floor((z - z0) / FIRE_CELL) * 65536 + Math.floor((x - x0) / FIRE_CELL);
42158
+ function ignite(x, z) {
42159
+ const key = fireKey(x, z);
42160
+ if (burning.has(key) || burning.size >= budget.maxBurningCells) return;
42161
+ const index = vertexOf(x, z).index;
42162
+ if (scorch[index] >= 1) return;
42163
+ burning.set(key, FIRE_SECONDS);
42164
+ }
42165
+ function updateFire(dt) {
42166
+ for (const [key, remaining] of [...burning]) {
42167
+ const cellRow = Math.floor(key / 65536);
42168
+ const cellColumn = key - cellRow * 65536;
42169
+ const x = x0 + (cellColumn + 0.5) * FIRE_CELL;
42170
+ const z = z0 + (cellRow + 0.5) * FIRE_CELL;
42171
+ burn({ x, z, radius: FIRE_CELL * 0.75, temperature: 600, seconds: dt });
42172
+ if (random() < FIRE_SPREAD_PER_SECOND * dt) {
42173
+ const angle = Math.floor(random() * 4) * (Math.PI / 2);
42174
+ const nextX = x + Math.cos(angle) * FIRE_CELL;
42175
+ const nextZ = z + Math.sin(angle) * FIRE_CELL;
42176
+ if (contains(nextX, nextZ)) {
42177
+ const { column, row, index } = vertexOf(nextX, nextZ);
42178
+ if (vertexMaterial(column, row).fire?.flammable && scorch[index] < 1) ignite(nextX, nextZ);
42179
+ }
42180
+ }
42181
+ if (remaining - dt <= 0) burning.delete(key);
42182
+ else burning.set(key, remaining - dt);
42183
+ }
42184
+ }
42185
+ function updateSlides() {
42186
+ if (sliding.size === 0) return;
42187
+ let budgetLeft = budget.maxSlideCellsPerFrame;
42188
+ let minX = Infinity;
42189
+ let minZ = Infinity;
42190
+ let maxX = -Infinity;
42191
+ let maxZ = -Infinity;
42192
+ for (const index of sliding) {
42193
+ if (budgetLeft-- <= 0) break;
42194
+ sliding.delete(index);
42195
+ const column = index % stride;
42196
+ const row = (index - column) / stride;
42197
+ if (!isSoftExposed(index)) continue;
42198
+ const material = vertexMaterial(column, row);
42199
+ const slope = Math.tan((material.reposeDegrees ?? 90) * Math.PI / 180) * cellSize;
42200
+ const height = vertexHeight(column, row);
42201
+ const neighbours = [[column - 1, row], [column + 1, row], [column, row - 1], [column, row + 1]];
42202
+ for (const [nextColumn, nextRow] of neighbours) {
42203
+ if (nextColumn < 0 || nextRow < 0 || nextColumn > columns || nextRow > rows) continue;
42204
+ const excess = height - vertexHeight(nextColumn, nextRow) - slope;
42205
+ if (excess <= SLIDE_EPSILON) continue;
42206
+ const moved = removeFromVertex(index, excess * SLIDE_SHARE);
42207
+ if (moved <= 0) continue;
42208
+ const next = nextRow * stride + nextColumn;
42209
+ addToVertex(next, moved, isSoftExposed(next));
42210
+ sliding.add(index);
42211
+ sliding.add(next);
42212
+ touchVertex(column, row);
42213
+ touchVertex(nextColumn, nextRow);
42214
+ const px = x0 + column * cellSize;
42215
+ const pz = z0 + row * cellSize;
42216
+ minX = Math.min(minX, px);
42217
+ maxX = Math.max(maxX, px);
42218
+ minZ = Math.min(minZ, pz);
42219
+ maxZ = Math.max(maxZ, pz);
42220
+ }
42221
+ }
42222
+ if (minX <= maxX) emitDeform((minX + maxX) / 2, (minZ + maxZ) / 2, Math.hypot(maxX - minX, maxZ - minZ) / 2 + cellSize * 2);
42223
+ }
42224
+ function settle(piece) {
42225
+ const material = materials.get(piece.material);
42226
+ if (!material || material.reposeDegrees === void 0) return false;
42227
+ if (!contains(piece.position[0], piece.position[2])) return true;
42228
+ const { index, column, row } = vertexOf(piece.position[0], piece.position[2]);
42229
+ addToVertex(index, 4 / 3 * Math.PI * piece.radius ** 3 / (cellSize * cellSize), false);
42230
+ sliding.add(index);
42231
+ touchVertex(column, row);
42232
+ return true;
42233
+ }
42234
+ function applyHit(hit) {
42235
+ const [x, y, z] = hit.point;
42236
+ switch (hit.type) {
42237
+ case "ballistic":
42238
+ case "blunt":
42239
+ case "slash": {
42240
+ if (!hit.energy) return null;
42241
+ const contactRadius = hit.type === "blunt" ? hit.radius : void 0;
42242
+ return strike(hit.type, { x, z, energy: hit.energy, ...hit.direction ? { direction: hit.direction } : {}, ...contactRadius !== void 0 ? { contactRadius } : {} });
42243
+ }
42244
+ case "explosive": {
42245
+ if (!hit.energy) return null;
42246
+ const above2 = Math.max(0, y - heightAt(x, z));
42247
+ const reach = (hit.radius ?? 1) * BLAST_HEIGHT_REACH;
42248
+ if (above2 >= reach) return null;
42249
+ return strike("explosive", { x, z, energy: hit.energy * (1 - above2 / reach) ** 2 });
42250
+ }
42251
+ case "fire":
42252
+ case "heat":
42253
+ if (y - heightAt(x, z) > (hit.radius ?? 0.2) + FLAME_REACH) return null;
42254
+ burn({ x, z, radius: hit.radius ?? 0.2, temperature: hit.temperature ?? 0, seconds: hit.duration ?? 0.016 });
42255
+ return null;
42256
+ default:
42257
+ return null;
42258
+ }
42259
+ }
42260
+ function raycast(origin, direction, maxDistance) {
42261
+ const [ox = 0, oy = 0, oz = 0] = origin;
42262
+ const [dx = 0, dy = 0, dz = 0] = direction;
42263
+ const step = cellSize * 0.75;
42264
+ let previous = 0;
42265
+ for (let t = 0; t <= maxDistance; t += step) {
42266
+ const x = ox + dx * t;
42267
+ const z = oz + dz * t;
42268
+ if (contains(x, z) && oy + dy * t <= heightAt(x, z)) {
42269
+ let low = previous;
42270
+ let high = t;
42271
+ for (let i = 0; i < 6; i++) {
42272
+ const middle = (low + high) / 2;
42273
+ if (oy + dy * middle <= heightAt(ox + dx * middle, oz + dz * middle)) high = middle;
42274
+ else low = middle;
42275
+ }
42276
+ const px = ox + dx * high;
42277
+ const pz = oz + dz * high;
42278
+ const gx = heightAt(px + cellSize, pz) - heightAt(px - cellSize, pz);
42279
+ const gz = heightAt(px, pz + cellSize) - heightAt(px, pz - cellSize);
42280
+ const length3 = Math.hypot(gx, 2 * cellSize, gz);
42281
+ return { point: [px, oy + dy * high, pz], distance: high, normal: [-gx / length3, 2 * cellSize / length3, -gz / length3] };
42282
+ }
42283
+ previous = t;
42284
+ }
42285
+ return null;
42286
+ }
42287
+ function chunk(index) {
42288
+ const chunkZ = Math.floor(index / chunksX);
42289
+ const chunkX = index - chunkZ * chunksX;
42290
+ return { index, column: chunkX * chunkCells, row: chunkZ * chunkCells, cells: chunkCells, pristine: !touched[index] && !mixed[index] && !options.baseHeight };
42291
+ }
42292
+ function reset() {
42293
+ dug.fill(0);
42294
+ loose.fill(0);
42295
+ scorch.fill(0);
42296
+ barBend.fill(0);
42297
+ touched.fill(0);
42298
+ marks.clear();
42299
+ debrisStore.clear();
42300
+ sliding.clear();
42301
+ burning.clear();
42302
+ applyLayout(currentLayout);
42303
+ }
42304
+ applyLayout(currentLayout);
42305
+ return {
42306
+ bounds: options.bounds,
42307
+ cellSize,
42308
+ columns,
42309
+ rows,
42310
+ stride,
42311
+ chunkCells,
42312
+ chunksX,
42313
+ chunksZ,
42314
+ budget,
42315
+ dug,
42316
+ loose,
42317
+ scorch,
42318
+ barBend,
42319
+ profileIndex,
42320
+ profileList,
42321
+ material: (id) => {
42322
+ const material = materials.get(id);
42323
+ if (!material) throw new Error(`game-terrain: unknown material "${id}"`);
42324
+ return material;
42325
+ },
42326
+ layout: () => currentLayout,
42327
+ setLayout(layout) {
42328
+ currentLayout = layout;
42329
+ reset();
42330
+ },
42331
+ heightAt,
42332
+ vertexHeight,
42333
+ vertexMaterial,
42334
+ columnAt,
42335
+ contains,
42336
+ raycast,
42337
+ strike,
42338
+ burn,
42339
+ applyHit,
42340
+ onDeform(listener) {
42341
+ deformListeners.add(listener);
42342
+ return () => deformListeners.delete(listener);
42343
+ },
42344
+ update(dt) {
42345
+ debrisStore.update(dt, { heightAt, gravity, settle });
42346
+ updateSlides();
42347
+ updateFire(dt);
42348
+ },
42349
+ drainDirtyChunks(max2 = budget.maxDirtyChunksPerFrame) {
42350
+ const taken = [];
42351
+ for (const index of dirty) {
42352
+ if (taken.length >= max2) break;
42353
+ dirty.delete(index);
42354
+ taken.push(chunk(index));
42355
+ }
42356
+ return taken;
42357
+ },
42358
+ chunk,
42359
+ marks: () => marks.list,
42360
+ debris: () => debrisStore.list,
42361
+ burningPoints: () => [...burning.keys()].map((key) => {
42362
+ const cellRow = Math.floor(key / 65536);
42363
+ const cellColumn = key - cellRow * 65536;
42364
+ return [x0 + (cellColumn + 0.5) * FIRE_CELL, z0 + (cellRow + 0.5) * FIRE_CELL];
42365
+ }),
42366
+ get marksVersion() {
42367
+ return marks.version;
42368
+ },
42369
+ stats: () => ({ marks: marks.list.length, debris: debrisStore.list.length, debrisMoving: debrisStore.list.filter((piece) => !piece.atRest).length, dirtyChunks: dirty.size, burning: burning.size }),
42370
+ reset,
42371
+ serialize() {
42372
+ return { layout: currentLayout, dug: sparse(dug), loose: sparse(loose), scorch: sparse(scorch), barBend: sparse(barBend), marks: marks.list.map((mark) => ({ ...mark })) };
42373
+ },
42374
+ load(snapshot) {
42375
+ currentLayout = snapshot.layout;
42376
+ reset();
42377
+ fillSparse(dug, snapshot.dug);
42378
+ fillSparse(loose, snapshot.loose);
42379
+ fillSparse(scorch, snapshot.scorch);
42380
+ fillSparse(barBend, snapshot.barBend ?? []);
42381
+ marks.load(snapshot.marks);
42382
+ for (let chunkIndex = 0; chunkIndex < touched.length; chunkIndex++) markChunk(chunkIndex);
42383
+ }
42384
+ };
42385
+ }
42386
+ var PART_ID = "ground";
42387
+ var PART_DEPTH = 3;
42388
+ var RAY_BACKOFF = 1;
42389
+ function createTerrainImpl(terrain, options) {
42390
+ const { id } = options;
42391
+ const [x0, z0, x1, z1] = terrain.bounds;
42392
+ const spec = () => ({
42393
+ name: options.name ?? "Ground",
42394
+ parts: [
42395
+ {
42396
+ id: PART_ID,
42397
+ material: terrain.columnAt((x0 + x1) / 2, (z0 + z1) / 2).material,
42398
+ shape: { kind: "box", size: [x1 - x0, PART_DEPTH, z1 - z0] },
42399
+ position: [(x0 + x1) / 2, -PART_DEPTH / 2, (z0 + z1) / 2],
42400
+ hp: 1e12,
42401
+ anchored: true,
42402
+ tags: ["terrain"]
42403
+ }
42404
+ ],
42405
+ joints: []
42406
+ });
42407
+ const resolve2 = (hit) => {
42408
+ if (hit.type !== "ballistic" && hit.type !== "blunt" && hit.type !== "slash") return hit;
42409
+ if (!hit.direction) return { ...hit, point: [hit.point[0], terrain.heightAt(hit.point[0], hit.point[2]), hit.point[2]] };
42410
+ const [dx, dy, dz] = hit.direction;
42411
+ const from = [hit.point[0] - dx * RAY_BACKOFF, hit.point[1] - dy * RAY_BACKOFF, hit.point[2] - dz * RAY_BACKOFF];
42412
+ const onGround = terrain.raycast(from, hit.direction, RAY_BACKOFF + PART_DEPTH);
42413
+ return onGround ? { ...hit, point: onGround.point } : hit;
42414
+ };
42415
+ return {
42416
+ id,
42417
+ ...options.name ? { name: options.name } : {},
42418
+ describe: spec,
42419
+ hit(hit) {
42420
+ const landed = resolve2(hit);
42421
+ const result2 = terrain.applyHit(landed);
42422
+ if (!result2 || result2.radius <= 0) return [];
42423
+ options.onBlow?.(result2, landed);
42424
+ return [{ type: "damage", object: id, time: 0, part: PART_ID, amount: 0, hp: 1e12, damageType: hit.type, ...hit.source ? { source: hit.source } : {} }];
42425
+ },
42426
+ update(dt) {
42427
+ terrain.update(dt);
42428
+ return [];
42429
+ },
42430
+ state() {
42431
+ return {
42432
+ id,
42433
+ position: [0, 0, 0],
42434
+ rotation: [0, 0, 0, 1],
42435
+ parts: [],
42436
+ joints: [],
42437
+ groups: [[PART_ID]],
42438
+ destroyed: false
42439
+ };
42440
+ },
42441
+ reset: () => terrain.reset(),
42442
+ serialize: () => terrain.serialize(),
42443
+ load: (data) => terrain.load(data)
42444
+ };
42445
+ }
42446
+ var TERRAIN_PATTERNS = ["plain", "grass", "soil", "sand", "asphalt", "concrete", "tile", "planks", "stone"];
42447
+
42448
+ // ../game-terrain-three/dist/index.js
42449
+ var patternIndex = (pattern) => Math.max(0, TERRAIN_PATTERNS.indexOf(pattern ?? "plain"));
42450
+ var SURFACE_GLSL = (
42451
+ /* glsl */
42452
+ `
42453
+ float tsHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
42454
+ float tsNoise(vec2 p) {
42455
+ vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f);
42456
+ return mix(mix(tsHash(i), tsHash(i + vec2(1.0, 0.0)), f.x), mix(tsHash(i + vec2(0.0, 1.0)), tsHash(i + vec2(1.0, 1.0)), f.x), f.y);
42457
+ }
42458
+ float tsFbm(vec2 p) { return tsNoise(p) * 0.55 + tsNoise(p * 2.3 + 7.1) * 0.3 + tsNoise(p * 5.1 + 3.3) * 0.15; }
42459
+ float tsLine(float value, float period, float width) { float d = abs(fract(value / period + 0.5) - 0.5) * period; return 1.0 - smoothstep(width * 0.6, width, d); }
42460
+ vec3 terrainSurface(vec3 base, vec2 xz, float kind) {
42461
+ float k = floor(kind + 0.5);
42462
+ if (k < 0.5) return base * (0.86 + 0.28 * tsFbm(xz * 9.0));
42463
+ if (k < 1.5) {
42464
+ float blades = tsNoise(xz * vec2(70.0, 55.0)) * 0.6 + tsNoise(xz * 22.0) * 0.4;
42465
+ float patches = tsFbm(xz * 1.3);
42466
+ return base * (0.55 + 0.8 * blades) * mix(vec3(1.25, 1.05, 0.7), vec3(0.8, 1.1, 0.85), patches);
42467
+ }
42468
+ if (k < 2.5) return base * (0.6 + 0.75 * tsFbm(xz * 6.0)) * (0.82 + 0.36 * tsNoise(xz * 48.0));
42469
+ if (k < 3.5) return base * (0.88 + 0.12 * sin(xz.x * 22.0 + tsFbm(xz * 2.0) * 9.0)) * (0.92 + 0.16 * tsNoise(xz * 90.0));
42470
+ if (k < 4.5) { float grain = tsNoise(xz * 120.0); return base * (0.55 + 1.1 * grain * grain) * (0.85 + 0.3 * tsFbm(xz * 0.8)); }
42471
+ if (k < 5.5) {
42472
+ float joint = max(tsLine(xz.x, 3.0, 0.012), tsLine(xz.y, 3.0, 0.012));
42473
+ return base * (0.82 + 0.3 * tsFbm(xz * 3.5)) * (0.94 + 0.12 * tsNoise(xz * 130.0)) * (1.0 - 0.6 * joint);
42474
+ }
42475
+ if (k < 6.5) {
42476
+ vec2 cell = floor(xz / 0.6);
42477
+ float grout = max(tsLine(xz.x, 0.6, 0.006), tsLine(xz.y, 0.6, 0.006));
42478
+ return mix(base * (0.93 + 0.14 * tsHash(cell)) * (0.97 + 0.06 * tsFbm(xz * 7.0)), vec3(0.14, 0.13, 0.12), grout);
42479
+ }
42480
+ if (k < 7.5) {
42481
+ float row = floor(xz.y / 0.14);
42482
+ float along = xz.x + tsHash(vec2(row, 3.0)) * 2.4;
42483
+ float seam = max(tsLine(xz.y, 0.14, 0.004), tsLine(along, 2.4, 0.004));
42484
+ float grain = tsNoise(vec2(along * 3.0, xz.y * 70.0)) * 0.6 + tsNoise(vec2(along * 11.0, xz.y * 180.0)) * 0.4;
42485
+ return base * (0.72 + 0.56 * tsHash(vec2(row, floor(along / 2.4)))) * (0.75 + 0.5 * grain) * (1.0 - 0.75 * seam);
42486
+ }
42487
+ vec2 cell = floor(xz / 0.22);
42488
+ float seams = max(tsLine(xz.x, 0.22, 0.012), tsLine(xz.y, 0.22, 0.012));
42489
+ return base * (0.7 + 0.6 * tsHash(cell)) * (0.85 + 0.3 * tsFbm(xz * 16.0)) * (1.0 - 0.55 * seams);
42490
+ }
42491
+ `
42492
+ );
42493
+ function createSurfaceMaterial(THREE) {
42494
+ const material = new THREE.MeshStandardMaterial({ roughness: 0.95, metalness: 0 });
42495
+ material.onBeforeCompile = (shader) => {
42496
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", "#include <common>\nattribute float aPattern;\nattribute vec3 aBase;\nattribute vec2 aShade;\nflat varying float vPattern;\nflat varying vec3 vBase;\nvarying vec2 vShade;\nvarying vec2 vGroundXZ;").replace("#include <begin_vertex>", "#include <begin_vertex>\nvPattern = aPattern;\nvBase = aBase;\nvShade = aShade;\nvGroundXZ = position.xz;");
42497
+ shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
42498
+ flat varying float vPattern;
42499
+ flat varying vec3 vBase;
42500
+ varying vec2 vShade;
42501
+ varying vec2 vGroundXZ;
42502
+ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment>\ndiffuseColor.rgb = mix(terrainSurface(vBase, vGroundXZ, vPattern), vec3(0.012, 0.011, 0.01), vShade.y) * vShade.x;");
42503
+ };
42504
+ material.customProgramCacheKey = () => "game-terrain-surface";
42505
+ return material;
42506
+ }
42507
+ var DEPTH_SHADE = 1.6;
42508
+ var MIN_DEPTH_LIGHT = 0.55;
42509
+ var LOOSE_VISIBLE = 4e-3;
42510
+ var THIN_SKIN = 0.1;
42511
+ var SKIRT_QUADS = 4;
42512
+ var BROKEN_DEPTH = 4e-3;
42513
+ var BROKEN_TONE = 0.62;
42514
+ function vertexLook(terrain, column, row, base, shade) {
42515
+ const index = row * terrain.stride + column;
42516
+ let material = terrain.vertexMaterial(column, row);
42517
+ if (terrain.loose[index] > LOOSE_VISIBLE) {
42518
+ const profile = terrain.profileList[terrain.profileIndex[index]];
42519
+ const first = profile.strata[0];
42520
+ material = terrain.material(first.thickness < THIN_SKIN && profile.strata[1] ? profile.strata[1].material : first.material);
42521
+ }
42522
+ const broken = material.pattern === "concrete" && terrain.dug[index] > BROKEN_DEPTH;
42523
+ const tone = broken ? BROKEN_TONE : 1;
42524
+ base[0] = material.color[0] * tone;
42525
+ base[1] = material.color[1] * tone;
42526
+ base[2] = material.color[2] * tone;
42527
+ shade[0] = Math.max(MIN_DEPTH_LIGHT, 1 - terrain.dug[index] / DEPTH_SHADE);
42528
+ shade[1] = terrain.scorch[index];
42529
+ return patternIndex(broken ? "soil" : material.pattern);
42530
+ }
42531
+ function buildChunkGeometry(THREE, terrain, chunk, step) {
42532
+ const [x0, z0] = terrain.bounds;
42533
+ const { cellSize, columns, rows } = terrain;
42534
+ const lastColumn = Math.min(columns, chunk.column + chunk.cells);
42535
+ const lastRow = Math.min(rows, chunk.row + chunk.cells);
42536
+ const across = Math.ceil((lastColumn - chunk.column) / step) + 1;
42537
+ const down = Math.ceil((lastRow - chunk.row) / step) + 1;
42538
+ const positions = new Float32Array(across * down * 3);
42539
+ const normals = new Float32Array(across * down * 3);
42540
+ const bases = new Float32Array(across * down * 3);
42541
+ const shades = new Float32Array(across * down * 2);
42542
+ const patterns = new Float32Array(across * down);
42543
+ const base = [0, 0, 0];
42544
+ const shade = [1, 0];
42545
+ let at3 = 0;
42546
+ for (let j = 0; j < down; j++) {
42547
+ const row = Math.min(lastRow, chunk.row + j * step);
42548
+ for (let i = 0; i < across; i++) {
42549
+ const column = Math.min(lastColumn, chunk.column + i * step);
42550
+ const height = terrain.vertexHeight(column, row);
42551
+ positions[at3 * 3] = x0 + column * cellSize;
42552
+ positions[at3 * 3 + 1] = height;
42553
+ positions[at3 * 3 + 2] = z0 + row * cellSize;
42554
+ const left = terrain.vertexHeight(Math.max(0, column - 1), row);
42555
+ const right = terrain.vertexHeight(Math.min(columns, column + 1), row);
42556
+ const back = terrain.vertexHeight(column, Math.max(0, row - 1));
42557
+ const front = terrain.vertexHeight(column, Math.min(rows, row + 1));
42558
+ const nx = left - right;
42559
+ const ny = 2 * cellSize;
42560
+ const nz = back - front;
42561
+ const length3 = Math.hypot(nx, ny, nz);
42562
+ normals[at3 * 3] = nx / length3;
42563
+ normals[at3 * 3 + 1] = ny / length3;
42564
+ normals[at3 * 3 + 2] = nz / length3;
42565
+ patterns[at3] = vertexLook(terrain, column, row, base, shade);
42566
+ bases.set(base, at3 * 3);
42567
+ shades.set(shade, at3 * 2);
42568
+ at3++;
42569
+ }
42570
+ }
42571
+ const indices = new Uint32Array((across - 1) * (down - 1) * 6);
42572
+ let cursor = 0;
42573
+ for (let j = 0; j < down - 1; j++) {
42574
+ for (let i = 0; i < across - 1; i++) {
42575
+ const a = j * across + i;
42576
+ const b = a + 1;
42577
+ const c = a + across;
42578
+ const d = c + 1;
42579
+ indices[cursor++] = a;
42580
+ indices[cursor++] = c;
42581
+ indices[cursor++] = b;
42582
+ indices[cursor++] = c;
42583
+ indices[cursor++] = d;
42584
+ indices[cursor++] = b;
42585
+ }
42586
+ }
42587
+ const geometry = new THREE.BufferGeometry();
42588
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
42589
+ geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
42590
+ geometry.setAttribute("aBase", new THREE.BufferAttribute(bases, 3));
42591
+ geometry.setAttribute("aShade", new THREE.BufferAttribute(shades, 2));
42592
+ geometry.setAttribute("aPattern", new THREE.BufferAttribute(patterns, 1));
42593
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
42594
+ return geometry;
42595
+ }
42596
+ function buildPristineGeometry(THREE, terrain, chunks, skirt) {
42597
+ const [x0, z0] = terrain.bounds;
42598
+ const { cellSize, columns, rows } = terrain;
42599
+ const [, , x1, z1] = terrain.bounds;
42600
+ const quads = chunks.length + SKIRT_QUADS;
42601
+ const positions = new Float32Array(quads * 12);
42602
+ const normals = new Float32Array(quads * 12);
42603
+ const bases = new Float32Array(quads * 12);
42604
+ const shades = new Float32Array(quads * 8);
42605
+ const patterns = new Float32Array(quads * 4);
42606
+ const indices = new Uint32Array(quads * 6);
42607
+ const base = [0, 0, 0];
42608
+ const shade = [1, 0];
42609
+ chunks.forEach((chunk, n) => {
42610
+ const lastColumn = Math.min(columns, chunk.column + chunk.cells);
42611
+ const lastRow = Math.min(rows, chunk.row + chunk.cells);
42612
+ const corners = [[chunk.column, chunk.row], [lastColumn, chunk.row], [chunk.column, lastRow], [lastColumn, lastRow]];
42613
+ const pattern = vertexLook(terrain, chunk.column, chunk.row, base, shade);
42614
+ corners.forEach(([column, row], corner) => {
42615
+ const at3 = n * 4 + corner;
42616
+ positions[at3 * 3] = x0 + column * cellSize;
42617
+ positions[at3 * 3 + 1] = terrain.vertexHeight(column, row);
42618
+ positions[at3 * 3 + 2] = z0 + row * cellSize;
42619
+ normals[at3 * 3 + 1] = 1;
42620
+ bases.set(base, at3 * 3);
42621
+ shades.set(shade, at3 * 2);
42622
+ patterns[at3] = pattern;
42623
+ });
42624
+ const first = n * 4;
42625
+ indices.set([first, first + 2, first + 1, first + 2, first + 3, first + 1], n * 6);
42626
+ });
42627
+ const surface = terrain.material(terrain.profileList.find((profile) => profile.id === terrain.layout().surface)?.strata[0]?.material ?? terrain.profileList[terrain.profileIndex[0]].strata[0].material);
42628
+ const rects = [
42629
+ [x0 - skirt, z0 - skirt, x1 + skirt, z0],
42630
+ [x0 - skirt, z1, x1 + skirt, z1 + skirt],
42631
+ [x0 - skirt, z0, x0, z1],
42632
+ [x1, z0, x1 + skirt, z1]
42633
+ ];
42634
+ rects.forEach(([ax, az, bx, bz], r) => {
42635
+ const n = chunks.length + r;
42636
+ const corners = [[ax, az], [bx, az], [ax, bz], [bx, bz]];
42637
+ corners.forEach(([x, z], corner) => {
42638
+ const at3 = n * 4 + corner;
42639
+ positions[at3 * 3] = x;
42640
+ positions[at3 * 3 + 2] = z;
42641
+ normals[at3 * 3 + 1] = 1;
42642
+ bases.set(surface.color, at3 * 3);
42643
+ shades[at3 * 2] = 1;
42644
+ patterns[at3] = patternIndex(surface.pattern);
42645
+ });
42646
+ const first = n * 4;
42647
+ indices.set([first, first + 2, first + 1, first + 2, first + 3, first + 1], n * 6);
42648
+ });
42649
+ const geometry = new THREE.BufferGeometry();
42650
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
42651
+ geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
42652
+ geometry.setAttribute("aBase", new THREE.BufferAttribute(bases, 3));
42653
+ geometry.setAttribute("aShade", new THREE.BufferAttribute(shades, 2));
42654
+ geometry.setAttribute("aPattern", new THREE.BufferAttribute(patterns, 1));
42655
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
42656
+ return geometry;
42657
+ }
42658
+ var MARK_LIFT = 6e-3;
42659
+ var MARK_REACH = 1.5;
42660
+ var MARK_TEXTURE_SIZE = 64;
42661
+ var MARK_DARKEN = 0.85;
42662
+ var PIECE_SHAPE = { clods: [1, 0.85, 1], chips: [1, 0.6, 0.8], shards: [1.3, 0.2, 1], splinters: [0.3, 0.25, 2.4], dust: [1, 0.8, 1] };
42663
+ function createMarkTexture(THREE) {
42664
+ const size = MARK_TEXTURE_SIZE;
42665
+ const data = new Uint8Array(size * size * 4);
42666
+ for (let y = 0; y < size; y++) {
42667
+ for (let x = 0; x < size; x++) {
42668
+ const u = (x + 0.5) / size - 0.5;
42669
+ const v = (y + 0.5) / size - 0.5;
42670
+ const angle = Math.atan2(v, u);
42671
+ const lobes = 0.86 + 0.09 * Math.sin(angle * 5) + 0.05 * Math.sin(angle * 13 + 1);
42672
+ const q = Math.hypot(u, v) / (0.5 * lobes);
42673
+ const shade = q < 0.55 ? 0.42 + q * 0.5 : 0.7 + (q - 0.55) * 0.8;
42674
+ const at3 = (y * size + x) * 4;
42675
+ data[at3] = data[at3 + 1] = data[at3 + 2] = Math.round(Math.min(1, shade) * 255);
42676
+ data[at3 + 3] = q < 0.8 ? 255 : q < 1 ? Math.round((1 - (q - 0.8) / 0.2) * 255) : 0;
42677
+ }
42678
+ }
42679
+ const texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
42680
+ texture.magFilter = THREE.LinearFilter;
42681
+ texture.minFilter = THREE.LinearFilter;
42682
+ texture.needsUpdate = true;
42683
+ return texture;
42684
+ }
42685
+ function createMarkLayer2(THREE, terrain, drawDistance) {
42686
+ const geometry = new THREE.PlaneGeometry(1, 1);
42687
+ geometry.rotateX(-Math.PI / 2);
42688
+ const texture = createMarkTexture(THREE);
42689
+ const material = new THREE.MeshStandardMaterial({ map: texture, transparent: true, depthWrite: false, roughness: 1, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 });
42690
+ const mesh2 = new THREE.InstancedMesh(geometry, material, terrain.budget.maxMarks);
42691
+ mesh2.name = "terrain-marks";
42692
+ mesh2.count = 0;
42693
+ mesh2.frustumCulled = false;
42694
+ mesh2.receiveShadow = true;
42695
+ mesh2.renderOrder = 1;
42696
+ const matrix = new THREE.Matrix4();
42697
+ const rotation = new THREE.Matrix4();
42698
+ const scale2 = new THREE.Matrix4();
42699
+ const colour = new THREE.Color();
42700
+ let seen = "";
42701
+ let cameraX = Infinity;
42702
+ let cameraZ = Infinity;
42703
+ return {
42704
+ mesh: mesh2,
42705
+ sync(camera2) {
42706
+ const marks = terrain.marks();
42707
+ let blows = 0;
42708
+ for (const mark of marks) blows += mark.count;
42709
+ const stamp = `${terrain.marksVersion}:${blows}`;
42710
+ if (stamp === seen && Math.hypot(camera2.x - cameraX, camera2.z - cameraZ) < 5) return;
42711
+ seen = stamp;
42712
+ cameraX = camera2.x;
42713
+ cameraZ = camera2.z;
42714
+ let count = 0;
42715
+ for (const mark of marks) {
42716
+ if (Math.hypot(mark.x - camera2.x, mark.z - camera2.z) > drawDistance) continue;
42717
+ const size = mark.radius * 2 * MARK_REACH;
42718
+ rotation.makeRotationY(mark.id * 2.399);
42719
+ scale2.makeScale(size, 1, size);
42720
+ matrix.multiplyMatrices(rotation, scale2);
42721
+ matrix.setPosition(mark.x, terrain.heightAt(mark.x, mark.z) + MARK_LIFT, mark.z);
42722
+ mesh2.setMatrixAt(count, matrix);
42723
+ const [r, g, b] = terrain.material(mark.material).color;
42724
+ colour.setRGB(r * MARK_DARKEN + 0.04, g * MARK_DARKEN + 0.04, b * MARK_DARKEN + 0.04);
42725
+ mesh2.setColorAt(count, colour);
42726
+ count++;
42727
+ }
42728
+ mesh2.count = count;
42729
+ mesh2.instanceMatrix.needsUpdate = true;
42730
+ if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
42731
+ },
42732
+ dispose() {
42733
+ geometry.dispose();
42734
+ material.dispose();
42735
+ texture.dispose();
42736
+ mesh2.removeFromParent();
42737
+ }
42738
+ };
42739
+ }
42740
+ function createDebrisLayer(THREE, terrain) {
42741
+ const geometry = new THREE.IcosahedronGeometry(1, 0);
42742
+ const material = new THREE.MeshStandardMaterial({ roughness: 0.95, flatShading: true });
42743
+ const mesh2 = new THREE.InstancedMesh(geometry, material, terrain.budget.maxDebris);
42744
+ mesh2.name = "terrain-debris";
42745
+ mesh2.count = 0;
42746
+ mesh2.castShadow = true;
42747
+ mesh2.frustumCulled = false;
42748
+ const matrix = new THREE.Matrix4();
42749
+ const position = new THREE.Vector3();
42750
+ const scale2 = new THREE.Vector3();
42751
+ const quaternion = new THREE.Quaternion();
42752
+ const euler = new THREE.Euler();
42753
+ const colour = new THREE.Color();
42754
+ return {
42755
+ mesh: mesh2,
42756
+ sync() {
42757
+ const pieces = terrain.debris();
42758
+ let count = 0;
42759
+ for (const piece of pieces) {
42760
+ const [x, y, z] = piece.position;
42761
+ const travelled = x * 1.7 + z * 1.3 + y;
42762
+ euler.set(piece.spin[0] * travelled * 0.2 + piece.id, piece.spin[1] * travelled * 0.2, piece.spin[2] * travelled * 0.2);
42763
+ const shape = PIECE_SHAPE[piece.kind];
42764
+ scale2.set(shape[0] * piece.radius, shape[1] * piece.radius, shape[2] * piece.radius);
42765
+ matrix.compose(position.set(x, y, z), quaternion.setFromEuler(euler), scale2);
42766
+ mesh2.setMatrixAt(count, matrix);
42767
+ const [r, g, b] = terrain.material(piece.material).color;
42768
+ const tone = 0.85 + piece.id * 37 % 30 / 100;
42769
+ mesh2.setColorAt(count, colour.setRGB(r * tone, g * tone, b * tone));
42770
+ count++;
42771
+ }
42772
+ mesh2.count = count;
42773
+ mesh2.instanceMatrix.needsUpdate = true;
42774
+ if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
42775
+ },
42776
+ dispose() {
42777
+ geometry.dispose();
42778
+ material.dispose();
42779
+ mesh2.removeFromParent();
42780
+ }
42781
+ };
42782
+ }
42783
+ var BAR_RADIUS = 6e-3;
42784
+ var EXPOSED_MARGIN = 2e-3;
42785
+ var MAX_SAG = 0.07;
42786
+ function buildRebarGeometry(THREE, terrain, chunk) {
42787
+ const [x0, z0] = terrain.bounds;
42788
+ const { cellSize, columns, rows, stride } = terrain;
42789
+ const lastColumn = Math.min(columns, chunk.column + chunk.cells);
42790
+ const lastRow = Math.min(rows, chunk.row + chunk.cells);
42791
+ const boxes = [];
42792
+ const coverOf = (index) => {
42793
+ let top = 0;
42794
+ for (const stratum of terrain.profileList[terrain.profileIndex[index]].strata) {
42795
+ if (stratum.reinforcement) return { depth: top + stratum.reinforcement.cover, spacing: stratum.reinforcement.spacing };
42796
+ top += stratum.thickness;
42797
+ }
42798
+ return null;
42799
+ };
42800
+ const showing = (index, depth) => {
42801
+ const profile = terrain.profileList[terrain.profileIndex[index]];
42802
+ const limit = profile.strata.reduce((sum2, stratum) => sum2 + stratum.thickness, 0);
42803
+ return terrain.dug[index] > depth + EXPOSED_MARGIN && terrain.dug[index] <= limit + EXPOSED_MARGIN;
42804
+ };
42805
+ const onBar = (coordinate, spacing) => Math.floor(coordinate / spacing) !== Math.floor((coordinate + cellSize) / spacing);
42806
+ const addBox = (ax, az, bx, bz, y) => {
42807
+ boxes.push(Math.min(ax, bx) - BAR_RADIUS, y - BAR_RADIUS, Math.min(az, bz) - BAR_RADIUS, Math.max(ax, bx) + BAR_RADIUS, y + BAR_RADIUS, Math.max(az, bz) + BAR_RADIUS);
42808
+ };
42809
+ for (let row = chunk.row; row <= lastRow; row++) {
42810
+ for (let column = chunk.column; column <= lastColumn; column++) {
42811
+ const index = row * stride + column;
42812
+ const cover = coverOf(index);
42813
+ if (!cover || !showing(index, cover.depth)) continue;
42814
+ const x = x0 + column * cellSize;
42815
+ const z = z0 + row * cellSize;
42816
+ const y = -cover.depth - BAR_RADIUS - Math.min(1, terrain.barBend[index]) * MAX_SAG;
42817
+ if (onBar(z, cover.spacing) && column < lastColumn && showing(index + 1, cover.depth)) addBox(x, z, x + cellSize, z, y);
42818
+ if (onBar(x, cover.spacing) && row < lastRow && showing(index + stride, cover.depth)) addBox(x, z, x, z + cellSize, y);
42819
+ }
42820
+ }
42821
+ if (boxes.length === 0) return null;
42822
+ const count = boxes.length / 6;
42823
+ const positions = new Float32Array(count * 24 * 3);
42824
+ const normals = new Float32Array(count * 24 * 3);
42825
+ const indices = new Uint32Array(count * 36);
42826
+ const FACES2 = [
42827
+ [[0, 4, 2, 0, 4, 5, 3, 4, 5, 3, 4, 2], [0, 1, 0]],
42828
+ [[0, 1, 5, 0, 1, 2, 3, 1, 2, 3, 1, 5], [0, -1, 0]],
42829
+ [[0, 1, 5, 3, 1, 5, 3, 4, 5, 0, 4, 5], [0, 0, 1]],
42830
+ [[3, 1, 2, 0, 1, 2, 0, 4, 2, 3, 4, 2], [0, 0, -1]],
42831
+ [[3, 1, 5, 3, 1, 2, 3, 4, 2, 3, 4, 5], [1, 0, 0]],
42832
+ [[0, 1, 2, 0, 1, 5, 0, 4, 5, 0, 4, 2], [-1, 0, 0]]
42833
+ ];
42834
+ let vertex = 0;
42835
+ let cursor = 0;
42836
+ for (let n = 0; n < count; n++) {
42837
+ const box2 = boxes.slice(n * 6, n * 6 + 6);
42838
+ for (const [corners, normal] of FACES2) {
42839
+ const first = vertex;
42840
+ for (let c = 0; c < 4; c++) {
42841
+ positions[vertex * 3] = box2[corners[c * 3]];
42842
+ positions[vertex * 3 + 1] = box2[corners[c * 3 + 1]];
42843
+ positions[vertex * 3 + 2] = box2[corners[c * 3 + 2]];
42844
+ normals.set(normal, vertex * 3);
42845
+ vertex++;
42846
+ }
42847
+ indices.set([first, first + 1, first + 2, first, first + 2, first + 3], cursor);
42848
+ cursor += 6;
42849
+ }
42850
+ }
42851
+ const geometry = new THREE.BufferGeometry();
42852
+ geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
42853
+ geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
42854
+ geometry.setIndex(new THREE.BufferAttribute(indices, 1));
42855
+ return geometry;
42856
+ }
42857
+ var DEFAULT_LOD_DISTANCE = 30;
42858
+ var DEFAULT_MARK_DISTANCE = 45;
42859
+ var DEFAULT_SKIRT = 300;
42860
+ var LOD_CHECK_METRES = 4;
42861
+ var REBAR_COLOUR = 4863272;
42862
+ function createTerrainRenderer(THREE, options) {
42863
+ const { terrain } = options;
42864
+ const lodDistance = options.lodDistance ?? DEFAULT_LOD_DISTANCE;
42865
+ const skirt = options.skirt ?? DEFAULT_SKIRT;
42866
+ const group = markKitOrigin(new THREE.Group(), { owner: "game-terrain-three", kind: "ground" });
42867
+ group.name = "floor";
42868
+ const surface = createSurfaceMaterial(THREE);
42869
+ const rebarMaterial = new THREE.MeshStandardMaterial({ color: REBAR_COLOUR, roughness: 0.75, metalness: 0.35 });
42870
+ const pristineMesh = new THREE.Mesh(new THREE.BufferGeometry(), surface);
42871
+ pristineMesh.receiveShadow = true;
42872
+ pristineMesh.name = "terrain-pristine";
42873
+ pristineMesh.frustumCulled = false;
42874
+ group.add(pristineMesh);
42875
+ const drawn = /* @__PURE__ */ new Map();
42876
+ const marks = createMarkLayer2(THREE, terrain, options.markDistance ?? DEFAULT_MARK_DISTANCE);
42877
+ const debris2 = createDebrisLayer(THREE, terrain);
42878
+ group.add(marks.mesh, debris2.mesh);
42879
+ let pristineStale = true;
42880
+ let pristineCount = 0;
42881
+ let lodX = Infinity;
42882
+ let lodZ = Infinity;
42883
+ let firstSync = true;
42884
+ const distanceTo = (chunk, camera2) => {
42885
+ const half = chunk.cells * terrain.cellSize / 2;
42886
+ const centreX = terrain.bounds[0] + chunk.column * terrain.cellSize + half;
42887
+ const centreZ = terrain.bounds[1] + chunk.row * terrain.cellSize + half;
42888
+ return Math.hypot(centreX - camera2.x, centreZ - camera2.z);
42889
+ };
42890
+ const stepFor = (distance4) => distance4 > lodDistance * 2 ? 4 : distance4 > lodDistance ? 2 : 1;
42891
+ function drop(index) {
42892
+ const entry = drawn.get(index);
42893
+ if (!entry) return;
42894
+ group.remove(entry.mesh);
42895
+ entry.mesh.geometry.dispose();
42896
+ if (entry.rebar) {
42897
+ group.remove(entry.rebar);
42898
+ entry.rebar.geometry.dispose();
42899
+ }
42900
+ drawn.delete(index);
42901
+ }
42902
+ function draw2(chunk, camera2) {
42903
+ drop(chunk.index);
42904
+ const step = stepFor(distanceTo(chunk, camera2));
42905
+ const mesh2 = new THREE.Mesh(buildChunkGeometry(THREE, terrain, chunk, step), surface);
42906
+ mesh2.receiveShadow = true;
42907
+ group.add(mesh2);
42908
+ const bars = buildRebarGeometry(THREE, terrain, chunk);
42909
+ let rebar = null;
42910
+ if (bars) {
42911
+ rebar = new THREE.Mesh(bars, rebarMaterial);
42912
+ rebar.castShadow = true;
42913
+ group.add(rebar);
42914
+ }
42915
+ drawn.set(chunk.index, { mesh: mesh2, rebar, step });
42916
+ }
42917
+ function rebuildPristine() {
42918
+ const chunks = [];
42919
+ for (let index = 0; index < terrain.chunksX * terrain.chunksZ; index++) {
42920
+ const chunk = terrain.chunk(index);
42921
+ if (chunk.pristine) chunks.push(chunk);
42922
+ }
42923
+ pristineMesh.geometry.dispose();
42924
+ pristineMesh.geometry = buildPristineGeometry(THREE, terrain, chunks, skirt);
42925
+ pristineCount = chunks.length;
42926
+ pristineStale = false;
42927
+ }
42928
+ return {
42929
+ group,
42930
+ sync(camera2) {
42931
+ const limit = firstSync ? Infinity : void 0;
42932
+ firstSync = false;
42933
+ for (const chunk of terrain.drainDirtyChunks(limit)) {
42934
+ if (chunk.pristine) {
42935
+ if (drawn.has(chunk.index)) drop(chunk.index);
42936
+ pristineStale = true;
42937
+ } else {
42938
+ if (!drawn.has(chunk.index)) pristineStale = true;
42939
+ draw2(chunk, camera2);
42940
+ }
42941
+ }
42942
+ if (pristineStale) rebuildPristine();
42943
+ if (Math.hypot(camera2.x - lodX, camera2.z - lodZ) > LOD_CHECK_METRES) {
42944
+ lodX = camera2.x;
42945
+ lodZ = camera2.z;
42946
+ for (const [index, entry] of drawn) {
42947
+ const chunk = terrain.chunk(index);
42948
+ if (stepFor(distanceTo(chunk, camera2)) !== entry.step) draw2(chunk, camera2);
42949
+ }
42950
+ }
42951
+ marks.sync(camera2);
42952
+ debris2.sync();
42953
+ },
42954
+ stats: () => ({ chunkMeshes: drawn.size, pristineChunks: pristineCount, marksDrawn: marks.mesh.count, debrisDrawn: debris2.mesh.count }),
42955
+ dispose() {
42956
+ for (const index of [...drawn.keys()]) drop(index);
42957
+ pristineMesh.geometry.dispose();
42958
+ surface.dispose();
42959
+ rebarMaterial.dispose();
42960
+ marks.dispose();
42961
+ debris2.dispose();
42962
+ group.removeFromParent();
42963
+ }
42964
+ };
42965
+ }
42966
+ var TERRAIN_STRIPS = ["dirt", "lawn", "sand", "road", "concrete_slab", "suspended_slab", "wood_floor", "tile_floor", "stone_paving"];
42967
+ var STRIP_WIDTH = 3;
42968
+ var STRIP_Z0 = 4;
42969
+ var STRIP_Z1 = 13;
42970
+ var stripRect = (index) => {
42971
+ const x0 = (index - TERRAIN_STRIPS.length / 2) * STRIP_WIDTH;
42972
+ return [x0, STRIP_Z0, x0 + STRIP_WIDTH, STRIP_Z1];
42973
+ };
42974
+ var TERRAIN_AREA_GROUND = {
42975
+ surface: "concrete_slab",
42976
+ zones: TERRAIN_STRIPS.map((surface, index) => ({ rect: stripRect(index), surface }))
42977
+ };
42978
+
40422
42979
  // src/avatar.ts
40423
42980
  var DEFAULT_STRIKE_CLIPS = { hand: "punch_right", fists: "punch_right", shove: "shove_two_hands", kick: "front_kick" };
40424
42981
  var DEFAULT_HAND_STANCES = { fists: "fists" };
@@ -40600,6 +43157,8 @@ void main(){
40600
43157
  "act.strike.hand": "{weapon}: {clip} with the {hand} hand begins, impact in {seconds} s",
40601
43158
  "hand.left": "left",
40602
43159
  "hand.right": "right",
43160
+ "act.combo.on": "{name}: on (each blow follows the sequence)",
43161
+ "act.combo.off": "{name}: off",
40603
43162
  "act.flame.hit": "{weapon}: flame at {temp} \xB0C on {where}",
40604
43163
  "act.flame.air": "{weapon}: flame, nothing in range",
40605
43164
  "act.heat.hit": "{weapon}: heating {where} at {temp} \xB0C",
@@ -40623,8 +43182,10 @@ void main(){
40623
43182
  "act.extinguish.hit": "{weapon}: foam on {what}",
40624
43183
  "act.extinguish.air": "{weapon}: foam, nothing in range",
40625
43184
  "act.grab": "Grabbed {name} ({kg} kg)",
43185
+ "act.bump": "Pushed {name} ({kg} kg) walking into it",
40626
43186
  "act.drop": "Dropped {name}",
40627
43187
  "act.throw": "Threw {name} at {speed} m/s",
43188
+ "ground.name": "Ground",
40628
43189
  "act.landing": "{name} landed at {speed} m/s: {energy} J",
40629
43190
  "act.bench_failed": "{name} failed: {error}",
40630
43191
  "loading.working": "Working\u2026",
@@ -40658,6 +43219,14 @@ void main(){
40658
43219
  "hud.nothing_to_grab": "Nothing to grab here",
40659
43220
  "hud.cannot_grab": "{name} cannot be picked up",
40660
43221
  "hud.too_heavy": "{name} is too heavy ({kg} kg)",
43222
+ "hud.push_on": "Pushing {name}: W pushes, S pulls, let go of E to stop",
43223
+ "hud.push_too_heavy": "{name} does not budge ({kg} kg)",
43224
+ "hud.push_blocked": "{name} is stuck: there is something in the way",
43225
+ "act.push_grip": "leans on {name} ({kg} kg)",
43226
+ "act.push_engaged": "starts pushing {name} ({kg} kg)",
43227
+ "act.push_too_heavy": "strains against {name}: {kg} kg is too much",
43228
+ "act.push_obstacle": "{name} hits something and locks",
43229
+ "act.push_release": "lets go of {name}",
40661
43230
  "hud.empty_container": "{name} is empty",
40662
43231
  "hud.refused": "Wrong tool for the {kind}",
40663
43232
  "hud.drop": "drop",
@@ -40665,7 +43234,7 @@ void main(){
40665
43234
  "hud.throw": "throw",
40666
43235
  "hud.tilt": "tilt (hold)",
40667
43236
  "hud.equip": "equip {weapon}",
40668
- "ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use (bare hands: punch) \xB7 E: grab/drop \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 Tab: panels \xB7 Esc: free the mouse",
43237
+ "ui.help": "Click: look \xB7 WASD: walk \xB7 Shift: run \xB7 C: crouch (tap stays, hold while pressed) \xB7 Space: jump \xB7 1-0 / wheel: tool \xB7 LMB: use (bare hands: punch) \xB7 E: grab/drop (hold against furniture: push) \xB7 G or RMB: throw \xB7 R: reload \xB7 Q: fire mode \xB7 X: clear a jam \xB7 B: safety \xB7 Y: detonate charges set down \xB7 K: kick \xB7 N: strike combo on/off \xB7 Tab: panels \xB7 Esc: free the mouse",
40669
43238
  "ui.help.camera": "V: 1st/3rd person (Shift+V: kick)",
40670
43239
  "camera.first": "1st person",
40671
43240
  "camera.third": "3rd person",
@@ -40712,6 +43281,7 @@ void main(){
40712
43281
  "ui.slow": "Slow motion 0.25\xD7",
40713
43282
  "ui.fast": "Time \xD710",
40714
43283
  "ui.tap": "Tap",
43284
+ "ui.kick": "Kick",
40715
43285
  "ui.fullscreen": "Full screen",
40716
43286
  "ui.fullscreen.hint": "Full screen opened from here keeps the whole keyboard in the game, Ctrl+W included (F11 does not)",
40717
43287
  "ui.blast.shelter": "Shelter",
@@ -40911,6 +43481,8 @@ void main(){
40911
43481
  "act.strike.hand": "{weapon}: {clip} com a m\xE3o {hand} come\xE7a, impacto em {seconds} s",
40912
43482
  "hand.left": "esquerda",
40913
43483
  "hand.right": "direita",
43484
+ "act.combo.on": "{name}: ligada (cada golpe segue a sequ\xEAncia)",
43485
+ "act.combo.off": "{name}: desligada",
40914
43486
  "act.flame.hit": "{weapon}: chama a {temp} \xB0C em {where}",
40915
43487
  "act.flame.air": "{weapon}: chama, nada ao alcance",
40916
43488
  "act.heat.hit": "{weapon}: esquentando {where} a {temp} \xB0C",
@@ -40934,8 +43506,10 @@ void main(){
40934
43506
  "act.extinguish.hit": "{weapon}: espuma em {what}",
40935
43507
  "act.extinguish.air": "{weapon}: espuma, nada ao alcance",
40936
43508
  "act.grab": "Pegou {name} ({kg} kg)",
43509
+ "act.bump": "Empurrou {name} ({kg} kg) ao esbarrar",
40937
43510
  "act.drop": "Soltou {name}",
40938
43511
  "act.throw": "Arremessou {name} a {speed} m/s",
43512
+ "ground.name": "Ch\xE3o",
40939
43513
  "act.landing": "{name} caiu a {speed} m/s: {energy} J",
40940
43514
  "act.bench_failed": "{name} falhou: {error}",
40941
43515
  "loading.working": "Trabalhando\u2026",
@@ -40969,6 +43543,14 @@ void main(){
40969
43543
  "hud.nothing_to_grab": "Nada para pegar aqui",
40970
43544
  "hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
40971
43545
  "hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
43546
+ "hud.push_on": "Empurrando {name}: W empurra, S puxa, solte o E para parar",
43547
+ "hud.push_too_heavy": "{name} n\xE3o se mexe ({kg} kg)",
43548
+ "hud.push_blocked": "{name} travou: tem algo no caminho",
43549
+ "act.push_grip": "encosta em {name} ({kg} kg)",
43550
+ "act.push_engaged": "come\xE7a a empurrar {name} ({kg} kg)",
43551
+ "act.push_too_heavy": "faz for\xE7a contra {name}: {kg} kg \xE9 demais",
43552
+ "act.push_obstacle": "{name} bate em algo e trava",
43553
+ "act.push_release": "solta {name}",
40972
43554
  "hud.empty_container": "{name} est\xE1 vazio",
40973
43555
  "hud.refused": "Ferramenta errada para {kind}",
40974
43556
  "hud.drop": "soltar",
@@ -40976,7 +43558,7 @@ void main(){
40976
43558
  "hud.throw": "arremessar",
40977
43559
  "hud.tilt": "inclinar (segurar)",
40978
43560
  "hud.equip": "equipar {weapon}",
40979
- "ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar (m\xE3o livre: soco) \xB7 E: pegar/soltar \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
43561
+ "ui.help": "Clique: olhar \xB7 WASD: andar \xB7 Shift: correr \xB7 C: agachar (toque fixa, segurar s\xF3 enquanto aperta) \xB7 Espa\xE7o: pular \xB7 1-0 / roda: ferramenta \xB7 bot\xE3o esquerdo: usar (m\xE3o livre: soco) \xB7 E: pegar/soltar (segurar encostado no m\xF3vel: empurrar) \xB7 G ou bot\xE3o direito: arremessar \xB7 R: recarregar \xB7 Q: modo de tiro \xB7 X: destravar \xB7 B: trava \xB7 Y: detonar cargas postas \xB7 K: chute \xB7 N: liga/desliga a sequ\xEAncia de golpes \xB7 Tab: pain\xE9is \xB7 Esc: soltar o mouse",
40980
43562
  "ui.help.camera": "V: 1\xAA/3\xAA pessoa (Shift+V: chute)",
40981
43563
  "camera.first": "1\xAA pessoa",
40982
43564
  "camera.third": "3\xAA pessoa",
@@ -41023,6 +43605,7 @@ void main(){
41023
43605
  "ui.slow": "C\xE2mera lenta 0,25\xD7",
41024
43606
  "ui.fast": "Tempo \xD710",
41025
43607
  "ui.tap": "Torneira",
43608
+ "ui.kick": "Chute",
41026
43609
  "ui.fullscreen": "Tela cheia",
41027
43610
  "ui.fullscreen.hint": "A tela cheia aberta por aqui segura o teclado todo no jogo, inclusive Ctrl+W (o F11 n\xE3o)",
41028
43611
  "ui.blast.shelter": "Abrigo",
@@ -41489,6 +44072,8 @@ void main(){
41489
44072
  .tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
41490
44073
  .tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
41491
44074
  .tr-cross:before{left:8px;top:0;width:2px;height:18px}.tr-cross:after{top:8px;left:0;height:2px;width:18px}
44075
+ .tr-ring{position:absolute;left:50%;top:50%;width:64px;height:64px;margin:-32px 0 0 -32px;transform:rotate(-90deg);display:none;pointer-events:none;filter:drop-shadow(0 0 2px #000)}
44076
+ .tr-ring circle{fill:none;stroke-width:5}.tr-ring .tr-ring-bg{stroke:rgba(255,255,255,.25)}.tr-ring .tr-ring-fill{stroke:#ffd166;stroke-linecap:round}
41492
44077
  .tr-cross i{position:absolute;left:6px;top:6px;width:6px;height:6px;background:#0b0c0e;border-radius:50%;z-index:1}
41493
44078
  .tr-top{position:absolute;top:8px;left:50%;transform:translateX(-50%);text-align:center;max-width:46vw}
41494
44079
  .tr-tool{display:inline-block;background:rgba(10,12,16,.72);padding:4px 12px;border-radius:6px;font-size:14px;font-weight:600}
@@ -41592,6 +44177,22 @@ void main(){
41592
44177
  o.container.appendChild(root);
41593
44178
  const cross5 = mk3("div", "tr-cross");
41594
44179
  cross5.appendChild(mk3("i"));
44180
+ const ring2 = document.createElementNS("http://www.w3.org/2000/svg", "svg");
44181
+ ring2.setAttribute("class", "tr-ring");
44182
+ ring2.setAttribute("viewBox", "0 0 64 64");
44183
+ const ringCircle = (className) => {
44184
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
44185
+ circle.setAttribute("class", className);
44186
+ circle.setAttribute("cx", "32");
44187
+ circle.setAttribute("cy", "32");
44188
+ circle.setAttribute("r", "27");
44189
+ ring2.appendChild(circle);
44190
+ return circle;
44191
+ };
44192
+ ringCircle("tr-ring-bg");
44193
+ const ringFill = ringCircle("tr-ring-fill");
44194
+ const RING_LENGTH = 2 * Math.PI * 27;
44195
+ ringFill.style.strokeDasharray = String(RING_LENGTH);
41595
44196
  const top = mk3("div", "tr-top");
41596
44197
  const toolBox = mk3("div", "tr-tool");
41597
44198
  const status = mk3("div", "tr-status");
@@ -41793,6 +44394,8 @@ void main(){
41793
44394
  o.onFast(fast);
41794
44395
  });
41795
44396
  const tapBtn = ctl(t("ui.tap"), () => o.onTap());
44397
+ const comboBtn = o.combo ? ctl(`${o.combo.label} (N)`, () => o.combo.onToggle()) : null;
44398
+ if (o.kick) ctl(o.kick.label, () => o.kick.onKick());
41796
44399
  const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
41797
44400
  fullscreenBtn.title = t("ui.fullscreen.hint");
41798
44401
  const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key) => ({ key, b: ctl(t(`ui.blast.${key}`), () => o.onBlast(key)) }));
@@ -41875,7 +44478,7 @@ void main(){
41875
44478
  o.onResize();
41876
44479
  };
41877
44480
  toggle.onclick = togglePanels;
41878
- root.append(cross5, top, bottom, help, left, right, toggle);
44481
+ root.append(cross5, ring2, top, bottom, help, left, right, toggle);
41879
44482
  const bar2 = (value, max2, cls) => {
41880
44483
  const b = mk3("div", `tr-bar ${cls}`);
41881
44484
  const i = mk3("i");
@@ -41956,6 +44559,10 @@ void main(){
41956
44559
  let lastInspect = "";
41957
44560
  let hudPage = 0;
41958
44561
  const api = {
44562
+ holdRing(value) {
44563
+ ring2.style.display = value === null ? "none" : "block";
44564
+ if (value !== null) ringFill.style.strokeDashoffset = String(RING_LENGTH * (1 - value));
44565
+ },
41959
44566
  hud(s) {
41960
44567
  toolBox.textContent = "";
41961
44568
  toolBox.append(document.createTextNode(s.toolName));
@@ -42004,6 +44611,7 @@ void main(){
42004
44611
  }
42005
44612
  slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
42006
44613
  tapBtn.classList.toggle("on", s.tap);
44614
+ comboBtn?.classList.toggle("on", !!s.combo);
42007
44615
  for (const { key: key2, b } of blastBtns) b.classList.toggle("on", s.blast[key2]);
42008
44616
  count.textContent = t("ui.objects", { n: s.objects });
42009
44617
  const ak = JSON.stringify(s.animations);
@@ -42053,6 +44661,18 @@ void main(){
42053
44661
 
42054
44662
  // src/room.ts
42055
44663
  var FIXED_DT = 1 / 60;
44664
+ var SOLID_SPAN = 3;
44665
+ var STEP_UP = 0.45;
44666
+ var BODY_HEIGHT = 1.75;
44667
+ var KICK_MASS = 6;
44668
+ var KICK_HEIGHT = 0.6;
44669
+ var GROUND_ID2 = "room_ground";
44670
+ var TERRAIN_CELL = 0.1;
44671
+ var TERRAIN_MAX_CELLS = 1e3;
44672
+ var GROUND_BURST_MAX = 28;
44673
+ var GROUND_BURST_PER_METRE = 30;
44674
+ var GROUND_DUST_RADIUS = 0.2;
44675
+ var GROUND_FIRE_EVERY = 0.3;
42056
44676
  var SHOVE_SPEED = 0.04;
42057
44677
  var METRICS_INTERVAL_S = 0.25;
42058
44678
  var CARRY_NEAR = 0.62;
@@ -42113,10 +44733,12 @@ void main(){
42113
44733
  const ownWorld = !options.world;
42114
44734
  const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...options.worldOptions?.materials ?? []] });
42115
44735
  const pristineWorld = ownWorld ? world.serialize() : null;
42116
- const topOk = (t2) => {
42117
- const p = world.part(t2.id, t2.topPart);
42118
- return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes";
44736
+ const partStands = (object, part) => {
44737
+ const p = world.part(object, part);
44738
+ return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes" && !renderer.piece(object);
42119
44739
  };
44740
+ const topOk = (t2) => t2.boards ? t2.boards.some((b) => partStands(t2.id, b.part)) : partStands(t2.id, t2.topPart);
44741
+ const topHolds = (t2, x, z) => t2.boards ? t2.boards.some((b) => x > b.minX && x < b.maxX && z > b.minZ && z < b.maxZ && partStands(t2.id, b.part)) : topOk(t2);
42120
44742
  const restingTops = [];
42121
44743
  const restingTopAt = (x, z) => {
42122
44744
  let top = 0;
@@ -42125,10 +44747,18 @@ void main(){
42125
44747
  };
42126
44748
  const sceneryTopAt = (x, z) => {
42127
44749
  let top = 0;
42128
- for (const t2 of arena.tops) if (t2.top > top && x > t2.minX && x < t2.maxX && z > t2.minZ && z < t2.maxZ && topOk(t2)) top = t2.top;
44750
+ for (const t2 of arena.tops) if (t2.top > top && x > t2.minX && x < t2.maxX && z > t2.minZ && z < t2.maxZ && topHolds(t2, x, z)) top = t2.top;
42129
44751
  return top;
42130
44752
  };
42131
- const groundAt = (x, z) => Math.max(sceneryTopAt(x, z), restingTopAt(x, z));
44753
+ const defaultGround = options.ground ?? { surface: DEFAULT_PROFILE };
44754
+ const terrain = createTerrain({ bounds: [-arenaSize, -arenaSize, arenaSize, arenaSize], cellSize: Math.max(TERRAIN_CELL, 2 * arenaSize / TERRAIN_MAX_CELLS), layout: defaultGround, ...options.terrainBudget ? { budget: options.terrainBudget } : {} });
44755
+ const terrainView = createTerrainRenderer(THREE, { terrain });
44756
+ arena.group.add(terrainView.group);
44757
+ const groundAt = (x, z) => {
44758
+ const top = Math.max(sceneryTopAt(x, z), restingTopAt(x, z));
44759
+ const floor2 = terrain.heightAt(x, z);
44760
+ return top > 0 ? Math.max(top, floor2) : floor2;
44761
+ };
42132
44762
  const ownFluids = !options.fluids;
42133
44763
  const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
42134
44764
  const pristineFluids = ownFluids ? fluids.serialize() : null;
@@ -42149,13 +44779,6 @@ void main(){
42149
44779
  destructionSound?.onSound(key, point, volume);
42150
44780
  options.onSound?.(key, point, volume);
42151
44781
  };
42152
- const craterField = options.craters ? createGroundField() : void 0;
42153
- const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
42154
- if (craterPatch) {
42155
- const floor2 = arena.group.getObjectByName("floor");
42156
- if (floor2) floor2.visible = false;
42157
- scene2.add(craterPatch.mesh);
42158
- }
42159
44782
  const vfx = createVfxRenderer(THREE, {
42160
44783
  scene: scene2,
42161
44784
  camera: camera2,
@@ -42164,8 +44787,7 @@ void main(){
42164
44787
  pointScale,
42165
44788
  vfx: createVfx({
42166
44789
  profile: quality,
42167
- groundAt: (x, z) => groundAt(x, z) + (craterField?.heightAt(x, z) ?? 0),
42168
- ...craterField ? { ground: craterField } : {}
44790
+ groundAt
42169
44791
  })
42170
44792
  });
42171
44793
  const renderer = createDestructionRenderer(THREE, {
@@ -42186,6 +44808,7 @@ void main(){
42186
44808
  const e = entries.get(objectId);
42187
44809
  if (!e) return;
42188
44810
  e.piece = piece;
44811
+ leaveFootprint(objectId);
42189
44812
  rendererPieces.set(objectId, piece);
42190
44813
  action("loosened", t("act.loosened", { name: e.name }), { objectId, itemId: e.so.itemId });
42191
44814
  },
@@ -42210,6 +44833,31 @@ void main(){
42210
44833
  if (best) splitOf.set(piece.objectId, best);
42211
44834
  }
42212
44835
  });
44836
+ const showGroundBlow = (result2) => {
44837
+ const material = terrain.material(result2.removed[0]?.material ?? result2.exposed);
44838
+ const def = world.materials.get(material.id);
44839
+ const size = Math.min(GROUND_BURST_MAX, Math.ceil(result2.radius * GROUND_BURST_PER_METRE));
44840
+ renderer.emit(material.debris === "clods" ? "dust" : material.debris, result2.point, [0, 1, 0], Math.max(2, size), Math.min(4, 1 + result2.radius * 4));
44841
+ if (result2.radius > GROUND_DUST_RADIUS) renderer.emit("dust", result2.point, [0, 1, 0], size, 1.4);
44842
+ playSound(impactKey(def), result2.point, Math.min(1, 0.25 + result2.radius));
44843
+ };
44844
+ terrain.onDeform((region) => renderer.wakeRegion(region.x, region.z, region.radius));
44845
+ const groundImpl = createTerrainImpl(terrain, { id: GROUND_ID2, name: t("ground.name"), onBlow: showGroundBlow });
44846
+ const registerGround = () => {
44847
+ if (!world.has(GROUND_ID2)) world.register(groundImpl, { id: GROUND_ID2 });
44848
+ };
44849
+ registerGround();
44850
+ let groundFireClock = 0;
44851
+ const showGroundFire = (dt) => {
44852
+ groundFireClock += dt;
44853
+ if (groundFireClock < GROUND_FIRE_EVERY) return;
44854
+ groundFireClock = 0;
44855
+ for (const [x, z] of terrain.burningPoints()) {
44856
+ const y = terrain.heightAt(x, z);
44857
+ renderer.emit("fire", [x, y + 0.05, z], [0, 1, 0], 3, 1.6);
44858
+ if (rnd2() < 0.25) renderer.emit("smoke", [x, y + 0.2, z], [0, 1, 0], 1, 1);
44859
+ }
44860
+ };
42213
44861
  const fluidView = createFluidRenderer(THREE, { scene: scene2, fluids, quality, pointScale, vfx: vfx.world });
42214
44862
  const filmRandom = createFilmLcg(1);
42215
44863
  const film = createFilmManager(THREE, {
@@ -42344,12 +44992,79 @@ void main(){
42344
44992
  let useHeld = false;
42345
44993
  let useFirst = false;
42346
44994
  const blockers = /* @__PURE__ */ new Set();
44995
+ const drawnScenery = (i) => {
44996
+ const c = arena.colliders[i];
44997
+ return arena.colliderIds[i] !== null && c[2] - c[0] <= SOLID_SPAN && c[3] - c[1] <= SOLID_SPAN;
44998
+ };
44999
+ const standingColliders = [];
45000
+ const standingIds = [];
45001
+ const footprintsWithBody = /* @__PURE__ */ new Set();
45002
+ const standFootprints = () => {
45003
+ standingColliders.length = 0;
45004
+ standingIds.length = 0;
45005
+ footprintsWithBody.clear();
45006
+ arena.colliders.forEach((collider, i) => {
45007
+ if (drawnScenery(i)) return;
45008
+ standingColliders.push(collider);
45009
+ standingIds.push(arena.colliderIds[i] ?? null);
45010
+ });
45011
+ };
45012
+ standFootprints();
45013
+ const leaveFootprint = (objectId) => {
45014
+ const i = standingIds.indexOf(objectId);
45015
+ if (i < 0) return;
45016
+ standingIds.splice(i, 1);
45017
+ standingColliders.splice(i, 1);
45018
+ footprintsWithBody.add(objectId);
45019
+ };
45020
+ const footing = (x, z, radius, feet) => {
45021
+ let floor2 = -Infinity;
45022
+ let blocked = false;
45023
+ for (const s of renderer.solidsNear(x, z, radius)) {
45024
+ if ((s.piece?.mass ?? s.loose?.mass ?? Infinity) < KICK_MASS) continue;
45025
+ const dx = Math.max(s.min[0] - x, 0, x - s.max[0]);
45026
+ const dz = Math.max(s.min[2] - z, 0, z - s.max[2]);
45027
+ if (dx * dx + dz * dz > radius * radius) continue;
45028
+ if (s.max[1] <= feet + STEP_UP) floor2 = Math.max(floor2, s.max[1]);
45029
+ else if (s.min[1] < feet + BODY_HEIGHT) blocked = true;
45030
+ }
45031
+ return { floor: floor2, blocked };
45032
+ };
45033
+ const kickFrom = { x: 0, z: 0 };
45034
+ const kickLight = (dt) => {
45035
+ const x = player.x;
45036
+ const z = player.z;
45037
+ const speed = dt > 0 ? Math.hypot(x - kickFrom.x, z - kickFrom.z) / dt : 0;
45038
+ kickFrom.x = x;
45039
+ kickFrom.z = z;
45040
+ if (speed < 0.2 || speed > 20) return;
45041
+ const kicked = /* @__PURE__ */ new Set();
45042
+ for (const s of renderer.solidsNear(x, z, PLAYER_RADIUS)) {
45043
+ const pc = s.piece;
45044
+ const body = pc ?? s.loose;
45045
+ if (!body || body.mass >= KICK_MASS || kicked.has(body) || s.min[1] > player.feet + KICK_HEIGHT) continue;
45046
+ kicked.add(body);
45047
+ const cx = (s.min[0] + s.max[0]) / 2 - x;
45048
+ const cz = (s.min[2] + s.max[2]) / 2 - z;
45049
+ const d = Math.hypot(cx, cz) || 1;
45050
+ const v = speed * 1.2;
45051
+ if (!pc) {
45052
+ s.loose?.push(cx / d * v, cz / d * v);
45053
+ continue;
45054
+ }
45055
+ if (pc.velocity[0] * cx + pc.velocity[2] * cz >= v * d) continue;
45056
+ pc.velocity = [cx / d * v, Math.max(pc.velocity[1], 0.6), cz / d * v];
45057
+ pc.sleeping = false;
45058
+ }
45059
+ };
42347
45060
  const player = createPlayer(THREE, {
42348
45061
  camera: camera2,
42349
45062
  dom,
42350
- colliders: arena.colliders,
45063
+ colliders: standingColliders,
45064
+ footing: (x, z, radius, feet) => footing(x, z, radius, feet),
42351
45065
  blockers,
42352
45066
  limit: arenaSize,
45067
+ floorAt: terrain.heightAt,
42353
45068
  start: arena.playerStart,
42354
45069
  crouchKeys: options.crouch?.keys,
42355
45070
  hooks: {
@@ -42373,6 +45088,68 @@ void main(){
42373
45088
  let counter = 0;
42374
45089
  let slot = 0;
42375
45090
  const maxCarry = options.maxCarryKg ?? 60;
45091
+ const PUSH_AHEAD = 0.6;
45092
+ const heaviestMaterial = (id) => {
45093
+ let best;
45094
+ for (const p of world.state(id)?.parts ?? []) if (p.status !== "broken" && (!best || p.mass > best.mass)) best = { mass: p.mass, material: p.material };
45095
+ return best?.material;
45096
+ };
45097
+ const leanedOn = () => {
45098
+ const fx = -Math.sin(player.yaw);
45099
+ const fz = -Math.cos(player.yaw);
45100
+ let best = null;
45101
+ let bestDistance = PUSH_REACH;
45102
+ for (const e of entries.values()) {
45103
+ if (e.arena || !e.piece && !e.free) continue;
45104
+ const b = bodyBox(e);
45105
+ if (!b) continue;
45106
+ const dx = Math.max(b.minX, Math.min(b.maxX, player.x)) - player.x;
45107
+ const dz = Math.max(b.minZ, Math.min(b.maxZ, player.z)) - player.z;
45108
+ const distance4 = Math.hypot(dx, dz);
45109
+ if (distance4 >= bestDistance || distance4 > 1e-3 && (dx * fx + dz * fz) / distance4 < PUSH_AHEAD) continue;
45110
+ best = e;
45111
+ bestDistance = distance4;
45112
+ }
45113
+ return best ? { objectId: best.so.objectId, root: best.so.root, name: nameOf(best.so.objectId), piece: best.piece ?? null } : null;
45114
+ };
45115
+ const push = createRoomPush({
45116
+ THREE,
45117
+ player,
45118
+ world,
45119
+ arena: { limit: arenaSize, colliders: standingColliders },
45120
+ blockers,
45121
+ ...options.push ? { rules: options.push } : {},
45122
+ pick: () => {
45123
+ if (heldPiece || tool.def.category !== "hand" && toolId !== "fists") return null;
45124
+ aim();
45125
+ const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
45126
+ const aimed = r ? rendererPieces.get(r.object)?.root ?? (entries.get(r.object)?.free ? entries.get(r.object)?.so.root : void 0) : void 0;
45127
+ if (r && aimed) return { objectId: r.object, root: aimed, name: nameOf(r.object), piece: rendererPieces.get(r.object) ?? null };
45128
+ return leanedOn();
45129
+ },
45130
+ massOf: (objectId) => massOfObject(objectId),
45131
+ materialOf: heaviestMaterial,
45132
+ makePiece: (objectId) => {
45133
+ const piece = renderer.makePiece(objectId, { mass: Math.max(0.2, massOfObject(objectId)) });
45134
+ const e = entries.get(objectId);
45135
+ if (e) e.piece = piece;
45136
+ rendererPieces.set(objectId, piece);
45137
+ leaveFootprint(objectId);
45138
+ return piece;
45139
+ },
45140
+ dragSound: (objectId, point, volume) => playSound(dragSoundKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, volume),
45141
+ hitSound: (objectId, point) => playSound(impactKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, 0.5),
45142
+ onEvent: (ev) => {
45143
+ const vars = { name: ev.name, kg: num(ev.massKg, 0) };
45144
+ action(`push_${ev.type}`, t(`act.push_${ev.type}`, vars), { objectId: ev.objectId, itemId: entryOf(ev.objectId)?.so.itemId, data: { massKg: Math.round(ev.massKg * 10) / 10 } });
45145
+ fire("push", ev);
45146
+ if (ev.type === "engaged") say(t("hud.push_on", vars));
45147
+ else if (ev.type === "too_heavy") say(t("hud.push_too_heavy", vars));
45148
+ else if (ev.type === "obstacle") say(t("hud.push_blocked", vars));
45149
+ },
45150
+ ring: (value) => ui?.holdRing(value)
45151
+ });
45152
+ let pushByKey = false;
42376
45153
  const massOfObject = (id) => {
42377
45154
  const st = world.state(id);
42378
45155
  let m = 0;
@@ -42440,6 +45217,45 @@ void main(){
42440
45217
  Object.defineProperty(holder, "visible", { get: () => built.visible, set: (v) => void (built.visible = v), configurable: true });
42441
45218
  return holder;
42442
45219
  };
45220
+ const sceneryObjects = /* @__PURE__ */ new Map();
45221
+ const placeScenery = (objectId, item, position, rotationY) => {
45222
+ const root = buildRoot(item);
45223
+ root.position.set(position[0], position[1], position[2]);
45224
+ root.rotation.y = rotationY;
45225
+ root.name = item.id;
45226
+ scene2.add(root);
45227
+ root.updateMatrixWorld(true);
45228
+ const own = item.impl?.(root, objectId);
45229
+ const { spec, parts } = own ? { spec: structuredCloneSpec(own.describe()), parts: void 0 } : resolveSpec(item, root);
45230
+ root.updateMatrixWorld(true);
45231
+ const q = root.getWorldQuaternion(tmpQ);
45232
+ if (own) world.register(own, { id: objectId, position, rotation: [q.x, q.y, q.z, q.w] });
45233
+ else world.add(spec, { id: objectId, position, rotation: [q.x, q.y, q.z, q.w] });
45234
+ const e = registerEntry(objectId, item.id, root, spec, item.name, parts, item, true, false);
45235
+ fire("spawn", e.so);
45236
+ return e;
45237
+ };
45238
+ const restoreScenery = () => {
45239
+ for (const [objectId, o] of sceneryObjects) if (!entries.has(objectId)) placeScenery(objectId, o.item, o.position, o.rotationY);
45240
+ };
45241
+ const addScenery = (itemId, position, rotationY = 0) => {
45242
+ const item = catalogMap.get(itemId);
45243
+ if (!item) return null;
45244
+ const objectId = `${item.id}_scenery_${sceneryObjects.size + 1}`;
45245
+ sceneryObjects.set(objectId, { item, position: [position[0], position[1], position[2]], rotationY });
45246
+ const e = placeScenery(objectId, item, position, rotationY);
45247
+ action("spawn", t("act.spawn", { name: item.name, x: num(position[0], 1), z: num(position[2], 1) }), { objectId, itemId: item.id, data: { position, movable: false, scenery: true } });
45248
+ return {
45249
+ objectId,
45250
+ get root() {
45251
+ return entries.get(objectId)?.so.root ?? e.so.root;
45252
+ },
45253
+ remove() {
45254
+ sceneryObjects.delete(objectId);
45255
+ removeEntry(objectId);
45256
+ }
45257
+ };
45258
+ };
42443
45259
  const spawn2 = (itemId, position, rotationY) => {
42444
45260
  const item = catalogMap.get(itemId);
42445
45261
  if (!item) throw new Error(`game-test-room: unknown item "${itemId}"`);
@@ -42491,6 +45307,77 @@ void main(){
42491
45307
  fire("spawn", e.so);
42492
45308
  return e.so;
42493
45309
  };
45310
+ const STEP_OVER = STEP_UP;
45311
+ const HEADROOM = 1.9;
45312
+ const PUSH_WALK_N = 250;
45313
+ const PUSH_RUN_N = 450;
45314
+ const FLOOR_FRICTION = 0.5;
45315
+ const bodyBoxes = /* @__PURE__ */ new Map();
45316
+ const bodyBounds = new THREE.Box3();
45317
+ let frameStamp = 0;
45318
+ const bodyBox = (e) => {
45319
+ const id = e.so.objectId;
45320
+ const known = bodyBoxes.get(id);
45321
+ if (known && (known.stamp === frameStamp || !e.piece || e.piece.sleeping)) return known;
45322
+ if (!e.so.root.parent || world.state(id)?.destroyed) {
45323
+ bodyBoxes.delete(id);
45324
+ return null;
45325
+ }
45326
+ bodyBounds.setFromObject(e.so.root);
45327
+ if (bodyBounds.isEmpty()) return null;
45328
+ const box2 = { minX: bodyBounds.min.x, maxX: bodyBounds.max.x, minZ: bodyBounds.min.z, maxZ: bodyBounds.max.z, base: bodyBounds.min.y, top: bodyBounds.max.y, stamp: frameStamp };
45329
+ bodyBoxes.set(id, box2);
45330
+ return box2;
45331
+ };
45332
+ const footPos = new THREE.Vector3();
45333
+ const inTheWay = (e, x, z, r) => {
45334
+ if (e.arena && !footprintsWithBody.has(e.so.objectId) || e.piece && e.piece.held) return null;
45335
+ const ud = e.so.root.userData;
45336
+ if ("collider" in ud && !ud.collider?.kind) return null;
45337
+ const b = bodyBox(e);
45338
+ if (!b || b.base > player.feet + HEADROOM) return null;
45339
+ const c = ud.collider;
45340
+ if (c?.kind === "cylinder" && c.radius != null) {
45341
+ e.so.root.getWorldPosition(footPos);
45342
+ const reach = c.radius + r;
45343
+ return (x - footPos.x) ** 2 + (z - footPos.z) ** 2 < reach * reach ? { ...b, top: Math.min(b.top, footPos.y + (c.height ?? b.top - footPos.y)) } : null;
45344
+ }
45345
+ return x > b.minX - r && x < b.maxX + r && z > b.minZ - r && z < b.maxZ + r ? b : null;
45346
+ };
45347
+ blockers.add((x, z, r) => {
45348
+ for (const e of entries.values()) {
45349
+ const b = inTheWay(e, x, z, r);
45350
+ if (b && b.top - b.base >= STEP_OVER) return true;
45351
+ }
45352
+ return false;
45353
+ });
45354
+ const pushByPlayer = (dt) => {
45355
+ const [wx, wz] = player.takeWish();
45356
+ const len = Math.hypot(wx, wz);
45357
+ if (dt <= 0 || len < 1e-6) return;
45358
+ const dx = wx / len;
45359
+ const dz = wz / len;
45360
+ const speed = len / dt;
45361
+ const force = player.running ? PUSH_RUN_N : PUSH_WALK_N;
45362
+ const ax = player.x + dx * (Math.min(len, 0.5) + 0.03);
45363
+ const az = player.z + dz * (Math.min(len, 0.5) + 0.03);
45364
+ for (const e of entries.values()) {
45365
+ const piece = e.piece;
45366
+ if (!piece) continue;
45367
+ const b = inTheWay(e, ax, az, PLAYER_RADIUS);
45368
+ if (!b) continue;
45369
+ const grip = FLOOR_FRICTION * piece.mass * 9.81;
45370
+ if (force <= grip) continue;
45371
+ const along = piece.velocity[0] * dx + piece.velocity[2] * dz;
45372
+ const impulse = Math.min(piece.mass * Math.max(0, speed - along), (force - grip) * dt);
45373
+ if (impulse <= 1e-4) continue;
45374
+ const y = (b.base + Math.max(b.base, Math.min(b.top, player.feet + 1.4))) / 2;
45375
+ const point = [Math.max(b.minX, Math.min(b.maxX, player.x)), y, Math.max(b.minZ, Math.min(b.maxZ, player.z))];
45376
+ piece.sleeping = false;
45377
+ renderer.impulse(e.so.objectId, [dx * impulse, 0, dz * impulse], point);
45378
+ log.coalesce(`bump:${e.so.objectId}`, { channel: "action", type: "bump", text: t("act.bump", { name: e.name, kg: num(piece.mass, 1) }), objectId: e.so.objectId, itemId: e.so.itemId, data: { massKg: Math.round(piece.mass * 10) / 10, pushN: force } }, 1);
45379
+ }
45380
+ };
42494
45381
  const massGuess = (spec) => {
42495
45382
  let m = 0;
42496
45383
  for (const p of spec.parts) {
@@ -42543,6 +45430,7 @@ void main(){
42543
45430
  explosives.clear();
42544
45431
  fireZones.length = 0;
42545
45432
  burningStains.length = 0;
45433
+ terrain.setLayout(defaultGround);
42546
45434
  for (const id of [...entries.keys()]) if (!entries.get(id)?.arena) removeEntry(id);
42547
45435
  clearLoose();
42548
45436
  for (const e of entries.values()) {
@@ -42580,6 +45468,8 @@ void main(){
42580
45468
  action("reset", t("act.reset"));
42581
45469
  };
42582
45470
  const hardReset = () => {
45471
+ pushByKey = false;
45472
+ push.end();
42583
45473
  clearEverything();
42584
45474
  for (const id of [...entries.keys()]) removeEntry(id);
42585
45475
  tools.clear();
@@ -42587,6 +45477,7 @@ void main(){
42587
45477
  if (pristineWorld) {
42588
45478
  for (const id of world.ids()) world.remove(id);
42589
45479
  world.load(pristineWorld);
45480
+ registerGround();
42590
45481
  }
42591
45482
  if (pristineFluids) {
42592
45483
  fluids.load(pristineFluids);
@@ -42600,6 +45491,7 @@ void main(){
42600
45491
  prevVy.clear();
42601
45492
  lastImpact.clear();
42602
45493
  addArena();
45494
+ standFootprints();
42603
45495
  timeScale = 1;
42604
45496
  fastForward = false;
42605
45497
  walkPlan = null;
@@ -42664,10 +45556,11 @@ void main(){
42664
45556
  }
42665
45557
  feetPrev = [x, z];
42666
45558
  avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
42667
- avatarState.crouch = crouchForced || player.crouching;
45559
+ avatarState.crouch = crouchForced || player.crouching || push.isBent;
42668
45560
  avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
42669
45561
  avatarState.pitch = player.pitch;
42670
45562
  avatarState.holding = heldPiece !== null;
45563
+ avatarState.push = push.avatarState;
42671
45564
  avatarState.itemUse = useItemPlayer ? { id: useItemName, ...useItemPlayer.pose() } : heldPiece && heldTilt > 0.01 ? { id: "tilt", position: [0.2, -0.1, -0.38], rotation: [-heldTilt, 0, 0] } : null;
42672
45565
  avatar.root.position.set(x, player.feet, z);
42673
45566
  avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
@@ -42683,11 +45576,11 @@ void main(){
42683
45576
  const stepFirstPerson = (dt) => {
42684
45577
  const arms = avatar?.firstPerson;
42685
45578
  if (!arms) return;
42686
- const targets = handSource?.() ?? carriedHand;
45579
+ const targets = push.handTargets() ?? handSource?.() ?? carriedHand;
42687
45580
  const state = {
42688
45581
  camera: camera2,
42689
45582
  targets,
42690
- stance: handStances[toolId] ?? (targets ? "hold" : "relaxed"),
45583
+ stance: push.active ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
42691
45584
  speed: avatarState.speed,
42692
45585
  running: avatarState.running,
42693
45586
  crouch: avatarState.crouch,
@@ -42708,6 +45601,19 @@ void main(){
42708
45601
  camera2.updateMatrixWorld(true);
42709
45602
  };
42710
45603
  };
45604
+ const KICK_HIP = 0.9;
45605
+ const KICK_DROP = 0.35;
45606
+ const legAim = () => {
45607
+ aim();
45608
+ const low = aimO[1] < player.feet + KICK_HIP;
45609
+ const pitch = Math.max(-0.9, Math.min(0.1, player.pitch - (low ? 0 : KICK_DROP)));
45610
+ aimO[0] = player.x;
45611
+ aimO[1] = Math.min(player.feet + KICK_HIP, aimO[1]);
45612
+ aimO[2] = player.z;
45613
+ aimD[0] = -Math.sin(player.yaw) * Math.cos(pitch);
45614
+ aimD[1] = Math.sin(pitch);
45615
+ aimD[2] = -Math.cos(player.yaw) * Math.cos(pitch);
45616
+ };
42711
45617
  const aim = () => {
42712
45618
  camera2.updateMatrixWorld(true);
42713
45619
  camera2.getWorldPosition(tmpV2);
@@ -42874,17 +45780,51 @@ void main(){
42874
45780
  if (!rendererPieces.has(objectId)) return;
42875
45781
  renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
42876
45782
  };
45783
+ const shotRay = new THREE.Raycaster();
45784
+ const LIQUID_SLOWING = 0.25;
45785
+ const SHOT_GRIP = 0.3;
45786
+ const shotPush = (s, objectId, results, fallback2) => {
45787
+ const e = entries.get(objectId);
45788
+ const speed = Math.max(80, s.speed);
45789
+ const onIt = results.filter((r) => r.object === objectId);
45790
+ const last = onIt[onIt.length - 1];
45791
+ let kept = last && last.penetrated ? Math.sqrt(Math.max(0, Math.min(1, last.exitEnergy / Math.max(s.energy, 1e-6)))) : 0;
45792
+ let point = fallback2;
45793
+ let inward = [s.direction[0], s.direction[1], s.direction[2]];
45794
+ if (e) {
45795
+ shotRay.set(new THREE.Vector3(...s.origin), new THREE.Vector3(...s.direction).normalize());
45796
+ shotRay.far = 60;
45797
+ shotRay.camera = camera2;
45798
+ const hit = shotRay.intersectObject(e.so.root, true).find((h) => h.object.isMesh && !h.object.userData.destructionDebris && h.face);
45799
+ if (hit?.face) {
45800
+ point = [hit.point.x, hit.point.y, hit.point.z];
45801
+ const n = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
45802
+ if (n.x * s.direction[0] + n.y * s.direction[1] + n.z * s.direction[2] > 0) n.negate();
45803
+ inward = [-n.x, -n.y, -n.z];
45804
+ }
45805
+ const c = kept > 0 && e.containerId ? fluids.container(e.containerId) : null;
45806
+ if (c && c.liters > 1e-4) {
45807
+ const size = new THREE.Box3().setFromObject(e.so.root).getSize(new THREE.Vector3());
45808
+ kept *= Math.exp(-(Math.min(size.x, size.z) * 0.9) / LIQUID_SLOWING);
45809
+ }
45810
+ }
45811
+ const momentum = 2 * s.energy / speed * (1 - kept);
45812
+ const along = s.direction[0] * inward[0] + s.direction[1] * inward[1] + s.direction[2] * inward[2];
45813
+ const impulse = [0, 1, 2].map((k) => momentum * (along * inward[k] + SHOT_GRIP * (s.direction[k] - along * inward[k])));
45814
+ return { impulse, point };
45815
+ };
42877
45816
  const shoveFree = (pushes) => {
42878
- for (const [objectId, push] of pushes) {
45817
+ for (const [objectId, push2] of pushes) {
42879
45818
  const e = entries.get(objectId);
42880
45819
  if (!e || e.piece || !e.free) continue;
42881
45820
  const mass = Math.max(0.2, massOfObject(objectId));
42882
- if (Math.hypot(...push.impulse) / mass < SHOVE_SPEED) continue;
45821
+ if (Math.hypot(...push2.impulse) / mass < SHOVE_SPEED) continue;
42883
45822
  const piece = renderer.makePiece(objectId, { mass });
42884
45823
  e.piece = piece;
42885
45824
  rendererPieces.set(objectId, piece);
45825
+ leaveFootprint(objectId);
42886
45826
  action("loosened", t("act.shoved", { name: e.name }), { objectId, itemId: e.so.itemId });
42887
- renderer.impulse(objectId, push.impulse, push.point);
45827
+ renderer.impulse(objectId, push2.impulse, push2.point);
42888
45828
  }
42889
45829
  };
42890
45830
  const fireShots = (shots, recoil) => {
@@ -42907,12 +45847,11 @@ void main(){
42907
45847
  hits++;
42908
45848
  end3 = r0.point;
42909
45849
  if (!first) first = { object: r0.object, part: r0.part, point: r0.point, normal: [0, 1, 0], distance: dist3(s.origin, r0.point) };
42910
- const speed = Math.max(80, s.speed);
42911
- const momentum = 2 * s.energy / speed;
42912
- pushPiece(r0.object, s.direction, momentum, r0.point);
42913
- const push = pushes.get(r0.object) ?? { impulse: [0, 0, 0], point: r0.point };
42914
- for (let k = 0; k < 3; k++) push.impulse[k] += s.direction[k] * momentum;
42915
- pushes.set(r0.object, push);
45850
+ const blow3 = shotPush(s, r0.object, results, r0.point);
45851
+ if (rendererPieces.has(r0.object)) renderer.impulse(r0.object, blow3.impulse, blow3.point);
45852
+ const push2 = pushes.get(r0.object) ?? { impulse: [0, 0, 0], point: blow3.point };
45853
+ for (let k = 0; k < 3; k++) push2.impulse[k] += blow3.impulse[k];
45854
+ pushes.set(r0.object, push2);
42916
45855
  }
42917
45856
  if (shots.length <= 3 || rnd2() < 0.34) tracer(m, end3);
42918
45857
  }
@@ -42990,8 +45929,22 @@ void main(){
42990
45929
  1
42991
45930
  );
42992
45931
  };
42993
- const meleeUse = (u, id = toolId) => {
45932
+ const kickHeights = options.kickHeights ?? [0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85];
45933
+ const footRay = (u) => {
45934
+ const flat = Math.hypot(u.direction[0], u.direction[2]);
45935
+ const direction = flat > 1e-6 ? [u.direction[0] / flat, 0, u.direction[2] / flat] : [-Math.sin(player.yaw), 0, -Math.cos(player.yaw)];
45936
+ const feet = player.position;
45937
+ let best = null;
45938
+ for (const height2 of kickHeights) {
45939
+ const origin = [feet[0], feet[1] + height2, feet[2]];
45940
+ const hit = rayWorld(origin, direction, u.reach);
45941
+ if (hit && (!best || hit.distance < best.distance)) best = { origin, distance: hit.distance };
45942
+ }
45943
+ return best ? { ...u, origin: best.origin, direction } : u;
45944
+ };
45945
+ const meleeUse = (use, id = toolId) => {
42994
45946
  const w = toolName(id);
45947
+ const u = id === "kick" ? footRay(use) : use;
42995
45948
  const r = rayWorld(u.origin, u.direction, u.reach);
42996
45949
  if (!r) {
42997
45950
  action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
@@ -43012,27 +45965,49 @@ void main(){
43012
45965
  const punchHand = options.punchHand ?? "alternate";
43013
45966
  const punchTools = new Set(options.punchTools ?? ["hand", "fists"]);
43014
45967
  let nextHand = "right";
45968
+ const combo = options.strikeCombo;
45969
+ let comboOn = !!combo && options.strikeComboOn === true;
45970
+ let comboNext = 0;
45971
+ let comboAt = -Infinity;
45972
+ const setStrikeCombo = (on) => {
45973
+ if (!combo || on === comboOn) return;
45974
+ comboOn = on;
45975
+ comboNext = 0;
45976
+ const name = combo.name?.[locale] ?? combo.id;
45977
+ action("combo", t(on ? "act.combo.on" : "act.combo.off", { name }), { data: { combo: combo.id, on } });
45978
+ say(t(on ? "act.combo.on" : "act.combo.off", { name }));
45979
+ };
43015
45980
  const strikeUse = (u, id = toolId) => {
43016
- const clip = strikeClips[id];
45981
+ const stepIndex = comboOn && combo?.tools.includes(id) && combo.steps.length > 0 ? simTime - comboAt > combo.reset ? 0 : comboNext : null;
45982
+ const step2 = stepIndex === null ? void 0 : combo.steps[stepIndex];
45983
+ const clip = step2 ? step2.clip : strikeClips[id];
43017
45984
  const leg = id === "kick" ? kickLeg === "alternate" ? nextLeg : kickLeg : void 0;
43018
- const hand = punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
45985
+ const hand = step2 ? step2.hand : punchTools.has(id) ? punchHand === "alternate" ? nextHand : punchHand : void 0;
43019
45986
  const side = leg ? { leg } : hand ? { hand } : void 0;
43020
45987
  const seconds = clip === void 0 ? null : avatar?.strike?.(clip, side) ?? null;
43021
45988
  if (leg && seconds !== null && kickLeg === "alternate") nextLeg = leg === "right" ? "left" : "right";
43022
- if (hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
45989
+ if (!step2 && hand && seconds !== null && punchHand === "alternate") nextHand = hand === "right" ? "left" : "right";
45990
+ if (step2 && stepIndex !== null) {
45991
+ comboNext = (stepIndex + 1) % combo.steps.length;
45992
+ comboAt = simTime;
45993
+ }
43023
45994
  if (clip === void 0 || seconds === null) {
43024
- meleeUse(u, id);
45995
+ if (leg) {
45996
+ legAim();
45997
+ meleeUse({ ...u, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, id);
45998
+ } else meleeUse(u, id);
43025
45999
  return;
43026
46000
  }
43027
46001
  const text = leg ? "act.strike.leg" : hand ? "act.strike.hand" : "act.strike";
43028
46002
  action("strike", t(text, { weapon: toolName(id), clip, seconds: num(seconds, 2), leg: leg ? t(`leg.${leg}`) : "", hand: hand ? t(`hand.${hand}`) : "" }), {
43029
- data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {} }
46003
+ data: { weapon: id, clip, impactIn: seconds, ...leg ? { leg } : {}, ...hand ? { hand } : {}, ...step2 ? { combo: combo.id, step: stepIndex } : {} }
43030
46004
  });
43031
46005
  strikes.push({
43032
46006
  key: id,
43033
46007
  remaining: seconds,
43034
46008
  land: () => {
43035
- aim();
46009
+ if (leg) legAim();
46010
+ else aim();
43036
46011
  meleeUse({ ...u, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, id);
43037
46012
  }
43038
46013
  });
@@ -43218,12 +46193,14 @@ void main(){
43218
46193
  if (res) applyUses(res, 0);
43219
46194
  };
43220
46195
  const kick = () => {
43221
- if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return;
46196
+ if (!arsenal.weapons.has("kick") || strikes.some((strike) => strike.key === "kick")) return false;
43222
46197
  aim();
43223
46198
  const res = toolState("kick").trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 0, false, "player");
43224
- if (res.status !== "ok") return;
46199
+ if (res.status !== "ok") return false;
43225
46200
  for (const u of res.uses) if (u.kind === "melee") strikeUse(u, "kick");
46201
+ return true;
43226
46202
  };
46203
+ const kickKeys = options.kick?.keys ?? ["KeyK"];
43227
46204
  const setTool = (id) => {
43228
46205
  if (!arsenal.weapons.has(id)) throw new Error(`game-test-room: unknown weapon "${id}"`);
43229
46206
  releaseTrigger();
@@ -43271,6 +46248,7 @@ void main(){
43271
46248
  else useTool(FIXED_DT, false);
43272
46249
  };
43273
46250
  const grab = () => {
46251
+ if (push.active) return false;
43274
46252
  if (heldPiece) {
43275
46253
  const name = heldName;
43276
46254
  releaseHeld();
@@ -43486,7 +46464,7 @@ void main(){
43486
46464
  const reach = Math.max(size.x, size.y, size.z) * 0.5;
43487
46465
  const origin = [centre.x + dir[0] * (reach + 0.02), centre.y + dir[1] * (reach + 0.02), centre.z + dir[2] * (reach + 0.02)];
43488
46466
  const r = withoutHeld(() => world.raycast(origin, dir, speed * FIXED_DT * 2 + 0.06));
43489
- if (!r || r.object === piece.objectId) return;
46467
+ if (!r || r.object === piece.objectId || r.object === GROUND_ID2) return;
43490
46468
  const energy = 0.5 * piece.mass * speed * speed;
43491
46469
  lastImpact.set(piece.objectId, simTime);
43492
46470
  world.hitPart(r.object, r.part, { type: "blunt", point: r.point, direction: dir, energy, radius: 0.08, source: "thrown" });
@@ -43499,12 +46477,21 @@ void main(){
43499
46477
  data: { energyJ: Math.round(energy), speedMs: Math.round(speed * 10) / 10, target: r.object, targetPart: r.part }
43500
46478
  });
43501
46479
  };
46480
+ const strikeGroundWith = (piece, energy) => {
46481
+ const box2 = tmpBox.setFromObject(piece.root);
46482
+ const centre = box2.getCenter(tmpV2);
46483
+ const size = box2.getSize(tmpV22);
46484
+ if (sceneryTopAt(centre.x, centre.z) > 0 || restingTopAt(centre.x, centre.z) > 0) return;
46485
+ const result2 = terrain.strike("blunt", { x: centre.x, z: centre.z, energy, contactRadius: Math.max(size.x, size.z) / 2 });
46486
+ if (result2.radius > 0) showGroundBlow(result2);
46487
+ };
43502
46488
  const land = (piece) => {
43503
46489
  const vy2 = piece.velocity[1];
43504
46490
  const prev = prevVy.get(piece.objectId) ?? 0;
43505
46491
  prevVy.set(piece.objectId, vy2);
43506
46492
  if (piece.held || prev > -MIN_LANDING_LOG || vy2 < prev + 2 || !world.has(piece.objectId)) return;
43507
46493
  const energy = 0.5 * piece.mass * prev * prev;
46494
+ strikeGroundWith(piece, energy);
43508
46495
  action("landing", t("act.landing", { name: nameOf(piece.objectId), speed: num(-prev, 1), energy: num(energy, energy < 100 ? 1 : 0) }), {
43509
46496
  objectId: piece.objectId,
43510
46497
  itemId: entryOf(piece.objectId)?.so.itemId,
@@ -43525,6 +46512,10 @@ void main(){
43525
46512
  const onKey = (code, down, e) => {
43526
46513
  if (!down) {
43527
46514
  if (code === "KeyZ") tiltKey = 0;
46515
+ if (code === "KeyE" && pushByKey) {
46516
+ pushByKey = false;
46517
+ if (push.end()) grab();
46518
+ }
43528
46519
  return;
43529
46520
  }
43530
46521
  const digit = /^Digit(\d)$/.exec(code);
@@ -43535,9 +46526,15 @@ void main(){
43535
46526
  if (idx < loadout.length) setTool(loadout[idx]);
43536
46527
  return;
43537
46528
  }
46529
+ if (kickKeys.includes(code) && !e.ctrlKey && !e.metaKey) {
46530
+ kick();
46531
+ return;
46532
+ }
43538
46533
  switch (code) {
43539
46534
  case "KeyE":
43540
- grab();
46535
+ if (e.repeat) break;
46536
+ pushByKey = !heldPiece && push.begin();
46537
+ if (!pushByKey) grab();
43541
46538
  break;
43542
46539
  case "KeyG":
43543
46540
  throwHeld();
@@ -43580,6 +46577,9 @@ void main(){
43580
46577
  case "KeyY":
43581
46578
  explosives.detonateCharges();
43582
46579
  break;
46580
+ case "KeyN":
46581
+ setStrikeCombo(!comboOn);
46582
+ break;
43583
46583
  case "KeyH": {
43584
46584
  const w = (heldEntry ?? entryOf(inspectTarget()?.object ?? ""))?.item?.weapon;
43585
46585
  if (w) equip(w);
@@ -43708,8 +46708,9 @@ void main(){
43708
46708
  }
43709
46709
  syncLiters();
43710
46710
  renderer.update(dt);
46711
+ terrainView.sync(camera2.position);
46712
+ showGroundFire(dt);
43711
46713
  vfx.update(dt);
43712
- craterPatch?.sync();
43713
46714
  updateImpacts();
43714
46715
  fluidView.update(dt);
43715
46716
  filmBridge.update(dt);
@@ -43733,12 +46734,21 @@ void main(){
43733
46734
  };
43734
46735
  const update = (dt) => {
43735
46736
  if (!ownsLoop) roomMetrics.frame(dt);
46737
+ frameStamp++;
43736
46738
  player.update(dt);
46739
+ if (pushByKey && !player.keys.has("KeyE")) {
46740
+ pushByKey = false;
46741
+ push.end();
46742
+ }
46743
+ push.step(paused ? 0 : dt);
46744
+ if (!paused) kickLight(dt);
46745
+ if (!paused) pushByPlayer(dt);
46746
+ else player.takeWish();
43737
46747
  stepAvatar(paused ? 0 : dt);
43738
46748
  if (tiltKey || heldTilt > 0) {
43739
46749
  heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
43740
46750
  }
43741
- if ((useHeld || useFirst) && !paused) {
46751
+ if ((useHeld || useFirst) && !paused && !push.active) {
43742
46752
  if (tool.def.category === "hand") {
43743
46753
  if (useFirst) handUse();
43744
46754
  } else if (useHeld) useTool(dt, !useFirst);
@@ -43780,6 +46790,7 @@ void main(){
43780
46790
  timeScale,
43781
46791
  fast: fastForward,
43782
46792
  tap: isTapOn(),
46793
+ combo: combo ? comboOn : null,
43783
46794
  blast: { shelter: blastOn("shelter"), fragmentHits: blastOn("fragmentHits"), confinement: blastOn("confinement") },
43784
46795
  quality: qualityName,
43785
46796
  camera: avatar ? cameraMode : null,
@@ -44055,6 +47066,9 @@ void main(){
44055
47066
  onClear(listener) {
44056
47067
  clearListeners.add(listener);
44057
47068
  return () => clearListeners.delete(listener);
47069
+ },
47070
+ addObject(itemId, place) {
47071
+ return addScenery(itemId, place.position, place.rotationY);
44058
47072
  }
44059
47073
  };
44060
47074
  let sendingInitial = false;
@@ -44209,6 +47223,7 @@ void main(){
44209
47223
  const n = Math.max(1, Math.round(seconds / dt));
44210
47224
  for (let i = 0; i < n; i++) {
44211
47225
  player.update(0);
47226
+ push.step(dt);
44212
47227
  followHeld(dt);
44213
47228
  stepSim(dt);
44214
47229
  }
@@ -44251,6 +47266,7 @@ void main(){
44251
47266
  } finally {
44252
47267
  scenarioRunning = false;
44253
47268
  paused = wasPaused;
47269
+ restoreScenery();
44254
47270
  }
44255
47271
  return report;
44256
47272
  };
@@ -44284,6 +47300,7 @@ void main(){
44284
47300
  const openArea = (area) => {
44285
47301
  clear();
44286
47302
  const mod2 = [...modules.values()].find((m) => m.id === area || m.group === area);
47303
+ if (mod2?.ground) terrain.setLayout(mod2.ground);
44287
47304
  const label = mod2?.group ?? S.groups[area] ?? area;
44288
47305
  ui?.showGroup(label);
44289
47306
  activeBench = mod2 && benches.has(mod2.id) ? mod2.id : lastBench();
@@ -44450,7 +47467,13 @@ void main(){
44450
47467
  setFastForward(on) {
44451
47468
  fastForward = on;
44452
47469
  },
47470
+ setStrikeCombo,
47471
+ kick,
47472
+ get strikeCombo() {
47473
+ return combo ? comboOn : null;
47474
+ },
44453
47475
  equip,
47476
+ terrain,
44454
47477
  openArea,
44455
47478
  modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
44456
47479
  openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
@@ -44502,6 +47525,12 @@ void main(){
44502
47525
  debugView.updateProjectionMatrix();
44503
47526
  debugView.updateMatrixWorld(true);
44504
47527
  },
47528
+ get playerPosition() {
47529
+ return player.position;
47530
+ },
47531
+ push: { begin: () => push.begin(), end: () => push.end(), setWalk: (walk) => push.setWalk(walk), get state() {
47532
+ return push.state;
47533
+ } },
44505
47534
  setCameraMode,
44506
47535
  get cameraMode() {
44507
47536
  return cameraMode;
@@ -44523,7 +47552,6 @@ void main(){
44523
47552
  loading.dispose();
44524
47553
  renderer.dispose();
44525
47554
  vfx.dispose();
44526
- craterPatch?.dispose();
44527
47555
  fluidView.dispose();
44528
47556
  filmBridge.dispose();
44529
47557
  film.dispose();
@@ -44601,6 +47629,8 @@ void main(){
44601
47629
  setTool(loadout[index]);
44602
47630
  },
44603
47631
  onTap: () => toggleTap(),
47632
+ ...combo ? { combo: { label: combo.name?.[locale] ?? combo.id, onToggle: () => setStrikeCombo(!comboOn) } } : {},
47633
+ ...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((key) => key.replace(/^Key/, "")).join("/")})`, onKick: () => void kick() } } : {},
44604
47634
  onBlast: (key) => toggleBlast(key),
44605
47635
  onPlay: (objectId, clipId) => {
44606
47636
  playAnimation(clipId, objectId);