@adatechnology/game-destruction 0.2.0 → 0.3.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/README.md CHANGED
@@ -80,6 +80,9 @@ makes a through hole, through 5 cm concrete it only cracks and lodges. Grazing h
80
80
  | `fire` | `temperature`, `radius`, `duration` | heats towards the flame temperature and ignites at the material's ignition point |
81
81
  | `heat` | same | heat without flame (cutting torch, hot plate); melts what it can reach |
82
82
 
83
+ Brittle materials that `shatter` (glass, ceramic) take blunt and ballistic damage multiplied by `fragility(ductility, thickness)` (x1 from 2 cm up, to x118
84
+ for 3 mm glass), so a 9 mm shatters a window pane in one shot while a 5 cm block of glass takes several.
85
+
83
86
  `hit(objectId, hit)` finds the part along `direction`; `hitPart` targets a known part; `shoot` and `raycast` work in world space.
84
87
 
85
88
  ## Fire, melting and leaks
@@ -125,13 +128,20 @@ without any `structure` behave exactly as before.
125
128
  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.
126
129
  - **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
127
130
  that way. A `partition` is supported but supports nothing, so breaking it never drops anything.
128
- - **Overload**: above stress 1 a part takes `(stress - 1) * maxHp * 0.5` damage per second, emits `stress` (once per 0.5 s) and cascades; brittle
131
+ - **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,
132
+ 1.5 in ~2 s, anything above 1.7 in ~1.7 s; deterministic), emits `stress` (once per 0.5 s) and cascades; brittle
129
133
  parts `crack` from stress 0.8. Metal never fractures, so overloaded steel only reports stress.
130
134
  - **Collapse**: a part with no support path to an anchored part falls: it is cut from the supported pieces into its own group (index 1 or more) and
131
- one `collapse` event lists the group. Non-structural parts that hang only on collapsed parts fall with them. Make the group its own object with
135
+ one `collapse` event lists the group, and every part in it that is not already lost gets `PartState.status === 'collapsed'` (kept through
136
+ `serialize`/`load`, cleared by `reset`; a part that later breaks becomes `broken`). Non-structural parts that hang only on collapsed parts fall with them. Make the group its own object with
132
137
  `splitGroup`.
133
138
  - `buildFrame({ spanX, spanZ, columnsX, columnsZ, height, floors, walls, roof, columnCapacity, ... })` returns a test structure: columns on
134
139
  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.
140
+ - `sizeStructure(spec, { safety?, redundancy? })` returns a copy of the spec with `structure.capacity` set from the loads the world itself solves
141
+ 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
142
+ raised where needed so that losing any single `column` keeps every other part at or below that stress: the structure stands with one column
143
+ gone and, without help, falls with two neighbours gone. Raw material capacities are rarely a sensible design (a 54 t roof on 20 cm columns is
144
+ already over 1 at rest); size the structure instead of hand-tuning. Parts without `structure` or with role `none` are untouched.
135
145
 
136
146
  ## Contracts: implement your own or use the default world
137
147
 
@@ -182,7 +192,7 @@ world continues with the same events as the original. The loading world must use
182
192
  `raycast`, `shoot`, `disassemble`, `workableJoints`, `nearestJoint`, `extinguish`, `setLiters`, `update`, `state`, `part`, `reset`, `on`,
183
193
  `register`, `serialize`, `load`, `time`. `state()` and `part()` return live views; do not mutate them.
184
194
 
185
- Also exported: `buildFrame`, `partCapacity`, `checkDestructible`, `checkHit`, `IMPACT_CONTRACT`, `MATERIALS`, `JOINT_DEFAULTS`, `JOINT_BY_MATERIALS`, `inferJoints`, `guessMaterial`, `partVolume`, `partMass`, `partThickness`, the
195
+ Also exported: `buildFrame`, `sizeStructure`, `partCapacity`, `checkDestructible`, `checkHit`, `IMPACT_CONTRACT`, `MATERIALS`, `JOINT_DEFAULTS`, `JOINT_BY_MATERIALS`, `inferJoints`, `guessMaterial`, `partVolume`, `partMass`, `partThickness`, the
186
196
  helpers `vec`, `quat`, `quatFromEuler`, `rotateVec`, `toLocal`, `toWorld`, the locale (`locales`, `resolveStrings`, `DestructionStrings`) and all types.
187
197
 
188
198
  The locale table has `en` and `pt-BR` strings for material names, part statuses, joint kinds, tools, damage types, event labels and fracture styles:
