@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.
- package/README.md +5 -2
- package/dist/index.d.ts +88 -5
- package/dist/index.global.js +3293 -263
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +843 -94
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
package/dist/index.global.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2901
|
-
|
|
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,
|
|
4409
|
+
const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
|
|
4159
4410
|
var _a, _b;
|
|
4160
|
-
if (!p.spec.units ||
|
|
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 <
|
|
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
|
|
4492
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
|
|
16038
|
-
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
|
|
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
|
|
16430
|
-
const
|
|
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,
|
|
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 (
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
33584
|
+
const drag2 = cal.dragPer100m ?? 0.1;
|
|
32991
33585
|
const energyAt = (distance22) => {
|
|
32992
|
-
const f = Math.pow(1 -
|
|
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
|
|
35738
|
-
velocity[offset] *=
|
|
35739
|
-
velocity[offset + 1] *=
|
|
35740
|
-
velocity[offset + 2] *=
|
|
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
|
|
35970
|
-
if (
|
|
35971
|
-
|
|
35972
|
-
const progress = Math.min(1,
|
|
35973
|
-
const here =
|
|
35974
|
-
const ahead =
|
|
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 = -
|
|
35984
|
-
const across = offset /
|
|
35985
|
-
const stripe =
|
|
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 =
|
|
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 (
|
|
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,
|
|
35994
|
-
const velocity = [directionColumn *
|
|
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)
|
|
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
|
-
/**
|
|
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((
|
|
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
|
|
38434
|
-
|
|
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
|
|
40167
|
-
|
|
40168
|
-
|
|
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
|
|
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
|
-
|
|
40176
|
-
|
|
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)
|
|
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 = () =>
|
|
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
|
|
41010
|
+
return isCrouched();
|
|
40331
41011
|
},
|
|
40332
41012
|
keys,
|
|
40333
41013
|
update(dt) {
|
|
40334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40342
|
-
|
|
40343
|
-
if (
|
|
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
|
-
|
|
40359
|
-
if (y
|
|
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 <=
|
|
40363
|
-
y =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
42117
|
-
const p = world.part(
|
|
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 &&
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
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(...
|
|
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,
|
|
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
|
|
42911
|
-
|
|
42912
|
-
|
|
42913
|
-
|
|
42914
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|