@its-not-rocket-science/ananke 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -10,6 +10,33 @@ Versioning follows [Semantic Versioning](https://semver.org/).
10
10
 
11
11
  ---
12
12
 
13
+ ## [0.1.3] — 2026-03-20
14
+
15
+ ### Changed
16
+
17
+ - `src/index.ts` (CE-4) now exports only the Tier 1 stable surface defined in `STABLE_API.md`.
18
+ Tier 2 (experimental) and Tier 3 (internal) exports have been removed from the root barrel
19
+ and are accessible via direct module paths (e.g. `dist/src/sim/aging.js`).
20
+ - `createWorld`, `loadScenario`, `validateScenario`, `ARCHETYPE_MAP`, `ITEM_MAP` promoted to
21
+ Tier 1 (were incorrectly placed under Tier 3 in 0.1.2).
22
+ - `describeCharacter`, `formatCharacterSheet`, `formatOneLine` added to root barrel (were
23
+ listed as Tier 1 in `STABLE_API.md` but missing from the 0.1.2 export).
24
+
25
+ ---
26
+
27
+ ## [0.1.2] — 2026-03-19
28
+
29
+ ### Added
30
+
31
+ - `createWorld(seed, entities)` — Tier-1 convenience factory; builds a `WorldState` from
32
+ `EntitySpec[]` (archetype, weapon, armour string IDs) without manual entity construction
33
+ - `loadScenario(json)` / `validateScenario(json)` — JSON-driven world creation for
34
+ non-TypeScript consumers (Godot GDScript, Unity C#, scenario files)
35
+ - `ARCHETYPE_MAP` — `ReadonlyMap` of all 21 built-in archetypes (7 base + 14 species)
36
+ - `ITEM_MAP` — `ReadonlyMap` of all historical and starter weapons/armour
37
+
38
+ ---
39
+
13
40
  ## [0.1.1] — 2026-03-19
14
41
 
15
42
  ### Documentation
@@ -3,6 +3,7 @@ export * from "./types.js";
3
3
  export * from "./archetypes.js";
4
4
  export * from "./generate.js";
5
5
  export * from "./equipment.js";
6
+ export * from "./describe.js";
6
7
  export * from "./sim/vec3.js";
7
8
  export * from "./sim/condition.js";
8
9
  export * from "./sim/injury.js";
@@ -14,29 +15,5 @@ export * from "./sim/world.js";
14
15
  export * from "./model3d.js";
15
16
  export * from "./replay.js";
16
17
  export * from "./bridge/index.js";
17
- export * from "./channels.js";
18
- export * from "./traits.js";
19
- export * from "./derive.js";
20
- export * from "./sim/intent.js";
21
- export * from "./sim/action.js";
22
- export * from "./sim/combat.js";
23
- export * from "./quest.js";
24
- export * from "./quest-generators.js";
25
- export * from "./relationships.js";
26
- export * from "./relationships-effects.js";
27
- export * from "./inventory.js";
28
- export * from "./item-durability.js";
29
- export * from "./settlement.js";
30
- export * from "./settlement-services.js";
31
- export * from "./chronicle.js";
32
- export * from "./story-arcs.js";
33
- export * from "./narrative-render.js";
34
- export * from "./world-generation.js";
35
- export * from "./sim/trace.js";
36
- export * from "./rng.js";
37
- export * from "./dist.js";
38
- export * from "./lod.js";
39
- export * from "./sim/impairment.js";
40
- export * from "./sim/indexing.js";
41
- export * from "./sim/tuning.js";
42
- export * from "./sim/testing.js";
18
+ export * from "./world-factory.js";
19
+ export * from "./scenario.js";
package/dist/src/index.js CHANGED
@@ -1,54 +1,28 @@
1
1
  // ── Tier 1 — Stable host API ─────────────────────────────────────────────────
2
- // These exports form the public integration surface. Breaking changes require
3
- // a major semver bump (x.0.0) and a migration guide in CHANGELOG.md.
4
- // Safe to import directly in host applications and typed as stable in STABLE_API.md.
5
- export * from "./units.js"; // q(), SCALE, qMul, mulDiv fixed-point arithmetic
2
+ // This is the only import path companion projects and hosts should use:
3
+ // import { stepWorld, createWorld, q, SCALE } from "@its-not-rocket-science/ananke"
4
+ //
5
+ // Breaking changes to any export here require a major semver bump (x.0.0) and
6
+ // a migration guide in CHANGELOG.md. See STABLE_API.md for the full contract.
7
+ //
8
+ // Tier 2 (experimental) and Tier 3 (internal) exports are accessible via direct
9
+ // module imports, e.g. import { stepAging } from ".../dist/src/sim/aging.js"
10
+ export * from "./units.js"; // q(), SCALE, qMul, qDiv, clampQ, mulDiv, to, from, sqrtQ
6
11
  export * from "./types.js"; // IndividualAttributes, core scalar types
7
12
  export * from "./archetypes.js"; // Archetype, BodyPlan, built-in species presets
8
13
  export * from "./generate.js"; // generateIndividual()
9
14
  export * from "./equipment.js"; // WEAPONS database, EquipmentCatalogue
10
- export * from "./sim/vec3.js"; // Vec3, lerpVec3, addVec3 — 3-D vector helpers
15
+ export * from "./describe.js"; // describeCharacter(), formatCharacterSheet(), formatOneLine()
16
+ export * from "./sim/vec3.js"; // Vec3, lerpVec3, addVec3
11
17
  export * from "./sim/condition.js"; // ConditionSnapshot, condition constants
12
18
  export * from "./sim/injury.js"; // InjuryRegion, BodyRegion, injury constants
13
19
  export * from "./sim/entity.js"; // Entity (stable fields: id, pos, mass_kg, attributes…)
14
20
  export * from "./sim/commands.js"; // CommandMap, EntityCommand, action verbs
15
21
  export * from "./sim/kernel.js"; // stepWorld(), applyImpactToInjury(), applyExplosion()
16
22
  export * from "./sim/body.js"; // BodyPlan, BodySegment, humanoid / quadruped plans
17
- export * from "./sim/world.js"; // WorldState, KernelContext, createWorld()
18
- export * from "./model3d.js"; // extractRigSnapshots(), deriveAnimationHints(), RigSnapshot, AnimationHints, GrapplePoseConstraint
23
+ export * from "./sim/world.js"; // WorldState, KernelContext
24
+ export * from "./model3d.js"; // extractRigSnapshots(), deriveAnimationHints(), RigSnapshot, AnimationHints
19
25
  export * from "./replay.js"; // ReplayRecorder, replayTo(), serializeReplay(), deserializeReplay()
20
- export * from "./bridge/index.js"; // BridgeEngine, InterpolatedState, BridgeConfig — 3D renderer bridge
21
- // ── Tier 2 Advanced / experimental API ─────────────────────────────────────
22
- // Tested and usable subsystems under active development. May change between
23
- // minor versions (0.x.0); CHANGELOG.md will document any breaking change.
24
- // Reference STABLE_API.md §Tier 2 for the full export list per module.
25
- export * from "./channels.js"; // damage channel constants (BLUNT, SLASH, …)
26
- export * from "./traits.js"; // trait descriptors
27
- export * from "./derive.js"; // derived attribute helpers
28
- export * from "./sim/intent.js"; // IntentMap, buildIntent()
29
- export * from "./sim/action.js"; // ActionResult, resolveAction()
30
- export * from "./sim/combat.js"; // resolveHit(), resolveParry(), applyCombat()
31
- export * from "./quest.js"; // Quest, QuestObjective, questStep()
32
- export * from "./quest-generators.js"; // generateQuest(), generateQuestChain()
33
- export * from "./relationships.js"; // RelationshipMap, updateRelationship()
34
- export * from "./relationships-effects.js"; // applyRelationshipEffect()
35
- export * from "./inventory.js"; // Inventory, equipItem(), addItemToInventory()
36
- export * from "./item-durability.js"; // durability helpers, resolveRepair()
37
- export * from "./settlement.js"; // Settlement, stepSettlement()
38
- export * from "./settlement-services.js"; // service resolution helpers
39
- export * from "./chronicle.js"; // ChronicleEntry, addChronicleEntry()
40
- export * from "./story-arcs.js"; // StoryArc, detectArcs()
41
- export * from "./narrative-render.js"; // renderEntry(), renderChronicle(), generateNarrative()
42
- export * from "./world-generation.js"; // WorldTemplate, generateWorld()
43
- export * from "./sim/trace.js"; // SimTrace, traceStep() — debugging / profiling
44
- // ── Tier 3 — Internal / kernel API ───────────────────────────────────────────
45
- // Exported for power users and diagnostic tooling. Not stability-guaranteed;
46
- // may change at any time without a changelog entry. Prefer Tier 1/2 surfaces
47
- // in production host code. See STABLE_API.md §Tier 3 for rationale.
48
- export * from "./rng.js"; // makeRng(), eventSeed() — RNG internals
49
- export * from "./dist.js"; // distribution primitives
50
- export * from "./lod.js"; // level-of-detail helpers
51
- export * from "./sim/impairment.js"; // low-level impairment accumulators
52
- export * from "./sim/indexing.js"; // SpatialIndex internals
53
- export * from "./sim/tuning.js"; // kernel tuning constants (may be adjusted)
54
- export * from "./sim/testing.js"; // mkHumanoidEntity() and other test helpers
26
+ export * from "./bridge/index.js"; // BridgeEngine, InterpolatedState, BridgeConfig
27
+ export * from "./world-factory.js"; // createWorld(), EntitySpec, ARCHETYPE_MAP, ITEM_MAP
28
+ export * from "./scenario.js"; // loadScenario(), validateScenario(), AnankeScenario
@@ -0,0 +1,37 @@
1
+ /**
2
+ * CE-3: JSON scenario loader.
3
+ *
4
+ * Provides typed AnankeScenario interface, structural validation, and
5
+ * a loadScenario() function that converts validated JSON into a WorldState.
6
+ */
7
+ import type { WorldState } from "./sim/world.js";
8
+ export interface AnankeScenarioEntity {
9
+ id: number;
10
+ teamId: number;
11
+ archetype: string;
12
+ weapon: string;
13
+ armour?: string;
14
+ x_m?: number;
15
+ y_m?: number;
16
+ }
17
+ export interface AnankeScenario {
18
+ $schema?: string;
19
+ id: string;
20
+ seed: number;
21
+ maxTicks: number;
22
+ tractionCoeff?: number;
23
+ entities: AnankeScenarioEntity[];
24
+ }
25
+ /**
26
+ * Validate structural correctness of a JSON scenario object.
27
+ * Returns an array of error strings — empty array means valid.
28
+ * Does NOT perform simulation-level lookups (e.g. archetype/weapon existence).
29
+ */
30
+ export declare function validateScenario(json: unknown): string[];
31
+ /**
32
+ * Parse and load a scenario from JSON, returning a WorldState ready for stepWorld().
33
+ *
34
+ * Calls validateScenario first — throws an Error with all validation messages if invalid.
35
+ * Maps AnankeScenarioEntity.id as the entity seed.
36
+ */
37
+ export declare function loadScenario(json: unknown): WorldState;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * CE-3: JSON scenario loader.
3
+ *
4
+ * Provides typed AnankeScenario interface, structural validation, and
5
+ * a loadScenario() function that converts validated JSON into a WorldState.
6
+ */
7
+ import { createWorld } from "./world-factory.js";
8
+ // ── Validation ────────────────────────────────────────────────────────────────
9
+ /**
10
+ * Validate structural correctness of a JSON scenario object.
11
+ * Returns an array of error strings — empty array means valid.
12
+ * Does NOT perform simulation-level lookups (e.g. archetype/weapon existence).
13
+ */
14
+ export function validateScenario(json) {
15
+ const errors = [];
16
+ // Must be a plain object
17
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
18
+ errors.push("scenario must be a plain object");
19
+ return errors; // can't continue checking fields
20
+ }
21
+ const obj = json;
22
+ // id — non-empty string
23
+ if (typeof obj["id"] !== "string" || obj["id"].length === 0) {
24
+ errors.push("scenario.id must be a non-empty string");
25
+ }
26
+ // seed — positive integer
27
+ if (typeof obj["seed"] !== "number" ||
28
+ !Number.isInteger(obj["seed"]) ||
29
+ obj["seed"] <= 0) {
30
+ errors.push("scenario.seed must be a positive integer");
31
+ }
32
+ // maxTicks — positive integer
33
+ if (typeof obj["maxTicks"] !== "number" ||
34
+ !Number.isInteger(obj["maxTicks"]) ||
35
+ obj["maxTicks"] <= 0) {
36
+ errors.push("scenario.maxTicks must be a positive integer");
37
+ }
38
+ // entities — non-empty array
39
+ if (!Array.isArray(obj["entities"])) {
40
+ errors.push("scenario.entities must be an array");
41
+ return errors; // can't check entity elements
42
+ }
43
+ const rawEntities = obj["entities"];
44
+ if (rawEntities.length === 0) {
45
+ errors.push("scenario.entities must not be empty");
46
+ return errors;
47
+ }
48
+ // Validate each entity element
49
+ const seenIds = new Set();
50
+ for (let i = 0; i < rawEntities.length; i++) {
51
+ const ent = rawEntities[i];
52
+ if (ent === null || typeof ent !== "object" || Array.isArray(ent)) {
53
+ errors.push(`scenario.entities[${i}] must be a plain object`);
54
+ continue;
55
+ }
56
+ const e = ent;
57
+ if (typeof e["id"] !== "number") {
58
+ errors.push(`scenario.entities[${i}].id must be a number`);
59
+ }
60
+ else {
61
+ const eid = e["id"];
62
+ if (seenIds.has(eid)) {
63
+ errors.push(`scenario.entities[${i}].id ${eid} is a duplicate`);
64
+ }
65
+ seenIds.add(eid);
66
+ }
67
+ if (typeof e["teamId"] !== "number") {
68
+ errors.push(`scenario.entities[${i}].teamId must be a number`);
69
+ }
70
+ if (typeof e["archetype"] !== "string") {
71
+ errors.push(`scenario.entities[${i}].archetype must be a string`);
72
+ }
73
+ if (typeof e["weapon"] !== "string") {
74
+ errors.push(`scenario.entities[${i}].weapon must be a string`);
75
+ }
76
+ }
77
+ return errors;
78
+ }
79
+ // ── Loader ────────────────────────────────────────────────────────────────────
80
+ /**
81
+ * Parse and load a scenario from JSON, returning a WorldState ready for stepWorld().
82
+ *
83
+ * Calls validateScenario first — throws an Error with all validation messages if invalid.
84
+ * Maps AnankeScenarioEntity.id as the entity seed.
85
+ */
86
+ export function loadScenario(json) {
87
+ const errors = validateScenario(json);
88
+ if (errors.length > 0) {
89
+ throw new Error(`loadScenario: invalid scenario:\n ${errors.join("\n ")}`);
90
+ }
91
+ const scenario = json;
92
+ const specs = scenario.entities.map(ent => {
93
+ const spec = {
94
+ id: ent.id,
95
+ teamId: ent.teamId,
96
+ seed: ent.id, // use entity id as deterministic seed
97
+ archetype: ent.archetype,
98
+ weaponId: ent.weapon,
99
+ };
100
+ if (ent.armour !== undefined)
101
+ spec.armourId = ent.armour;
102
+ if (ent.x_m !== undefined)
103
+ spec.x_m = ent.x_m;
104
+ if (ent.y_m !== undefined)
105
+ spec.y_m = ent.y_m;
106
+ return spec;
107
+ });
108
+ return createWorld(scenario.seed, specs);
109
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CE-2: createWorld convenience factory.
3
+ *
4
+ * Builds a deterministic WorldState from a simple declarative entity spec.
5
+ * No Math.random() — all randomness flows through generateIndividual(spec.seed, archetype).
6
+ * All position coordinates are fixed-point (SCALE.m multiplier + Math.round).
7
+ */
8
+ import type { Archetype } from "./archetypes.js";
9
+ import type { Item } from "./equipment.js";
10
+ import type { WorldState } from "./sim/world.js";
11
+ /** Map of string keys to Archetype objects for use with createWorld(). */
12
+ export declare const ARCHETYPE_MAP: ReadonlyMap<string, Archetype>;
13
+ /** Map of item id → Item for weapons and armour usable with createWorld(). */
14
+ export declare const ITEM_MAP: ReadonlyMap<string, Item>;
15
+ export interface EntitySpec {
16
+ id: number;
17
+ teamId: number;
18
+ seed: number;
19
+ archetype: string;
20
+ weaponId: string;
21
+ armourId?: string;
22
+ x_m?: number;
23
+ y_m?: number;
24
+ }
25
+ /**
26
+ * Build a deterministic WorldState from a declarative entity spec list.
27
+ *
28
+ * - Uses spec.seed for generateIndividual() — no Math.random().
29
+ * - Position coordinates are fixed-point: Math.round(metres * SCALE.m).
30
+ * - Throws on unknown archetype, weaponId, or armourId.
31
+ * - Throws on duplicate entity ids.
32
+ */
33
+ export declare function createWorld(seed: number, entities: EntitySpec[]): WorldState;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * CE-2: createWorld convenience factory.
3
+ *
4
+ * Builds a deterministic WorldState from a simple declarative entity spec.
5
+ * No Math.random() — all randomness flows through generateIndividual(spec.seed, archetype).
6
+ * All position coordinates are fixed-point (SCALE.m multiplier + Math.round).
7
+ */
8
+ import { q, SCALE } from "./units.js";
9
+ import { generateIndividual } from "./generate.js";
10
+ import { HUMAN_BASE, AMATEUR_BOXER, PRO_BOXER, GRECO_WRESTLER, KNIGHT_INFANTRY, LARGE_PACIFIC_OCTOPUS, SERVICE_ROBOT, } from "./archetypes.js";
11
+ import { ELF_SPECIES, DWARF_SPECIES, HALFLING_SPECIES, ORC_SPECIES, OGRE_SPECIES, GOBLIN_SPECIES, TROLL_SPECIES, VULCAN_SPECIES, KLINGON_SPECIES, ROMULAN_SPECIES, DRAGON_SPECIES, CENTAUR_SPECIES, SATYR_SPECIES, HEECHEE_SPECIES, } from "./species.js";
12
+ import { ALL_HISTORICAL_MELEE, ALL_HISTORICAL_RANGED } from "./weapons.js";
13
+ import { STARTER_WEAPONS, STARTER_ARMOUR, STARTER_ARMOUR_11C } from "./equipment.js";
14
+ import { v3 } from "./sim/vec3.js";
15
+ import { defaultIntent } from "./sim/intent.js";
16
+ import { defaultAction } from "./sim/action.js";
17
+ import { defaultCondition } from "./sim/condition.js";
18
+ import { defaultInjury } from "./sim/injury.js";
19
+ // ── Static archetype map ──────────────────────────────────────────────────────
20
+ /** Map of string keys to Archetype objects for use with createWorld(). */
21
+ export const ARCHETYPE_MAP = new Map([
22
+ // Direct archetypes from archetypes.ts
23
+ ["HUMAN_BASE", HUMAN_BASE],
24
+ ["AMATEUR_BOXER", AMATEUR_BOXER],
25
+ ["PRO_BOXER", PRO_BOXER],
26
+ ["GRECO_WRESTLER", GRECO_WRESTLER],
27
+ ["KNIGHT_INFANTRY", KNIGHT_INFANTRY],
28
+ ["LARGE_PACIFIC_OCTOPUS", LARGE_PACIFIC_OCTOPUS],
29
+ ["SERVICE_ROBOT", SERVICE_ROBOT],
30
+ // Species archetypes from species.ts
31
+ ["ELF", ELF_SPECIES.archetype],
32
+ ["DWARF", DWARF_SPECIES.archetype],
33
+ ["HALFLING", HALFLING_SPECIES.archetype],
34
+ ["ORC", ORC_SPECIES.archetype],
35
+ ["OGRE", OGRE_SPECIES.archetype],
36
+ ["GOBLIN", GOBLIN_SPECIES.archetype],
37
+ ["TROLL", TROLL_SPECIES.archetype],
38
+ ["VULCAN", VULCAN_SPECIES.archetype],
39
+ ["KLINGON", KLINGON_SPECIES.archetype],
40
+ ["ROMULAN", ROMULAN_SPECIES.archetype],
41
+ ["DRAGON", DRAGON_SPECIES.archetype],
42
+ ["CENTAUR", CENTAUR_SPECIES.archetype],
43
+ ["SATYR", SATYR_SPECIES.archetype],
44
+ ["HEECHEE", HEECHEE_SPECIES.archetype],
45
+ ]);
46
+ // ── Static item map ───────────────────────────────────────────────────────────
47
+ function buildItemMap() {
48
+ const map = new Map();
49
+ const allItems = [
50
+ ...ALL_HISTORICAL_MELEE,
51
+ ...ALL_HISTORICAL_RANGED,
52
+ ...STARTER_WEAPONS,
53
+ ...STARTER_ARMOUR,
54
+ ...STARTER_ARMOUR_11C,
55
+ ];
56
+ for (const item of allItems) {
57
+ map.set(item.id, item);
58
+ }
59
+ return map;
60
+ }
61
+ /** Map of item id → Item for weapons and armour usable with createWorld(). */
62
+ export const ITEM_MAP = buildItemMap();
63
+ // ── createWorld ───────────────────────────────────────────────────────────────
64
+ /**
65
+ * Build a deterministic WorldState from a declarative entity spec list.
66
+ *
67
+ * - Uses spec.seed for generateIndividual() — no Math.random().
68
+ * - Position coordinates are fixed-point: Math.round(metres * SCALE.m).
69
+ * - Throws on unknown archetype, weaponId, or armourId.
70
+ * - Throws on duplicate entity ids.
71
+ */
72
+ export function createWorld(seed, entities) {
73
+ const built = [];
74
+ for (const spec of entities) {
75
+ // ── Archetype lookup ──────────────────────────────────────────────────────
76
+ const archetype = ARCHETYPE_MAP.get(spec.archetype);
77
+ if (archetype === undefined) {
78
+ throw new Error(`createWorld: unknown archetype "${spec.archetype}". ` +
79
+ `Valid keys: ${[...ARCHETYPE_MAP.keys()].join(", ")}`);
80
+ }
81
+ // ── Weapon lookup ─────────────────────────────────────────────────────────
82
+ const weapon = ITEM_MAP.get(spec.weaponId);
83
+ if (weapon === undefined) {
84
+ throw new Error(`createWorld: unknown weaponId "${spec.weaponId}"`);
85
+ }
86
+ // ── Optional armour lookup ────────────────────────────────────────────────
87
+ let armour;
88
+ if (spec.armourId !== undefined) {
89
+ armour = ITEM_MAP.get(spec.armourId);
90
+ if (armour === undefined) {
91
+ throw new Error(`createWorld: unknown armourId "${spec.armourId}"`);
92
+ }
93
+ }
94
+ // ── Generate individual attributes ────────────────────────────────────────
95
+ const attrs = generateIndividual(spec.seed, archetype);
96
+ // ── Default position ──────────────────────────────────────────────────────
97
+ // Team 1 defaults to x=0; all others default to x=0.6m.
98
+ const defaultX = spec.teamId === 1 ? 0 : 0.6;
99
+ const x_fixed = Math.round((spec.x_m ?? defaultX) * SCALE.m);
100
+ const y_fixed = Math.round((spec.y_m ?? 0) * SCALE.m);
101
+ // ── Build loadout ─────────────────────────────────────────────────────────
102
+ const items = [weapon];
103
+ if (armour !== undefined)
104
+ items.push(armour);
105
+ // ── Assemble entity ───────────────────────────────────────────────────────
106
+ const entity = {
107
+ id: spec.id,
108
+ teamId: spec.teamId,
109
+ attributes: attrs,
110
+ energy: { reserveEnergy_J: attrs.performance.reserveEnergy_J, fatigue: q(0) },
111
+ loadout: { items },
112
+ traits: [],
113
+ position_m: v3(x_fixed, y_fixed, 0),
114
+ velocity_mps: v3(0, 0, 0),
115
+ intent: defaultIntent(),
116
+ action: defaultAction(),
117
+ condition: defaultCondition(),
118
+ injury: defaultInjury(),
119
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0), position: "standing" },
120
+ };
121
+ built.push(entity);
122
+ }
123
+ // ── Sort by id ────────────────────────────────────────────────────────────
124
+ built.sort((a, b) => a.id - b.id);
125
+ // ── Duplicate id check ────────────────────────────────────────────────────
126
+ const ids = built.map(e => e.id);
127
+ const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
128
+ if (dupes.length > 0) {
129
+ throw new Error(`createWorld: duplicate entity IDs detected: ${[...new Set(dupes)].join(", ")}`);
130
+ }
131
+ return { tick: 0, seed, entities: built };
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",