feat(industry): add RecipeRegistry with seed recipes for tools and structures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 23:20:38 +00:00
parent e532c85d6b
commit 4399a1cd34
3 changed files with 173 additions and 0 deletions
@@ -0,0 +1,84 @@
import { describe, it, expect } from 'vitest';
import { RecipeRegistry, type Recipe } from '../recipeRegistry.js';
describe('RecipeRegistry', () => {
it('registers and retrieves a recipe', () => {
const registry = new RecipeRegistry();
const recipe: Recipe = {
id: 'craft_wooden_axe',
outputItemId: 'wooden_axe',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 2 }, { itemId: 'stone', quantity: 1 }],
source: 'seed',
};
registry.register(recipe);
expect(registry.get('craft_wooden_axe')).toEqual(recipe);
});
it('throws on duplicate registration', () => {
const registry = new RecipeRegistry();
const recipe: Recipe = {
id: 'test',
outputItemId: 'x',
outputQuantity: 1,
inputs: [],
source: 'seed',
};
registry.register(recipe);
expect(() => registry.register(recipe)).toThrow();
});
it('returns undefined for unknown recipe', () => {
const registry = new RecipeRegistry();
expect(registry.get('nonexistent')).toBeUndefined();
});
it('getAll returns all registered recipes', () => {
const registry = new RecipeRegistry();
registry.register({ id: 'a', outputItemId: 'x', outputQuantity: 1, inputs: [], source: 'seed' });
registry.register({ id: 'b', outputItemId: 'y', outputQuantity: 1, inputs: [], source: 'seed' });
expect(registry.getAll()).toHaveLength(2);
});
it('findCraftable returns recipes NPC has materials for', () => {
const registry = new RecipeRegistry();
registry.register({
id: 'craft_wooden_axe',
outputItemId: 'wooden_axe',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 2 }, { itemId: 'stone', quantity: 1 }],
source: 'seed',
});
registry.register({
id: 'craft_hammer',
outputItemId: 'hammer',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 2 }, { itemId: 'stone', quantity: 1 }],
source: 'seed',
});
const inv = new Map([['log', 3], ['stone', 1]]);
const craftable = registry.findCraftable(inv);
expect(craftable).toHaveLength(2);
});
it('findCraftable excludes recipes without enough materials', () => {
const registry = new RecipeRegistry();
registry.register({
id: 'craft_wooden_axe',
outputItemId: 'wooden_axe',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 2 }, { itemId: 'stone', quantity: 1 }],
source: 'seed',
});
const inv = new Map([['log', 1]]);
expect(registry.findCraftable(inv)).toHaveLength(0);
});
it('createDefault has seed recipes for axe, hammer, stockpile, workbench', () => {
const registry = RecipeRegistry.createDefault();
expect(registry.get('craft_wooden_axe')).toBeDefined();
expect(registry.get('craft_hammer')).toBeDefined();
expect(registry.get('build_stockpile')).toBeDefined();
expect(registry.get('build_workbench')).toBeDefined();
});
});
+14
View File
@@ -55,6 +55,20 @@ export class ItemRegistry {
category: 'resource',
source: 'seed',
});
registry.register({
id: 'wooden_axe',
name: 'Wooden Axe',
description: 'A crude axe made from logs and stone',
category: 'tool',
source: 'seed',
});
registry.register({
id: 'hammer',
name: 'Hammer',
description: 'A basic hammer for construction',
category: 'tool',
source: 'seed',
});
return registry;
}
}
+75
View File
@@ -0,0 +1,75 @@
import type { Inventory } from './inventoryHelpers.js';
export interface Recipe {
id: string;
outputItemId: string;
outputQuantity: number;
inputs: { itemId: string; quantity: number }[];
workshopType?: string; // required workshop (undefined = hand-craftable)
toolRequired?: string; // required tool in inventory (undefined = none)
source: 'seed' | 'invented';
}
export class RecipeRegistry {
private recipes = new Map<string, Recipe>();
register(recipe: Recipe): void {
if (this.recipes.has(recipe.id)) {
throw new Error(`Recipe '${recipe.id}' already registered`);
}
this.recipes.set(recipe.id, recipe);
}
get(id: string): Recipe | undefined {
return this.recipes.get(id);
}
getAll(): Recipe[] {
return [...this.recipes.values()];
}
findCraftable(inv: Inventory, workshopType?: string): Recipe[] {
return this.getAll().filter(recipe => {
if (recipe.workshopType && recipe.workshopType !== workshopType) return false;
return recipe.inputs.every(input => (inv.get(input.itemId) ?? 0) >= input.quantity);
});
}
static createDefault(): RecipeRegistry {
const registry = new RecipeRegistry();
registry.register({
id: 'craft_wooden_axe',
outputItemId: 'wooden_axe',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 2 }, { itemId: 'stone', quantity: 1 }],
source: 'seed',
});
registry.register({
id: 'craft_hammer',
outputItemId: 'hammer',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 2 }, { itemId: 'stone', quantity: 1 }],
source: 'seed',
});
registry.register({
id: 'build_stockpile',
outputItemId: 'stockpile',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 4 }],
source: 'seed',
});
registry.register({
id: 'build_workbench',
outputItemId: 'workbench',
outputQuantity: 1,
inputs: [{ itemId: 'log', quantity: 3 }, { itemId: 'stone', quantity: 2 }],
source: 'seed',
});
return registry;
}
}