package/dist/index.d.ts CHANGED
@@ -96,6 +96,18 @@ interface DestructionOptions {
96
96
  /** Distance at which a burning part heats parts of other objects, in metres (default 0.6). */
97
97
  fireSpread?: number;
98
98
  }
99
+ interface SizeStructureOptions {
100
+ /** Stress every structural part has at rest after sizing (`load / capacity`), 0..1 (default 0.65). */
101
+ safety?: number;
102
+ /**
103
+ * Redundancy target: when set, the capacity of a part is raised (never lowered) until losing any single `column` leaves every other
104
+ * structural part at this stress or below, e.g. 0.97 (an N-1 design: the building stands with one column gone). Omit to skip the check.
105
+ * Should be above `safety` and, to let the structure fall when a second column goes, below 1.
106
+ */
107
+ redundancy?: number;
108
+ /** Extra or overriding materials, the same list given to `createDestruction`. */
109
+ materials?: MaterialDef[];
110
+ }
99
111
  interface AddOptions {
100
112
  /** Object id (default a generated `obj_1`, `obj_2`...). */
101
113
  id?: string;
@@ -299,6 +311,15 @@ interface FrameOptions {
299
311
  */
300
312
  declare const buildFrame: (o?: FrameOptions) => DestructibleSpec;
301
313
 
314
+ /**
315
+ * Sizes the load capacity of a structure the way an engineer would, from the loads the world itself computes. The intact spec goes through the
316
+ * same load solver `createDestruction` uses, and each structural part (`structure.role` other than `none`) gets `structure.capacity = load / safety`,
317
+ * so its at-rest stress is exactly `safety`. With `redundancy` set, every `column` is also removed in turn and the capacity of the parts that
318
+ * would carry more is raised so they stay at or below `redundancy`. Parts without `structure`, with role `none`, or with no support path to an
319
+ * anchored part are left as they are. The spec is not modified; a copy is returned.
320
+ */
321
+ declare const sizeStructure: (spec: DestructibleSpec, options?: SizeStructureOptions) => DestructibleSpec;
322
+
302
323
  /** Small vector helpers (arrays of three numbers). Pure functions; they allocate a new array. */
303
324
  declare const vec: {
304
325
  add: (a: Vec3, b: Vec3) => Vec3;
@@ -366,4 +387,4 @@ declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions
366
387
  problems: string[];
367
388
  };
368
389
 
369
- export { type AddOptions, type CheckOptions, type DestructionLocale, type DestructionOptions, type DestructionSnapshot, type DestructionStrings, type DestructionWorld, type FrameOptions, JOINT_BY_MATERIALS, JOINT_DEFAULTS, MATERIALS, type MaterialDef, type MaterialThermal, type ObjectRuntime, type PartRuntime, buildFrame, checkDestructible, createDestruction, fragility, guessMaterial, inferJoints, locales, partCapacity, partMass, partThickness, partVolume, quat, quatFromEuler, resolveStrings, rotateVec, toLocal, toWorld, vec };
390
+ export { type AddOptions, type CheckOptions, type DestructionLocale, type DestructionOptions, type DestructionSnapshot, type DestructionStrings, type DestructionWorld, type FrameOptions, JOINT_BY_MATERIALS, JOINT_DEFAULTS, MATERIALS, type MaterialDef, type MaterialThermal, type ObjectRuntime, type PartRuntime, type SizeStructureOptions, buildFrame, checkDestructible, createDestruction, fragility, guessMaterial, inferJoints, locales, partCapacity, partMass, partThickness, partVolume, quat, quatFromEuler, resolveStrings, rotateVec, sizeStructure, toLocal, toWorld, vec };
@@ -41,6 +41,7 @@ var GameDestruction = (() => {
41
41
  quatFromEuler: () => quatFromEuler,
42
42
  resolveStrings: () => resolveStrings,
43
43
  rotateVec: () => rotateVec,
44
+ sizeStructure: () => sizeStructure,
44
45
  toLocal: () => toLocal,
45
46
  toWorld: () => toWorld,
46
47
  vec: () => vec
@@ -1461,6 +1462,7 @@ var GameDestruction = (() => {
1461
1462
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1462
1463
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1463
1464
  var clone = (v) => structuredClone(v);
1465
+ var overloadRate = (stress) => Math.min(0.6, 0.12 + 0.8 * (stress - 1));
1464
1466
  var updateBound = (o) => {
1465
1467
  if (!o.parts.length) return;
1466
1468
  const c = [0, 0, 0];
@@ -1771,7 +1773,10 @@ var GameDestruction = (() => {
1771
1773
  for (const g of o.st.groups) {
1772
1774
  const ps = g.map((id) => o.byId.get(id)).filter((p) => !isLost(p) && !o.collapsed.has(p.spec.id));
1773
1775
  if (ps.length === 0 || g.some((id) => o.byId.get(id).anchored) || !ps.some((p) => p.structural)) continue;
1774
- for (const p of ps) o.collapsed.add(p.spec.id);
1776
+ for (const p of ps) {
1777
+ o.collapsed.add(p.spec.id);
1778
+ p.st.status = "collapsed";
1779
+ }
1775
1780
  emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
1776
1781
  }
1777
1782
  }
@@ -2071,7 +2076,7 @@ var GameDestruction = (() => {
2071
2076
  const effM = p.thickness / Math.max(sin, 0.3);
2072
2077
  const thr = mat.penetration * effM * 1e3;
2073
2078
  const penetrated = E > thr;
2074
- const scale = mat.damageScale?.ballistic ?? 1;
2079
+ const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2075
2080
  const id = p.spec.id;
2076
2081
  if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
2077
2082
  const r = vec.sub(d, vec.scale(n, 2 * vec.dot(d, n)));
@@ -2421,7 +2426,7 @@ var GameDestruction = (() => {
2421
2426
  p.rt.stressT = 0.5;
2422
2427
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
2423
2428
  }
2424
- applyDamage(o, p, (stress - 1) * p.st.maxHp * 0.5 * dt, "blunt", null, null, 5, 3);
2429
+ applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
2425
2430
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
2426
2431
  p.rt.stressT = 0.5;
2427
2432
  const { c, q } = partXf(o, p);
@@ -2883,6 +2888,47 @@ var GameDestruction = (() => {
2883
2888
  return { name: "frame", parts, joints, tags: ["structure"] };
2884
2889
  };
2885
2890
 
2891
+ // src/size.ts
2892
+ var sizeStructure = (spec, options = {}) => {
2893
+ const safety = options.safety ?? 0.65;
2894
+ const redundancy = options.redundancy;
2895
+ const loads = (s) => {
2896
+ const w = createDestruction(options.materials ? { materials: options.materials } : {});
2897
+ const id = w.add(s);
2898
+ return new Map(w.state(id).parts.map((p) => [p.id, { load: p.load ?? 0, stress: p.stress ?? 0 }]));
2899
+ };
2900
+ const out = structuredClone(spec);
2901
+ const base = loads(out);
2902
+ const need = /* @__PURE__ */ new Map();
2903
+ for (const p of out.parts) {
2904
+ if (!p.structure || p.structure.role === "none") continue;
2905
+ const b = base.get(p.id);
2906
+ if (!b || b.stress <= 0) continue;
2907
+ need.set(p.id, b.load / safety);
2908
+ }
2909
+ if (redundancy !== void 0) {
2910
+ for (const col of out.parts) {
2911
+ if (col.structure?.role !== "column") continue;
2912
+ const reduced = {
2913
+ ...out,
2914
+ parts: out.parts.filter((p) => p.id !== col.id),
2915
+ joints: (out.joints ?? []).filter((j) => j.a !== col.id && j.b !== col.id)
2916
+ };
2917
+ if (!out.joints) delete reduced.joints;
2918
+ const l = loads(reduced);
2919
+ for (const [id, n] of need) {
2920
+ const r = l.get(id);
2921
+ if (r && r.stress > 0) need.set(id, Math.max(n, r.load / redundancy));
2922
+ }
2923
+ }
2924
+ }
2925
+ for (const p of out.parts) {
2926
+ const n = need.get(p.id);
2927
+ if (n !== void 0) p.structure = { ...p.structure, capacity: n };
2928
+ }
2929
+ return out;
2930
+ };
2931
+
2886
2932
  // src/locale.ts
2887
2933
  var locales = {
2888
2934
  en: {
@@ -2915,7 +2961,7 @@ var GameDestruction = (() => {
2915
2961
  wax: "wax",
2916
2962
  soil: "soil"
2917
2963
  },
2918
- partStatus: { intact: "intact", damaged: "damaged", broken: "broken", melted: "melted", ashes: "ashes" },
2964
+ partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
2919
2965
  jointKinds: {
2920
2966
  screw: "screw",
2921
2967
  bolt: "nut and bolt",
@@ -2996,7 +3042,7 @@ var GameDestruction = (() => {
2996
3042
  wax: "cera",
2997
3043
  soil: "terra"
2998
3044
  },
2999
- partStatus: { intact: "intacto", damaged: "danificado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
3045
+ partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
3000
3046
  jointKinds: {
3001
3047
  screw: "parafuso",
3002
3048
  bolt: "porca e parafuso",