@adatechnology/game-floorplan-structure 1.3.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { FloorplanEngine, ViewerLayer, RoomDef, DoorDef, WindowDef, FloorDef } from '@adatechnology/game-floorplan';
1
+ import { FloorplanEngine, ViewerLayer, RoomDef, DoorDef, WindowDef, FloorDef, PlanAxis } from '@adatechnology/game-floorplan';
2
+ export { LotPath } from '@adatechnology/game-floorplan';
2
3
  import { MaterialId, JointKind, StructureRole, SoilProfile, PileSpec, DestructibleSpec, WindExposure } from '@adatechnology/game-contracts';
3
4
  import { SoilDef, FrameOptions, WindOptions, SizeStructureOptions } from '@adatechnology/game-destruction';
4
5
 
@@ -256,7 +257,7 @@ interface StructureSource {
256
257
  link?: {
257
258
  from: number;
258
259
  to: number;
259
- } | null;
260
+ } | string | null;
260
261
  }[];
261
262
  facade: {
262
263
  roof?: string;
@@ -313,7 +314,7 @@ interface SourceDoor {
313
314
  u: number;
314
315
  v: number;
315
316
  width: number;
316
- side?: string;
317
+ side?: string | null;
317
318
  axis: 'u' | 'v';
318
319
  steps?: number;
319
320
  }
@@ -1159,6 +1160,254 @@ declare const towerLocales: {
1159
1160
  };
1160
1161
  };
1161
1162
 
