@adatechnology/game-contracts 0.1.0 → 0.1.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
@@ -416,6 +416,8 @@ interface DestructibleImpl {
416
416
  /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */
417
417
  disassemble?(jointId: string, tool: ToolKind, dt: number): number;
418
418
  extinguish?(partId?: string): void;
419
+ /** Optional: an animation moved a part; `position` / `rotation` (Euler XYZ, radians) are object-local. Keep raycast and hit shapes in sync. */
420
+ setPartPose?(partId: string, position: Vec3, rotation?: Vec3): void;
419
421
  /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */
420
422
  serialize?(): unknown;
421
423
  load?(data: unknown): void;
@@ -446,6 +448,25 @@ declare const checkDestructible: (impl: DestructibleImpl, options?: CheckOptions
446
448
  problems: string[];
447
449
  };
448
450
 
451
+ interface CheckSpecOptions {
452
+ /** Known material ids. When given, every part's material must be one of them. */
453
+ materials?: Iterable<string>;
454
+ }
455
+ interface SpecReport {
456
+ /** False when there is at least one problem (warnings do not count). */
457
+ ok: boolean;
458
+ problems: string[];
459
+ /** Things that are legal but probably wrong, e.g. structural parts that cannot reach an anchored part. */
460
+ warnings: string[];
461
+ }
462
+ /**
463
+ * Static checks on a `DestructibleSpec` (no world needed), for items that give a `spec` instead of an `impl`: unique part ids, known materials
464
+ * (when `options.materials` is given), joints that reference existing, different parts, finite positive sizes, non-negative mass / hp / capacity,
465
+ * valid structure roles and `bears` directions. A structural part with no path of joints to an anchored part is a warning, not a problem: it
466
+ * is allowed, it just falls.
467
+ */
468
+ declare const checkSpec: (spec: DestructibleSpec, options?: CheckSpecOptions) => SpecReport;
469
+
449
470
  /**
450
471
  * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).
451
472
  */
@@ -678,4 +699,4 @@ interface TestableModule<Context = unknown> {
678
699
  scenarios?(locale: string): Array<ModuleScenario<Context>>;
679
700
  }
680
701
 
681
- export { type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type CheckOptions, type ContainerSpec, type Crack, type DamageType, type DebrisKind, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type FluidKind, type FractureStyle, type Fragment, type Hit, type HitResult, type Hole, IMPACT_CONTRACT, type ImpactContract, type ItemAnimationContract, type ItemUseDef, type JointKind, type JointSpec, type JointState, type LiquidId, type MaterialId, type ModuleItemFluid, type ModuleScenario, type ObjectState, type PartShape, type PartSpec, type PartState, type PartStatus, type Quat, type RayHit, type Shot, type StructureRole, type TestableItem, type TestableModule, type ToolKind, type Use, type Vec3, checkAnimation, checkDestructible, checkHit, shotToHit };
702
+ export { type AnimationClipSpec, type AnimationEventSpec, type AnimationKey, type AnimationPose, type CheckOptions, type CheckSpecOptions, type ContainerSpec, type Crack, type DamageType, type DebrisKind, type Dent, type DestructibleImpl, type DestructibleSpec, type DestructionEvent, type DestructionEventType, type FluidKind, type FractureStyle, type Fragment, type Hit, type HitResult, type Hole, IMPACT_CONTRACT, type ImpactContract, type ItemAnimationContract, type ItemUseDef, type JointKind, type JointSpec, type JointState, type LiquidId, type MaterialId, type ModuleItemFluid, type ModuleScenario, type ObjectState, type PartShape, type PartSpec, type PartState, type PartStatus, type Quat, type RayHit, type Shot, type SpecReport, type StructureRole, type TestableItem, type TestableModule, type ToolKind, type Use, type Vec3, checkAnimation, checkDestructible, checkHit, checkSpec, shotToHit };
@@ -25,6 +25,7 @@ var GameContracts = (() => {
25
25
  checkAnimation: () => checkAnimation,
26
26
  checkDestructible: () => checkDestructible,
27
27
  checkHit: () => checkHit,
28
+ checkSpec: () => checkSpec,
28
29
  shotToHit: () => shotToHit
29
30
  });
30
31
 
@@ -137,6 +138,84 @@ var GameContracts = (() => {
137
138
  return { ok: problems.length === 0, problems };
138
139
  };
139
140
 
141
+ // src/spec.ts
142
+ var STRUCTURE_ROLES = ["foundation", "column", "beam", "slab", "wall", "partition", "roof", "stair", "none"];
143
+ var BEARS = [true, false, "a_on_b", "b_on_a"];
144
+ var finite = (n) => typeof n === "number" && Number.isFinite(n);
145
+ var positive = (n) => finite(n) && n > 0;
146
+ var vec3 = (v) => Array.isArray(v) && v.length === 3 && v.every(finite);
147
+ var nonNegative = (n) => n === void 0 || finite(n) && n >= 0;
148
+ var checkSpec = (spec, options = {}) => {
149
+ const problems = [];
150
+ const warnings = [];
151
+ const materials = options.materials ? new Set(options.materials) : null;
152
+ const parts = Array.isArray(spec?.parts) ? spec.parts : [];
153
+ if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push("the spec needs at least one part");
154
+ const ids = /* @__PURE__ */ new Set();
155
+ for (const p of parts) {
156
+ const id = String(p?.id ?? "");
157
+ const label = `part "${id}"`;
158
+ if (id === "") problems.push("a part has no id");
159
+ else if (ids.has(id)) problems.push(`duplicate part id "${id}"`);
160
+ ids.add(id);
161
+ if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material "${String(p?.material)}"`);
162
+ else if (typeof p?.material !== "string" || p.material === "") problems.push(`${label} needs a material`);
163
+ const s = p?.shape;
164
+ if (s?.kind === "box") {
165
+ if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`);
166
+ } else if (s?.kind === "cylinder") {
167
+ if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`);
168
+ } else if (s?.kind === "sphere") {
169
+ if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`);
170
+ } else problems.push(`${label} has an unknown shape`);
171
+ if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`);
172
+ if (p?.rotation !== void 0 && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`);
173
+ for (const f of ["mass", "hp", "thickness"]) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`);
174
+ if (p?.fill !== void 0 && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`);
175
+ if (p?.structure) {
176
+ if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role "${String(p.structure.role)}"`);
177
+ if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`);
178
+ }
179
+ if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`);
180
+ }
181
+ const jointIds = /* @__PURE__ */ new Set();
182
+ const edges = [];
183
+ for (const j of spec?.joints ?? []) {
184
+ const id = j.id ?? `${j.a}~${j.b}`;
185
+ const label = `joint "${id}"`;
186
+ if (jointIds.has(id)) problems.push(`duplicate joint id "${id}"`);
187
+ jointIds.add(id);
188
+ if (!ids.has(j.a)) problems.push(`${label} references unknown part "${String(j.a)}"`);
189
+ if (!ids.has(j.b)) problems.push(`${label} references unknown part "${String(j.b)}"`);
190
+ if (j.a === j.b) problems.push(`${label} joins part "${String(j.a)}" to itself`);
191
+ if (j.bears !== void 0 && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears "${String(j.bears)}"`);
192
+ if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`);
193
+ if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`);
194
+ if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b]);
195
+ }
196
+ const structural = parts.filter((p) => p?.structure && p.structure.role !== "none");
197
+ if (structural.length && spec.joints) {
198
+ const anchored = parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id);
199
+ const adj = /* @__PURE__ */ new Map();
200
+ for (const [a, b] of edges) {
201
+ adj.set(a, [...adj.get(a) ?? [], b]);
202
+ adj.set(b, [...adj.get(b) ?? [], a]);
203
+ }
204
+ const seen = new Set(anchored);
205
+ const queue = [...anchored];
206
+ while (queue.length) {
207
+ for (const n of adj.get(queue.pop()) ?? []) {
208
+ if (seen.has(n)) continue;
209
+ seen.add(n);
210
+ queue.push(n);
211
+ }
212
+ }
213
+ const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id);
214
+ if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `"${i}"`).join(", ")} cannot reach an anchored part, so they will fall`);
215
+ }
216
+ return { ok: problems.length === 0, problems, warnings };
217
+ };
218
+
140
219
  // src/weapons.ts
141
220
  var shotToHit = (shot, point, normal) => {
142
221
  void normal;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/impact.ts","../src/weapons.ts","../src/animation.ts"],"sourcesContent":["/**\n * @adatechnology/game-contracts: the contracts every game module (vehicles, furniture, plants, houses, weapons) depends on, and nothing else.\n * Types plus tiny pure checkers, no runtime dependencies. The default destruction world, the renderers and the test room are optional.\n */\nexport type {\n Crack,\n DamageType,\n DebrisKind,\n Dent,\n DestructibleSpec,\n DestructionEvent,\n DestructionEventType,\n FluidKind,\n Fragment,\n FractureStyle,\n Hit,\n HitResult,\n Hole,\n JointKind,\n JointSpec,\n JointState,\n MaterialId,\n ObjectState,\n PartShape,\n PartSpec,\n PartState,\n PartStatus,\n Quat,\n RayHit,\n StructureRole,\n ToolKind,\n Vec3,\n} from './destruction'\nexport { IMPACT_CONTRACT, checkDestructible, checkHit } from './impact'\nexport type { CheckOptions, DestructibleImpl, ImpactContract } from './impact'\nexport { shotToHit } from './weapons'\nexport type { Shot, Use } from './weapons'\nexport type { ContainerSpec, ItemUseDef, LiquidId } from './fluids'\nexport { checkAnimation } from './animation'\nexport type { AnimationClipSpec, AnimationEventSpec, AnimationKey, AnimationPose, ItemAnimationContract } from './animation'\nexport type { ModuleItemFluid, ModuleScenario, TestableItem, TestableModule } from './module'\n","import type { DamageType, DestructibleSpec, DestructionEvent, Hit, JointSpec, ObjectState, ToolKind } from './destruction'\n\n/**\n * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The\n * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,\n * reset and `on` all route to it.\n *\n * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or\n * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by\n * `describe()`.\n */\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */\n describe(): DestructibleSpec\n /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */\n hit(hit: Hit): DestructionEvent[]\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n state(): ObjectState\n reset?(): void\n /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */\n disassemble?(jointId: string, tool: ToolKind, dt: number): number\n extinguish?(partId?: string): void\n /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */\n serialize?(): unknown\n load?(data: unknown): void\n}\n\n/** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */\nexport interface ImpactContract {\n required: string[]\n optional: string[]\n note: string\n}\n\nexport const IMPACT_CONTRACT: Record<DamageType, ImpactContract> = {\n ballistic: { required: ['type', 'point', 'direction', 'energy', 'caliber'], optional: ['space', 'source'], note: 'bullets and pellets: joules and diameter in millimetres' },\n blunt: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'contact radius in metres (default 3 cm)' },\n slash: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'axes, knives, machetes' },\n explosive: { required: ['type', 'point', 'energy', 'radius'], optional: ['space', 'source'], note: 'point is the centre of the blast, radius the range in metres' },\n fire: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'flame temperature in °C; duration in seconds (call every frame with `dt`)' },\n heat: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'heat without flame (cutting torch, hot plate)' },\n}\n\n/** Problems with a hit according to the impact contract (empty when it is valid). */\nexport const checkHit = (hit: Hit): string[] => {\n const c = IMPACT_CONTRACT[hit.type]\n if (!c) return [`unknown damage type \"${String(hit.type)}\"`]\n const out: string[] = []\n for (const f of c.required) {\n const v = (hit as unknown as Record<string, unknown>)[f]\n if (v === undefined || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === 'number' && !Number.isFinite(v))) out.push(`${hit.type} hit needs \"${f}\"`)\n }\n return out\n}\n\nexport interface CheckOptions {\n /**\n * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of\n * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.\n */\n materials?: Iterable<string>\n}\n\nconst finiteJson = (v: unknown, path: string, out: string[]) => {\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) out.push(`${path} is not a finite number`)\n } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out))\n else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out)\n else if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') out.push(`${path} is not JSON`)\n}\n\n/**\n * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`\n * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl\n * (and calls `reset()` afterwards when there is one).\n */\nexport const checkDestructible = (impl: DestructibleImpl, options: CheckOptions = {}): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const known = options.materials ? new Set(options.materials) : null\n let spec: DestructibleSpec | undefined\n try {\n spec = impl.describe()\n } catch (e) {\n return { ok: false, problems: [`describe() threw: ${String(e)}`] }\n }\n if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ['describe() must return at least one part'] }\n const ids = new Set<string>()\n for (const p of spec.parts) {\n if (ids.has(p.id)) problems.push(`duplicate part id \"${p.id}\"`)\n ids.add(p.id)\n if (known && !known.has(p.material)) problems.push(`part \"${p.id}\" has unknown material \"${p.material}\"`)\n if (!p.shape || !['box', 'cylinder', 'sphere'].includes(p.shape.kind)) problems.push(`part \"${p.id}\" has no valid shape`)\n if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part \"${p.id}\" has no position`)\n }\n const jointIds = new Set<string>()\n for (const j of (spec.joints ?? []) as JointSpec[]) {\n if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint \"${j.id ?? `${j.a}~${j.b}`}\" references an unknown part`)\n const jid = j.id ?? `${j.a}~${j.b}`\n if (jointIds.has(jid)) problems.push(`duplicate joint id \"${jid}\"`)\n jointIds.add(jid)\n }\n const checkState = (label: string) => {\n let st: ObjectState\n try {\n st = impl.state()\n } catch (e) {\n problems.push(`state() threw ${label}: ${String(e)}`)\n return\n }\n const sids = st.parts.map((p) => p.id).sort().join(',')\n if (sids !== [...ids].sort().join(',')) problems.push(`state() part ids do not match describe() ${label}`)\n for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part \"${p.id}\" has negative or invalid hp ${label}`)\n }\n const checkEvents = (events: unknown, label: string) => {\n if (!Array.isArray(events)) {\n problems.push(`${label} must return an array`)\n return\n }\n const evs = events as DestructionEvent[]\n evs.forEach((e, i) => {\n if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object \"${String(e.object)}\", expected \"${impl.id}\"`)\n const part = (e as { part?: string }).part\n if (part !== undefined && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part \"${part}\"`)\n })\n const bad: string[] = []\n finiteJson(evs, 'events', bad)\n problems.push(...bad.map((b) => `${label}: ${b}`))\n try {\n const s = JSON.stringify(evs)\n if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`)\n } catch (e) {\n problems.push(`${label} events are not JSON-serializable: ${String(e)}`)\n }\n }\n checkState('before the hit')\n const target = spec.parts[0]\n try {\n const hit: Hit = { type: 'ballistic', point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 }\n checkEvents(impl.hit(hit), 'hit()')\n } catch (e) {\n problems.push(`hit() threw: ${String(e)}`)\n }\n checkState('after the hit')\n if (impl.update) {\n try {\n checkEvents(impl.update(0.016), 'update()')\n } catch (e) {\n problems.push(`update() threw: ${String(e)}`)\n }\n checkState('after update')\n }\n try {\n impl.reset?.()\n } catch (e) {\n problems.push(`reset() threw: ${String(e)}`)\n }\n return { ok: problems.length === 0, problems }\n}\n","import type { Hit, Vec3 } from './destruction'\n\n/**\n * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).\n */\nexport interface Shot {\n origin: Vec3\n /** Unit vector. */\n direction: Vec3\n /** Muzzle energy in joules. */\n energy: number\n /** Bullet diameter in millimetres. */\n caliber: number\n /** snake_case calibre id, e.g. `9x19`. */\n caliberId: string\n /** Muzzle speed in m/s. */\n speed: number\n /** Energy at a distance in metres, with the calibre's drag. */\n energyAt(distance: number): number\n pellet: number\n /** snake_case weapon id. */\n weapon: string\n source?: string\n}\n\n/** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */\nexport type Use =\n | { kind: 'melee'; damage: 'blunt' | 'slash'; energy: number; radius: number; reach: number; origin: Vec3; direction: Vec3 }\n | { kind: 'flame'; temperature: number; radius: number; range: number; cone: boolean; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'heat'; temperature: number; radius: number; range: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'throw'; energy: number; radius: number; fuse: number; velocity: Vec3; origin: Vec3; fire?: { temperature: number; radius: number; seconds: number } }\n | { kind: 'tool'; tool: string; reach: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'extinguish'; range: number; coolC: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'hand'; reach: number; origin: Vec3; direction: Vec3 }\n\n/**\n * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin\n * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not\n * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.\n */\nexport const shotToHit = (shot: Shot, point: Vec3, normal?: Vec3): Hit => {\n void normal\n const dx = point[0] - shot.origin[0]\n const dy = point[1] - shot.origin[1]\n const dz = point[2] - shot.origin[2]\n const distance = Math.hypot(dx, dy, dz)\n return {\n type: 'ballistic',\n point: [point[0], point[1], point[2]],\n direction: [shot.direction[0], shot.direction[1], shot.direction[2]],\n energy: shot.energyAt(distance),\n caliber: shot.caliber,\n source: shot.source ?? shot.weapon,\n }\n}\n","import type { Vec3 } from './destruction'\n\n/** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */\nexport interface AnimationPose {\n position?: Vec3\n rotation?: Vec3\n}\n\nexport interface AnimationKey {\n /** Seconds from the start of the clip. */\n t: number\n /** Pose of the item's root. */\n position?: Vec3\n rotation?: Vec3\n /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */\n parts?: Record<string, AnimationPose>\n}\n\nexport interface AnimationEventSpec {\n /** Seconds from the start of the clip. */\n t: number\n /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */\n type: string\n data?: Record<string, unknown>\n}\n\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/impact.ts","../src/spec.ts","../src/weapons.ts","../src/animation.ts"],"sourcesContent":["/**\n * @adatechnology/game-contracts: the contracts every game module (vehicles, furniture, plants, houses, weapons) depends on, and nothing else.\n * Types plus tiny pure checkers, no runtime dependencies. The default destruction world, the renderers and the test room are optional.\n */\nexport type {\n Crack,\n DamageType,\n DebrisKind,\n Dent,\n DestructibleSpec,\n DestructionEvent,\n DestructionEventType,\n FluidKind,\n Fragment,\n FractureStyle,\n Hit,\n HitResult,\n Hole,\n JointKind,\n JointSpec,\n JointState,\n MaterialId,\n ObjectState,\n PartShape,\n PartSpec,\n PartState,\n PartStatus,\n Quat,\n RayHit,\n StructureRole,\n ToolKind,\n Vec3,\n} from './destruction'\nexport { IMPACT_CONTRACT, checkDestructible, checkHit } from './impact'\nexport type { CheckOptions, DestructibleImpl, ImpactContract } from './impact'\nexport { checkSpec } from './spec'\nexport type { CheckSpecOptions, SpecReport } from './spec'\nexport { shotToHit } from './weapons'\nexport type { Shot, Use } from './weapons'\nexport type { ContainerSpec, ItemUseDef, LiquidId } from './fluids'\nexport { checkAnimation } from './animation'\nexport type { AnimationClipSpec, AnimationEventSpec, AnimationKey, AnimationPose, ItemAnimationContract } from './animation'\nexport type { ModuleItemFluid, ModuleScenario, TestableItem, TestableModule } from './module'\n","import type { DamageType, DestructibleSpec, DestructionEvent, Hit, JointSpec, ObjectState, ToolKind, Vec3 } from './destruction'\n\n/**\n * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The\n * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,\n * reset and `on` all route to it.\n *\n * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or\n * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by\n * `describe()`.\n */\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */\n describe(): DestructibleSpec\n /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */\n hit(hit: Hit): DestructionEvent[]\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n state(): ObjectState\n reset?(): void\n /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */\n disassemble?(jointId: string, tool: ToolKind, dt: number): number\n extinguish?(partId?: string): void\n /** Optional: an animation moved a part; `position` / `rotation` (Euler XYZ, radians) are object-local. Keep raycast and hit shapes in sync. */\n setPartPose?(partId: string, position: Vec3, rotation?: Vec3): void\n /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */\n serialize?(): unknown\n load?(data: unknown): void\n}\n\n/** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */\nexport interface ImpactContract {\n required: string[]\n optional: string[]\n note: string\n}\n\nexport const IMPACT_CONTRACT: Record<DamageType, ImpactContract> = {\n ballistic: { required: ['type', 'point', 'direction', 'energy', 'caliber'], optional: ['space', 'source'], note: 'bullets and pellets: joules and diameter in millimetres' },\n blunt: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'contact radius in metres (default 3 cm)' },\n slash: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'axes, knives, machetes' },\n explosive: { required: ['type', 'point', 'energy', 'radius'], optional: ['space', 'source'], note: 'point is the centre of the blast, radius the range in metres' },\n fire: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'flame temperature in °C; duration in seconds (call every frame with `dt`)' },\n heat: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'heat without flame (cutting torch, hot plate)' },\n}\n\n/** Problems with a hit according to the impact contract (empty when it is valid). */\nexport const checkHit = (hit: Hit): string[] => {\n const c = IMPACT_CONTRACT[hit.type]\n if (!c) return [`unknown damage type \"${String(hit.type)}\"`]\n const out: string[] = []\n for (const f of c.required) {\n const v = (hit as unknown as Record<string, unknown>)[f]\n if (v === undefined || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === 'number' && !Number.isFinite(v))) out.push(`${hit.type} hit needs \"${f}\"`)\n }\n return out\n}\n\nexport interface CheckOptions {\n /**\n * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of\n * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.\n */\n materials?: Iterable<string>\n}\n\nconst finiteJson = (v: unknown, path: string, out: string[]) => {\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) out.push(`${path} is not a finite number`)\n } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out))\n else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out)\n else if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') out.push(`${path} is not JSON`)\n}\n\n/**\n * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`\n * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl\n * (and calls `reset()` afterwards when there is one).\n */\nexport const checkDestructible = (impl: DestructibleImpl, options: CheckOptions = {}): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const known = options.materials ? new Set(options.materials) : null\n let spec: DestructibleSpec | undefined\n try {\n spec = impl.describe()\n } catch (e) {\n return { ok: false, problems: [`describe() threw: ${String(e)}`] }\n }\n if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ['describe() must return at least one part'] }\n const ids = new Set<string>()\n for (const p of spec.parts) {\n if (ids.has(p.id)) problems.push(`duplicate part id \"${p.id}\"`)\n ids.add(p.id)\n if (known && !known.has(p.material)) problems.push(`part \"${p.id}\" has unknown material \"${p.material}\"`)\n if (!p.shape || !['box', 'cylinder', 'sphere'].includes(p.shape.kind)) problems.push(`part \"${p.id}\" has no valid shape`)\n if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part \"${p.id}\" has no position`)\n }\n const jointIds = new Set<string>()\n for (const j of (spec.joints ?? []) as JointSpec[]) {\n if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint \"${j.id ?? `${j.a}~${j.b}`}\" references an unknown part`)\n const jid = j.id ?? `${j.a}~${j.b}`\n if (jointIds.has(jid)) problems.push(`duplicate joint id \"${jid}\"`)\n jointIds.add(jid)\n }\n const checkState = (label: string) => {\n let st: ObjectState\n try {\n st = impl.state()\n } catch (e) {\n problems.push(`state() threw ${label}: ${String(e)}`)\n return\n }\n const sids = st.parts.map((p) => p.id).sort().join(',')\n if (sids !== [...ids].sort().join(',')) problems.push(`state() part ids do not match describe() ${label}`)\n for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part \"${p.id}\" has negative or invalid hp ${label}`)\n }\n const checkEvents = (events: unknown, label: string) => {\n if (!Array.isArray(events)) {\n problems.push(`${label} must return an array`)\n return\n }\n const evs = events as DestructionEvent[]\n evs.forEach((e, i) => {\n if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object \"${String(e.object)}\", expected \"${impl.id}\"`)\n const part = (e as { part?: string }).part\n if (part !== undefined && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part \"${part}\"`)\n })\n const bad: string[] = []\n finiteJson(evs, 'events', bad)\n problems.push(...bad.map((b) => `${label}: ${b}`))\n try {\n const s = JSON.stringify(evs)\n if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`)\n } catch (e) {\n problems.push(`${label} events are not JSON-serializable: ${String(e)}`)\n }\n }\n checkState('before the hit')\n const target = spec.parts[0]\n try {\n const hit: Hit = { type: 'ballistic', point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 }\n checkEvents(impl.hit(hit), 'hit()')\n } catch (e) {\n problems.push(`hit() threw: ${String(e)}`)\n }\n checkState('after the hit')\n if (impl.update) {\n try {\n checkEvents(impl.update(0.016), 'update()')\n } catch (e) {\n problems.push(`update() threw: ${String(e)}`)\n }\n checkState('after update')\n }\n try {\n impl.reset?.()\n } catch (e) {\n problems.push(`reset() threw: ${String(e)}`)\n }\n return { ok: problems.length === 0, problems }\n}\n","import type { DestructibleSpec } from './destruction'\n\nexport interface CheckSpecOptions {\n /** Known material ids. When given, every part's material must be one of them. */\n materials?: Iterable<string>\n}\n\nexport interface SpecReport {\n /** False when there is at least one problem (warnings do not count). */\n ok: boolean\n problems: string[]\n /** Things that are legal but probably wrong, e.g. structural parts that cannot reach an anchored part. */\n warnings: string[]\n}\n\nconst STRUCTURE_ROLES = ['foundation', 'column', 'beam', 'slab', 'wall', 'partition', 'roof', 'stair', 'none']\nconst BEARS = [true, false, 'a_on_b', 'b_on_a']\n\nconst finite = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n)\nconst positive = (n: unknown): boolean => finite(n) && n > 0\nconst vec3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every(finite)\nconst nonNegative = (n: unknown): boolean => n === undefined || (finite(n) && n >= 0)\n\n/**\n * Static checks on a `DestructibleSpec` (no world needed), for items that give a `spec` instead of an `impl`: unique part ids, known materials\n * (when `options.materials` is given), joints that reference existing, different parts, finite positive sizes, non-negative mass / hp / capacity,\n * valid structure roles and `bears` directions. A structural part with no path of joints to an anchored part is a warning, not a problem: it\n * is allowed, it just falls.\n */\nexport const checkSpec = (spec: DestructibleSpec, options: CheckSpecOptions = {}): SpecReport => {\n const problems: string[] = []\n const warnings: string[] = []\n const materials = options.materials ? new Set(options.materials) : null\n const parts = Array.isArray(spec?.parts) ? spec.parts : []\n if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push('the spec needs at least one part')\n const ids = new Set<string>()\n for (const p of parts) {\n const id = String(p?.id ?? '')\n const label = `part \"${id}\"`\n if (id === '') problems.push('a part has no id')\n else if (ids.has(id)) problems.push(`duplicate part id \"${id}\"`)\n ids.add(id)\n if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material \"${String(p?.material)}\"`)\n else if (typeof p?.material !== 'string' || p.material === '') problems.push(`${label} needs a material`)\n const s = p?.shape\n if (s?.kind === 'box') {\n if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`)\n } else if (s?.kind === 'cylinder') {\n if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`)\n } else if (s?.kind === 'sphere') {\n if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`)\n } else problems.push(`${label} has an unknown shape`)\n if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`)\n if (p?.rotation !== undefined && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`)\n for (const f of ['mass', 'hp', 'thickness'] as const) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`)\n if (p?.fill !== undefined && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`)\n if (p?.structure) {\n if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role \"${String(p.structure.role)}\"`)\n if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`)\n }\n if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`)\n }\n\n const jointIds = new Set<string>()\n const edges: Array<[string, string]> = []\n for (const j of spec?.joints ?? []) {\n const id = j.id ?? `${j.a}~${j.b}`\n const label = `joint \"${id}\"`\n if (jointIds.has(id)) problems.push(`duplicate joint id \"${id}\"`)\n jointIds.add(id)\n if (!ids.has(j.a)) problems.push(`${label} references unknown part \"${String(j.a)}\"`)\n if (!ids.has(j.b)) problems.push(`${label} references unknown part \"${String(j.b)}\"`)\n if (j.a === j.b) problems.push(`${label} joins part \"${String(j.a)}\" to itself`)\n if (j.bears !== undefined && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears \"${String(j.bears)}\"`)\n if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`)\n if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`)\n if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b])\n }\n\n // structural parts must reach an anchored part over the joints (only when the spec lists its joints: otherwise they are inferred later)\n const structural = parts.filter((p) => p?.structure && p.structure.role !== 'none')\n if (structural.length && spec.joints) {\n const anchored = parts.filter((p) => p.anchored || p.structure?.role === 'foundation').map((p) => p.id)\n const adj = new Map<string, string[]>()\n for (const [a, b] of edges) {\n adj.set(a, [...(adj.get(a) ?? []), b])\n adj.set(b, [...(adj.get(b) ?? []), a])\n }\n const seen = new Set(anchored)\n const queue = [...anchored]\n while (queue.length) {\n for (const n of adj.get(queue.pop()!) ?? []) {\n if (seen.has(n)) continue\n seen.add(n)\n queue.push(n)\n }\n }\n const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id)\n if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `\"${i}\"`).join(', ')} cannot reach an anchored part, so they will fall`)\n }\n return { ok: problems.length === 0, problems, warnings }\n}\n","import type { Hit, Vec3 } from './destruction'\n\n/**\n * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).\n */\nexport interface Shot {\n origin: Vec3\n /** Unit vector. */\n direction: Vec3\n /** Muzzle energy in joules. */\n energy: number\n /** Bullet diameter in millimetres. */\n caliber: number\n /** snake_case calibre id, e.g. `9x19`. */\n caliberId: string\n /** Muzzle speed in m/s. */\n speed: number\n /** Energy at a distance in metres, with the calibre's drag. */\n energyAt(distance: number): number\n pellet: number\n /** snake_case weapon id. */\n weapon: string\n source?: string\n}\n\n/** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */\nexport type Use =\n | { kind: 'melee'; damage: 'blunt' | 'slash'; energy: number; radius: number; reach: number; origin: Vec3; direction: Vec3 }\n | { kind: 'flame'; temperature: number; radius: number; range: number; cone: boolean; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'heat'; temperature: number; radius: number; range: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'throw'; energy: number; radius: number; fuse: number; velocity: Vec3; origin: Vec3; fire?: { temperature: number; radius: number; seconds: number } }\n | { kind: 'tool'; tool: string; reach: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'extinguish'; range: number; coolC: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'hand'; reach: number; origin: Vec3; direction: Vec3 }\n\n/**\n * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin\n * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not\n * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.\n */\nexport const shotToHit = (shot: Shot, point: Vec3, normal?: Vec3): Hit => {\n void normal\n const dx = point[0] - shot.origin[0]\n const dy = point[1] - shot.origin[1]\n const dz = point[2] - shot.origin[2]\n const distance = Math.hypot(dx, dy, dz)\n return {\n type: 'ballistic',\n point: [point[0], point[1], point[2]],\n direction: [shot.direction[0], shot.direction[1], shot.direction[2]],\n energy: shot.energyAt(distance),\n caliber: shot.caliber,\n source: shot.source ?? shot.weapon,\n }\n}\n","import type { Vec3 } from './destruction'\n\n/** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */\nexport interface AnimationPose {\n position?: Vec3\n rotation?: Vec3\n}\n\nexport interface AnimationKey {\n /** Seconds from the start of the clip. */\n t: number\n /** Pose of the item's root. */\n position?: Vec3\n rotation?: Vec3\n /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */\n parts?: Record<string, AnimationPose>\n}\n\nexport interface AnimationEventSpec {\n /** Seconds from the start of the clip. */\n t: number\n /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */\n type: string\n data?: Record<string, unknown>\n}\n\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -107,6 +107,84 @@ var checkDestructible = (impl, options = {}) => {
107
107
  return { ok: problems.length === 0, problems };
108
108
  };
