@adatechnology/game-floorplan-three 0.6.0 → 0.8.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/dist/index.d.ts +5 -1
- package/dist/index.global.js +549 -98
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +389 -74
- package/dist/index.js.map +1 -1
- package/package.json +20 -21
package/dist/index.js
CHANGED
|
@@ -6236,6 +6236,10 @@ function installDestructionPieces(K, shared) {
|
|
|
6236
6236
|
function collapseRange(mesh, v0, v1) {
|
|
6237
6237
|
const pos = mesh.geometry.attributes.position;
|
|
6238
6238
|
const a = pos.array;
|
|
6239
|
+
const u = mesh.userData;
|
|
6240
|
+
if (!u._p0) u._p0 = a.slice();
|
|
6241
|
+
const list = u.collapsedRanges ?? (u.collapsedRanges = []);
|
|
6242
|
+
if (!list.some((r) => r[0] === v0 && r[1] === v1)) list.push([v0, v1]);
|
|
6239
6243
|
const saved = a.slice(v0 * 3, v1 * 3);
|
|
6240
6244
|
const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
|
|
6241
6245
|
for (let k = v0; k < v1; k++) {
|
|
@@ -6250,65 +6254,57 @@ function restoreRange(mesh, v0, saved) {
|
|
|
6250
6254
|
const pos = mesh.geometry.attributes.position;
|
|
6251
6255
|
pos.array.set(saved, v0 * 3);
|
|
6252
6256
|
pos.needsUpdate = true;
|
|
6257
|
+
const list = mesh.userData.collapsedRanges;
|
|
6258
|
+
if (list) mesh.userData.collapsedRanges = list.filter((r) => r[0] !== v0);
|
|
6253
6259
|
}
|
|
6260
|
+
function noise2(x, y, cell) {
|
|
6261
|
+
const h = (i2, j2) => {
|
|
6262
|
+
const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
|
|
6263
|
+
return s - Math.floor(s);
|
|
6264
|
+
};
|
|
6265
|
+
const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
|
|
6266
|
+
const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
|
|
6267
|
+
return (h(i, j) * (1 - su) + h(i + 1, j) * su) * (1 - sv) + (h(i, j + 1) * (1 - su) + h(i + 1, j + 1) * su) * sv;
|
|
6268
|
+
}
|
|
6269
|
+
var MASK_TEXEL = 0.02;
|
|
6270
|
+
var SPALL_MIN = 0.025;
|
|
6271
|
+
var SPALL_RAGGED = 0.045;
|
|
6272
|
+
var COVER = 0.015;
|
|
6273
|
+
var MAX_FLAKES = 64;
|
|
6254
6274
|
function installDestructionBreach(K, _shared) {
|
|
6255
6275
|
const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
|
|
6256
6276
|
function createBreachView(opts) {
|
|
6257
6277
|
const { house, structure } = opts;
|
|
6258
6278
|
const views = /* @__PURE__ */ new Map();
|
|
6279
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
6280
|
+
const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
|
|
6281
|
+
const dropped = [];
|
|
6259
6282
|
let lod = 0;
|
|
6260
|
-
const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
|
|
6261
6283
|
const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|
6262
6284
|
const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
|
|
6263
|
-
const wallColour = (items) => {
|
|
6264
|
-
for (const it of items) {
|
|
6265
|
-
if (!it.mesh) continue;
|
|
6266
|
-
const c = it.mesh.geometry.attributes.color;
|
|
6267
|
-
if (!c) continue;
|
|
6268
|
-
const a = c.array;
|
|
6269
|
-
tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
|
|
6270
|
-
return tmp.c.getHex();
|
|
6271
|
-
}
|
|
6272
|
-
return null;
|
|
6273
|
-
};
|
|
6274
6285
|
const local = (p) => {
|
|
6275
6286
|
house.updateMatrixWorld(true);
|
|
6276
6287
|
const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
|
|
6277
6288
|
return [v.x, v.y, v.z];
|
|
6278
6289
|
};
|
|
6279
|
-
function
|
|
6290
|
+
function cells(part, units) {
|
|
6280
6291
|
const [u0, v0, u1, v1] = part.aabb;
|
|
6281
6292
|
const alongU = u1 - u0 >= v1 - v0;
|
|
6282
6293
|
const len = alongU ? u1 - u0 : v1 - v0;
|
|
6283
|
-
const thick = alongU ? v1 - v0 : u1 - u0;
|
|
6284
6294
|
const [ul, uh] = units.size;
|
|
6285
6295
|
const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
|
|
6286
6296
|
const rowH = (part.y1 - part.y0) / rows;
|
|
6287
6297
|
const per = Math.max(1, Math.round(len / ul));
|
|
6288
6298
|
const w = len / per;
|
|
6289
|
-
const
|
|
6290
|
-
const list = [];
|
|
6299
|
+
const out = [];
|
|
6291
6300
|
for (let r = 0; r < rows; r++) {
|
|
6292
6301
|
const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
a = Math.max(0, a);
|
|
6297
|
-
b = Math.min(len, b);
|
|
6298
|
-
if (b - a < 1e-3) continue;
|
|
6299
|
-
const mid = (a + b) / 2;
|
|
6300
|
-
const x = alongU ? u0 + mid : (u0 + u1) / 2;
|
|
6301
|
-
const z = alongU ? (v0 + v1) / 2 : v0 + mid;
|
|
6302
|
-
const y = part.y0 + (r + 0.5) * rowH;
|
|
6303
|
-
const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
|
|
6304
|
-
if (opening && d < opening.radius) continue;
|
|
6305
|
-
const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
|
|
6306
|
-
if (chips.some(gone)) continue;
|
|
6307
|
-
const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
|
|
6308
|
-
list.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
|
|
6302
|
+
for (let i = 0; i < per + (shift ? 1 : 0); i++) {
|
|
6303
|
+
const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
|
|
6304
|
+
if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
|
|
6309
6305
|
}
|
|
6310
6306
|
}
|
|
6311
|
-
return
|
|
6307
|
+
return out;
|
|
6312
6308
|
}
|
|
6313
6309
|
const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
|
|
6314
6310
|
const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
|
|
@@ -6322,6 +6318,200 @@ function installDestructionBreach(K, _shared) {
|
|
|
6322
6318
|
return null;
|
|
6323
6319
|
}
|
|
6324
6320
|
const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
|
|
6321
|
+
function shell(it, mask) {
|
|
6322
|
+
const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
|
|
6323
|
+
const src = it.mesh.userData._p0 || g0.attributes.position.array;
|
|
6324
|
+
for (const [name, attr] of Object.entries(g0.attributes)) {
|
|
6325
|
+
const k = attr.itemSize;
|
|
6326
|
+
g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
|
|
6327
|
+
}
|
|
6328
|
+
if (g0.index) {
|
|
6329
|
+
const ix = g0.index.array.slice(it.i0, it.i1);
|
|
6330
|
+
for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
|
|
6331
|
+
g.setIndex(new THREE.BufferAttribute(ix, 1));
|
|
6332
|
+
}
|
|
6333
|
+
g.computeBoundingBox();
|
|
6334
|
+
g.computeBoundingSphere();
|
|
6335
|
+
const base = it.mesh.material;
|
|
6336
|
+
const material = base.clone();
|
|
6337
|
+
const toHouse = { value: new THREE.Matrix4() };
|
|
6338
|
+
material.onBeforeCompile = (sh) => {
|
|
6339
|
+
base.onBeforeCompile?.(sh);
|
|
6340
|
+
sh.uniforms.uToHouse = toHouse;
|
|
6341
|
+
sh.uniforms.uMask = { value: mask.texture };
|
|
6342
|
+
sh.uniforms.uMaskRect = { value: mask.rect };
|
|
6343
|
+
sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
|
|
6344
|
+
sh.vertexShader = sh.vertexShader.replace("void main() {", "uniform mat4 uToHouse;\nvarying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\nvHousePos = (uToHouse * vec4(transformed, 1.0)).xyz;");
|
|
6345
|
+
sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uMask;\nuniform vec4 uMaskRect;\nuniform float uMaskAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 mUv = vec2(((uMaskAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uMaskRect.x) / uMaskRect.z, (vHousePos.y - uMaskRect.y) / uMaskRect.w);\n if (mUv.x >= 0.0 && mUv.x <= 1.0 && mUv.y >= 0.0 && mUv.y <= 1.0 && texture2D(uMask, mUv).r > 0.5) discard;");
|
|
6346
|
+
};
|
|
6347
|
+
material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
|
|
6348
|
+
const mesh = new THREE.Mesh(g, material);
|
|
6349
|
+
mesh.name = "breach-shell";
|
|
6350
|
+
mesh.userData.breachMask = mask;
|
|
6351
|
+
mesh.userData.cutAt = (p) => {
|
|
6352
|
+
const v = local([p.x, p.y, p.z]);
|
|
6353
|
+
const i = Math.floor(((mask.alongU ? v[0] : v[2]) - mask.start) / mask.len * mask.w), j = Math.floor((v[1] - mask.y0) / mask.height * mask.h);
|
|
6354
|
+
return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
|
|
6355
|
+
};
|
|
6356
|
+
mesh.position.copy(it.mesh.position);
|
|
6357
|
+
mesh.quaternion.copy(it.mesh.quaternion);
|
|
6358
|
+
mesh.scale.copy(it.mesh.scale);
|
|
6359
|
+
mesh.castShadow = it.mesh.castShadow;
|
|
6360
|
+
mesh.receiveShadow = it.mesh.receiveShadow;
|
|
6361
|
+
mesh.renderOrder = it.mesh.renderOrder;
|
|
6362
|
+
mesh.onBeforeRender = () => {
|
|
6363
|
+
house.updateMatrixWorld();
|
|
6364
|
+
toHouse.value.copy(house.matrixWorld).invert().multiply(mesh.matrixWorld);
|
|
6365
|
+
};
|
|
6366
|
+
it.mesh.parent.add(mesh);
|
|
6367
|
+
mesh.updateMatrixWorld(true);
|
|
6368
|
+
toHouse.value.copy(house.matrixWorld).invert().multiply(mesh.matrixWorld);
|
|
6369
|
+
return mesh;
|
|
6370
|
+
}
|
|
6371
|
+
function unitsMaterial(core) {
|
|
6372
|
+
const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
|
|
6373
|
+
material.onBeforeCompile = (sh) => {
|
|
6374
|
+
sh.uniforms.uCore = { value: core.texture };
|
|
6375
|
+
sh.uniforms.uCoreRect = { value: core.rect };
|
|
6376
|
+
sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
|
|
6377
|
+
sh.vertexShader = sh.vertexShader.replace("void main() {", "varying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\n#ifdef USE_INSTANCING\nvHousePos = (instanceMatrix * vec4(transformed, 1.0)).xyz;\n#else\nvHousePos = transformed;\n#endif");
|
|
6378
|
+
sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uCore;\nuniform vec4 uCoreRect;\nuniform float uCoreAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 cUv = vec2(((uCoreAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uCoreRect.x) / uCoreRect.z, (vHousePos.y - uCoreRect.y) / uCoreRect.w);\n if (cUv.x >= 0.0 && cUv.x <= 1.0 && cUv.y >= 0.0 && cUv.y <= 1.0 && texture2D(uCore, cUv).r > 0.5) discard;").replace("#include <color_fragment>", "#include <color_fragment>\nif (!gl_FrontFacing) diffuseColor.rgb *= 0.55;");
|
|
6379
|
+
};
|
|
6380
|
+
material.customProgramCacheKey = () => "breachCore";
|
|
6381
|
+
material.userData.breachCore = core;
|
|
6382
|
+
return material;
|
|
6383
|
+
}
|
|
6384
|
+
function maskOf(hostId, parts) {
|
|
6385
|
+
const host = structure.byId.get(hostId);
|
|
6386
|
+
const [u0, v0, u1, v1] = host.aabb;
|
|
6387
|
+
const alongU = u1 - u0 >= v1 - v0;
|
|
6388
|
+
const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
|
|
6389
|
+
const len = alongU ? u1 - u0 : v1 - v0;
|
|
6390
|
+
const w = Math.max(2, Math.min(2048, Math.ceil(len / MASK_TEXEL))), h = Math.max(2, Math.min(1024, Math.ceil((y1 - y0) / MASK_TEXEL)));
|
|
6391
|
+
const data = new Uint8Array(w * h);
|
|
6392
|
+
const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
|
|
6393
|
+
texture.magFilter = THREE.LinearFilter;
|
|
6394
|
+
texture.minFilter = THREE.LinearFilter;
|
|
6395
|
+
texture.needsUpdate = true;
|
|
6396
|
+
return { data, texture, w, h, alongU, start: alongU ? u0 : v0, len, y0, height: y1 - y0, rect: new THREE.Vector4(alongU ? u0 : v0, y0, len, y1 - y0) };
|
|
6397
|
+
}
|
|
6398
|
+
const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
|
|
6399
|
+
const ellipseDistance = (s, y, cs, cy, rs, ry) => (Math.hypot((s - cs) / Math.max(rs, 1e-4), (y - cy) / Math.max(ry, 1e-4)) - 1) * Math.min(rs, ry);
|
|
6400
|
+
function covered(c, boxes) {
|
|
6401
|
+
const n = 8;
|
|
6402
|
+
let hit = 0;
|
|
6403
|
+
for (let j = 0; j < n; j++) {
|
|
6404
|
+
const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
|
|
6405
|
+
for (let i = 0; i < n; i++) {
|
|
6406
|
+
const s = c.a + (i + 0.5) / n * (c.b - c.a);
|
|
6407
|
+
if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
return hit / (n * n);
|
|
6411
|
+
}
|
|
6412
|
+
function paint(view) {
|
|
6413
|
+
const { mask, core } = view;
|
|
6414
|
+
const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
|
|
6415
|
+
const holes = [];
|
|
6416
|
+
const pits = [];
|
|
6417
|
+
const discs = [];
|
|
6418
|
+
const flakes = [];
|
|
6419
|
+
const shown = [];
|
|
6420
|
+
for (const id of view.parts) {
|
|
6421
|
+
const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id), spalls = view.spalls.get(id);
|
|
6422
|
+
const all = cells(part, units2);
|
|
6423
|
+
const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
|
|
6424
|
+
const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
|
|
6425
|
+
if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
|
|
6426
|
+
for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
|
|
6427
|
+
const boxes = chips.map((k) => ({ s0: along(k.center) - k.along, s1: along(k.center) + k.along, y0: k.center[1] - k.up, y1: k.center[1] + k.up }));
|
|
6428
|
+
const gone = (c) => covered(c, boxes.filter((k) => k.s1 > c.a && k.s0 < c.b && k.y1 > c.y0 && k.y0 < c.y1)) >= 0.9 || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
|
|
6429
|
+
const kept = [];
|
|
6430
|
+
for (const c of all) (gone(c) ? holes : kept).push(c);
|
|
6431
|
+
for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
|
|
6432
|
+
for (const k of boxes) {
|
|
6433
|
+
if (holes.some((h) => k.s0 >= h.a - 1e-4 && k.s1 <= h.b + 1e-4 && k.y0 >= h.y0 - 1e-4 && k.y1 <= h.y1 + 1e-4)) continue;
|
|
6434
|
+
pits.push({ s: (k.s0 + k.s1) / 2, y: (k.y0 + k.y1) / 2, rs: (k.s1 - k.s0) / 2, ry: (k.y1 - k.y0) / 2 });
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
const reach = SPALL_MIN + SPALL_RAGGED;
|
|
6438
|
+
const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
|
|
6439
|
+
mask.data.fill(0);
|
|
6440
|
+
core.data.fill(0);
|
|
6441
|
+
const stamp = (m, s0, s1, y0, y1, inside) => {
|
|
6442
|
+
const i0 = Math.max(0, Math.floor((s0 - reach) / m.len * m.w)), i1 = Math.min(m.w - 1, Math.ceil((s1 + reach) / m.len * m.w));
|
|
6443
|
+
const j0 = Math.max(0, Math.floor((y0 - reach - m.y0) / m.height * m.h)), j1 = Math.min(m.h - 1, Math.ceil((y1 + reach - m.y0) / m.height * m.h));
|
|
6444
|
+
for (let j = j0; j <= j1; j++) {
|
|
6445
|
+
const y = m.y0 + (j + 0.5) / m.h * m.height;
|
|
6446
|
+
for (let i = i0; i <= i1; i++) {
|
|
6447
|
+
const s = (i + 0.5) / m.w * m.len;
|
|
6448
|
+
if (inside(s, y)) m.data[j * m.w + i] = 255;
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
};
|
|
6452
|
+
const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
|
|
6453
|
+
for (const c of holes) stamp(mask, c.a, c.b, c.y0, c.y1, (s, y) => plaster(boxDistance(s, y, c.a, c.b, c.y0, c.y1), s, y));
|
|
6454
|
+
for (const d of discs) stamp(mask, d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => plaster(Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r), s, y));
|
|
6455
|
+
for (const f of flakes) {
|
|
6456
|
+
const rough = (s, y) => Math.hypot(s - f.s, y - f.y) - f.r * (0.65 + 0.7 * ragged(s * 2.3, y * 2.3));
|
|
6457
|
+
stamp(mask, f.s - 2 * f.r, f.s + 2 * f.r, f.y - 2 * f.r, f.y + 2 * f.r, (s, y) => rough(s, y) <= 0);
|
|
6458
|
+
}
|
|
6459
|
+
for (const p of pits) {
|
|
6460
|
+
stamp(mask, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => plaster(Math.max(0, ellipseDistance(s, y, p.s, p.y, p.rs, p.ry)), s, y));
|
|
6461
|
+
stamp(core, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => ellipseDistance(s, y, p.s, p.y, p.rs, p.ry) <= -0.35 * Math.min(p.rs, p.ry) * noise2(s * 1.7, y * 1.3, 0.012));
|
|
6462
|
+
}
|
|
6463
|
+
mask.texture.needsUpdate = true;
|
|
6464
|
+
core.texture.needsUpdate = true;
|
|
6465
|
+
const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach) || pits.some((p) => boxDistance(p.s, p.y, c.a, c.b, c.y0, c.y1) <= Math.max(p.rs, p.ry) + reach) || flakes.some((f) => boxDistance(f.s, f.y, c.a, c.b, c.y0, c.y1) <= 2 * f.r);
|
|
6466
|
+
const list = [];
|
|
6467
|
+
const tone = (hex, k) => tmp.c.setHex(hex).multiplyScalar(k).getHex();
|
|
6468
|
+
for (const { c, part, units: units2, thick, mid } of shown) {
|
|
6469
|
+
if (!near(c)) continue;
|
|
6470
|
+
const centre = mask.start + (c.a + c.b) / 2, y = (c.y0 + c.y1) / 2, len = c.b - c.a, h = c.y1 - c.y0;
|
|
6471
|
+
const at = (s, dy, d, colour) => list.push({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour });
|
|
6472
|
+
const base = fragmentMaterial(part.material).color.getHex();
|
|
6473
|
+
const n = noise2(centre * 3.1, y * 2.7, 0.07);
|
|
6474
|
+
if (units2.mortar === "concrete") {
|
|
6475
|
+
at(len, h, thick - 2 * COVER, tone(base, 0.9 + 0.04 * n));
|
|
6476
|
+
} else {
|
|
6477
|
+
at(len, h, thick - 2 * COVER - 0.012, 10327948);
|
|
6478
|
+
at(len - 0.012, h - 0.012, thick - 2 * COVER, tone(base, 0.8 + 0.35 * n));
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
if (view.units) {
|
|
6482
|
+
view.units.removeFromParent();
|
|
6483
|
+
view.units.dispose();
|
|
6484
|
+
}
|
|
6485
|
+
const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list.length));
|
|
6486
|
+
units.name = "breach:" + view.host;
|
|
6487
|
+
list.forEach((u, i) => {
|
|
6488
|
+
tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
|
|
6489
|
+
units.setMatrixAt(i, tmp.m);
|
|
6490
|
+
units.setColorAt(i, tmp.c.setHex(u.colour));
|
|
6491
|
+
});
|
|
6492
|
+
units.count = list.length;
|
|
6493
|
+
units.instanceMatrix.needsUpdate = true;
|
|
6494
|
+
if (units.instanceColor) units.instanceColor.needsUpdate = true;
|
|
6495
|
+
units.castShadow = true;
|
|
6496
|
+
units.receiveShadow = true;
|
|
6497
|
+
units.visible = lod === 0;
|
|
6498
|
+
house.add(units);
|
|
6499
|
+
view.units = units;
|
|
6500
|
+
view.gone = holes.length;
|
|
6501
|
+
view.pits = pits.length;
|
|
6502
|
+
view.flakes = flakes.length;
|
|
6503
|
+
}
|
|
6504
|
+
function chipBox(chip, alongU) {
|
|
6505
|
+
house.updateMatrixWorld(true);
|
|
6506
|
+
const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
|
|
6507
|
+
let a = 0, u = 0;
|
|
6508
|
+
for (let k = 0; k < 3; k++) {
|
|
6509
|
+
const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
|
|
6510
|
+
a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
|
|
6511
|
+
u += Math.abs(axis.y) * chip.size[k] / 2;
|
|
6512
|
+
}
|
|
6513
|
+
return { center: local(chip.center), along: a, up: u };
|
|
6514
|
+
}
|
|
6325
6515
|
function draw2(partId, change) {
|
|
6326
6516
|
if (lod !== 0) return false;
|
|
6327
6517
|
const hostId = hostOf(partId);
|
|
@@ -6331,40 +6521,50 @@ function installDestructionBreach(K, _shared) {
|
|
|
6331
6521
|
const items = structure.meshesOf(hostId).filter((it) => it.mesh);
|
|
6332
6522
|
if (!items.length) return false;
|
|
6333
6523
|
const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
|
|
6334
|
-
|
|
6524
|
+
if (!parts.includes(partId)) return false;
|
|
6525
|
+
const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
|
|
6526
|
+
view = {
|
|
6527
|
+
host: hostId,
|
|
6528
|
+
parts,
|
|
6529
|
+
items,
|
|
6530
|
+
mask,
|
|
6531
|
+
core,
|
|
6532
|
+
unitMaterial: unitsMaterial(core),
|
|
6533
|
+
shells: [],
|
|
6534
|
+
saved: null,
|
|
6535
|
+
units: null,
|
|
6536
|
+
gone: 0,
|
|
6537
|
+
pits: 0,
|
|
6538
|
+
chips: new Map(parts.map((id) => [id, []])),
|
|
6539
|
+
spalls: new Map(parts.map((id) => [id, []])),
|
|
6540
|
+
openings: /* @__PURE__ */ new Map(),
|
|
6541
|
+
flakes: 0
|
|
6542
|
+
};
|
|
6335
6543
|
}
|
|
6336
6544
|
if (!view.chips.has(partId)) return false;
|
|
6337
6545
|
if (change.opening) {
|
|
6338
6546
|
if (view.openings.has(partId)) return false;
|
|
6339
6547
|
view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
|
|
6340
|
-
if (partId === hostId) view.opening = view.openings.get(partId);
|
|
6341
6548
|
}
|
|
6342
|
-
if (change.chip) view.chips.get(partId).push(
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6549
|
+
if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
|
|
6550
|
+
if (change.spall) {
|
|
6551
|
+
const list = view.spalls.get(partId);
|
|
6552
|
+
list.push({ center: local(change.spall.center), radius: change.spall.radius });
|
|
6553
|
+
if (list.length > MAX_FLAKES) list.shift();
|
|
6554
|
+
}
|
|
6555
|
+
if (!view.saved) {
|
|
6556
|
+
view.shells = view.items.map((it) => shell(it, view.mask));
|
|
6557
|
+
for (const m of view.shells) m.userData.behind = () => view.units;
|
|
6558
|
+
view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
|
|
6348
6559
|
}
|
|
6349
|
-
const mesh = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list.length));
|
|
6350
|
-
mesh.name = "breach:" + hostId;
|
|
6351
|
-
list.forEach((u, i) => {
|
|
6352
|
-
tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
|
|
6353
|
-
mesh.setMatrixAt(i, tmp.m);
|
|
6354
|
-
mesh.setColorAt(i, tmp.c.setHex(u.colour));
|
|
6355
|
-
});
|
|
6356
|
-
mesh.count = list.length;
|
|
6357
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
6358
|
-
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
|
6359
|
-
mesh.castShadow = true;
|
|
6360
|
-
mesh.receiveShadow = true;
|
|
6361
|
-
house.add(mesh);
|
|
6362
|
-
if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
|
|
6363
|
-
view.mesh = mesh;
|
|
6364
|
-
view.units = list.length;
|
|
6365
6560
|
views.set(hostId, view);
|
|
6366
|
-
|
|
6367
|
-
|
|
6561
|
+
if (opts.deferPaint && change.spall) dirty.add(view);
|
|
6562
|
+
else {
|
|
6563
|
+
dirty.delete(view);
|
|
6564
|
+
paint(view);
|
|
6565
|
+
invalidate();
|
|
6566
|
+
invalidateShadow();
|
|
6567
|
+
}
|
|
6368
6568
|
return true;
|
|
6369
6569
|
}
|
|
6370
6570
|
function plain(view, shown) {
|
|
@@ -6373,30 +6573,88 @@ function installDestructionBreach(K, _shared) {
|
|
|
6373
6573
|
else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
|
|
6374
6574
|
}
|
|
6375
6575
|
}
|
|
6576
|
+
function flush(budgetMs = paintBudgetMs) {
|
|
6577
|
+
if (dirty.size === 0) return 0;
|
|
6578
|
+
const deadline = performance.now() + budgetMs;
|
|
6579
|
+
for (const view of dirty) {
|
|
6580
|
+
dirty.delete(view);
|
|
6581
|
+
paint(view);
|
|
6582
|
+
if (performance.now() >= deadline) break;
|
|
6583
|
+
}
|
|
6584
|
+
invalidate();
|
|
6585
|
+
invalidateShadow();
|
|
6586
|
+
return dirty.size;
|
|
6587
|
+
}
|
|
6588
|
+
function dispose(view) {
|
|
6589
|
+
dirty.delete(view);
|
|
6590
|
+
if (view.units) {
|
|
6591
|
+
view.units.removeFromParent();
|
|
6592
|
+
view.units.dispose();
|
|
6593
|
+
}
|
|
6594
|
+
view.unitMaterial.dispose();
|
|
6595
|
+
for (const m of view.shells) {
|
|
6596
|
+
m.removeFromParent();
|
|
6597
|
+
m.geometry.dispose();
|
|
6598
|
+
m.material.dispose();
|
|
6599
|
+
}
|
|
6600
|
+
view.mask.texture.dispose();
|
|
6601
|
+
view.core.texture.dispose();
|
|
6602
|
+
}
|
|
6376
6603
|
return {
|
|
6377
|
-
/** Handles a `stage` event of the house's object. Returns true when the wall was
|
|
6604
|
+
/** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
|
|
6378
6605
|
stage(event) {
|
|
6379
6606
|
return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
|
|
6380
6607
|
},
|
|
6381
|
-
/**
|
|
6608
|
+
/**
|
|
6609
|
+
* Handles a `spall` of a wall of units: the coat comes off in a ragged patch and the units show under it, drawn by the house (the destruction
|
|
6610
|
+
* renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
|
|
6611
|
+
*/
|
|
6612
|
+
spall(event) {
|
|
6613
|
+
const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
|
|
6614
|
+
return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
|
|
6615
|
+
},
|
|
6616
|
+
/**
|
|
6617
|
+
* Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
|
|
6618
|
+
* throws the bits and leaves its patch out. Call it once the spec is known.
|
|
6619
|
+
*/
|
|
6620
|
+
claim() {
|
|
6621
|
+
var _a;
|
|
6622
|
+
for (const q of opts.spec()?.parts ?? []) {
|
|
6623
|
+
if (!q.units) continue;
|
|
6624
|
+
const host = hostOf(q.id);
|
|
6625
|
+
if (!host) continue;
|
|
6626
|
+
for (const it of structure.meshesOf(host)) {
|
|
6627
|
+
if (!it.mesh) continue;
|
|
6628
|
+
const list = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
|
|
6629
|
+
if (!list.includes(q.id)) list.push(q.id);
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
},
|
|
6633
|
+
/**
|
|
6634
|
+
* Handles a `chip` event: what came out (a unit, or a crater a shot broke through one) leaves a hole in the wall where it was; a `torn` chip (what a blow
|
|
6635
|
+
* tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
|
|
6636
|
+
*/
|
|
6382
6637
|
chip(event) {
|
|
6383
|
-
|
|
6638
|
+
if (event.torn) return false;
|
|
6639
|
+
return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
|
|
6384
6640
|
},
|
|
6385
|
-
/** The part broke or fell:
|
|
6641
|
+
/** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
|
|
6386
6642
|
drop(partId) {
|
|
6387
6643
|
const v = views.get(partId);
|
|
6388
6644
|
if (!v) return;
|
|
6389
|
-
v
|
|
6390
|
-
v.mesh.dispose();
|
|
6645
|
+
dispose(v);
|
|
6391
6646
|
views.delete(partId);
|
|
6647
|
+
if (v.saved) dropped.push(v);
|
|
6392
6648
|
},
|
|
6393
|
-
/** LOD 0 draws the
|
|
6649
|
+
/** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
|
|
6394
6650
|
setLod(level) {
|
|
6651
|
+
flush(Infinity);
|
|
6395
6652
|
const was = lod;
|
|
6396
6653
|
lod = level;
|
|
6397
6654
|
if (was === 0 === (level === 0)) return;
|
|
6398
6655
|
for (const v of views.values()) {
|
|
6399
|
-
v.
|
|
6656
|
+
v.units.visible = level === 0;
|
|
6657
|
+
for (const m of v.shells) m.visible = level === 0;
|
|
6400
6658
|
plain(v, level !== 0);
|
|
6401
6659
|
}
|
|
6402
6660
|
invalidate();
|
|
@@ -6405,23 +6663,33 @@ function installDestructionBreach(K, _shared) {
|
|
|
6405
6663
|
/** Everything back to what it was (after `world.reset`). */
|
|
6406
6664
|
restore() {
|
|
6407
6665
|
for (const v of views.values()) {
|
|
6408
|
-
v
|
|
6409
|
-
v.mesh.dispose();
|
|
6666
|
+
dispose(v);
|
|
6410
6667
|
if (lod === 0) plain(v, true);
|
|
6411
6668
|
}
|
|
6412
6669
|
views.clear();
|
|
6670
|
+
for (const v of dropped) plain(v, true);
|
|
6671
|
+
dropped.length = 0;
|
|
6413
6672
|
},
|
|
6414
|
-
|
|
6673
|
+
flush,
|
|
6674
|
+
/** The wall has damage drawn. */
|
|
6415
6675
|
has(partId) {
|
|
6416
6676
|
return views.has(partId);
|
|
6417
6677
|
},
|
|
6418
|
-
|
|
6678
|
+
/** Walls with damage drawn, units out of them, craters shots broke through units that stay, and units laid bare at the edge of the cuts. */
|
|
6679
|
+
statistics: () => (flush(Infinity), {
|
|
6680
|
+
walls: views.size,
|
|
6681
|
+
gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
|
|
6682
|
+
craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
|
|
6683
|
+
flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
|
|
6684
|
+
units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
|
|
6685
|
+
})
|
|
6419
6686
|
};
|
|
6420
6687
|
}
|
|
6421
6688
|
Object.assign(K, { createBreachView });
|
|
6422
6689
|
}
|
|
6423
6690
|
|
|
6424
6691
|
// src/destruction/world.ts
|
|
6692
|
+
var FOLLOW_FLUSH_FALLBACK_MS = 100;
|
|
6425
6693
|
function installDestructionWorld(K, shared) {
|
|
6426
6694
|
const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
|
|
6427
6695
|
function createHouseDestruction(root, house, structure, parts, colliders, water) {
|
|
@@ -6430,7 +6698,7 @@ function installDestructionWorld(K, shared) {
|
|
|
6430
6698
|
const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
|
|
6431
6699
|
D.renderer = R;
|
|
6432
6700
|
D.spec = null;
|
|
6433
|
-
const breach = createBreachView({ house, structure, spec: () => D.spec });
|
|
6701
|
+
const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
|
|
6434
6702
|
D.breach = breach;
|
|
6435
6703
|
D.onEvent = (fn) => {
|
|
6436
6704
|
listeners.add(fn);
|
|
@@ -6615,6 +6883,9 @@ function installDestructionWorld(K, shared) {
|
|
|
6615
6883
|
case "chip":
|
|
6616
6884
|
breach.chip(e);
|
|
6617
6885
|
break;
|
|
6886
|
+
case "spall":
|
|
6887
|
+
breach.spall(e);
|
|
6888
|
+
break;
|
|
6618
6889
|
case "collapse":
|
|
6619
6890
|
for (const id of e.parts) {
|
|
6620
6891
|
breach.drop(id);
|
|
@@ -6656,7 +6927,8 @@ function installDestructionWorld(K, shared) {
|
|
|
6656
6927
|
D.specOptions = specOptions || null;
|
|
6657
6928
|
D.spec = structure.spec(specOptions);
|
|
6658
6929
|
D.id = D.world.add(D.spec, { id: "house3d:" + ++shared.destructionSeq, position: [p.x, p.y, p.z], rotation: [q.x, q.y, q.z, q.w] });
|
|
6659
|
-
|
|
6930
|
+
breach.claim();
|
|
6931
|
+
if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
|
|
6660
6932
|
off = D.world.on("*", handleEvent);
|
|
6661
6933
|
D.active = true;
|
|
6662
6934
|
return D;
|
|
@@ -6671,6 +6943,8 @@ function installDestructionWorld(K, shared) {
|
|
|
6671
6943
|
};
|
|
6672
6944
|
D.follow = (world, id, spec) => {
|
|
6673
6945
|
D.spec = spec;
|
|
6946
|
+
breach.claim();
|
|
6947
|
+
let fallback = null;
|
|
6674
6948
|
const offFollow = world.on("*", (e) => {
|
|
6675
6949
|
if (e.object !== id) return;
|
|
6676
6950
|
switch (e.type) {
|
|
@@ -6680,6 +6954,9 @@ function installDestructionWorld(K, shared) {
|
|
|
6680
6954
|
case "chip":
|
|
6681
6955
|
breach.chip(e);
|
|
6682
6956
|
break;
|
|
6957
|
+
case "spall":
|
|
6958
|
+
breach.spall(e);
|
|
6959
|
+
break;
|
|
6683
6960
|
case "fracture":
|
|
6684
6961
|
breach.drop(e.part);
|
|
6685
6962
|
break;
|
|
@@ -6689,10 +6966,16 @@ function installDestructionWorld(K, shared) {
|
|
|
6689
6966
|
default:
|
|
6690
6967
|
break;
|
|
6691
6968
|
}
|
|
6969
|
+
if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
|
|
6970
|
+
fallback = null;
|
|
6971
|
+
breach.flush(Infinity);
|
|
6972
|
+
}, FOLLOW_FLUSH_FALLBACK_MS);
|
|
6692
6973
|
});
|
|
6693
6974
|
return { restore: () => breach.restore(), dispose: () => {
|
|
6694
6975
|
offFollow();
|
|
6695
6976
|
breach.restore();
|
|
6977
|
+
}, update: () => {
|
|
6978
|
+
breach.flush();
|
|
6696
6979
|
} };
|
|
6697
6980
|
};
|
|
6698
6981
|
D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
|
|
@@ -6725,6 +7008,7 @@ function installDestructionWorld(K, shared) {
|
|
|
6725
7008
|
return r;
|
|
6726
7009
|
};
|
|
6727
7010
|
D.animate = (dt) => {
|
|
7011
|
+
breach.flush();
|
|
6728
7012
|
if (R) return;
|
|
6729
7013
|
let moved = false;
|
|
6730
7014
|
for (const [id, list] of bodies) {
|
|
@@ -8191,6 +8475,7 @@ function createFloorplanTestModule(options = {}) {
|
|
|
8191
8475
|
houseRenderer = create(THREE, {
|
|
8192
8476
|
locale,
|
|
8193
8477
|
lod: { interior: 10, hide: 40 },
|
|
8478
|
+
breachPaintBudgetMs: 4,
|
|
8194
8479
|
...options.house,
|
|
8195
8480
|
quality: settings.quality,
|
|
8196
8481
|
...options.world ? { destruction: options.world } : {},
|
|
@@ -8346,9 +8631,39 @@ function createFloorplanTestModule(options = {}) {
|
|
|
8346
8631
|
host?.log(text.steps.no_door);
|
|
8347
8632
|
return;
|
|
8348
8633
|
}
|
|
8349
|
-
|
|
8634
|
+
const fo = target.target;
|
|
8635
|
+
const reach = fo && host?.reachFor && fo.type === "swing" && fo.gs?.[0] && fo.handles?.[0] ? doorReach(p, fo) : null;
|
|
8636
|
+
if (reach && host?.reachFor) {
|
|
8637
|
+
host.reachFor({ ...reach, onGrip: () => void p.house.toggleDoor(fo) });
|
|
8638
|
+
} else p.house.interact(eyeOf(p));
|
|
8350
8639
|
host?.log(fill(text.steps.door, { text: String(target.text) }));
|
|
8351
8640
|
};
|
|
8641
|
+
const doorReach = (p, fo) => {
|
|
8642
|
+
const g = fo.gs[0];
|
|
8643
|
+
const pivot = fo.handles[0].pivot;
|
|
8644
|
+
const at = (angle) => {
|
|
8645
|
+
const c = Math.cos(angle);
|
|
8646
|
+
const s = Math.sin(angle);
|
|
8647
|
+
const v = new THREE.Vector3(pivot[0] * c + pivot[2] * s, pivot[1], -pivot[0] * s + pivot[2] * c).add(g.position);
|
|
8648
|
+
return g.parent ? g.parent.localToWorld(v) : v;
|
|
8649
|
+
};
|
|
8650
|
+
const camera = host?.camera;
|
|
8651
|
+
const eye = new THREE.Vector3(camera.position.x, 0, camera.position.z);
|
|
8652
|
+
const flat = (v) => Math.hypot(v.x - eye.x, v.z - eye.z);
|
|
8653
|
+
const closed = at(0);
|
|
8654
|
+
const open = at(fo.rot);
|
|
8655
|
+
const opening = !fo.open;
|
|
8656
|
+
return {
|
|
8657
|
+
handle: () => {
|
|
8658
|
+
g.updateMatrixWorld(true);
|
|
8659
|
+
return g.localToWorld(new THREE.Vector3(pivot[0], pivot[1], pivot[2])).toArray();
|
|
8660
|
+
},
|
|
8661
|
+
hinge: () => g.getWorldPosition(new THREE.Vector3()).toArray(),
|
|
8662
|
+
seconds: fo.locked || fo.blockedFlag ? 0.6 : 0.55,
|
|
8663
|
+
pull: opening ? flat(open) <= flat(closed) : flat(closed) <= flat(open),
|
|
8664
|
+
locked: !!(fo.locked || fo.blockedFlag)
|
|
8665
|
+
};
|
|
8666
|
+
};
|
|
8352
8667
|
const strike = () => {
|
|
8353
8668
|
const world = options.world;
|
|
8354
8669
|
if (!world || !host) return;
|