1163
+ /** A point in the world: `[u, v, y]` (y is the absolute height). */
1164
+ type Point3 = [number, number, number];
1165
+ /** Something fixed to the inner face of a wall of a room: a socket, a switch, the panel. */
1166
+ interface WallFixture {
1167
+ id: string;
1168
+ room: string;
1169
+ floor: number;
1170
+ u: number;
1171
+ v: number;
1172
+ /** Height above the storey floor. */
1173
+ y: number;
1174
+ /** Position along the wall. */
1175
+ c: number;
1176
+ axis: PlanAxis;
1177
+ pos: number;
1178
+ frontU: number;
1179
+ frontV: number;
1180
+ }
1181
+ interface Outlet extends WallFixture {
1182
+ type: string;
1183
+ voltage: number;
1184
+ }
1185
+ interface LightSwitch extends WallFixture {
1186
+ door: string;
1187
+ /** On the latch side of the door. */
1188
+ latchSide: boolean;
1189
+ /** Lamp ids it turns on. */
1190
+ controls: string[];
1191
+ }
1192
+ interface ElectricalPanel extends WallFixture {
1193
+ w: number;
1194
+ h: number;
1195
+ breakers: number;
1196
+ }
1197
+ interface ElectricMeter {
1198
+ id: string;
1199
+ place: string;
1200
+ u: number;
1201
+ v: number;
1202
+ y: number;
1203
+ side: string;
1204
+ w: number;
1205
+ d: number;
1206
+ h: number;
1207
+ }
1208
+ /** Pole, drop, meter, descent and the run to the panel; a tap has no meter and a bare connection instead. */
1209
+ interface ServiceEntrance {
1210
+ pole: {
1211
+ u: number;
1212
+ v: number;
1213
+ h: number;
1214
+ };
1215
+ drop: Point3[];
1216
+ meter: ElectricMeter | null;
1217
+ descent: Point3[];
1218
+ internalRun: Point3[] | null;
1219
+ tap?: boolean;
1220
+ connection?: {
1221
+ u: number;
1222
+ v: number;
1223
+ y: number;
1224
+ };
1225
+ }
1226
+ interface Circuit {
1227
+ id: string;
1228
+ room: string;
1229
+ floor: number;
1230
+ outlets: string[];
1231
+ switches: string[];
1232
+ lamps: string[];
1233
+ exposed: boolean;
1234
+ gauge_mm2: number;
1235
+ }
1236
+ interface Conduit {
1237
+ id: string;
1238
+ circuit: string;
1239
+ from: string;
1240
+ to: string;
1241
+ exposed: boolean;
1242
+ diameter: number;
1243
+ points: Point3[];
1244
+ }
1245
+ interface OutdoorLamp {
1246
+ id: string;
1247
+ u: number;
1248
+ v: number;
1249
+ y: number;
1250
+ door: string;
1251
+ switch: string | null;
1252
+ }
1253
+ interface PlumbingPoint {
1254
+ id: string;
1255
+ kind: string;
1256
+ item: string;
1257
+ room: string | null;
1258
+ floor: number;
1259
+ u: number;
1260
+ v: number;
1261
+ y: number;
1262
+ flowLpm: number | undefined;
1263
+ run: string | null;
1264
+ }
1265
+ interface PlumbingRun {
1266
+ id: string;
1267
+ from: string;
1268
+ /** A point, a room, or `ceiling`. */
1269
+ to: string | null;
1270
+ points: Point3[];
1271
+ diameter: number;
1272
+ exposed: string;
1273
+ }
1274
+ interface Plumbing {
1275
+ tank: 'water_tank' | null;
1276
+ capacityL: number;
1277
+ points: PlumbingPoint[];
1278
+ visibleRuns: PlumbingRun[];
1279
+ }
1280
+ interface SolarPanel {
1281
+ id: string;
1282
+ u: number;
1283
+ v: number;
1284
+ w: number;
1285
+ d: number;
1286
+ length: number;
1287
+ }
1288
+ interface Inverter {
1289
+ id: string;
1290
+ u: number;
1291
+ v: number;
1292
+ y: number;
1293
+ side: string;
1294
+ w: number;
1295
+ d: number;
1296
+ h: number;
1297
+ outdoor: boolean;
1298
+ link: string;
1299
+ }
1300
+ interface Solar {
1301
+ type: 'slab_rack' | 'roof';
1302
+ orientation: string;
1303
+ north: 'front' | 'back';
1304
+ slopeSide: 'front' | 'back' | null;
1305
+ tilt: number;
1306
+ area: number[];
1307
+ areaM2: number;
1308
+ count: number;
1309
+ floor: number;
1310
+ y: number;
1311
+ panels: SolarPanel[];
1312
+ inverter: Inverter | null;
1313
+ }
1314
+ /** A door or a window in the wall of an outbuilding. */
1315
+ interface OutbuildingOpening {
1316
+ id: string;
1317
+ axis: PlanAxis;
1318
+ pos: number;
1319
+ at: number;
1320
+ u: number;
1321
+ v: number;
1322
+ width: number;
1323
+ side: string;
1324
+ height?: number;
1325
+ exterior?: boolean;
1326
+ y0?: number;
1327
+ y1?: number;
1328
+ /** Swing area of a door. */
1329
+ zone?: Rect;
1330
+ }
1331
+ interface OutbuildingWall {
1332
+ axis: PlanAxis;
1333
+ u0: number;
1334
+ v0: number;
1335
+ u1: number;
1336
+ v1: number;
1337
+ thickness: number;
1338
+ h: number;
1339
+ side: string;
1340
+ }
1341
+ interface OutbuildingItem {
1342
+ type: string;
1343
+ category: string;
1344
+ u: number;
1345
+ v: number;
1346
+ w: number;
1347
+ d: number;
1348
+ h: number;
1349
+ ry: number;
1350
+ frontU: number;
1351
+ frontV: number;
1352
+ aabb: Rect;
1353
+ }
1354
+ interface Outbuilding {
1355
+ id: string;
1356
+ type: 'outbuilding';
1357
+ use: string;
1358
+ floor: number;
1359
+ aabb: Rect;
1360
+ interior: Rect[];
1361
+ ceiling: number;
1362
+ thickness: number;
1363
+ front: [number, number];
1364
+ walls: OutbuildingWall[];
1365
+ doors: OutbuildingOpening[];
1366
+ windows: OutbuildingOpening[];
1367
+ items: OutbuildingItem[];
1368
+ }
1369
+
1370
+ type EnergyMode = 'grid' | 'tap' | 'offgrid';
1371
+ /** What a template says about its energy (a shack taps the grid without a meter, a cabin may run off-grid on solar). */
1372
+ interface EnergyConfig {
1373
+ mode: EnergyMode;
1374
+ solar?: boolean;
1375
+ }
1376
+ interface Electrical {
1377
+ mode: EnergyMode;
1378
+ serviceEntrance: ServiceEntrance | null;
1379
+ panel: ElectricalPanel | null;
1380
+ outlets: Outlet[];
1381
+ switches: LightSwitch[];
1382
+ circuits: Circuit[];
1383
+ conduits: Conduit[];
1384
+ exposed: boolean;
1385
+ outdoorLamps: OutdoorLamp[];
1386
+ source?: 'solar' | 'lamp';
1387
+ }
1388
+
1389
+ interface GasMeter {
1390
+ id: 'gas.meter';
1391
+ u: number;
1392
+ v: number;
1393
+ y: number;
1394
+ side: string;
1395
+ w: number;
1396
+ d: number;
1397
+ h: number;
1398
+ }
1399
+ interface Gas {
1400
+ type: 'piped' | 'cylinder';
1401
+ meter: GasMeter | null;
1402
+ }
1403
+
1404
+ interface Utilities {
1405
+ gas: Gas | null;
1406
+ electrical: Electrical;
1407
+ solar: Solar | null;
1408
+ plumbing: Plumbing;
1409
+ }
1410
+
1162
1411
  declare const version = "0.1.0";
1163
1412
 
