@adatechnology/game-floorplan-structure 2.0.0 → 2.0.2

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_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
2386
2392
  var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
2387
2393
  var DEFAULT_IGNITION_RULES = {
2388
2394
  fillCurve: [
@@ -5209,7 +5215,13 @@ var GameFloorplanStructure = (() => {
5209
5215
  const typed = listeners.get(e.type);
5210
5216
  if (typed) for (const fn of [...typed]) fn(e);
5211
5217
  };
5212
- const base = (o, source) => source === void 0 ? { object: o.id, time } : { object: o.id, time, source };
5218
+ let blow;
5219
+ const base = (o, source) => ({
5220
+ object: o.id,
5221
+ time,
5222
+ ...source === void 0 ? {} : { source },
5223
+ ...blow === void 0 ? {} : { blow }
5224
+ });
5213
5225
  const pendingCollapse = /* @__PURE__ */ new Map();
5214
5226
  let runDepth = 0;
5215
5227
  const drainBlasts = () => {
@@ -6760,7 +6772,7 @@ var GameFloorplanStructure = (() => {
6760
6772
  return;
6761
6773
  }
6762
6774
  if (ex) {
6763
- emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
6775
+ emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.tnt !== void 0 ? { tnt: ex.tnt } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
6764
6776
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, source: src });
6765
6777
  }
6766
6778
  partLost(o, p, src);
@@ -7354,7 +7366,17 @@ var GameFloorplanStructure = (() => {
7354
7366
  res.events = cur.slice(start);
7355
7367
  return res;
7356
7368
  };
7357
- const doHit = (o, hit, only) => {
7369
+ const withBlow = (type, fn) => () => {
7370
+ const outer = blow;
7371
+ blow = type;
7372
+ try {
7373
+ return fn();
7374
+ } finally {
7375
+ blow = outer;
7376
+ }
7377
+ };
7378
+ const doHit = (o, hit, only) => withBlow(hit.type, () => resolveHit(o, hit, only))();
7379
+ const resolveHit = (o, hit, only) => {
7358
7380
  let point = hit.point;
7359
7381
  let dir = hit.direction;
7360
7382
  if (hit.space === "local") {
@@ -7793,6 +7815,44 @@ var GameFloorplanStructure = (() => {
7793
7815
  blastProbe() {
7794
7816
  return { ...blastProbe };
7795
7817
  },
7818
+ blastReach(point, target, options2 = {}) {
7819
+ const L = vec.distance(point, target);
7820
+ if (L < 1e-4) return 1;
7821
+ const shelter = { ...SHELTER_DEFAULTS, ...options2.shelter };
7822
+ const ignore = new Set(options2.ignore ?? []);
7823
+ const d = vec.scale(vec.sub(target, point), 1 / L);
7824
+ let t = 1;
7825
+ for (const o of objects.values()) {
7826
+ if (ignore.has(o.id) || raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
7827
+ for (const p of o.parts) {
7828
+ if (isLost(p) || p.st.stripped) continue;
7829
+ const r = rayPart(o, p, point, d);
7830
+ if (!r) continue;
7831
+ const t0 = Math.max(r.t0, 0);
7832
+ const path = Math.min(r.t1, L) - t0;
7833
+ if (path < shelter.minPath) continue;
7834
+ if (p.st.opening && insideOpening(o, p, toLocal(o.st.position, o.st.rotation, vec.madd(point, d, t0 + path / 2)))) continue;
7835
+ t *= Math.exp(-(p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1)) / shelter.k);
7836
+ if (t < 1e-4) return 0;
7837
+ }
7838
+ }
7839
+ for (const im of impls.values()) {
7840
+ if (ignore.has(im.impl.id)) continue;
7841
+ for (const ps of im.impl.describe().parts) {
7842
+ const mat = materials.get(ps.material);
7843
+ if (!mat) continue;
7844
+ const ov = im.poses.get(ps.id);
7845
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), point, d, ov?.pos);
7846
+ if (!r || r.t1 < 0 || r.t0 > L) continue;
7847
+ let path = Math.min(r.t1, L) - Math.max(r.t0, 0);
7848
+ if (ps.thickness !== void 0 && ps.thickness > 0) path = Math.min(path, 2 * ps.thickness);
7849
+ else if (path < shelter.minPath) continue;
7850
+ t *= Math.exp(-(mat.hpPerKg * mat.density * path * (mat.damageScale?.shelter ?? 1)) / shelter.k);
7851
+ if (t < 1e-4) return 0;
7852
+ }
7853
+ }
7854
+ return t;
7855
+ },
7796
7856
  explode(point, energy, radius, source, blast) {
7797
7857
  blastProbe.partsTested = 0;
7798
7858
  blastProbe.partsNear = 0;
@@ -7817,7 +7877,7 @@ var GameFloorplanStructure = (() => {
7817
7877
  },
7818
7878
  shoot(origin, direction, energy, caliber, source, round3) {
7819
7879
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
7820
- return withCollapses(run(() => {
7880
+ return withCollapses(run(withBlow("ballistic", () => {
7821
7881
  const results = [];
7822
7882
  const d = vec.normalize(direction);
7823
7883
  const ignore = /* @__PURE__ */ new Set();
@@ -7828,10 +7888,12 @@ var GameFloorplanStructure = (() => {
7828
7888
  if (!rh) break;
7829
7889
  const im = impls.get(rh.object);
7830
7890
  if (im) {
7831
- const r = implHit(im, { type: "ballistic", point: rh.point, part: rh.part, normal: rh.normal, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
7891
+ const scaled = extras.penetration && E < energy ? { ...extras, penetration: Object.fromEntries(Object.entries(extras.penetration).map(([k, mm]) => [k, mm * (E / energy)])) } : extras;
7892
+ 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 } : {} });
7832
7893
  r.part = rh.part;
7833
7894
  r.point = rh.point;
7834
- r.exitEnergy = r.penetrated ? E * 0.5 : 0;
7895
+ const left = r.events.find((e) => e.type === "hole" && e.energyLeft !== void 0);
7896
+ r.exitEnergy = r.penetrated ? left?.energyLeft ?? E * 0.5 : 0;
7835
7897
  results.push(r);
7836
7898
  ignore.add(`${im.impl.id}\0${rh.part}`);
7837
7899
  if (!r.penetrated) break;
@@ -7850,7 +7912,7 @@ var GameFloorplanStructure = (() => {
7850
7912
  E = out.exitEnergy;
7851
7913
  }
7852
7914
  return results;
7853
- }));
7915
+ })));
7854
7916
  },
7855
7917
  disassemble(objectId, jointId, tool, dt) {
7856
7918
  const im = impls.get(objectId);