@adatechnology/game-destruction 0.7.2 → 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/README.md CHANGED
@@ -94,6 +94,22 @@ melts anything. Melting emits `melt` and `drip`; a melted part is lost and its j
94
94
  when the part has `explodes`). `contains` parts leak through punctures: the `leak` event carries the hole's world point, the outward normal as
95
95
  `direction` and the rate in litres per second, and `PartState.liters` drops in `update` (`setLiters` fills or empties a container).
96
96
 
97
+ ### Ignition of a leak
98
+
99
+ A round that goes through a `contains` part with a flammable fluid (`gas`, `fuel`: a tank, a jerrycan, a gas bottle) **leaks and does not light by itself**. The leak lights
100
+ (a jet that burns, or the explosion of a part with `explodes`) when:
101
+
102
+ - the round carries a **spark** and hits **metal**: `Hit.ignition.spark` (the weapons build it from the core and the jacket of the round) times the factor of the struck
103
+ material (`ignition.sparkTarget`: steel and iron 1, aluminium 0.25, other metals `sparkMetalDefault` 0.5, a part that is not metal never sparks);
104
+ - the round carries a **flame**: `Hit.ignition.flame`, or the flags `Hit.incendiary` / `Hit.tracer` (0.7 and 0.2 when the host sends only the flags), whatever the wall;
105
+ - a **fire is near**: a burning part or a jet within `ignition.fireNear.radius` (1.5 m) lights the leak at `chancePerSecond` (0.5); or
106
+ - a **blast** (`explode`) goes off within its radius of the leaking container.
107
+
108
+ Both chances are weighed by how full the container is, `ignition.fillCurve` (points `[filled, factor]`; a full container is too rich to burn and mostly leaks, a nearly empty one lights
109
+ easiest; default 0.6 empty, 1 at 10 percent, 0.6 at 35, 0.3 at 65, 0.15 at 90, 0.12 full; the flame feels half of it, `flameFillInfluence` 0.5) and rolled with the seeded generator of the
110
+ world. `createDestruction({ ignition })` changes any number (`IgnitionRules` and `DEFAULT_IGNITION_RULES` of `@adatechnology/game-contracts`, which also export `ignitionChance` and
111
+ `fillFactor`). A round without `ignition`, `incendiary` and `tracer` never lights a leak; a part heated to 350 degrees C still does, as before.
112
+
97
113
  ### Chain reaction
98
114
 
