feat: crafting system with chemistry engine integration (Phase 4.4)

craftFromInventory checks reagent availability, runs ReactionEngine,
consumes reagents on success, adds products. Failed attempts return
educational reasons (noble gas inertness, missing conditions, etc.).
11 new tests (185 total).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Денис Шкабатур
2026-02-12 13:27:22 +03:00
parent e77b9df6e4
commit b097ce738f
2 changed files with 291 additions and 0 deletions

102
src/player/crafting.ts Normal file
View File

@@ -0,0 +1,102 @@
/**
* Crafting System — Chemistry Engine Integration
*
* Combines items from inventory using the reaction engine.
* On success: consumes reagents, adds products.
* On failure: returns educational explanation of why it didn't work.
*/
import { ReactionEngine } from '../chemistry/engine';
import type { ReactionConditions, ReactionData } from '../chemistry/types';
import type { Inventory } from './inventory';
/** What the player wants to combine */
export interface CraftInput {
id: string; // element symbol or compound id
count: number; // quantity to use
}
/** Result of a crafting attempt */
export interface CraftResult {
success: boolean;
/** Products added to inventory (count may differ from reaction output if inventory full) */
products?: Array<{ id: string; count: number }>;
/** What was consumed from inventory */
consumedReactants?: CraftInput[];
/** Reaction data (for codex, description display) */
reaction?: ReactionData;
/** Why it failed (English) */
failureReason?: string;
/** Why it failed (Russian) */
failureReasonRu?: string;
}
/**
* Attempt to craft from inventory items.
*
* 1. Checks inventory has all reagents
* 2. Runs reaction engine (O(1) lookup + condition check)
* 3. On success: consumes reagents, adds products
* 4. On failure: returns educational reason
*
* Reagents are only consumed on success — failed attempts are free.
*/
export function craftFromInventory(
inventory: Inventory,
reactants: CraftInput[],
conditions?: Partial<ReactionConditions>,
): CraftResult {
// Empty input
if (reactants.length === 0) {
return {
success: false,
failureReason: 'Select at least one substance to combine.',
failureReasonRu: 'Выберите хотя бы одно вещество для комбинирования.',
};
}
// 1. Check inventory has all reagents
for (const r of reactants) {
const have = inventory.getCount(r.id);
if (have < r.count) {
return {
success: false,
failureReason: `Not enough ${r.id} — have ${have}, need ${r.count}.`,
failureReasonRu: `Недостаточно ${r.id} — есть ${have}, нужно ${r.count}.`,
};
}
}
// 2. Run reaction engine
const result = ReactionEngine.react(
reactants.map(r => ({ id: r.id, count: r.count })),
conditions,
);
if (!result.success) {
return {
success: false,
failureReason: result.failureReason,
failureReasonRu: result.failureReasonRu,
};
}
// 3. Consume reagents
for (const r of reactants) {
inventory.removeItem(r.id, r.count);
}
// 4. Add products to inventory
const addedProducts: Array<{ id: string; count: number }> = [];
for (const p of result.products!) {
const added = inventory.addItem(p.id, p.count);
addedProducts.push({ id: p.id, count: added });
}
return {
success: true,
products: addedProducts,
consumedReactants: reactants,
reaction: result.reaction,
};
}