109
109
 
110
+ // src/spec.ts
111
+ var STRUCTURE_ROLES = ["foundation", "column", "beam", "slab", "wall", "partition", "roof", "stair", "none"];
112
+ var BEARS = [true, false, "a_on_b", "b_on_a"];
113
+ var finite = (n) => typeof n === "number" && Number.isFinite(n);
114
+ var positive = (n) => finite(n) && n > 0;
115
+ var vec3 = (v) => Array.isArray(v) && v.length === 3 && v.every(finite);
116
+ var nonNegative = (n) => n === void 0 || finite(n) && n >= 0;
117
+ var checkSpec = (spec, options = {}) => {
118
+ const problems = [];
119
+ const warnings = [];
120
+ const materials = options.materials ? new Set(options.materials) : null;
121
+ const parts = Array.isArray(spec?.parts) ? spec.parts : [];
122
+ if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push("the spec needs at least one part");
123
+ const ids = /* @__PURE__ */ new Set();
124
+ for (const p of parts) {
125
+ const id = String(p?.id ?? "");
126
+ const label = `part "${id}"`;
127
+ if (id === "") problems.push("a part has no id");
128
+ else if (ids.has(id)) problems.push(`duplicate part id "${id}"`);
129
+ ids.add(id);
130
+ if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material "${String(p?.material)}"`);
131
+ else if (typeof p?.material !== "string" || p.material === "") problems.push(`${label} needs a material`);
132
+ const s = p?.shape;
133
+ if (s?.kind === "box") {
134
+ if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`);
135
+ } else if (s?.kind === "cylinder") {
136
+ if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`);
137
+ } else if (s?.kind === "sphere") {
138
+ if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`);
139
+ } else problems.push(`${label} has an unknown shape`);
140
+ if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`);
141
+ if (p?.rotation !== void 0 && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`);
142
+ for (const f of ["mass", "hp", "thickness"]) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`);
143
+ if (p?.fill !== void 0 && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`);
144
+ if (p?.structure) {
145
+ if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role "${String(p.structure.role)}"`);
146
+ if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`);
147
+ }
148
+ if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`);
149
+ }
150
+ const jointIds = /* @__PURE__ */ new Set();
151
+ const edges = [];
152
+ for (const j of spec?.joints ?? []) {
153
+ const id = j.id ?? `${j.a}~${j.b}`;
154
+ const label = `joint "${id}"`;
155
+ if (jointIds.has(id)) problems.push(`duplicate joint id "${id}"`);
156
+ jointIds.add(id);
157
+ if (!ids.has(j.a)) problems.push(`${label} references unknown part "${String(j.a)}"`);
158
+ if (!ids.has(j.b)) problems.push(`${label} references unknown part "${String(j.b)}"`);
159
+ if (j.a === j.b) problems.push(`${label} joins part "${String(j.a)}" to itself`);
160
+ if (j.bears !== void 0 && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears "${String(j.bears)}"`);
161
+ if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`);
162
+ if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`);
163
+ if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b]);
164
+ }
165
+ const structural = parts.filter((p) => p?.structure && p.structure.role !== "none");
166
+ if (structural.length && spec.joints) {
167
+ const anchored = parts.filter((p) => p.anchored || p.structure?.role === "foundation").map((p) => p.id);
168
+ const adj = /* @__PURE__ */ new Map();
169
+ for (const [a, b] of edges) {
170
+ adj.set(a, [...adj.get(a) ?? [], b]);
171
+ adj.set(b, [...adj.get(b) ?? [], a]);
172
+ }
173
+ const seen = new Set(anchored);
174
+ const queue = [...anchored];
175
+ while (queue.length) {
176
+ for (const n of adj.get(queue.pop()) ?? []) {
177
+ if (seen.has(n)) continue;
178
+ seen.add(n);
179
+ queue.push(n);
180
+ }
181
+ }
182
+ const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id);
183
+ if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `"${i}"`).join(", ")} cannot reach an anchored part, so they will fall`);
184
+ }
185
+ return { ok: problems.length === 0, problems, warnings };
186
+ };
187
+
110
188
  // src/weapons.ts
111
189
  var shotToHit = (shot, point, normal) => {
112
190
  void normal;
@@ -168,6 +246,7 @@ export {
168
246
  checkAnimation,
169
247
  checkDestructible,
170
248
  checkHit,
249
+ checkSpec,
171
250
  shotToHit
172
251
  };
173
252
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/impact.ts","../src/weapons.ts","../src/animation.ts"],"sourcesContent":["import type { DamageType, DestructibleSpec, DestructionEvent, Hit, JointSpec, ObjectState, ToolKind } from './destruction'\n\n/**\n * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The\n * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,\n * reset and `on` all route to it.\n *\n * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or\n * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by\n * `describe()`.\n */\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */\n describe(): DestructibleSpec\n /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */\n hit(hit: Hit): DestructionEvent[]\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n state(): ObjectState\n reset?(): void\n /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */\n disassemble?(jointId: string, tool: ToolKind, dt: number): number\n extinguish?(partId?: string): void\n /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */\n serialize?(): unknown\n load?(data: unknown): void\n}\n\n/** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */\nexport interface ImpactContract {\n required: string[]\n optional: string[]\n note: string\n}\n\nexport const IMPACT_CONTRACT: Record<DamageType, ImpactContract> = {\n ballistic: { required: ['type', 'point', 'direction', 'energy', 'caliber'], optional: ['space', 'source'], note: 'bullets and pellets: joules and diameter in millimetres' },\n blunt: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'contact radius in metres (default 3 cm)' },\n slash: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'axes, knives, machetes' },\n explosive: { required: ['type', 'point', 'energy', 'radius'], optional: ['space', 'source'], note: 'point is the centre of the blast, radius the range in metres' },\n fire: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'flame temperature in °C; duration in seconds (call every frame with `dt`)' },\n heat: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'heat without flame (cutting torch, hot plate)' },\n}\n\n/** Problems with a hit according to the impact contract (empty when it is valid). */\nexport const checkHit = (hit: Hit): string[] => {\n const c = IMPACT_CONTRACT[hit.type]\n if (!c) return [`unknown damage type \"${String(hit.type)}\"`]\n const out: string[] = []\n for (const f of c.required) {\n const v = (hit as unknown as Record<string, unknown>)[f]\n if (v === undefined || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === 'number' && !Number.isFinite(v))) out.push(`${hit.type} hit needs \"${f}\"`)\n }\n return out\n}\n\nexport interface CheckOptions {\n /**\n * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of\n * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.\n */\n materials?: Iterable<string>\n}\n\nconst finiteJson = (v: unknown, path: string, out: string[]) => {\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) out.push(`${path} is not a finite number`)\n } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out))\n else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out)\n else if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') out.push(`${path} is not JSON`)\n}\n\n/**\n * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`\n * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl\n * (and calls `reset()` afterwards when there is one).\n */\nexport const checkDestructible = (impl: DestructibleImpl, options: CheckOptions = {}): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const known = options.materials ? new Set(options.materials) : null\n let spec: DestructibleSpec | undefined\n try {\n spec = impl.describe()\n } catch (e) {\n return { ok: false, problems: [`describe() threw: ${String(e)}`] }\n }\n if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ['describe() must return at least one part'] }\n const ids = new Set<string>()\n for (const p of spec.parts) {\n if (ids.has(p.id)) problems.push(`duplicate part id \"${p.id}\"`)\n ids.add(p.id)\n if (known && !known.has(p.material)) problems.push(`part \"${p.id}\" has unknown material \"${p.material}\"`)\n if (!p.shape || !['box', 'cylinder', 'sphere'].includes(p.shape.kind)) problems.push(`part \"${p.id}\" has no valid shape`)\n if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part \"${p.id}\" has no position`)\n }\n const jointIds = new Set<string>()\n for (const j of (spec.joints ?? []) as JointSpec[]) {\n if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint \"${j.id ?? `${j.a}~${j.b}`}\" references an unknown part`)\n const jid = j.id ?? `${j.a}~${j.b}`\n if (jointIds.has(jid)) problems.push(`duplicate joint id \"${jid}\"`)\n jointIds.add(jid)\n }\n const checkState = (label: string) => {\n let st: ObjectState\n try {\n st = impl.state()\n } catch (e) {\n problems.push(`state() threw ${label}: ${String(e)}`)\n return\n }\n const sids = st.parts.map((p) => p.id).sort().join(',')\n if (sids !== [...ids].sort().join(',')) problems.push(`state() part ids do not match describe() ${label}`)\n for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part \"${p.id}\" has negative or invalid hp ${label}`)\n }\n const checkEvents = (events: unknown, label: string) => {\n if (!Array.isArray(events)) {\n problems.push(`${label} must return an array`)\n return\n }\n const evs = events as DestructionEvent[]\n evs.forEach((e, i) => {\n if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object \"${String(e.object)}\", expected \"${impl.id}\"`)\n const part = (e as { part?: string }).part\n if (part !== undefined && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part \"${part}\"`)\n })\n const bad: string[] = []\n finiteJson(evs, 'events', bad)\n problems.push(...bad.map((b) => `${label}: ${b}`))\n try {\n const s = JSON.stringify(evs)\n if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`)\n } catch (e) {\n problems.push(`${label} events are not JSON-serializable: ${String(e)}`)\n }\n }\n checkState('before the hit')\n const target = spec.parts[0]\n try {\n const hit: Hit = { type: 'ballistic', point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 }\n checkEvents(impl.hit(hit), 'hit()')\n } catch (e) {\n problems.push(`hit() threw: ${String(e)}`)\n }\n checkState('after the hit')\n if (impl.update) {\n try {\n checkEvents(impl.update(0.016), 'update()')\n } catch (e) {\n problems.push(`update() threw: ${String(e)}`)\n }\n checkState('after update')\n }\n try {\n impl.reset?.()\n } catch (e) {\n problems.push(`reset() threw: ${String(e)}`)\n }\n return { ok: problems.length === 0, problems }\n}\n","import type { Hit, Vec3 } from './destruction'\n\n/**\n * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).\n */\nexport interface Shot {\n origin: Vec3\n /** Unit vector. */\n direction: Vec3\n /** Muzzle energy in joules. */\n energy: number\n /** Bullet diameter in millimetres. */\n caliber: number\n /** snake_case calibre id, e.g. `9x19`. */\n caliberId: string\n /** Muzzle speed in m/s. */\n speed: number\n /** Energy at a distance in metres, with the calibre's drag. */\n energyAt(distance: number): number\n pellet: number\n /** snake_case weapon id. */\n weapon: string\n source?: string\n}\n\n/** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */\nexport type Use =\n | { kind: 'melee'; damage: 'blunt' | 'slash'; energy: number; radius: number; reach: number; origin: Vec3; direction: Vec3 }\n | { kind: 'flame'; temperature: number; radius: number; range: number; cone: boolean; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'heat'; temperature: number; radius: number; range: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'throw'; energy: number; radius: number; fuse: number; velocity: Vec3; origin: Vec3; fire?: { temperature: number; radius: number; seconds: number } }\n | { kind: 'tool'; tool: string; reach: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'extinguish'; range: number; coolC: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'hand'; reach: number; origin: Vec3; direction: Vec3 }\n\n/**\n * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin\n * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not\n * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.\n */\nexport const shotToHit = (shot: Shot, point: Vec3, normal?: Vec3): Hit => {\n void normal\n const dx = point[0] - shot.origin[0]\n const dy = point[1] - shot.origin[1]\n const dz = point[2] - shot.origin[2]\n const distance = Math.hypot(dx, dy, dz)\n return {\n type: 'ballistic',\n point: [point[0], point[1], point[2]],\n direction: [shot.direction[0], shot.direction[1], shot.direction[2]],\n energy: shot.energyAt(distance),\n caliber: shot.caliber,\n source: shot.source ?? shot.weapon,\n }\n}\n","import type { Vec3 } from './destruction'\n\n/** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */\nexport interface AnimationPose {\n position?: Vec3\n rotation?: Vec3\n}\n\nexport interface AnimationKey {\n /** Seconds from the start of the clip. */\n t: number\n /** Pose of the item's root. */\n position?: Vec3\n rotation?: Vec3\n /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */\n parts?: Record<string, AnimationPose>\n}\n\nexport interface AnimationEventSpec {\n /** Seconds from the start of the clip. */\n t: number\n /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */\n type: string\n data?: Record<string, unknown>\n}\n\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/impact.ts","../src/spec.ts","../src/weapons.ts","../src/animation.ts"],"sourcesContent":["import type { DamageType, DestructibleSpec, DestructionEvent, Hit, JointSpec, ObjectState, ToolKind, Vec3 } from './destruction'\n\n/**\n * What a game module implements for an object whose behaviour it owns (a vehicle with its own panel deformation, a creature, a machine). The\n * world (`DestructionWorld.register`) hosts it next to its own objects: raycast, shoot, explode, hit, disassemble, extinguish, update, state,\n * reset and `on` all route to it.\n *\n * Space: the impl receives hits in WORLD space and returns events in WORLD space (it owns its transform; the transform given to `register` or\n * `setTransform` is only used by the world for raycasts and blast range). Every event must carry `object === id` and name parts listed by\n * `describe()`.\n */\nexport interface DestructibleImpl {\n readonly id: string\n readonly name?: string\n /** Parts, joints and structure in the impl's local space, for raycast, inspector and log. Called often: keep it cheap. */\n describe(): DestructibleSpec\n /** Apply one hit (see `IMPACT_CONTRACT` for the fields per type) and return the events it caused. */\n hit(hit: Hit): DestructionEvent[]\n /** Advance fire, melting, collapse and other timed behaviour. */\n update?(dt: number): DestructionEvent[]\n state(): ObjectState\n reset?(): void\n /** Same contract as `DestructionWorld.disassemble`: progress 0..1, or -1 when the tool cannot undo the joint. */\n disassemble?(jointId: string, tool: ToolKind, dt: number): number\n extinguish?(partId?: string): void\n /** Optional: an animation moved a part; `position` / `rotation` (Euler XYZ, radians) are object-local. Keep raycast and hit shapes in sync. */\n setPartPose?(partId: string, position: Vec3, rotation?: Vec3): void\n /** Plain JSON of the impl's own state, stored in `DestructionSnapshot.impls`. */\n serialize?(): unknown\n load?(data: unknown): void\n}\n\n/** The fields a weapon or hazard must put in a `Hit`, per damage type. THE impact contract. */\nexport interface ImpactContract {\n required: string[]\n optional: string[]\n note: string\n}\n\nexport const IMPACT_CONTRACT: Record<DamageType, ImpactContract> = {\n ballistic: { required: ['type', 'point', 'direction', 'energy', 'caliber'], optional: ['space', 'source'], note: 'bullets and pellets: joules and diameter in millimetres' },\n blunt: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'contact radius in metres (default 3 cm)' },\n slash: { required: ['type', 'point', 'energy'], optional: ['direction', 'radius', 'space', 'source'], note: 'axes, knives, machetes' },\n explosive: { required: ['type', 'point', 'energy', 'radius'], optional: ['space', 'source'], note: 'point is the centre of the blast, radius the range in metres' },\n fire: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'flame temperature in °C; duration in seconds (call every frame with `dt`)' },\n heat: { required: ['type', 'point', 'temperature', 'radius'], optional: ['duration', 'space', 'source'], note: 'heat without flame (cutting torch, hot plate)' },\n}\n\n/** Problems with a hit according to the impact contract (empty when it is valid). */\nexport const checkHit = (hit: Hit): string[] => {\n const c = IMPACT_CONTRACT[hit.type]\n if (!c) return [`unknown damage type \"${String(hit.type)}\"`]\n const out: string[] = []\n for (const f of c.required) {\n const v = (hit as unknown as Record<string, unknown>)[f]\n if (v === undefined || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === 'number' && !Number.isFinite(v))) out.push(`${hit.type} hit needs \"${f}\"`)\n }\n return out\n}\n\nexport interface CheckOptions {\n /**\n * Ids of every material the impl may use (the contracts package does not know the materials of your world: pass the ids of\n * `world.materials`, or the built-in plus your own). When omitted, the material check is skipped.\n */\n materials?: Iterable<string>\n}\n\nconst finiteJson = (v: unknown, path: string, out: string[]) => {\n if (typeof v === 'number') {\n if (!Number.isFinite(v)) out.push(`${path} is not a finite number`)\n } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out))\n else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out)\n else if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') out.push(`${path} is not JSON`)\n}\n\n/**\n * Contract checker for the test room (it needs no world): `describe()` is valid, a 500 J ballistic hit at a part centre returns well-formed events of this impl, `state()`\n * matches `describe()`, hit points never go negative, `update(0.016)` returns an array and every event is JSON-serializable. It hits the impl\n * (and calls `reset()` afterwards when there is one).\n */\nexport const checkDestructible = (impl: DestructibleImpl, options: CheckOptions = {}): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const known = options.materials ? new Set(options.materials) : null\n let spec: DestructibleSpec | undefined\n try {\n spec = impl.describe()\n } catch (e) {\n return { ok: false, problems: [`describe() threw: ${String(e)}`] }\n }\n if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ['describe() must return at least one part'] }\n const ids = new Set<string>()\n for (const p of spec.parts) {\n if (ids.has(p.id)) problems.push(`duplicate part id \"${p.id}\"`)\n ids.add(p.id)\n if (known && !known.has(p.material)) problems.push(`part \"${p.id}\" has unknown material \"${p.material}\"`)\n if (!p.shape || !['box', 'cylinder', 'sphere'].includes(p.shape.kind)) problems.push(`part \"${p.id}\" has no valid shape`)\n if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part \"${p.id}\" has no position`)\n }\n const jointIds = new Set<string>()\n for (const j of (spec.joints ?? []) as JointSpec[]) {\n if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint \"${j.id ?? `${j.a}~${j.b}`}\" references an unknown part`)\n const jid = j.id ?? `${j.a}~${j.b}`\n if (jointIds.has(jid)) problems.push(`duplicate joint id \"${jid}\"`)\n jointIds.add(jid)\n }\n const checkState = (label: string) => {\n let st: ObjectState\n try {\n st = impl.state()\n } catch (e) {\n problems.push(`state() threw ${label}: ${String(e)}`)\n return\n }\n const sids = st.parts.map((p) => p.id).sort().join(',')\n if (sids !== [...ids].sort().join(',')) problems.push(`state() part ids do not match describe() ${label}`)\n for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part \"${p.id}\" has negative or invalid hp ${label}`)\n }\n const checkEvents = (events: unknown, label: string) => {\n if (!Array.isArray(events)) {\n problems.push(`${label} must return an array`)\n return\n }\n const evs = events as DestructionEvent[]\n evs.forEach((e, i) => {\n if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object \"${String(e.object)}\", expected \"${impl.id}\"`)\n const part = (e as { part?: string }).part\n if (part !== undefined && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part \"${part}\"`)\n })\n const bad: string[] = []\n finiteJson(evs, 'events', bad)\n problems.push(...bad.map((b) => `${label}: ${b}`))\n try {\n const s = JSON.stringify(evs)\n if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`)\n } catch (e) {\n problems.push(`${label} events are not JSON-serializable: ${String(e)}`)\n }\n }\n checkState('before the hit')\n const target = spec.parts[0]\n try {\n const hit: Hit = { type: 'ballistic', point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 }\n checkEvents(impl.hit(hit), 'hit()')\n } catch (e) {\n problems.push(`hit() threw: ${String(e)}`)\n }\n checkState('after the hit')\n if (impl.update) {\n try {\n checkEvents(impl.update(0.016), 'update()')\n } catch (e) {\n problems.push(`update() threw: ${String(e)}`)\n }\n checkState('after update')\n }\n try {\n impl.reset?.()\n } catch (e) {\n problems.push(`reset() threw: ${String(e)}`)\n }\n return { ok: problems.length === 0, problems }\n}\n","import type { DestructibleSpec } from './destruction'\n\nexport interface CheckSpecOptions {\n /** Known material ids. When given, every part's material must be one of them. */\n materials?: Iterable<string>\n}\n\nexport interface SpecReport {\n /** False when there is at least one problem (warnings do not count). */\n ok: boolean\n problems: string[]\n /** Things that are legal but probably wrong, e.g. structural parts that cannot reach an anchored part. */\n warnings: string[]\n}\n\nconst STRUCTURE_ROLES = ['foundation', 'column', 'beam', 'slab', 'wall', 'partition', 'roof', 'stair', 'none']\nconst BEARS = [true, false, 'a_on_b', 'b_on_a']\n\nconst finite = (n: unknown): n is number => typeof n === 'number' && Number.isFinite(n)\nconst positive = (n: unknown): boolean => finite(n) && n > 0\nconst vec3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every(finite)\nconst nonNegative = (n: unknown): boolean => n === undefined || (finite(n) && n >= 0)\n\n/**\n * Static checks on a `DestructibleSpec` (no world needed), for items that give a `spec` instead of an `impl`: unique part ids, known materials\n * (when `options.materials` is given), joints that reference existing, different parts, finite positive sizes, non-negative mass / hp / capacity,\n * valid structure roles and `bears` directions. A structural part with no path of joints to an anchored part is a warning, not a problem: it\n * is allowed, it just falls.\n */\nexport const checkSpec = (spec: DestructibleSpec, options: CheckSpecOptions = {}): SpecReport => {\n const problems: string[] = []\n const warnings: string[] = []\n const materials = options.materials ? new Set(options.materials) : null\n const parts = Array.isArray(spec?.parts) ? spec.parts : []\n if (!Array.isArray(spec?.parts) || parts.length === 0) problems.push('the spec needs at least one part')\n const ids = new Set<string>()\n for (const p of parts) {\n const id = String(p?.id ?? '')\n const label = `part \"${id}\"`\n if (id === '') problems.push('a part has no id')\n else if (ids.has(id)) problems.push(`duplicate part id \"${id}\"`)\n ids.add(id)\n if (materials && !materials.has(p?.material)) problems.push(`${label} has unknown material \"${String(p?.material)}\"`)\n else if (typeof p?.material !== 'string' || p.material === '') problems.push(`${label} needs a material`)\n const s = p?.shape\n if (s?.kind === 'box') {\n if (!Array.isArray(s.size) || s.size.length !== 3 || !s.size.every(positive)) problems.push(`${label} box size must be three finite positive numbers`)\n } else if (s?.kind === 'cylinder') {\n if (!positive(s.radius) || !positive(s.height)) problems.push(`${label} cylinder radius and height must be finite and positive`)\n } else if (s?.kind === 'sphere') {\n if (!positive(s.radius)) problems.push(`${label} sphere radius must be finite and positive`)\n } else problems.push(`${label} has an unknown shape`)\n if (!vec3(p?.position)) problems.push(`${label} needs a finite position [x, y, z]`)\n if (p?.rotation !== undefined && !vec3(p.rotation)) problems.push(`${label} has an invalid rotation`)\n for (const f of ['mass', 'hp', 'thickness'] as const) if (!nonNegative(p?.[f])) problems.push(`${label} ${f} must be a finite number >= 0`)\n if (p?.fill !== undefined && !(finite(p.fill) && p.fill >= 0 && p.fill <= 1)) problems.push(`${label} fill must be between 0 and 1`)\n if (p?.structure) {\n if (!STRUCTURE_ROLES.includes(p.structure.role)) problems.push(`${label} has an invalid structure role \"${String(p.structure.role)}\"`)\n if (!nonNegative(p.structure.capacity)) problems.push(`${label} structure capacity must be a finite number >= 0`)\n }\n if (p?.contains && !nonNegative(p.contains.liters)) problems.push(`${label} contains.liters must be a finite number >= 0`)\n }\n\n const jointIds = new Set<string>()\n const edges: Array<[string, string]> = []\n for (const j of spec?.joints ?? []) {\n const id = j.id ?? `${j.a}~${j.b}`\n const label = `joint \"${id}\"`\n if (jointIds.has(id)) problems.push(`duplicate joint id \"${id}\"`)\n jointIds.add(id)\n if (!ids.has(j.a)) problems.push(`${label} references unknown part \"${String(j.a)}\"`)\n if (!ids.has(j.b)) problems.push(`${label} references unknown part \"${String(j.b)}\"`)\n if (j.a === j.b) problems.push(`${label} joins part \"${String(j.a)}\" to itself`)\n if (j.bears !== undefined && !BEARS.includes(j.bears)) problems.push(`${label} has an invalid bears \"${String(j.bears)}\"`)\n if (!nonNegative(j.strength)) problems.push(`${label} strength must be a finite number >= 0`)\n if (!nonNegative(j.seconds)) problems.push(`${label} seconds must be a finite number >= 0`)\n if (j.bears !== false && ids.has(j.a) && ids.has(j.b) && j.a !== j.b) edges.push([j.a, j.b])\n }\n\n // structural parts must reach an anchored part over the joints (only when the spec lists its joints: otherwise they are inferred later)\n const structural = parts.filter((p) => p?.structure && p.structure.role !== 'none')\n if (structural.length && spec.joints) {\n const anchored = parts.filter((p) => p.anchored || p.structure?.role === 'foundation').map((p) => p.id)\n const adj = new Map<string, string[]>()\n for (const [a, b] of edges) {\n adj.set(a, [...(adj.get(a) ?? []), b])\n adj.set(b, [...(adj.get(b) ?? []), a])\n }\n const seen = new Set(anchored)\n const queue = [...anchored]\n while (queue.length) {\n for (const n of adj.get(queue.pop()!) ?? []) {\n if (seen.has(n)) continue\n seen.add(n)\n queue.push(n)\n }\n }\n const lost = structural.filter((p) => !seen.has(p.id)).map((p) => p.id)\n if (lost.length) warnings.push(`structural part(s) ${lost.map((i) => `\"${i}\"`).join(', ')} cannot reach an anchored part, so they will fall`)\n }\n return { ok: problems.length === 0, problems, warnings }\n}\n","import type { Hit, Vec3 } from './destruction'\n\n/**\n * A ballistic shot (one per pellet) as a weapon module produces it. Plain data: any damage model can consume it (`shotToHit` makes the `Hit`).\n */\nexport interface Shot {\n origin: Vec3\n /** Unit vector. */\n direction: Vec3\n /** Muzzle energy in joules. */\n energy: number\n /** Bullet diameter in millimetres. */\n caliber: number\n /** snake_case calibre id, e.g. `9x19`. */\n caliberId: string\n /** Muzzle speed in m/s. */\n speed: number\n /** Energy at a distance in metres, with the calibre's drag. */\n energyAt(distance: number): number\n pellet: number\n /** snake_case weapon id. */\n weapon: string\n source?: string\n}\n\n/** One use of a non-firearm (melee swing, flame tick, heat tick, throw, tool tick, extinguisher tick). */\nexport type Use =\n | { kind: 'melee'; damage: 'blunt' | 'slash'; energy: number; radius: number; reach: number; origin: Vec3; direction: Vec3 }\n | { kind: 'flame'; temperature: number; radius: number; range: number; cone: boolean; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'heat'; temperature: number; radius: number; range: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'throw'; energy: number; radius: number; fuse: number; velocity: Vec3; origin: Vec3; fire?: { temperature: number; radius: number; seconds: number } }\n | { kind: 'tool'; tool: string; reach: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'extinguish'; range: number; coolC: number; duration: number; origin: Vec3; direction: Vec3 }\n | { kind: 'hand'; reach: number; origin: Vec3; direction: Vec3 }\n\n/**\n * The `Hit` a ballistic shot makes at `point` (world space): the energy is what is left of the shot at that distance from its origin\n * (`shot.energyAt`), the direction and calibre come from the shot. `normal` (the surface normal at the point, when the caller knows it) does not\n * change the hit; it is accepted so that callers can pass what their raycast returned and so that a stricter implementation could use it.\n */\nexport const shotToHit = (shot: Shot, point: Vec3, normal?: Vec3): Hit => {\n void normal\n const dx = point[0] - shot.origin[0]\n const dy = point[1] - shot.origin[1]\n const dz = point[2] - shot.origin[2]\n const distance = Math.hypot(dx, dy, dz)\n return {\n type: 'ballistic',\n point: [point[0], point[1], point[2]],\n direction: [shot.direction[0], shot.direction[1], shot.direction[2]],\n energy: shot.energyAt(distance),\n caliber: shot.caliber,\n source: shot.source ?? shot.weapon,\n }\n}\n","import type { Vec3 } from './destruction'\n\n/** Pose of one part (or of the whole item) at a key: position offset in metres and Euler XYZ rotation in radians, both relative to the rest pose. */\nexport interface AnimationPose {\n position?: Vec3\n rotation?: Vec3\n}\n\nexport interface AnimationKey {\n /** Seconds from the start of the clip. */\n t: number\n /** Pose of the item's root. */\n position?: Vec3\n rotation?: Vec3\n /** Poses of named parts: the key is a destruction part id (a pistol `reload` moves `mag`; a car door `open` rotates `door_l`). */\n parts?: Record<string, AnimationPose>\n}\n\nexport interface AnimationEventSpec {\n /** Seconds from the start of the clip. */\n t: number\n /** snake_case type the host maps (`sound`, `eject_casing`, `door_latch`...). */\n type: string\n data?: Record<string, unknown>\n}\n\n/** A use animation a module brings with its item. Poses between keys are interpolated by the player (the test room does it linearly). */\nexport interface AnimationClipSpec {\n /** snake_case: `reload`, `open`, `drink`... Unique within the item. */\n id: string\n /** Seconds. */\n duration: number\n keys: AnimationKey[]\n events?: AnimationEventSpec[]\n}\n\n/** What an item exposes when it brings its own animations. */\nexport interface ItemAnimationContract {\n animations: AnimationClipSpec[]\n}\n\nconst finite3 = (v: unknown): boolean => Array.isArray(v) && v.length === 3 && v.every((n) => typeof n === 'number' && Number.isFinite(n))\n\n/**\n * Problems with a clip (empty when it is valid): a snake_case id, a positive finite duration, at least one key, key and event times inside\n * `[0, duration]` and not going backwards, finite vectors, every named part one of `partIds`, event types that are non-empty strings and\n * JSON-serializable event data.\n */\nexport const checkAnimation = (clip: AnimationClipSpec, partIds: Iterable<string>): { ok: boolean; problems: string[] } => {\n const problems: string[] = []\n const parts = new Set(partIds)\n const id = String(clip?.id ?? '')\n if (!/^[a-z][a-z0-9_]*$/.test(id)) problems.push(`clip id \"${id}\" must be snake_case`)\n const label = `clip \"${id}\"`\n if (!(typeof clip?.duration === 'number' && Number.isFinite(clip.duration) && clip.duration > 0)) problems.push(`${label} needs a positive finite duration`)\n const dur = typeof clip?.duration === 'number' && clip.duration > 0 ? clip.duration : Infinity\n if (!Array.isArray(clip?.keys) || clip.keys.length === 0) {\n problems.push(`${label} needs at least one key`)\n return { ok: false, problems }\n }\n let prev = -Infinity\n clip.keys.forEach((k, i) => {\n if (!(typeof k.t === 'number' && Number.isFinite(k.t)) || k.t < 0 || k.t > dur + 1e-9) problems.push(`${label} key ${i} has t=${String(k.t)} outside 0..${dur}`)\n else if (k.t < prev) problems.push(`${label} key ${i} goes back in time (${k.t} < ${prev})`)\n else prev = k.t\n for (const f of ['position', 'rotation'] as const) if (k[f] !== undefined && !finite3(k[f])) problems.push(`${label} key ${i} has an invalid ${f}`)\n for (const [pid, pose] of Object.entries(k.parts ?? {})) {\n if (!parts.has(pid)) problems.push(`${label} key ${i} moves unknown part \"${pid}\"`)\n for (const f of ['position', 'rotation'] as const) if (pose?.[f] !== undefined && !finite3(pose[f])) problems.push(`${label} key ${i} part \"${pid}\" has an invalid ${f}`)\n }\n })\n let prevEv = -Infinity\n ;(clip.events ?? []).forEach((e, i) => {\n if (!(typeof e.t === 'number' && Number.isFinite(e.t)) || e.t < 0 || e.t > dur + 1e-9) problems.push(`${label} event ${i} has t=${String(e.t)} outside 0..${dur}`)\n else if (e.t < prevEv) problems.push(`${label} event ${i} goes back in time`)\n else prevEv = e.t\n if (typeof e.type !== 'string' || e.type === '') problems.push(`${label} event ${i} needs a type`)\n try {\n JSON.stringify(e.data ?? {})\n } catch {\n problems.push(`${label} event ${i} data is not JSON`)\n }\n })\n return { ok: problems.length === 0, problems }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/game-contracts",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "description": "The contracts every game module depends on, and nothing else: destruction (parts, hits, events, DestructibleImpl), weapons (shots), fluids (containers, item uses), item animations and the TestableModule a module hands the test room. Types plus tiny checkers, no runtime dependencies.",
6
6
  "keywords": [