99
115
  A part with `explodes.chain` (fuel barrel, gas bottle, car tank, gas pump, station tank) answers to what happens around it: `shot` (a bullet, once it has taken off
@@ -103,6 +119,9 @@ instead of tearing it apart on the spot, and a part goes off once. Blasts resolv
103
119
  part, so the timeline does not depend on the `dt` you pass to `update`. `explosivePart(explosive, part)` builds the part from an `ExplosiveDef` of `@adatechnology/game-weapons`
104
120
  (`explosives.get('fuel_barrel')`), so energy, reach and delays come from one place and a game changes them with `patch.explosives`.
105
121
 
122
+ A fluid with `contains.outflow` (object-local direction) leaves a hole that way instead of out of the hole: the water on a roof membrane runs down
123
+ (`[0, -1, 0]`) into the floor below.
124
+
106
125
  ## Joints and tools
107
126
 
108
127
  | Kind | Strength | Tools | Seconds |
@@ -132,9 +151,23 @@ solves loads: each structural part carries its own mass plus what rests on it, d
132
151
  `PartState.load` is in kg and `PartState.stress` is `load / capacity`. Parts without a role (windows, doors, furniture) carry nothing. Objects
133
152
  without any `structure` behave exactly as before.
134
153
 
135
- - **Capacity** defaults to `material.compressive` (kg per m²: concrete 444 000, brick 40 000, softwood 100 000, steel 2 000 000) times the section:
136
- the two smallest dimensions for columns, beams, roofs and stairs (foundations x10), thickness times longest side for slabs, thickness times
137
- horizontal length for walls. A 0.3 m concrete column carries 40 t, a 0.2 m brick wall 8 t per metre, a 0.1 x 0.2 m softwood beam 2 t.
154
+ - **A slab or beam is held by the sum of its supports.** Its load is shared among the supports it rests on that are still there (columns, bearing
155
+ walls, beams), so a slab on four columns puts a quarter on each; lose one and the other three carry the load of the four, lose two and the
156
+ two left carry twice their share. The structure falls when the capacity that is left (the sum over the supports) is less than the load.
157
+ Everything is config: the capacity of a part is `structure.capacity` or `material.compressive` times its section (set `compressive` on a
158
+ `MaterialDef` passed in `materials`), `sizeStructure({ safety, redundancy })` sizes them (`safety` 0.72 with `redundancy` 0.97 on four columns:
159
+ rest 0.72, one lost 0.96, two lost 1.44), and the world option `structure: { safetyFactor }` (default 1) multiplies every load before it meets
160
+ the capacities (stress = `load * safetyFactor / capacity`): above 1 the whole building is loaded harder than it was sized for.
161
+ - **Capacity** is `structure.capacity` when the spec gives it. Otherwise, for a material with real strengths (`MaterialDef.strength`: concrete C25
162
+ and ceramic brick ship with them), it is the compressive strength times the section, divided by g: a 0.3 x 0.3 m plain concrete column of 3 m
163
+ carries 203 t, a 0.2 m brick wall 2.8 m tall 41 t per metre. Masonry and plain concrete walls and columns (`strength.slender`, default for
164
+ `masonry` materials without steel) get the NBR 16868-1 reduction `(1 - 2e)(1 - (h/t/40)^3)` (`structure.masonry`: `minEccentricity` 0.05,
165
+ `slendernessLimit` 40); a reinforced column carries `0.85 fck Ac + As fy` (`masonry.concreteFactor` 0.85). Materials without `strength` keep
166
+ `material.compressive` (kg per m²: softwood 100 000, steel 2 000 000). The section is the two smallest dimensions for columns, beams, roofs and
167
+ stairs (foundations x10), thickness times longest side for slabs, thickness times horizontal length for walls. Because each storey passes its
168
+ load down, a bearing wall that carries too many storeys crushes at the ground floor first. `bearingWallThickness({ storeys, storeyHeight,
169
+ floorLoad, material })` gives the thickness a designer would give each storey (γf 1.4, γm 2.0, minimum 0.14 m): 16 storeys of 3.75 m with
170
+ 26 kN/m of floor per wall give 1.49 m at the base (the Monadnock Building has 1.8 m).
138
171
  - **Joints** carry load when `bears` is true (default when both parts have a role). `bears: 'a_on_b'` means part `a` rests on `b`: load flows only
139
172
  that way. A `partition` is supported but supports nothing, so breaking it never drops anything.
140
173
  - **Overload**: above stress 1 a part loses `min(0.6, 0.12 + 0.8 * (stress - 1))` of its `maxHp` per second (stress 1.02 gives way in ~7 s, 1.1 in ~5 s,
@@ -146,6 +179,8 @@ without any `structure` behave exactly as before.
146
179
  `splitGroup`.
147
180
  - `buildFrame({ spanX, spanZ, columnsX, columnsZ, height, floors, walls, roof, columnCapacity, ... })` returns a test structure: columns on
148
181
  foundations (`rebar`), beams, a slab per floor, optional brick walls (`mortar`) with a door opening, and a roof that rests (`resting`) on the top beams.
182
+ `continuousColumns` makes each column rest on the one below instead of on the slab (a tall frame needs it: otherwise the first-floor beams carry
183
+ the tower in bending), `columnCage` gives the columns a rebar cage and `roofThickness` sets the roof slab (default 0.75 x `slabThickness`).
149
184
  - `sizeStructure(spec, { safety?, redundancy?, redundancyRoles?, safetyByRole? })` returns a copy of the spec with `structure.capacity` set from the loads the world itself solves
150
185
  for the intact spec, so every structural part rests at stress `safety` (default 0.65). With `redundancy` (e.g. 0.97) the capacities are also
151
186
  raised where needed so that losing any single `column` keeps every other part at or below that stress: the structure stands with one column
@@ -155,6 +190,61 @@ without any `structure` behave exactly as before.
155
190
  stands with a bearing wall gone. `safetyByRole` overrides `safety` per role: `{ beam: 0.45, slab: 0.45, roof: 0.45, stair: 0.45 }` keeps the
156
191
  floors well under their limit so they outlast the member that fails.
157
192
 
193
+ ### Bending: concrete cracks in tension, the steel holds
194
+
195
+ Beams, slabs, roofs and stairs of a material with `strength.tensileMPa` that declare their steel (`structure.rebar: { ratio, yieldMPa?, cover? }`,
196
+ `ratio: 0` is plain concrete) also bend. The span comes from the supports (twice the farthest distance to one, so two walls 4 m apart give 4 m,
197
+ corner columns give the long side, a cantilever of c gives 2c, and losing a column doubles it); the bending load is the part's own weight plus
198
+ what rests on it away from the supports (a load within one depth of a support goes straight down, EN 1992-1-1 6.2.2(6)). `M = wL²/8` meets the
199
+ section: cracking moment `1.5 fct b h²/6` (NBR 6118 17.3.1) and the steel moment with the NBR 6118 rectangular block (`0.85 fck` over `0.8x`,
200
+ `x/d` at most 0.45, the rest as double reinforcement), CA-50 at 500 MPa, 3 cm cover. `PartState.flexure = M / capacity` (after cracking only the
201
+ steel counts) and `stress` is the larger of that and compression.
202
+
203
+ - **Blast**: a single-degree-of-freedom check (Biggs: KL 0.64, KM 0.5) with the Sadovsky impulse of the TNT-equivalent charge (4.184 MJ/kg),
204
+ reflected `2 + 4/Z`. Above the cracking moment the tension face cracks (`crack` on the face away from the charge, `PartState.cracked`); a part
205
+ without steel snaps (`fracture` style `snap`); with steel, yielding adds plastic rotation and at 10° (PDC-TR 06-08) the part hinges and falls in
206
+ one piece (`detach` from its supports, `collapse`).
207
+ - **At rest**: `flexure > 1` wears the part like a compression overload; a reinforced part then hinges and falls, a plain one snaps.
208
+ - **Joints**: `rebar` joints of a reinforced part are monolithic: blows and blasts do not wear them.
209
+ - **Options** (`structure.flexure`, `FLEXURE_DEFAULTS`): `enabled`, `roles`, `scope` (`'rebar'` default: only parts with `structure.rebar`;
210
+ `'reinforced'`: also parts with only a `reinforcement` cage; `'all'`: every concrete beam and slab, plain when they have no steel),
211
+ `crackFactor`, `rebarYieldMPa`, `cover`, `neutralAxisLimit`, `failRotationDeg`, `loadFactor`, `massFactor`, `tntJoules`, `impulse`,
212
+ `reflection`, `reflectionNear`, `nearScaled`. Helpers: `crackingMoment`, `steelMoment`, `sectionMoments`, `blastImpulse`, `slendernessFactor`,
213
+ `spanOf`, `bearingWallThickness`.
214
+ - **Sizing**: `sizeRebar(spec, { safety, redundancy, redundancyRoles, safetyByRole })` (also run at the end of `sizeStructure`) raises the steel
215
+ ratio of each reinforced beam or slab so it rests at `safety` and stays at or below `redundancy` after losing any part of `redundancyRoles`
216
+ (at most 4%, NBR 6118 17.3.5.2.4); plain concrete stays plain. `buildFrame({ rebar })` declares beams at 1% and slabs at 0.5% (`false` for
217
+ plain concrete).
218
+
219
+ ### Soil and foundations
220
+
221
+ Give the spec a `soil` (a soil class id such as `clay_soft`, or `{ layers: [{ soil, thickness }, ...], surface?, footingWidth? }`) and its foundations
222
+ bear on the ground instead of being fixed. Without it nothing changes.
223
+
224
+ - **Soil classes** (`SOILS`, `soils.json`, extend with `soil.soils`): allowable footing pressure (NBR 6122 basic pressures: dense sand 400 kPa,
225
+ stiff clay 200, medium clay 100, soft clay 30, rock 3000), SPT N, Young's modulus (Teixeira & Godoy) and Poisson's ratio.
226
+ - **Footings** (a `foundation` part: pad, strip or raft): a grid of springs with the subgrade modulus of a rigid footing `E / (B (1 - nu^2) Iw)`
227
+ that give way at `safety` (3, NBR 6122) times the allowable pressure of the weakest layer within two widths below. Structural parts resting on a
228
+ foundation tagged `terrain` get a footing at least 0.6 m wide each.
229
+ - **Piles** (`structure.pile: { length, diameter, count?, method? }` on the cap): end bearing and skin friction by Décourt-Quaresma through the
230
+ layers. `pileLength(profile, { load, diameter })` gives the shortest pile that carries a load with safety 2 (down to firm ground).
231
+ - **The object is a rigid body on those springs**, out of plumb by 1/200 (EN 1992-1-1) the weak way, with P-delta. It settles at once where the
232
+ soil holds; where it yields it moves to the new equilibrium at `sinkRate`/`tiltRate` (game time), and with no stable equilibrium it keeps sinking
233
+ and leaning until its centre of mass passes the edge of its foundations: it tips over (`settle` with `status: 'toppled'`, then a `collapse` of
234
+ everything).
235
+ - `ObjectState.soil` has `settlement`, `tilt`, `direction`, `bearing` (weight over what the foundations carry), `edge`, `status` and `offset`
236
+ (the settled pose relative to the host's). `ObjectState.position`/`rotation` include it; `setTransform` sets the host's pose under it. A `settle`
237
+ event goes out when it moves (at most every 0.5 s) or changes status.
238
+ - Options (`soil`, `SOIL_DEFAULTS`): `soils`, `site`, `safety`, `pileSafety`, `rigidity`, `influence`, `imperfection`, `cells`, `sinkRate`,
239
+ `tiltRate`, `footingWidth`, `pileElasticGPa`. `buildFrame({ footing, raft, piles, soil })` builds test frames on the ground.
240
+
241
+ ## Wind and sway
242
+ Off unless `createDestruction({ wind })` is given. Pressure `q = 0.613 V^2` grows with height by the NBR 6123 profile (terrain I-V, class by size), times a drag
243
+ coefficient, per 3 m band. The lateral system is the walls (cantilever) plus columns and T-beams (shear frame); the result is in `ObjectState.wind` (`deflection`,
244
+ `drift` against `driftLimit` H/500, `overturn`, `frequency`, `lean`, `sway`, `peak`, `status`) and a `sway` event. A gust (Davenport spectrum) shakes the first mode;
245
+ a part with `damper` (tuned mass damper, Den Hartog) reduces it. `world.setWind({ speed, direction })` changes it in play; `sizeStructure({ wind })` sizes for it;
246
+ on soft soil the wind moment tilts the building. Options and defaults: `wind`, `WIND_DEFAULTS`, `WIND_TERRAIN`. Sources in `specs/001-migracao-entregas-dos-times/design/T10-estrutura-real.md` section 10.
247
+
158
248
  ## Walls: layers, stages, breach and reinforcement
159
249
  A wall is one part that opts in with `layers`, `units`, `reinforcement` or `breach` (`PartSpec`, all optional; parts without them behave as before).
160
250
  `assemblyPart(id, { id, width, height, position, structure? })` builds one from the eight built-in **assemblies** (`ASSEMBLIES`, data in
@@ -224,15 +314,19 @@ the results for the push on pieces: `renderer.explode(point, energy, radius, { r
224
314
 
225
315
  ### Bullets and blasts against beams and columns
226
316
 
227
- `tests/beams-and-columns.test.ts` documents calibres of `@adatechnology/game-weapons` against a reinforced concrete column and beam, a steel
228
- I-beam, a hardwood beam and a brick pillar. Two options came out of it, both off or neutral by default:
317
+ `tests/beams-and-columns.test.ts` fires the loads of the ammo table of `@adatechnology/game-weapons` against a reinforced concrete column and beam, a steel
318
+ I-beam, a hardwood beam and a brick pillar (it prints the results table with `PRINT_TABLE=1`). Two corrections came out of it:
229
319
 
230
- - `Hit.piercing` (6th argument of `shoot`): how well the bullet cuts through for its energy; the energy compared with the penetration of a hard
231
- material (metal, masonry) is `energy * piercing`. `PIERCING_BY_KIND` (fmj 1, hp 0.6, ap 1.6, buck 0.5, slug 0.25) and `PIERCING_BY_CALIBER`
232
- (5.56, 5.45 and .22 0.5) give the numbers from `CaliberDef.kind` (`piercingOf(id, kind)`). Without it a 12 ga slug (3000 J) went through 8.5 mm of
233
- steel.
234
- - `breach.blastReach` (default `Infinity`, 0.12 recommended): a blast only opens a breach within `blastReach * cbrt(energy / 1000)` metres of the
235
- part. With 0.12 a hand grenade (200 kJ) at 1 m no longer cuts a reinforced column in two (at 0.2 m it strips it).
320
+ - `Hit.penetration` (the 6th argument of `shoot` is `{ penetration }` or a bare piercing factor): millimetres the round goes through, by material class
321
+ (`steel`, `concrete`, `brick`, `wood`). The weapons put it in the `Hit` (`shotToHit` / `shotExtras` of the contracts); the world reads no calibre names. The
322
+ material says which entry measures it (`MaterialDef.penetrationClass`, default its id) and how much more or less it resists
323
+ (`penetrationScale`: structural steel 1.3 against the RHA of the tables, softwood 1.5 against hardwood, aluminium 3 against steel). The round goes through when its
324
+ millimetres times that scale exceed the thickness of the part (slant counts); a material with no entry falls back to the energy rule, where `Hit.piercing`
325
+ (default 1) multiplies the energy against hard materials. M80 ball does not pierce an 8.5 mm structural flange, M993 AP does, and the .50 BMG M33 ball
326
+ pierces a 30 cm reinforced column. `PIERCING_BY_KIND`, `PIERCING_BY_CALIBER` and `piercingOf` are gone: the table lives in game-weapons.
327
+ - Blast reach of a breach (no option, it is the physics): a blast only opens a breach within 0.12 m per cube root of kilojoule of the part
328
+ (`BREACH_BLAST_REACH`). A hand grenade (200 kJ) at 1 m does not cut a reinforced column in two (at 0.2 m it strips it). Farther away the blast still
329
+ damages, dents and cracks. The old `breach.blastReach` option (default `Infinity`) is gone: it was a second path for a correction.
236
330
 
237
331
  ## Contracts: implement your own or use the default world
238
332
 
@@ -281,9 +375,9 @@ world continues with the same events as the original. The loading world must use
281
375
 
282
376
  `createDestruction(options)` returns a `DestructionWorld`: `add`, `remove`, `has`, `ids`, `setTransform`, `splitGroup`, `hit`, `hitPart`, `explode`,
283
377
  `raycast`, `shoot`, `disassemble`, `workableJoints`, `nearestJoint`, `extinguish`, `setLiters`, `update`, `state`, `part`, `reset`, `on`,
284
- `register`, `serialize`, `load`, `time`. `explode(point, energy, radius, source?, blastOptions?)`; `shoot(origin, direction, energy, caliber, source?, piercing?)`. `state()` and `part()` return live views; do not mutate them.
378
+ `register`, `serialize`, `load`, `time`. `explode(point, energy, radius, source?, blastOptions?)`; `shoot(origin, direction, energy, caliber, source?, round?)` (`round`: `Hit.penetration`, `piercing`, `incendiary`, `tracer`, `ignition` or a bare piercing factor). `state()` and `part()` return live views; do not mutate them.
285
379
 
286
- Also exported: `SHELTER_DEFAULTS`, `FRAGMENT_HIT_DEFAULTS`, `CONFINEMENT_DEFAULTS`, `PIERCING_BY_KIND`, `PIERCING_BY_CALIBER`, `piercingOf`, `buildFrame`, `sizeStructure`, `assemblyPart`, `registerAssembly`, `getAssembly`, `ASSEMBLIES`, `assemblyDepth`, `partCapacity`, `checkDestructible`, `checkHit`, `IMPACT_CONTRACT`, `MATERIALS`, `JOINT_DEFAULTS`, `JOINT_BY_MATERIALS`, `inferJoints`, `guessMaterial`, `partVolume`, `partMass`, `partThickness`, the
380
+ Also exported: `SHELTER_DEFAULTS`, `FRAGMENT_HIT_DEFAULTS`, `CONFINEMENT_DEFAULTS`, `buildFrame`, `sizeStructure`, `sizeRebar`, `FLEXURE_DEFAULTS`, `MASONRY_DEFAULTS`, `bearingWallThickness`, `SOILS`, `SOIL_DEFAULTS`, `pileCapacity`, `pileLength`, `pileStiffness`, `footingSoil`, `subgradeModulus`, `solveFoundation`, `assemblyPart`, `registerAssembly`, `getAssembly`, `ASSEMBLIES`, `assemblyDepth`, `partCapacity`, `checkDestructible`, `checkHit`, `IMPACT_CONTRACT`, `MATERIALS`, `JOINT_DEFAULTS`, `JOINT_BY_MATERIALS`, `inferJoints`, `guessMaterial`, `partVolume`, `partMass`, `partThickness`, the
287
381
  helpers `vec`, `quat`, `quatFromEuler`, `rotateVec`, `toLocal`, `toWorld`, the locale (`locales`, `resolveStrings`, `DestructionStrings`) and all types.
288
382
 
289
383
  The locale table has `en` and `pt-BR` strings for material names, part statuses, joint kinds, tools, damage types, event labels and fracture styles:
@@ -298,4 +392,8 @@ parafuso, chave inglesa para porca e parafuso, maçarico para solda). O mundo de
298
392
  determinístico (semente fixa) e serializável, então um host de LAN pode rodar o mundo e enviar só as entradas. Os nomes em português
299
393
  (materiais, ferramentas, juntas, eventos) estão em `resolveStrings('pt-BR')`.
300
394
 
395
+ Estrutura real: viga e laje de concreto com `structure.rebar` dobram. Na explosão a face tracionada racha e o aço segura; sem aço a peça parte; com o aço escoado até 10° de giro ela dobra e cai inteira (`structure.flexure`). Paredes e pilares de alvenaria resistem com a resistência real do tijolo e a redução por esbeltez da NBR 16868-1, então a parede portante do térreo esmaga quando há andares demais em cima (`bearingWallThickness` dá a espessura de projeto). Valores e fontes em `specs/001-migracao-entregas-dos-times/design/T10-estrutura-real.md`.
396
+
397
+ Solo e fundação: com `soil` na spec a fundação se apoia no solo (classes com a tensão admissível da NBR 6122). Prédio pesado em sapata sobre argila mole afunda e tomba; em estacas até o chão firme (`structure.pile`, Décourt-Quaresma, `pileLength`) fica de pé; casa em radier em solo firme não sente.
398
+
301
399
  Explosões: `blast.shelter` (paredes e portas entre a explosão e o alvo ficam com a sua parte: tijolo de 20 cm deixa passar ~19%, porta de madeira ~53%, vidro ~83%; a parede que quebra na mesma explosão deixa passar metade do que segurou), `blast.fragmentHits` (os pedaços do que quebra viram golpes de baixa energia no que está atrás) e `blast.confinement` (pressão num ambiente fechado, de 1 a 3 vezes). Tudo desligado por padrão, por opção do mundo ou da chamada, com os números em `SHELTER_DEFAULTS`, `FRAGMENT_HIT_DEFAULTS` e `CONFINEMENT_DEFAULTS`.