class WorkPlan {
constructor(
items,
craftingPlan = new CraftingPlan({}),
target = new WorkTarget()
) {
this.items = items;
this.craftingPlan = craftingPlan;
this.target = target;
}
toJSON() {
return {
results: this.target,
crafting: this.craftingPlan
};
}
static fromJSON(items, json) {
return new WorkPlan(
items,
CraftingPlan.fromJSON(items, json.crafting),
WorkTarget.fromJSON(items, json.results)
);
}
get urlParam() {
if (this.target.items.length == 0) {
return "";
}
var namespaces = {};
var items = {};
var tools = {};
var pending = [
...this.target.items.map(result => ({
item: result.item,
quantity: result.quantity
}))
];
while (pending.length > 0) {
var { item, quantity } = pending.shift();
var { recipe, stock } = this.craftingPlan.items[item.name]
? this.craftingPlan.items[item.name]
: { recipe: item.recipes[0], stock: 0 };
var tool = recipe.tool.item ? recipe.tool.item.name : recipe.tool.name;
var quantity = items[item.name] ? items[item.name].quantity : quantity;
items[item.name] = {
item: item.name,
quantity: quantity,
tool: tool,
ingredients: recipe.ingredients.map(ing => ing.item.name),
stock: stock
};
if (tool) {
tools[tool] = true;
}
pending = [
...pending,
...recipe.ingredients.map(ing => ({ item: ing.item, quantity: 0 }))
];
}
var itemNames = [...Object.keys(items), ...Object.keys(tools)];
var namespaceTable = itemNames
.map(name => name.split(':', 1)[0])
.filter((elt, idx, arr) => arr.indexOf(elt) === idx)
.sort();
var itemNameRewrite = Object.fromEntries(
itemNames.map(function(name) {
var [namespace, item] = name.split(':', 2);
namespace = namespaceTable.indexOf(namespace).toString();
return [name, `${namespace}.${item}`];
})
);
var itemNameTable = Object.values(itemNameRewrite).sort();
function itemNameIndex(name) {
return itemNameTable.indexOf(itemNameRewrite[name]);
}
items = Object.values(items)
.map(({ item, quantity, stock, tool, ingredients }) =>
[
itemNameIndex(item).toString(),
quantity > 0 ? quantity.toString() : '',
stock > 0 ? stock.toString() : '',
tool ? itemNameIndex(tool).toString() : '',
...ingredients.map(ing => itemNameIndex(ing).toString())
].join('.')
)
.join('*');
itemNames = itemNameTable.join('*');
namespaces = namespaceTable.join('*');
return [items, itemNames, namespaces].join('**');
}
static fromURLParam(allItems, param) {
var [items, names, namespaces] = param
.split('**', 3)
.map(elt => elt.split('*'));
var nameTable = Object.fromEntries(
names.map(function(name, idx) {
var [namespace, item] = name.split('.', 2);
namespace = namespaces[parseInt(namespace)];
return [idx.toString(), `${namespace}:${item}`];
})
);
var work = items
.map(function(item) {
var [nameIdx, quantity, stock, toolIdx, ...ingredients] = item.split(
'.'
);
return {
itemName: nameTable[nameIdx],
quantity: quantity.length > 0 ? parseInt(quantity) : 0,
stock: stock.length > 0 ? parseInt(stock) : 0,
toolName: nameTable[toolIdx],
ingredientNames: ingredients.map(ing => nameTable[ing])
};
})
.map(({ itemName, quantity, stock, toolName, ingredientNames }) => {
var item = allItems[itemName];
var recipe = item.recipes.find(
recipe =>
deepEqual(
recipe.ingredients.map(ing => ing.item.name).sort(),
ingredientNames.sort()
) &&
(recipe.tool.item
? recipe.tool.item.name == toolName
: recipe.tool.name
? recipe.tool.name == toolName
: !toolName)
);
return { item: item, quantity: quantity, stock: stock, recipe: recipe };
});
return new WorkPlan(
allItems,
new CraftingPlan(
Object.fromEntries(
work.map(({ item, recipe, stock }) => [
item.name,
new CraftingPlanItem(recipe, stock)
])
)
),
new WorkTarget(
...work
.filter(({ quantity }) => quantity > 0)
.map(({ quantity, item }) => new WorkTargetItem(item, quantity))
)
);
}
get craftableRecipes() {
var isResult = Object.fromEntries(
this.target.items.map(work => [work.item.name, work.quantity > 0])
);
var dependencies = Object.fromEntries(
this.target.items.map(work => [work.item.name, []])
);
var pending = [...this.target.items.map(work => work.item)];
var recipes = {};
while (pending.length > 0) {
var item = pending.shift();
var craftable = item.recipes.filter(
recipe =>
!(
recipe.tool.name == "_:stock" ||
recipe.ingredients.some(ing => {
var ingDeps = dependencies[ing.item.name] ?? [];
return ingDeps.includes(item);
})
)
);
if (craftable.length == 0) {
craftable = [
item.recipes.find(recipe => recipe.tool.name == "_:gather")
];
}
recipes[item.name] = craftable;
var selectedRecipe =
craftable.length == 1
? craftable[0]
: this.craftingPlan.items[item.name]
? this.craftingPlan.items[item.name].recipe
: null;
selectedRecipe = craftable.includes(selectedRecipe)
? selectedRecipe
: null;
if (selectedRecipe) {
var ingredients = selectedRecipe.ingredients.map(ing => ing.item);
pending = [...pending, ...ingredients];
dependencies = Object.fromEntries(
Object.entries({
...Object.fromEntries([[item.name, []]]),
...dependencies
}).map(([name, deps]) => [
name,
deps.includes(item) || item.name == name
? [...deps, ...ingredients].filter(
(val, idx, arr) => arr.indexOf(val) === idx
)
: deps
])
);
}
}
return recipes;
}
execute() {
var initialStock = Object.fromEntries(
Object.entries(this.craftingPlan.items).map(([name, planItem]) => [
name,
planItem.stock
])
);
var intermediates = {};
var steps = [];
var itemSteps = {};
var pending = [...this.target.items];
while (pending.length > 0) {
var work = { ...pending.shift() };
if (work.item.recipes.length > 0) {
if (intermediates.hasOwnProperty(work.item.name)) {
var stockToUse = Math.min(
work.quantity,
intermediates[work.item.name]
);
work.quantity -= stockToUse;
intermediates[work.item.name] -= stockToUse;
} else {
intermediates[work.item.name] = 0;
}
if (work.quantity == 0) {
continue;
}
var recipe = null;
var quantityToMake = 0;
if (
initialStock.hasOwnProperty(work.item.name) &&
initialStock[work.item.name] > 0
) {
var stockToUse = Math.min(
work.quantity,
initialStock[work.item.name]
);
initialStock[work.item.name] -= stockToUse;
recipe = work.item.recipes.find(
recipe => recipe.tool.name == '_:stock'
);
quantityToMake = stockToUse;
} else {
recipe = this.craftingPlan.items[work.item.name]
? this.craftingPlan.items[work.item.name].recipe
: work.item.recipes[0];
quantityToMake =
Math.ceil(work.quantity / recipe.quantity) * recipe.quantity;
}
var leftover = Math.max(quantityToMake - work.quantity, 0);
work.quantity = Math.max(work.quantity - quantityToMake, 0);
intermediates[work.item.name] += leftover;
var itemStepKey = `${work.item.name} - ${recipe.tool.name}`;
if (itemSteps[itemStepKey]) {
itemSteps[itemStepKey].quantity += quantityToMake;
} else {
var step = new WorkStep(recipe, quantityToMake, work.item);
steps.push(step);
itemSteps[itemStepKey] = step;
}
if (work.quantity > 0) {
pending.unshift(work);
}
pending = [
...recipe.ingredients.map(
ing =>
new WorkItem(
ing.item,
Math.ceil(quantityToMake / recipe.quantity) * ing.quantity,
recipe
)
),
...pending
];
}
}
var leftovers = Object.keys({ ...initialStock, ...intermediates })
.map(name => [
name,
(initialStock[name] ?? 0) + (intermediates[name] ?? 0)
])
.map(([name, quantity]) => ({
item: this.items[name],
quantity: quantity
}))
.filter(({ quantity }) => quantity > 0)
.filter(({ item }) => item.name != "_:fuel");
var craft = [];
var craftItems = [];
pending = [...steps].reverse();
while (pending.length > 0) {
var next = pending.find(work =>
work.recipe.ingredients.every(ing => craftItems.includes(ing.item))
);
craft.push(next);
craftItems.push(next.recipe.result);
pending = pending.filter(item => item !== next);
}
var substitutions = Object.fromEntries(
steps
.filter(work => work.recipe.tool.name == "_:substitute")
.map(work => [work.recipe.result.name, work.recipe.ingredients[0].item])
);
craft = craft
.filter(
step =>
!["_:gather", "_:substitute", "_:stock"].includes(
step.recipe.tool.name
)
)
.map(work => {
var recipe = new Recipe(
work.recipe.quantity,
work.recipe.nonFuelIngredients.map(ing =>
substitutions[ing.item.name]
? new Ingredient(substitutions[ing.item.name], ing.quantity)
: ing
),
work.recipe.tool,
!work.recipe.fuel
? work.recipe.fuel
: substitutions[work.recipe.fuel.item.name]
? new Ingredient(
substitutions[work.recipe.fuel.item.name],
work.recipe.fuel.quantity
)
: work.recipe.fuel
);
recipe.result = work.item;
return new WorkItem(work.item, work.quantity, recipe);
});
return {
steps: steps,
stock: steps.filter(step => step.recipe.tool.name == "_:stock"),
gather: steps.filter(step => step.recipe.tool.name == "_:gather"),
craft: craft,
leftovers: leftovers
};
}
}