@adatechnology/game-destruction 0.1.1 → 0.3.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
@@ -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
@@ -1456,9 +1457,12 @@ var GameDestruction = (() => {
1456
1457
  var DMG_PER_J = 0.02;
1457
1458
  var BLAST_COUPLING = 0.1;
1458
1459
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1460
+ var CUT_SLICE = 1;
1461
+ var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
1459
1462
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1460
1463
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1461
1464
  var clone = (v) => structuredClone(v);
1465
+ var overloadRate = (stress) => Math.min(0.6, 0.12 + 0.8 * (stress - 1));
1462
1466
  var updateBound = (o) => {
1463
1467
  if (!o.parts.length) return;
1464
1468
  const c = [0, 0, 0];
@@ -1608,6 +1612,7 @@ var GameDestruction = (() => {
1608
1612
  fuel0,
1609
1613
  area: shapeArea(ps.shape),
1610
1614
  thickness: partThickness(ps),
1615
+ cutFactor: cutFactorOf(ps.shape),
1611
1616
  dims: shapeDims(ps.shape),
1612
1617
  bound: shapeBound(ps.shape),
1613
1618
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
@@ -1768,7 +1773,10 @@ var GameDestruction = (() => {
1768
1773
  for (const g of o.st.groups) {
1769
1774
  const ps = g.map((id) => o.byId.get(id)).filter((p) => !isLost(p) && !o.collapsed.has(p.spec.id));
1770
1775
  if (ps.length === 0 || g.some((id) => o.byId.get(id).anchored) || !ps.some((p) => p.structural)) continue;
1771
- 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
+ }
1772
1780
  emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
1773
1781
  }
1774
1782
  }
@@ -2068,7 +2076,7 @@ var GameDestruction = (() => {
2068
2076
  const effM = p.thickness / Math.max(sin, 0.3);
2069
2077
  const thr = mat.penetration * effM * 1e3;
2070
2078
  const penetrated = E > thr;
2071
- const scale = mat.damageScale?.ballistic ?? 1;
2079
+ const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2072
2080
  const id = p.spec.id;
2073
2081
  if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
2074
2082
  const r = vec.sub(d, vec.scale(n, 2 * vec.dot(d, n)));
@@ -2129,7 +2137,7 @@ var GameDestruction = (() => {
2129
2137
  if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2130
2138
  else wearJoints(o, p, E * 0.3, point, 0.06, src);
2131
2139
  const scale = mat.damageScale?.[kind] ?? 1;
2132
- return applyDamage(o, p, Ec * DMG_PER_J * scale, kind, point, d, E * 0.1, 12, src);
2140
+ return applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2133
2141
  };
2134
2142
  const blastObject = (o, point, energy, radius, src) => {
2135
2143
  const start = cur.length;
@@ -2418,7 +2426,7 @@ var GameDestruction = (() => {
2418
2426
  p.rt.stressT = 0.5;
2419
2427
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
2420
2428
  }
2421
- 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);
2422
2430
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
2423
2431
  p.rt.stressT = 0.5;
2424
2432
  const { c, q } = partXf(o, p);
@@ -2880,6 +2888,47 @@ var GameDestruction = (() => {
2880
2888
  return { name: "frame", parts, joints, tags: ["structure"] };
2881
2889
  };
2882
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
+
2883
2932
  // src/locale.ts
2884
2933
  var locales = {
2885
2934
  en: {
@@ -2912,7 +2961,7 @@ var GameDestruction = (() => {
2912
2961
  wax: "wax",
2913
2962
  soil: "soil"
2914
2963
  },
2915
- 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" },
2916
2965
  jointKinds: {
2917
2966
  screw: "screw",
2918
2967
  bolt: "nut and bolt",
@@ -2993,7 +3042,7 @@ var GameDestruction = (() => {
2993
3042
  wax: "cera",
2994
3043
  soil: "terra"
2995
3044
  },
2996
- 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" },
2997
3046
  jointKinds: {
2998
3047
  screw: "parafuso",
2999
3048
  bolt: "porca e parafuso",