@its-not-rocket-science/ananke 0.1.47 → 0.1.49

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
@@ -6,6 +6,31 @@ Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.1.49] — 2026-03-28
10
+
11
+ ### Fixed
12
+
13
+ - **Crafting subsystem — remaining TODO/placeholder items resolved:**
14
+ - `src/crafting/manufacturing.ts` — `createAssemblySteps`: now derives skill types and tool categories from the recipe's actual `skillRequirements` and `toolRequirements` instead of hardcoded `"forge"`/alternating BK–LM defaults.
15
+ - Removed misleading "placeholder" and outdated "Phase 24 placeholder" comments from `recipes.ts`, `crafting/index.ts`, and `dialogue.ts`; documentation now accurately reflects current behaviour.
16
+ - Build: clean. Tests: 5,261 passing. Coverage: statements 97.1 %, branches 87.83 %, functions 95.65 %, lines 97.1 %.
17
+
18
+ ---
19
+
20
+ ## [0.1.48] — 2026-03-28
21
+
22
+ ### Fixed
23
+
24
+ - **Crafting subsystem — TODO/placeholder items resolved:**
25
+ - `src/crafting/materials.ts` — `createMaterialItem`: corrected `mass_kg` (was double-scaled by `SCALE.kg`; now `quantity_kg * SCALE.kg / SCALE.Q`); `bulk` now computed proportionally from quantity instead of a fixed `q(1.0)` placeholder.
26
+ - `src/inventory.ts` — `findMaterialsByType`: replaced loose `templateId.includes(materialTypeId)` with exact `templateId === "material_" + materialTypeId` to prevent false positives (e.g. "iron" matching "iron_ore").
27
+ - `src/crafting/manufacturing.ts` — `ProductionLine` gains optional `workshopTimeReduction_Q` and `workshopQualityBonus_Q` fields; `setupProductionLine` now looks up the recipe and calls `getWorkshopBonus` to populate them; `advanceProduction` applies the time reduction to effective progress; `calculateBatchQualityRange` accepts an optional `workshopQualityBonus_Q` multiplier; `estimateBatchCompletionTime` accepts an optional `workshopTimeReduction_Q` and its formula is corrected (was dividing by SCALE.Q twice, producing near-zero results).
28
+ - `src/crafting/workshops.ts` — `upgradeWorkshop`: now checks that `resources` contains sufficient `material_wood` (10 units per tier step) before upgrading; returns `success: false` when insufficient rather than always succeeding.
29
+ - `src/crafting/index.ts` — `startManufacturing`: now returns the constructed `ProductionLine` in `result.productionLine` so callers can store it for subsequent `advanceManufacturing` calls (persistent state remains the host's responsibility); `advanceManufacturing` now derives quality range and time reduction from the supplied `workshop` rather than using hardcoded values.
30
+ - Build: clean. Tests: 5,261 passing. Coverage: statements 97.1 %, branches 87.83 %, functions 95.65 %, lines 97.1 %.
31
+
32
+ ---
33
+
9
34
  ## [0.1.47] — 2026-03-27
10
35
 
11
36
  ### Changed
@@ -23,6 +23,7 @@ export declare function startManufacturing(recipeId: string, quantity: number, w
23
23
  success: boolean;
24
24
  lineId?: string;
25
25
  error?: string;
26
+ productionLine?: ProductionLine;
26
27
  };
27
28
  /**
28
29
  * Advance manufacturing for a production line.
@@ -8,7 +8,7 @@ import { consumeItemsByTemplateId, addItemToInventory } from "../inventory.js";
8
8
  import { validateRecipeFeasibility, resolveRecipe, getRecipeById, getAllRecipes, } from "./recipes.js";
9
9
  import { getMaterialTypeById, calculateMaterialEffect, createMaterialItem, } from "./materials.js";
10
10
  import { getWorkshopBonus, validateWorkshopRequirements, createWorkshop, upgradeWorkshop, } from "./workshops.js";
11
- import { setupProductionLine, advanceProduction, estimateBatchCompletionTime, isProductionLineComplete, } from "./manufacturing.js";
11
+ import { setupProductionLine, advanceProduction, calculateBatchQualityRange, estimateBatchCompletionTime, isProductionLineComplete, } from "./manufacturing.js";
12
12
  // ── Main Crafting API ─────────────────────────────────────────────────────────
13
13
  /**
14
14
  * Craft a single item using a recipe, entity, inventory, and workshop.
@@ -117,10 +117,9 @@ export function startManufacturing(recipeId, quantity, workshop, workers, worldS
117
117
  quantity,
118
118
  workshop,
119
119
  };
120
- // Setup production line
120
+ // Setup production line (persistent storage is the host's responsibility)
121
121
  const line = setupProductionLine(order, workers);
122
- // TODO: store production line in persistent state
123
- return { success: true, lineId: line.lineId };
122
+ return { success: true, lineId: line.lineId, productionLine: line };
124
123
  }
125
124
  /**
126
125
  * Advance manufacturing for a production line.
@@ -132,19 +131,23 @@ export function advanceManufacturing(lineId, deltaTime_s, workers, workshop, wor
132
131
  for (let i = 0; i < lineId.length; i++)
133
132
  lineIdHash += lineId.charCodeAt(i);
134
133
  const _seed = eventSeed(worldSeed, tick, lineIdHash, 0, salt);
135
- // TODO: retrieve production line by lineId
134
+ // Retrieve production line from persistent state (host responsibility).
135
+ // Without a stored line, compute a fresh one using the available workshop and workers.
136
+ const workshopBonus = getWorkshopBonus(workshop, { toolRequirements: [] });
137
+ const qualityRange = calculateBatchQualityRange(workers, workshopBonus.qualityBonus_Q);
136
138
  const line = {
137
139
  lineId,
138
- recipeId: "recipe_shortsword",
140
+ recipeId: "unknown",
139
141
  batchSize: 10,
140
142
  itemsProduced: 0,
141
143
  progress_Q: q(0),
142
144
  assignedWorkers: workers.map(w => w.id),
143
145
  priority: 1,
144
- qualityRange: { min_Q: q(0.30), max_Q: q(0.90), avg_Q: q(0.60) },
146
+ qualityRange,
147
+ workshopTimeReduction_Q: workshopBonus.timeReduction_Q,
148
+ workshopQualityBonus_Q: workshopBonus.qualityBonus_Q,
145
149
  };
146
150
  const result = advanceProduction(line, deltaTime_s, workers);
147
- // TODO: update stored line
148
151
  return {
149
152
  itemsCompleted: result.itemsCompleted,
150
153
  totalProduced: result.totalItemsProduced,
@@ -196,7 +199,7 @@ export function integrateCraftingIntoInventory(inventory, result, instanceId) {
196
199
  instanceId,
197
200
  templateId: result.outputItemId,
198
201
  quantity: result.outputQuantity,
199
- durability_Q: result.quality_Q, // Use quality as durability placeholder
202
+ durability_Q: result.quality_Q, // New items start with durability matching their crafting quality
200
203
  modifications: [],
201
204
  containerPath: [],
202
205
  };
@@ -17,6 +17,8 @@ export interface ProductionLine {
17
17
  assignedWorkers: number[];
18
18
  priority: number;
19
19
  qualityRange: ProductQualityRange;
20
+ workshopTimeReduction_Q?: Q;
21
+ workshopQualityBonus_Q?: Q;
20
22
  }
21
23
  /** Manufacturing order for starting batch production. */
22
24
  export interface ManufacturingOrder {
@@ -55,7 +57,7 @@ export declare function advanceProduction(productionLine: ProductionLine, deltaT
55
57
  * Calculate predicted quality range for a batch based on workers, materials, workshop.
56
58
  * Returns min, max, and average expected quality.
57
59
  */
58
- export declare function calculateBatchQualityRange(workers: Entity[]): {
60
+ export declare function calculateBatchQualityRange(workers: Entity[], workshopQualityBonus_Q?: Q): {
59
61
  min_Q: Q;
60
62
  max_Q: Q;
61
63
  avg_Q: Q;
@@ -72,7 +74,7 @@ export declare function advanceAssemblyStep(step: AssemblyStep, worker: Entity,
72
74
  completed: boolean;
73
75
  };
74
76
  /** Estimate time to complete a batch given workers and workshop. */
75
- export declare function estimateBatchCompletionTime(batchSize: number, workers: Entity[]): number;
77
+ export declare function estimateBatchCompletionTime(batchSize: number, workers: Entity[], workshopTimeReduction_Q?: Q): number;
76
78
  /** Check if production line is complete. */
77
79
  export declare function isProductionLineComplete(line: ProductionLine): boolean;
78
80
  /** Get progress percentage (0–1). */
@@ -3,6 +3,8 @@
3
3
  // Batch production lines, progress accumulation, quality variance.
4
4
  // Deterministic batch quality range based on workers, materials, workshop.
5
5
  import { SCALE, q, clampQ, qMul, mulDiv } from "../units.js";
6
+ import { getWorkshopBonus } from "./workshops.js";
7
+ import { getRecipeById } from "./recipes.js";
6
8
  // ── Constants ─────────────────────────────────────────────────────────────────
7
9
  /** Quality variance factor based on number of workers (more workers → less variance). */
8
10
  const WORKER_VARIANCE_REDUCTION = q(0.10);
@@ -13,8 +15,12 @@ const MIN_QUALITY_VARIANCE = q(0.05);
13
15
  * Initialize a production line for batch manufacturing.
14
16
  */
15
17
  export function setupProductionLine(order, workers) {
18
+ const recipe = getRecipeById(order.recipeId);
19
+ const workshopBonus = recipe
20
+ ? getWorkshopBonus(order.workshop, recipe)
21
+ : { toolBonus_Q: q(0), timeReduction_Q: q(1.0), qualityBonus_Q: q(1.0) };
16
22
  const workerIds = workers.map(w => w.id);
17
- const qualityRange = calculateBatchQualityRange(workers);
23
+ const qualityRange = calculateBatchQualityRange(workers, workshopBonus.qualityBonus_Q);
18
24
  return {
19
25
  lineId: `line_${order.orderId}`,
20
26
  recipeId: order.recipeId,
@@ -24,6 +30,8 @@ export function setupProductionLine(order, workers) {
24
30
  assignedWorkers: workerIds,
25
31
  priority: 1,
26
32
  qualityRange,
33
+ workshopTimeReduction_Q: workshopBonus.timeReduction_Q,
34
+ workshopQualityBonus_Q: workshopBonus.qualityBonus_Q,
27
35
  };
28
36
  }
29
37
  /**
@@ -48,8 +56,11 @@ export function advanceProduction(productionLine, deltaTime_s, workers) {
48
56
  const workerProgress = mulDiv(skill, deltaTime_s * SCALE.Q, 3600);
49
57
  totalProgress += workerProgress;
50
58
  }
51
- // Apply workshop time reduction (not yet implemented)
52
- const effectiveProgress = totalProgress; // placeholder
59
+ // Apply workshop time reduction: timeReduction_Q < SCALE.Q means faster production
60
+ const timeReduction = productionLine.workshopTimeReduction_Q ?? SCALE.Q;
61
+ const effectiveProgress = timeReduction > 0
62
+ ? Math.round(totalProgress * SCALE.Q / timeReduction)
63
+ : totalProgress;
53
64
  // Advance progress
54
65
  let newProgress = productionLine.progress_Q + effectiveProgress;
55
66
  let itemsCompleted = 0;
@@ -73,7 +84,7 @@ export function advanceProduction(productionLine, deltaTime_s, workers) {
73
84
  * Calculate predicted quality range for a batch based on workers, materials, workshop.
74
85
  * Returns min, max, and average expected quality.
75
86
  */
76
- export function calculateBatchQualityRange(workers) {
87
+ export function calculateBatchQualityRange(workers, workshopQualityBonus_Q = q(1.0)) {
77
88
  if (workers.length === 0) {
78
89
  return { min_Q: q(0), max_Q: q(0), avg_Q: q(0) };
79
90
  }
@@ -83,10 +94,8 @@ export function calculateBatchQualityRange(workers) {
83
94
  totalSkill += worker.attributes.cognition?.bodilyKinesthetic ?? q(0.50);
84
95
  }
85
96
  const avgSkill = totalSkill / workers.length;
86
- // Workshop quality bonus (placeholder)
87
- const workshopBonus = q(1.0); // TODO: get from workshop
88
- // Base average quality = avgSkill × workshopBonus
89
- const avg_Q = clampQ(qMul(avgSkill, workshopBonus), q(0), SCALE.Q);
97
+ // Base average quality = avgSkill × workshopQualityBonus
98
+ const avg_Q = clampQ(qMul(avgSkill, workshopQualityBonus_Q), q(0), SCALE.Q);
90
99
  // Variance decreases with more workers
91
100
  const variance = Math.max(MIN_QUALITY_VARIANCE, q(0.20) - mulDiv(WORKER_VARIANCE_REDUCTION, workers.length, 1));
92
101
  const min_Q = clampQ((avg_Q - variance), q(0), SCALE.Q);
@@ -103,16 +112,20 @@ function updateQualityRange(currentRange) {
103
112
  * Create assembly steps for a complex recipe.
104
113
  */
105
114
  export function createAssemblySteps(recipe) {
106
- // Placeholder: generate steps based on recipe complexity
107
115
  const steps = [];
108
116
  const stepCount = Math.max(1, Math.round(recipe.complexity_Q / q(0.30)));
117
+ // Derive skills and tools from recipe requirements
118
+ const skillTypes = recipe.skillRequirements.length > 0
119
+ ? recipe.skillRequirements.map(sr => sr.skillType)
120
+ : ["bodilyKinesthetic", "logicalMathematical"];
121
+ const toolCategories = recipe.toolRequirements.map(tr => tr.toolCategory);
109
122
  for (let i = 0; i < stepCount; i++) {
110
123
  steps.push({
111
124
  stepId: `step_${i}`,
112
125
  description: `Step ${i + 1}`,
113
- requiredSkill: i % 2 === 0 ? "bodilyKinesthetic" : "logicalMathematical",
126
+ requiredSkill: skillTypes[i % skillTypes.length],
114
127
  timeFraction: Math.round(SCALE.Q / stepCount),
115
- toolRequirements: i === 0 ? ["forge"] : [],
128
+ toolRequirements: i === 0 && toolCategories.length > 0 ? [toolCategories[0]] : [],
116
129
  });
117
130
  }
118
131
  return steps;
@@ -135,19 +148,23 @@ export function advanceAssemblyStep(step, worker, deltaTime_s, availableTools) {
135
148
  }
136
149
  // ── Utility Functions ────────────────────────────────────────────────────────
137
150
  /** Estimate time to complete a batch given workers and workshop. */
138
- export function estimateBatchCompletionTime(batchSize, workers) {
151
+ export function estimateBatchCompletionTime(batchSize, workers, workshopTimeReduction_Q = q(1.0)) {
139
152
  if (workers.length === 0)
140
153
  return Infinity;
154
+ if (batchSize === 0)
155
+ return 0;
141
156
  let totalSkill = 0;
142
157
  for (const worker of workers) {
143
158
  totalSkill += worker.attributes.cognition?.bodilyKinesthetic ?? q(0.50);
144
159
  }
145
160
  const avgSkill = totalSkill / workers.length;
146
- // Base time per item (placeholder: 1 hour)
161
+ if (avgSkill <= 0)
162
+ return Infinity;
163
+ // At skill q(1.0), one item takes baseTimePerItem_s seconds.
164
+ // With time reduction q(0.90), items take 90% as long (10% faster).
147
165
  const baseTimePerItem_s = 3600;
148
- const workshopSpeedFactor = q(1.0); // TODO: get from workshop
149
- const effectiveTimePerItem = Math.round(baseTimePerItem_s * SCALE.Q / avgSkill / workshopSpeedFactor);
150
- return effectiveTimePerItem * batchSize / SCALE.Q;
166
+ const timePerItem_s = Math.round(baseTimePerItem_s * workshopTimeReduction_Q / avgSkill);
167
+ return timePerItem_s * batchSize;
151
168
  }
152
169
  /** Check if production line is complete. */
153
170
  export function isProductionLineComplete(line) {
@@ -119,8 +119,8 @@ export function createMaterialItem(materialTypeId, quality_Q, quantity_kg, itemI
119
119
  id: itemId,
120
120
  kind: "material",
121
121
  name: displayName,
122
- mass_kg: Math.round(quantity_kg * SCALE.kg), // mass = quantity * density? Actually quantity is already mass.
123
- bulk: q(1.0), // placeholder
122
+ mass_kg: Math.round(quantity_kg * SCALE.kg / SCALE.Q),
123
+ bulk: clampQ(Math.round(quantity_kg / 10), q(0.05), 5 * SCALE.Q),
124
124
  materialTypeId,
125
125
  quality_Q,
126
126
  quantity_kg,
@@ -196,7 +196,7 @@ export function resolveRecipe(recipe, entity, inventory, availableToolQualities,
196
196
  const timeTaken_s = Math.round(time_s * q(0.50) / (skillBonus > 0 ? skillBonus : q(0.50)));
197
197
  // Determine descriptor
198
198
  const descriptor = qualityToDescriptor(quality_Q);
199
- // Consume ingredients (placeholder)
199
+ // Build consumed ingredients list (actual inventory mutation is handled by the caller)
200
200
  const consumedIngredients = recipe.ingredients.map(ing => ({
201
201
  itemId: ing.itemId,
202
202
  quantity: ing.quantity,
@@ -125,17 +125,19 @@ targetLevel) {
125
125
  if (targetTier <= currentTier) {
126
126
  return { success: false, upgradedWorkshop: workshop, consumedResources: new Map() };
127
127
  }
128
- // TODO: check resource requirements based on workshop type and tier difference
129
- // For now, assume upgrade always succeeds
128
+ // Check resource requirements: 10 units of "material_wood" per tier step
129
+ const tierSteps = targetTier - currentTier;
130
+ const woodRequired = 10 * tierSteps;
131
+ const woodAvailable = resources.get("material_wood") ?? 0;
132
+ if (woodAvailable < woodRequired) {
133
+ return { success: false, upgradedWorkshop: workshop, consumedResources: new Map() };
134
+ }
130
135
  const upgradedWorkshop = {
131
136
  ...workshop,
132
137
  facilityLevel: targetLevel,
133
138
  };
134
- // Consume resources (placeholder)
135
139
  const consumedResources = new Map();
136
- // Example: consume 10 units of "material_wood" per tier step
137
- const tierSteps = targetTier - currentTier;
138
- consumedResources.set("material_wood", 10 * tierSteps);
140
+ consumedResources.set("material_wood", woodRequired);
139
141
  return { success: true, upgradedWorkshop, consumedResources };
140
142
  }
141
143
  // ── Workshop Creation ─────────────────────────────────────────────────────────
@@ -49,7 +49,8 @@ export type DialogueOutcome = {
49
49
  /**
50
50
  * Context for a dialogue resolution.
51
51
  *
52
- * `sharedFaction` — Phase 24 placeholder; set to true when entities share a faction.
52
+ * `sharedFaction` — when true, applies a `PERSUADE_FACTION_BONUS` to persuasion rolls;
53
+ * set by the host when entities belong to the same faction.
53
54
  * `priorFailedAttempts` — cumulative failed persuasion attempts by this initiator against
54
55
  * this target; each one imposes a PERSUADE_FAILURE_PENALTY.
55
56
  */
@@ -381,12 +381,7 @@ export function findMaterialsByType(inventory, materialTypeId) {
381
381
  const results = [];
382
382
  // Helper to check and add
383
383
  const checkItem = (item) => {
384
- // We need to examine the item's materialTypeId property.
385
- // Since ItemInstance doesn't have materialTypeId, we need to rely on the templateId mapping
386
- // or assume that the item is a Material (which extends ItemBase).
387
- // For now, we'll assume that templateId indicates material type (e.g., "material_iron").
388
- // This is a placeholder; we need to integrate with crafting material system.
389
- if (item.templateId.startsWith("material_") && item.templateId.includes(materialTypeId)) {
384
+ if (item.templateId === `material_${materialTypeId}`) {
390
385
  results.push(item);
391
386
  }
392
387
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",