1164
- export { CONSTRUCTIONS, CONSTRUCTION_SYSTEMS, CORE_DEFAULTS, CURTAIN_DEFAULTS, type ChaletOptions, type Chimney, type Construction, type ConstructionBuild, type ConstructionBuilder, type ConstructionSystem, type ConstructionSystemDef, type CoreOptions, type CurtainWallOptions, DAMPER_DEFAULTS, DENSITY, type DamperOptions, type DestructibleSpecOptions, FLAT_ROOF_DEFAULTS, type FlatRoofOptions, type FoundationType, GROUND, MECHANICAL_DEFAULTS, MEMBER_CAGE, type MechanicalFloorOptions, type Opening, PILE_AREA, PILE_DEFAULTS, PLATE_DEFAULTS, PUNCHED_DEFAULTS, type PartRole, type PileDesign, type PlanContext, type PlateOptions, type PlateRect, type PlateStrip, type PunchedFacadeOptions, RAFT_THICKNESS, REBAR_RATIOS, ROOF_COVERINGS, type Rect, type Roof, type RoofCoveringDef, type RoofPane, type RoofShape, SKYSCRAPER_DEFAULTS, SKYSCRAPER_MAX_STOREYS, SLAB_PANEL_DEFAULTS, type ShackOptions, type SkyscraperLayout, type SkyscraperOptions, type SkyscraperPreset, type SlabPanelOptions, type SourceDoor, type SourceWall, type SourceWindow, type StoreyContext, type StoreyKind, type StripFill, type StripFiller, type StripId, type Structure, type StructureExtras, type StructureOptions, type StructurePackOptions, type StructurePart, type StructureSource, type Support, TOWER_DEFAULTS, type TankPlanOptions, type TankPlatform, type Tent, type TowerGeometry, type TowerMechanicalOptions, type TowerOptions, type TowerPlanOptions, type TowerPlate, type WallDetail, type WallMaterials, type WallSpecimenOptions, type WaterTank, type WoodFrameOptions, addCore, addCurtainWall, addFlatRoof, addMechanicalFloor, addPunchedFacade, addTunedMassDamper, applySlabPanels, beamDepthFor, beamSpan, buildSkyscraper, buildStructure, buildTower, buildWallSpecimen, campConstruction, chaletConstruction, constructionOfSystem, coreSize, damperMass, exteriorWindows, logCabinConstruction, masonryConstruction, mechanicalStoreys, pilesFor, planToSpace, planWaterTank, register, registerConstruction, registerConstructionSystem, registerRoofCovering, resolveSlabPanels, shackConstruction, skyscraperLayout, slabPanelView, slabPanelsOf, splitStrip, storeyKinds, stripDoor, structureLayer, toDestructibleSpec, towerFloors, towerLocales, towerPlate, towerWater, version, windExposureOf, woodFrameConstruction };
1413
+ export { CONSTRUCTIONS, CONSTRUCTION_SYSTEMS, CORE_DEFAULTS, CURTAIN_DEFAULTS, type ChaletOptions, type Chimney, type Circuit, type Conduit, type Construction, type ConstructionBuild, type ConstructionBuilder, type ConstructionSystem, type ConstructionSystemDef, type CoreOptions, type CurtainWallOptions, DAMPER_DEFAULTS, DENSITY, type DamperOptions, type DestructibleSpecOptions, type ElectricMeter, type Electrical, type ElectricalPanel, type EnergyConfig, type EnergyMode, FLAT_ROOF_DEFAULTS, type FlatRoofOptions, type FoundationType, GROUND, type Gas, type GasMeter, type Inverter, type LightSwitch, MECHANICAL_DEFAULTS, MEMBER_CAGE, type MechanicalFloorOptions, type Opening, type Outbuilding, type OutbuildingItem, type OutbuildingOpening, type OutbuildingWall, type OutdoorLamp, type Outlet, PILE_AREA, PILE_DEFAULTS, PLATE_DEFAULTS, PUNCHED_DEFAULTS, type PartRole, type PileDesign, type PlanContext, type PlateOptions, type PlateRect, type PlateStrip, type Plumbing, type PlumbingPoint, type PlumbingRun, type Point3, type PunchedFacadeOptions, RAFT_THICKNESS, REBAR_RATIOS, ROOF_COVERINGS, type Rect, type Roof, type RoofCoveringDef, type RoofPane, type RoofShape, SKYSCRAPER_DEFAULTS, SKYSCRAPER_MAX_STOREYS, SLAB_PANEL_DEFAULTS, type ServiceEntrance, type ShackOptions, type SkyscraperLayout, type SkyscraperOptions, type SkyscraperPreset, type SlabPanelOptions, type Solar, type SolarPanel, type SourceDoor, type SourceWall, type SourceWindow, type StoreyContext, type StoreyKind, type StripFill, type StripFiller, type StripId, type Structure, type StructureExtras, type StructureOptions, type StructurePackOptions, type StructurePart, type StructureSource, type Support, TOWER_DEFAULTS, type TankPlanOptions, type TankPlatform, type Tent, type TowerGeometry, type TowerMechanicalOptions, type TowerOptions, type TowerPlanOptions, type TowerPlate, type Utilities, type WallDetail, type WallFixture, type WallMaterials, type WallSpecimenOptions, type WaterTank, type WoodFrameOptions, addCore, addCurtainWall, addFlatRoof, addMechanicalFloor, addPunchedFacade, addTunedMassDamper, applySlabPanels, beamDepthFor, beamSpan, buildSkyscraper, buildStructure, buildTower, buildWallSpecimen, campConstruction, chaletConstruction, constructionOfSystem, coreSize, damperMass, exteriorWindows, logCabinConstruction, masonryConstruction, mechanicalStoreys, pilesFor, planToSpace, planWaterTank, register, registerConstruction, registerConstructionSystem, registerRoofCovering, resolveSlabPanels, shackConstruction, skyscraperLayout, slabPanelView, slabPanelsOf, splitStrip, storeyKinds, stripDoor, structureLayer, toDestructibleSpec, towerFloors, towerLocales, towerPlate, towerWater, version, windExposureOf, woodFrameConstruction };
@@ -1037,9 +1037,10 @@ var GameFloorplanStructure = (() => {
1037
1037
  }
1038
1038
 
1039
1039
  // src/structure/stairs.ts
1040
+ var isStair = (item) => item.category === "stairs" && typeof item.link === "object" && item.link !== null;
1040
1041
  function flightsOf(items) {
1041
1042
  const flights = /* @__PURE__ */ new Map();
1042
- for (const item of items.filter((it) => it.category === "stairs" && it.link)) {
1043
+ for (const item of items.filter(isStair)) {
1043
1044
  const key = `${item.link.from}_${item.link.to}`;
1044
1045
  if (!flights.has(key) || item.floor === item.link.from) flights.set(key, item);
1045
1046
  }
@@ -1617,12 +1618,12 @@ var GameFloorplanStructure = (() => {
1617
1618
  const rect = lot.rect;
1618
1619
  const poleU = clamp(gate ? gate.u + 3 : plan.w / 2, rect[0] + 0.5, rect[2] - 0.5);
1619
1620
  const poleV = rect[1] - 0.6;
1620
- let meter;
1621
+ let placed;
1621
1622
  if (gate) {
1622
1623
  const direction = gate.u - gate.width / 2 - 0.4 >= rect[0] + 0.3 ? -1 : 1;
1623
- meter = { place: "gate_pillar", u: round22(gate.u + direction * (gate.width / 2 + 0.25)), v: round22(gate.v), y: 1.4, side: "front", ...METER_SIZE };
1624
- } else meter = { place: "facade", u: round22(door ? door.at + 1.2 : plan.w / 2), v: -0.1, y: 1.4, side: "front", ...METER_SIZE };
1625
- meter.id = "meter";
1624
+ placed = { place: "gate_pillar", u: round22(gate.u + direction * (gate.width / 2 + 0.25)), v: round22(gate.v), y: 1.4, side: "front", ...METER_SIZE };
1625
+ } else placed = { place: "facade", u: round22(door ? door.at + 1.2 : plan.w / 2), v: -0.1, y: 1.4, side: "front", ...METER_SIZE };
1626
+ const meter = Object.assign(placed, { id: "meter" });
1626
1627
  electrical.serviceEntrance = { pole: { u: round22(poleU), v: round22(poleV), h: 7.5 }, drop: [[round22(poleU), round22(poleV), 6.5], [meter.u, meter.v, 2.6]], meter, descent: [[meter.u, meter.v, 2.6], [meter.u, meter.v, 1.65]], internalRun: null };
1627
1628
  } else if (lot === null || plan.style === "commercial") {
1628
1629
  const front = door ? { u: round22(door.at + 1), v: -0.1 } : { u: round22(plan.w / 2), v: -0.1 };
@@ -1656,7 +1657,8 @@ var GameFloorplanStructure = (() => {
1656
1657
  }
1657
1658
  if (electrical.serviceEntrance && electrical.panel) {
1658
1659
  const entrance = electrical.serviceEntrance;
1659
- entrance.internalRun = [[entrance.meter.u, entrance.meter.v, 0.1], [electrical.panel.u, electrical.panel.v, heightOf(plan, electrical.panel.floor, electrical.panel.y)]];
1660
+ const meterOf = entrance.meter;
1661
+ entrance.internalRun = [[meterOf.u, meterOf.v, 0.1], [electrical.panel.u, electrical.panel.v, heightOf(plan, electrical.panel.floor, electrical.panel.y)]];
1660
1662
  }
1661
1663
  }
1662
1664
 
@@ -1745,9 +1747,9 @@ var GameFloorplanStructure = (() => {
1745
1747
  placePanel(layout);
1746
1748
  addSwitches(layout);
1747
1749
  addConduits(layout);
1748
- if (mode === "tap" && electrical.serviceEntrance?.meter) {
1749
- const entrance = electrical.serviceEntrance;
1750
- const meter = entrance.meter;
1750
+ const entrance = electrical.serviceEntrance;
1751
+ const meter = entrance?.meter;
1752
+ if (mode === "tap" && entrance && meter) {
1751
1753
  entrance.tap = true;
1752
1754
  entrance.connection = { u: meter.u, v: meter.v, y: 2.6 };
1753
1755
  entrance.meter = null;
@@ -1778,11 +1780,11 @@ var GameFloorplanStructure = (() => {
1778
1780
  };
1779
1781
  }
1780
1782
  function besideStove(context, stove, room, solids, doorZones) {
1781
- const t = [-stove.frontV, stove.frontU];
1783
+ const t = [-(stove.frontV ?? 1), stove.frontU ?? 0];
1782
1784
  const parts = room.parts.map(inner);
1783
1785
  for (const s of context.rng() < 0.5 ? [1, -1] : [-1, 1]) {
1784
- const cx = stove.x + t[0] * s * (stove.w / 2 + CYLINDER.w / 2 + 0.03) - stove.frontU * (stove.d - CYLINDER.d) / 2;
1785
- const cz = stove.z + t[1] * s * (stove.w / 2 + CYLINDER.w / 2 + 0.03) - stove.frontV * (stove.d - CYLINDER.d) / 2;
1786
+ const cx = stove.x + t[0] * s * (stove.w / 2 + CYLINDER.w / 2 + 0.03) - (stove.frontU ?? 0) * (stove.d - CYLINDER.d) / 2;
1787
+ const cz = stove.z + t[1] * s * (stove.w / 2 + CYLINDER.w / 2 + 0.03) - (stove.frontV ?? 1) * (stove.d - CYLINDER.d) / 2;
1786
1788
  const rect = [cx - CYLINDER.w / 2, cz - CYLINDER.d / 2, cx + CYLINDER.w / 2, cz + CYLINDER.d / 2].map(round22);
1787
1789
  const inside = parts.some((q) => rect[0] >= q[0] - 0.011 && rect[2] <= q[2] + 0.011 && rect[1] >= q[1] - 0.011 && rect[3] <= q[3] + 0.011);
1788
1790
  if (inside && !solids.some((o) => overlaps(rect, o, 0.011)) && !doorZones.some((z) => overlaps(rect, z, 0.011))) return { mode: "beside", x: cx, z: cz, rect };
@@ -1838,8 +1840,8 @@ var GameFloorplanStructure = (() => {
1838
1840
  d: CYLINDER.d,
1839
1841
  h: CYLINDER.h,
1840
1842
  y: 0,
1841
- frontU: -stove.frontU,
1842
- frontV: -stove.frontV,
1843
+ frontU: -(stove.frontU ?? 0),
1844
+ frontV: -(stove.frontV ?? 1),
1843
1845
  room: stove.room,
1844
1846
  floor: stove.floor,
1845
1847
  outdoor: !!spot.outdoor,
@@ -2099,9 +2101,11 @@ var GameFloorplanStructure = (() => {
2099
2101
  for (const item of items.filter((m) => m.category === "outbuilding")) {
2100
2102
  const rng = context.rngFor(streamOf(item.id));
2101
2103
  const [a0, a1, a2, a3] = item.aabb;
2102
- const n = [item.frontU, item.frontV];
2103
- const t = [-item.frontV, item.frontU];
2104
- const horizontal = Math.abs(item.frontU) === 1;
2104
+ const frontU = item.frontU ?? 0;
2105
+ const frontV = item.frontV ?? 1;
2106
+ const n = [frontU, frontV];
2107
+ const t = [-frontV, frontU];
2108
+ const horizontal = Math.abs(frontU) === 1;
2105
2109
  const cx = (a0 + a2) / 2;
2106
2110
  const cz = (a1 + a3) / 2;
2107
2111
  const depth = horizontal ? a2 - a0 : a3 - a1;
@@ -2338,7 +2342,7 @@ var GameFloorplanStructure = (() => {
2338
2342
  }
2339
2343
  function structureExtension(context) {
2340
2344
  const cache = cacheOf(context.result);
2341
- const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: context.result.waterTank ?? null, wallDetail: context.options.wallDetail ?? "panels" };
2345
+ const options = { construction: cache.build.construction, extras: cache.build.extras ?? {}, tank: tankOf(context), wallDetail: context.options.wallDetail ?? "panels" };
2342
2346
  const { roof, ...structure } = buildStructure(context.result, options);
2343
2347
  cache.roof = roof;
2344
2348
  for (const part of structure.parts) {
@@ -2349,6 +2353,7 @@ var GameFloorplanStructure = (() => {
2349
2353
  return structure;
2350
2354
  }
2351
2355
  var COOKING_CYLINDER = { type: "cooking_gas_cylinder", category: "cooking_gas_cylinder", w: 0.36, d: 0.36, h: 0.48 };
2356
+ var tankOf = (context) => context.result.waterTank ?? null;
2352
2357
  function energyOf(context) {
2353
2358
  const energy = context.plan.tpl.energy;
2354
2359
  return typeof energy === "function" ? energy(context.options, context.plan.w, context.plan.d) : void 0;
@@ -2379,10 +2384,11 @@ var GameFloorplanStructure = (() => {
2379
2384
  if (context.result.lot) addOutbuildingPaths(context.result.lot, outbuildings, context.result.items);
2380
2385
  return outbuildings;
2381
2386
  }, { after: ["contents"] });
2382
- engine.registerResultExtension("utilities", (context) => planUtilities({ plan: context.plan, items: context.result.items, stateItems: context.result.stateItems, tank: context.result.waterTank ?? null, ...energyOf(context) ? { energy: energyOf(context) } : {}, rngFor: (key) => engine.createRng(context.seed, key) }), { after: ["outbuildings", "waterTank"] });
2387
+ engine.registerResultExtension("utilities", (context) => planUtilities({ plan: context.plan, items: context.result.items, stateItems: context.result.stateItems, tank: tankOf(context), ...energyOf(context) ? { energy: energyOf(context) } : {}, rngFor: (key) => engine.createRng(context.seed, key) }), { after: ["outbuildings", "waterTank"] });
2383
2388
  }
2384
2389
 
2385
2390
  // ../game-contracts/dist/index.js
2391
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
2386
2392
  var DEFAULT_IGNITION_RULES = {
2387
2393
  fillCurve: [
2388
2394
  [0, 0.6],
@@ -2426,6 +2432,54 @@ var GameFloorplanStructure = (() => {
2426
2432
  const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
2427
2433
  return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
2428
2434
  };
2435
+ var DEFERRED_QUEUE_ID = {
2436
+ DESTRUCTION_EVENTS: "destruction.events",
2437
+ BREACH_PAINT: "floorplan.breach-paint",
2438
+ AUDIO_VOICES: "audio.voices",
2439
+ VEHICLE_CREASE: "vehicles.crease"
2440
+ };
2441
+ var DEFERRED_PRIORITY = {
2442
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
2443
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
2444
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
2445
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
2446
+ };
2447
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
2448
+ var GAIT_IDS = ["walk", "run"];
2449
+ var LANDING_IDS = ["light", "heavy"];
2450
+ var BODY_SOUND_IDS = [
2451
+ "jump",
2452
+ "cloth_rustle",
2453
+ "body_fall",
2454
+ "fall_damage",
2455
+ "player_hurt",
2456
+ "player_breath_run",
2457
+ "player_death",
2458
+ "bite",
2459
+ "grab",
2460
+ "punch_swing",
2461
+ "punch_hit_body",
2462
+ "punch_hit_face",
2463
+ "punch_hit_object",
2464
+ "kick_swing",
2465
+ "kick_hit",
2466
+ "kick_hit_object",
2467
+ "shove_two_hands",
2468
+ "knife_swing",
2469
+ "knife_stab",
2470
+ "knife_slash",
2471
+ "knife_hit_object",
2472
+ "blunt_swing",
2473
+ "blunt_hit",
2474
+ "blunt_hit_object",
2475
+ "axe_swing",
2476
+ "axe_chop",
2477
+ "axe_hit_object"
2478
+ ];
2479
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
2480
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
2481
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
2482
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
2429
2483
 
2430
2484
  // ../game-destruction/dist/index.js
2431
2485
  var FIRE_PRIME_C = 300;
@@ -5137,6 +5191,7 @@ var GameFloorplanStructure = (() => {
5137
5191
  const ignitionRules = resolveIgnitionRules(options.ignition);
5138
5192
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
5139
5193
  const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
5194
+ const chainLinkCap = options.chainLinksPerUpdate !== void 0 && options.chainLinksPerUpdate >= 1 ? Math.floor(options.chainLinksPerUpdate) : Infinity;
5140
5195
  const rng = createRng(options.seed ?? 1);
5141
5196
  const rnd = () => rng.next();
5142
5197
  let time = 0;
@@ -5159,7 +5214,13 @@ var GameFloorplanStructure = (() => {
5159
5214
  const typed = listeners.get(e.type);
5160
5215
  if (typed) for (const fn of [...typed]) fn(e);
5161
5216
  };
5162
- const base = (o, source) => source === void 0 ? { object: o.id, time } : { object: o.id, time, source };
5217
+ let blow;
5218
+ const base = (o, source) => ({
5219
+ object: o.id,
5220
+ time,
5221
+ ...source === void 0 ? {} : { source },
5222
+ ...blow === void 0 ? {} : { blow }
5223
+ });
5163
5224
  const pendingCollapse = /* @__PURE__ */ new Map();
5164
5225
  let runDepth = 0;
5165
5226
  const drainBlasts = () => {
@@ -6084,7 +6145,7 @@ var GameFloorplanStructure = (() => {
6084
6145
  if (disc < 0) return true;
6085
6146
  return -b - Math.sqrt(disc) > maxDist;
6086
6147
  };
6087
- const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
6148
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false, precise = false) => {
6088
6149
  const d = vec.normalize(dir);
6089
6150
  let best = null;
6090
6151
  for (const o of objects.values()) {
@@ -6099,13 +6160,25 @@ var GameFloorplanStructure = (() => {
6099
6160
  }
6100
6161
  }
6101
6162
  for (const im of worldOnly ? [] : impls.values()) {
6163
+ let boxHit = null;
6102
6164
  for (const ps of im.impl.describe().parts) {
6103
6165
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
6104
6166
  const ov = im.poses.get(ps.id);
6105
6167
  const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
6106
- if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
6107
- if (!best || r.t0 < best.distance) best = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
6168
+ if (!r || r.t1 < 0 || r.t0 > maxDist) continue;
6169
+ if (r.t0 < 0) {
6170
+ if (precise && im.impl.surfaceRay) boxHit = boxHit ?? { object: im.impl.id, part: ps.id, point: origin, normal: r.n, distance: 0, pathLen: r.t1 };
6171
+ continue;
6172
+ }
6173
+ if (!boxHit || r.t0 < boxHit.distance) boxHit = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
6108
6174
  }
6175
+ if (!boxHit) continue;
6176
+ if (precise && im.impl.surfaceRay) {
6177
+ const far = best ? Math.min(maxDist, best.distance) : maxDist;
6178
+ const implId = im.impl.id;
6179
+ const s = im.impl.surfaceRay({ origin, direction: d, maxDistance: far, ...ignore ? { ignored: (part) => ignore.has(`${implId}\0${part}`) } : {} });
6180
+ if (s && (!best || s.distance < best.distance)) best = { object: implId, part: s.part, point: s.point, normal: s.normal, distance: s.distance, pathLen: s.pathLen };
6181
+ } else if (!best || boxHit.distance < best.distance) best = boxHit;
6109
6182
  }
6110
6183
  return best;
6111
6184
  };
@@ -7292,7 +7365,17 @@ var GameFloorplanStructure = (() => {
7292
7365
  res.events = cur.slice(start);
7293
7366
  return res;
7294
7367
  };
7295
- const doHit = (o, hit, only) => {
7368
+ const withBlow = (type, fn) => () => {
7369
+ const outer = blow;
7370
+ blow = type;
7371
+ try {
7372
+ return fn();
7373
+ } finally {
7374
+ blow = outer;
7375
+ }
7376
+ };
7377
+ const doHit = (o, hit, only) => withBlow(hit.type, () => resolveHit(o, hit, only))();
7378
+ const resolveHit = (o, hit, only) => {
7296
7379
  let point = hit.point;
7297
7380
  let dir = hit.direction;
7298
7381
  if (hit.space === "local") {
@@ -7589,7 +7672,9 @@ var GameFloorplanStructure = (() => {
7589
7672
  };
7590
7673
  const detonatePrimed = (until) => {
7591
7674
  const end = time;
7592
- for (let next = nextPrimed(until); next; next = nextPrimed(until)) {
7675
+ let fired = 0;
7676
+ for (let next = nextPrimed(until); next && fired < chainLinkCap; next = nextPrimed(until)) {
7677
+ fired++;
7593
7678
  primed.delete(next.p);
7594
7679
  time = Math.max(0, Math.min(end, next.p.rt.chainDue));
7595
7680
  next.p.rt.chainDue = void 0;
@@ -7659,6 +7744,7 @@ var GameFloorplanStructure = (() => {
7659
7744
  updateBound(o);
7660
7745
  },
7661
7746
  remove(id) {
7747
+ impls.get(id)?.impl.dispose?.();
7662
7748
  objects.delete(id);
7663
7749
  impls.delete(id);
7664
7750
  },
@@ -7728,13 +7814,51 @@ var GameFloorplanStructure = (() => {
7728
7814
  blastProbe() {
7729
7815
  return { ...blastProbe };
7730
7816
  },
7817
+ blastReach(point, target, options2 = {}) {
7818
+ const L = vec.distance(point, target);
7819
+ if (L < 1e-4) return 1;
7820
+ const shelter = { ...SHELTER_DEFAULTS, ...options2.shelter };
7821
+ const ignore = new Set(options2.ignore ?? []);
7822
+ const d = vec.scale(vec.sub(target, point), 1 / L);
7823
+ let t = 1;
7824
+ for (const o of objects.values()) {
7825
+ if (ignore.has(o.id) || raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
7826
+ for (const p of o.parts) {
7827
+ if (isLost(p) || p.st.stripped) continue;
7828
+ const r = rayPart(o, p, point, d);
7829
+ if (!r) continue;
7830
+ const t0 = Math.max(r.t0, 0);
7831
+ const path = Math.min(r.t1, L) - t0;
7832
+ if (path < shelter.minPath) continue;
7833
+ if (p.st.opening && insideOpening(o, p, toLocal(o.st.position, o.st.rotation, vec.madd(point, d, t0 + path / 2)))) continue;
7834
+ t *= Math.exp(-(p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1)) / shelter.k);
7835
+ if (t < 1e-4) return 0;
7836
+ }
7837
+ }
7838
+ for (const im of impls.values()) {
7839
+ if (ignore.has(im.impl.id)) continue;
7840
+ for (const ps of im.impl.describe().parts) {
7841
+ const mat = materials.get(ps.material);
7842
+ if (!mat) continue;
7843
+ const ov = im.poses.get(ps.id);
7844
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), point, d, ov?.pos);
7845
+ if (!r || r.t1 < 0 || r.t0 > L) continue;
7846
+ let path = Math.min(r.t1, L) - Math.max(r.t0, 0);
7847
+ if (ps.thickness !== void 0 && ps.thickness > 0) path = Math.min(path, 2 * ps.thickness);
7848
+ else if (path < shelter.minPath) continue;
7849
+ t *= Math.exp(-(mat.hpPerKg * mat.density * path * (mat.damageScale?.shelter ?? 1)) / shelter.k);
7850
+ if (t < 1e-4) return 0;
7851
+ }
7852
+ }
7853
+ return t;
7854
+ },
7731
7855
  explode(point, energy, radius, source, blast) {
7732
7856
  blastProbe.partsTested = 0;
7733
7857
  blastProbe.partsNear = 0;
7734
7858
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
7735
7859
  },
7736
7860
  raycast(origin, direction, maxDistance = Infinity) {
7737
- const r = raycastInternal(origin, direction, maxDistance);
7861
+ const r = raycastInternal(origin, direction, maxDistance, void 0, false, true);
7738
7862
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
7739
7863
  },
7740
7864
  sweepBox(box2, direction, distance2, options2 = {}) {
@@ -7752,21 +7876,23 @@ var GameFloorplanStructure = (() => {
7752
7876
  },
7753
7877
  shoot(origin, direction, energy, caliber, source, round3) {
7754
7878
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
7755
- return withCollapses(run(() => {
7879
+ return withCollapses(run(withBlow("ballistic", () => {
7756
7880
  const results = [];
7757
7881
  const d = vec.normalize(direction);
7758
7882
  const ignore = /* @__PURE__ */ new Set();
7759
7883
  let org = origin;
7760
7884
  let E = energy;
7761
7885
  for (let i = 0; i < 4 && E > 0; i++) {
7762
- const rh = raycastInternal(org, d, Infinity, ignore);
7886
+ const rh = raycastInternal(org, d, Infinity, ignore, false, true);
7763
7887
  if (!rh) break;
7764
7888
  const im = impls.get(rh.object);
7765
7889
  if (im) {
7766
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
7890
+ const scaled = extras.penetration && E < energy ? { ...extras, penetration: Object.fromEntries(Object.entries(extras.penetration).map(([k, mm]) => [k, mm * (E / energy)])) } : extras;
7891
+ const r = implHit(im, { type: "ballistic", point: rh.point, part: rh.part, normal: rh.normal, direction: d, energy: E, caliber, ...scaled, ...source !== void 0 ? { source } : {} });
7767
7892
  r.part = rh.part;
7768
7893
  r.point = rh.point;
7769
- r.exitEnergy = r.penetrated ? E * 0.5 : 0;
7894
+ const left = r.events.find((e) => e.type === "hole" && e.energyLeft !== void 0);
7895
+ r.exitEnergy = r.penetrated ? left?.energyLeft ?? E * 0.5 : 0;
7770
7896
  results.push(r);
7771
7897
  ignore.add(`${im.impl.id}\0${rh.part}`);
7772
7898
  if (!r.penetrated) break;
@@ -7785,7 +7911,7 @@ var GameFloorplanStructure = (() => {
7785
7911
  E = out.exitEnergy;
7786
7912
  }
7787
7913
  return results;
7788
- }));
7914
+ })));
7789
7915
  },
7790
7916
  disassemble(objectId, jointId, tool, dt) {
7791
7917
  const im = impls.get(objectId);
@@ -8085,12 +8211,27 @@ var GameFloorplanStructure = (() => {
8085
8211
  breach: { area: 0.1 }
8086
8212
  }
8087
8213
  ];
8214
+ var SHIPPED = structuredClone(assemblies_default);
8088
8215
  var ASSEMBLIES = assemblies_default;
8089
- var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
8216
+ var assembliesOver = (defs) => {
8217
+ const get = (id) => defs.find((a) => a.id === id);
8218
+ return {
8219
+ list: () => defs,
8220
+ get,
8221
+ register(def) {
8222
+ const at2 = defs.findIndex((a) => a.id === def.id);
8223
+ if (at2 >= 0) defs[at2] = def;
8224
+ else defs.push(def);
8225
+ },
8226
+ part: (assemblyId, options) => buildAssemblyPart(get(assemblyId), assemblyId, options)
8227
+ };
8228
+ };
8229
+ var defaultSet;
8230
+ var defaultAssemblies = () => defaultSet ?? (defaultSet = assembliesOver(ASSEMBLIES));
8231
+ var getAssembly = (id) => defaultAssemblies().get(id);
8090
8232
  var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
8091
8233
  var assemblyDepth = (def) => def.depth ?? def.core.thickness + (def.layers ?? []).reduce((sum, l) => sum + l.thickness * faceCount(l), 0);
8092
- var assemblyPart = (assemblyId, options) => {
8093
- const def = getAssembly(assemblyId);
8234
+ var buildAssemblyPart = (def, assemblyId, options) => {
8094
8235
  if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
8095
8236
  const densityOf2 = (id) => {
8096
8237
  const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
@@ -8117,6 +8258,7 @@ var GameFloorplanStructure = (() => {
8117
8258
  ...options.extra
8118
8259
  };
8119
8260
  };
8261
+ var assemblyPart = (assemblyId, options) => defaultAssemblies().part(assemblyId, options);
8120
8262
  var buildFrame = (o = {}) => {
8121
8263
  const spanX = o.spanX ?? 8, spanZ = o.spanZ ?? 8;
8122
8264
  const nx = Math.max(2, o.columnsX ?? 3), nz = Math.max(2, o.columnsZ ?? 3);