mineflayer-crafting-util
Version:
A plugin to simplify crafting recipes.
602 lines (601 loc) • 29.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports._build = _build;
exports.buildStatic = buildStatic;
function recipeInputs(recipe) {
if (recipe.ingredients != null && recipe.ingredients.length > 0)
return recipe.ingredients;
return recipe.delta.filter((item) => item.count < 0);
}
const strictItemMatcher = (wantedId, availableId) => wantedId === availableId;
function availableItemCount(availableItems, itemId, itemMatches = strictItemMatcher) {
return availableItems
.filter((item) => itemMatches(itemId, item.id))
.reduce((total, item) => total + item.count, 0);
}
function canCraftRecipe(availableItems, recipe, recipeApplications, itemMatches = strictItemMatcher) {
return recipeInputs(recipe).every((input) => availableItemCount(availableItems, input.id, itemMatches) >= -input.count * recipeApplications);
}
function recipeAvailabilityScore(availableItems, recipe, recipeApplications, itemMatches = strictItemMatcher) {
return recipeInputs(recipe).reduce((score, input) => {
const required = -input.count * recipeApplications;
const strictAvailable = Math.min(required, availableItemCount(availableItems, input.id));
const matchedAvailable = Math.min(required, availableItemCount(availableItems, input.id, itemMatches));
return score + strictAvailable * 2 + matchedAvailable;
}, 0);
}
function findBestCraftableRecipe(availableItems, recipes, count, itemMatches = strictItemMatcher) {
var _a;
return (_a = recipes
.map((recipe, index) => {
const recipeApplications = Math.ceil(count / recipe.result.count);
const craftable = canCraftRecipe(availableItems, recipe, recipeApplications, itemMatches);
const score = craftable
? recipeAvailabilityScore(availableItems, recipe, recipeApplications, itemMatches)
: -1;
return { recipe, index, score };
})
.filter(({ score }) => score >= 0)
.sort((a, b) => b.score - a.score || a.index - b.index)[0]) === null || _a === void 0 ? void 0 : _a.recipe;
}
function applyRecipeResults(items, recipesToDo, itemMatches = strictItemMatcher) {
for (const toDo of recipesToDo) {
for (const item of toDo.recipe.delta) {
if (item.count < 0) {
let count = -item.count * toDo.recipeApplications;
for (const existingItem of items) {
if (!itemMatches(item.id, existingItem.id))
continue;
const consumed = Math.min(existingItem.count, count);
existingItem.count -= consumed;
count -= consumed;
if (count <= 0)
break;
}
continue;
}
const index = items.findIndex((e) => e.id === item.id);
if (index !== -1) {
items[index].count += item.count * toDo.recipeApplications;
}
else {
items.push({ id: item.id, count: item.count * toDo.recipeApplications });
}
}
}
}
function replaceItems(targetItems, sourceItems) {
targetItems.splice(0, targetItems.length, ...sourceItems.map((item) => (Object.assign({}, item))));
}
function addItem(items, item) {
const existing = items.find((existingItem) => existingItem.id === item.id);
if (existing != null) {
existing.count += item.count;
}
else {
items.push(Object.assign({}, item));
}
}
function reserveItem(items, itemId, count, itemMatches = strictItemMatcher) {
const reservedItems = items.map((item) => (Object.assign({}, item)));
for (const item of reservedItems) {
if (!itemMatches(itemId, item.id))
continue;
const reserved = Math.min(item.count, count);
item.count -= reserved;
count -= reserved;
if (count <= 0)
break;
}
return reservedItems.filter((item) => item.count > 0);
}
function cloneAvailableItems(items) {
return items
.filter((item) => item.count > 0)
.map((item) => (Object.assign({}, item)));
}
function addItems(items, itemToAdd) {
const item = items.find((item) => item.id === itemToAdd.id);
if (item != null) {
item.count += itemToAdd.count;
}
else {
items.push(Object.assign({}, itemToAdd));
}
}
function mergeItems(items) {
const merged = [];
for (const item of items) {
if (item.count > 0)
addItems(merged, item);
}
return merged;
}
function summarizeItemsCreated(recipesToDo) {
var _a;
const map = new Map();
for (const toDo of recipesToDo) {
for (const item of toDo.recipe.delta) {
map.set(item.id, ((_a = map.get(item.id)) !== null && _a !== void 0 ? _a : 0) + item.count * toDo.recipeApplications);
}
}
return Array.from(map.entries())
.filter(([, count]) => count > 0)
.map(([id, count]) => ({ id, count }));
}
function isCompleteStatus(status) {
return status === 'complete';
}
function deriveStatus(complete, recipesToDo) {
if (complete)
return 'complete';
return recipesToDo.length > 0 ? 'partial_complete' : 'failure';
}
function createPlanResult(complete, itemsRequiredBase, recipesToDo, itemsCreated = summarizeItemsCreated(recipesToDo), itemsRequiredImmediate = itemsRequiredBase, itemsRemaining = []) {
return {
status: deriveStatus(complete, recipesToDo),
itemsRequiredBase,
itemsRequiredImmediate,
itemsRemaining,
itemsCreated,
recipesToDo
};
}
function isConcretePlanForAvailableItems(plan, availableItems, itemMatches = strictItemMatcher) {
if (!isCompleteStatus(plan.status))
return true;
if (!Array.isArray(plan.recipesToDo))
return false;
const currentItems = cloneAvailableItems(availableItems);
for (const toDo of plan.recipesToDo) {
if (toDo == null ||
toDo.recipe == null ||
toDo.recipe.result == null ||
!Array.isArray(toDo.recipe.delta) ||
!Number.isFinite(toDo.recipeApplications) ||
toDo.recipeApplications <= 0) {
return false;
}
if (!canCraftRecipe(currentItems, toDo.recipe, toDo.recipeApplications, itemMatches))
return false;
applyRecipeResults(currentItems, [toDo], itemMatches);
}
return true;
}
function _build(Recipe) {
const acceptedItemIds = new Map();
function getAcceptedItemIds(itemId) {
var _a;
const cached = acceptedItemIds.get(itemId);
if (cached != null)
return cached;
const ids = new Set([itemId]);
for (const recipe of Recipe.find(itemId, null)) {
if (((_a = recipe.result) === null || _a === void 0 ? void 0 : _a.id) != null)
ids.add(recipe.result.id);
}
acceptedItemIds.set(itemId, ids);
return ids;
}
function itemMatchesIngredient(ingredientId, availableId) {
return getAcceptedItemIds(ingredientId).has(availableId);
}
function canProvideIngredientDirectly(item, availableItems) {
if (availableItemCount(availableItems, item.id, itemMatchesIngredient) >= item.count)
return true;
return Recipe.find(item.id, null).some((recipe) => {
const recipeApplications = Math.ceil(item.count / recipe.result.count);
return canCraftRecipe(availableItems, recipe, recipeApplications, itemMatchesIngredient);
});
}
function getRecipeDeficits(availableItems, recipe, recipeApplications) {
return recipeInputs(recipe)
.map((input) => {
const required = -input.count * recipeApplications;
const available = availableItemCount(availableItems, input.id, itemMatchesIngredient);
return { id: input.id, count: required - available };
})
.filter((item) => item.count > 0);
}
function findPlannedRecipe(itemId, recipesToDo) {
for (let i = recipesToDo.length - 1; i >= 0; i--) {
if (recipesToDo[i].recipe.result.id === itemId)
return recipesToDo[i].recipe;
}
return undefined;
}
function getBaseRequirements(items, opts, recipesToDo = [], seen = new Set()) {
const requirements = [];
for (const item of items) {
const recipe = findPlannedRecipe(item.id, recipesToDo);
if (recipe != null && !seen.has(item.id)) {
seen.add(item.id);
const recipeApplications = Math.ceil(item.count / recipe.result.count);
const inputs = recipeInputs(recipe).map((input) => ({
id: input.id,
count: -input.count * recipeApplications
}));
for (const required of getBaseRequirements(inputs, opts, recipesToDo, seen)) {
addItems(requirements, required);
}
seen.delete(item.id);
continue;
}
const data = _newCraft(item, Object.assign(Object.assign({}, opts), { availableItems: undefined }), new Map());
for (const required of data.itemsRequiredBase)
addItems(requirements, required);
}
return mergeItems(requirements);
}
function getPartialRequirements(itemId, remainingCount, availableItems, recipesToDo, opts) {
const remaining = [{ id: itemId, count: remainingCount }];
const currentItems = cloneAvailableItems(availableItems);
applyRecipeResults(currentItems, recipesToDo, itemMatchesIngredient);
let completionRecipe;
for (let i = recipesToDo.length - 1; i >= 0; i--) {
if (recipesToDo[i].recipe.result.id === itemId) {
completionRecipe = recipesToDo[i].recipe;
break;
}
}
if (completionRecipe == null) {
return {
itemsRequiredBase: remaining,
itemsRequiredImmediate: remaining,
itemsRemaining: remaining
};
}
const recipeApplications = Math.ceil(remainingCount / completionRecipe.result.count);
const immediate = mergeItems(getRecipeDeficits(currentItems, completionRecipe, recipeApplications));
const base = getBaseRequirements(immediate, opts, recipesToDo);
return {
itemsRequiredBase: base,
itemsRequiredImmediate: immediate,
itemsRemaining: remaining
};
}
function _newCraft(item, opts = {}, seen = new Map(), target = item.count) {
var _a, _b, _c, _d;
const id = item.id;
let recipes = Recipe.find(id, null);
const availableItems = opts.availableItems;
const includeRecursion = (_a = opts.includeRecursion) !== null && _a !== void 0 ? _a : false;
const multipleRecipes = (_b = opts.multipleRecipes) !== null && _b !== void 0 ? _b : false;
let matchingItem;
let recipeWanted;
let count = item.count;
const ret0 = [];
const ret1 = [];
// disregard recipes that combine the item itself back together, as that is pointless for our usecase.
recipes = recipes.filter((r) => r.delta.slice(0, -1).some((e) => e.id !== id));
if (availableItems !== undefined) {
matchingItem = availableItems.find((e) => itemMatchesIngredient(id, e.id) && e.count >= target);
if (matchingItem != null) {
if (matchingItem.count >= target) {
return createPlanResult(true, [], []); // already have item, no need to craft it.
}
else {
count -= matchingItem.count;
}
}
if (recipes.length === 0) {
return createPlanResult(true, [item], []);
}
if (seen.has(id)) {
return createPlanResult(false, [item], []);
}
seen.set(id, item);
recipeWanted = findBestCraftableRecipe(availableItems, recipes, count, itemMatchesIngredient);
if (recipeWanted == null) {
// since no recipes exist with all items available, search for the recipe with the most amount of items available inline
const scoredRecipes = recipes
.map((recipe) => {
const ingredients = recipeInputs(recipe).map((e) => ({ id: e.id, count: -e.count }));
const score = ingredients.filter((ingredient) => canProvideIngredientDirectly(ingredient, availableItems)).length;
return { recipe, score };
})
.sort((a, b) => b.score - a.score);
const mostAmt = (_d = (_c = scoredRecipes[0]) === null || _c === void 0 ? void 0 : _c.score) !== null && _d !== void 0 ? _d : 0;
// store current amount of items available to be crafted
let craftedCount = 0;
let bestPartialCount = 0;
let bestPartialRecipes = [];
outer: for (const scoredRecipe of scoredRecipes) {
if (scoredRecipe.score !== mostAmt)
continue;
// Candidate planning mutates inventory counts, so isolate attempts until one fully succeeds.
const candidateItems = cloneAvailableItems(availableItems);
const candidateOpts = Object.assign(Object.assign({}, opts), { availableItems: candidateItems });
const currentItems = candidateItems;
const candidateSeen = new Map(seen);
const candidateRecipes = [];
const recipe = scoredRecipe.recipe;
const recipeIngredients = recipeInputs(recipe);
// all items that need to be crafted to craft this recipe
let ingredients = recipeIngredients.filter((i) => availableItemCount(currentItems, i.id, itemMatchesIngredient) < -i.count);
// store all results for crafting attempts on all ingredients of current recipe
const results = [];
const found = ingredients.find((e) => e.id === id);
if (found != null)
ingredients = [found];
// do craft on all ingredients of current recipe
inner: for (const ing of ingredients) {
const data = _newCraft({ id: ing.id, count: -ing.count }, candidateOpts, new Map(candidateSeen));
if (!isCompleteStatus(data.status))
continue inner;
results.push(data);
candidateRecipes.push(...data.recipesToDo);
applyRecipeResults(currentItems, data.recipesToDo, itemMatchesIngredient);
}
// if we successfully crafted all ingredients, we can craft this recipe
if (results.length === ingredients.length) {
// with our available items properly managed now, we can do the standard crafting option.
let test;
let attemptCount = count - craftedCount;
tester: for (; attemptCount > 0; attemptCount--) {
const recipeApplications = Math.ceil(attemptCount / recipe.result.count);
const attemptItems = cloneAvailableItems(currentItems);
const attemptRecipes = [...candidateRecipes];
const attemptOpts = Object.assign(Object.assign({}, candidateOpts), { availableItems: attemptItems });
const parentInputs = recipeInputs(recipe);
let pass = 0;
while (!canCraftRecipe(attemptItems, recipe, recipeApplications, itemMatchesIngredient)) {
let madeProgress = false;
pass++;
if (pass > parentInputs.length * (recipeApplications + 2))
continue tester;
for (const input of parentInputs) {
const required = -input.count * recipeApplications;
const available = availableItemCount(attemptItems, input.id, itemMatchesIngredient);
const deficit = required - available;
if (deficit <= 0)
continue;
const deficitItems = reserveItem(attemptItems, input.id, available, itemMatchesIngredient);
const deficitOpts = Object.assign(Object.assign({}, attemptOpts), { availableItems: deficitItems });
const data = _newCraft({ id: input.id, count: deficit }, deficitOpts, new Map(candidateSeen), deficit);
if (!isCompleteStatus(data.status) || data.recipesToDo.length === 0)
continue tester;
attemptRecipes.push(...data.recipesToDo);
replaceItems(attemptItems, deficitItems);
if (available > 0)
addItem(attemptItems, { id: input.id, count: available });
applyRecipeResults(attemptItems, data.recipesToDo, itemMatchesIngredient);
madeProgress = true;
}
if (!madeProgress)
continue tester;
}
replaceItems(currentItems, attemptItems);
candidateRecipes.splice(0, candidateRecipes.length, ...attemptRecipes);
test = { recipesToDo: [{ recipeApplications, recipe }] };
craftedCount += attemptCount;
break tester;
}
if (test === undefined)
continue outer;
candidateRecipes.push(...test.recipesToDo);
applyRecipeResults(currentItems, test.recipesToDo, itemMatchesIngredient);
if (craftedCount > bestPartialCount) {
bestPartialCount = craftedCount;
bestPartialRecipes = [...candidateRecipes];
}
if (craftedCount !== count) {
if (multipleRecipes && craftedCount > 0) {
const remainingSeen = new Map(candidateSeen);
remainingSeen.delete(id);
const remainingItems = cloneAvailableItems(currentItems).filter((item) => item.id !== id);
const data = _newCraft({ id, count: count - craftedCount }, Object.assign(Object.assign({}, opts), { availableItems: remainingItems }), remainingSeen, count - craftedCount);
if (isCompleteStatus(data.status)) {
return createPlanResult(true, ret0.concat(data.itemsRequiredBase), candidateRecipes.concat(data.recipesToDo));
}
}
continue outer;
}
return createPlanResult(true, ret0, candidateRecipes);
}
}
// TODO can implement partial completion of recipes here.
const hasNoRecipes = recipes.length === 0;
const weHaveItem = availableItems.find((e) => e.id === id && e.count >= count);
if (hasNoRecipes && weHaveItem != null) {
return createPlanResult(true, [], []);
}
else {
if (bestPartialCount > 0) {
const partialRequirements = getPartialRequirements(id, count - bestPartialCount, availableItems, bestPartialRecipes, opts);
return createPlanResult(false, partialRequirements.itemsRequiredBase, bestPartialRecipes, undefined, partialRequirements.itemsRequiredImmediate, partialRequirements.itemsRemaining);
}
if (!multipleRecipes || (hasNoRecipes && weHaveItem == null)) {
const new1 = { id, count: count - craftedCount };
return createPlanResult(false, [new1], []);
}
else {
const data = _newCraft({ id, count: count - craftedCount }, opts, seen, target);
return createPlanResult(isCompleteStatus(data.status), ret0.concat(data.itemsRequiredBase), ret1.concat(data.recipesToDo));
}
}
}
}
else {
// TODO : should be replaced by smelting recipe data
const found = recipes.find((r) => r.result.count > 1);
recipeWanted = found !== null && found !== void 0 ? found : recipes[0];
if (recipes.length === 0) {
return createPlanResult(true, [item], []);
}
if (seen.has(id)) {
if (!includeRecursion) {
return createPlanResult(true, [item], []);
}
return createPlanResult(true, [item], []);
}
seen.set(id, item);
}
const recipeApplications = Math.ceil(count / recipeWanted.result.count);
const items = recipeWanted.delta.slice(0, -1).map((e) => ({ id: e.id, count: -recipeApplications * e.count }));
const ret = items.reduce((acc, item) => {
const r = _newCraft(item, opts, seen);
return {
status: deriveStatus(isCompleteStatus(acc.status) && isCompleteStatus(r.status), []),
itemsRequiredBase: acc.itemsRequiredBase.concat(r.itemsRequiredBase),
itemsRequiredImmediate: [],
itemsRemaining: [],
itemsCreated: [],
recipesToDo: r.recipesToDo.concat(acc.recipesToDo)
};
}, {
status: 'complete',
itemsRequiredBase: [],
itemsRequiredImmediate: [],
itemsRemaining: [],
itemsCreated: [],
recipesToDo: [{ recipeApplications, recipe: recipeWanted }]
});
seen.clear();
return createPlanResult(isCompleteStatus(ret.status), ret.itemsRequiredBase, ret.recipesToDo);
}
function newCraft(item, opts = {}) {
const seen = new Map();
// rough, but easy way to patch out items that are already available.
// can clean up later.
if (opts.availableItems != null) {
if (opts.careAboutExisting !== true) {
const found = opts.availableItems.filter((e) => e.id === item.id);
for (const f of found) {
opts.availableItems.splice(opts.availableItems.indexOf(f), 1);
}
}
// normalize items, bug pointed out by Vakore.
const seen = new Set();
for (const item of opts.availableItems) {
if (seen.has(item.id)) {
opts.availableItems.splice(opts.availableItems.indexOf(item), 1);
const existing = opts.availableItems.find((e) => e.id === item.id);
if (existing != null)
existing.count += item.count;
}
seen.add(item.id);
}
}
const ret = _newCraft(item, opts, seen);
const availableItems = opts.availableItems;
const ret1 = ret;
// due to multiple recipes, preserve order of items required.
if (availableItems !== undefined) {
if (!isConcretePlanForAvailableItems(ret, availableItems, itemMatchesIngredient)) {
return newCraft(item, Object.assign(Object.assign({}, opts), { availableItems: undefined }));
}
ret1.requiresCraftingTable = ret.recipesToDo.some((r) => r.recipe.requiresTable);
return ret1;
}
ret.itemsRequiredBase = [];
ret.itemsRequiredImmediate = [];
ret.itemsRemaining = [];
const map = {};
if (opts.includeRecursion !== true) {
hey: while (ret.recipesToDo.length > 0) {
// remove single-level loops
let change = 0;
inner: for (const res1 of ret.recipesToDo) {
const res = res1.recipe.result;
const res2 = res1.recipe.delta.slice(0, 1);
const found = ret.recipesToDo.find((r1) => r1 !== res1 &&
r1.recipe.delta.length === res1.recipe.delta.length &&
!(r1.recipe.delta.find((i) => i.id !== r1.recipe.result.id && i.id === res.id) == null) &&
res2.find((i) => i.id === r1.recipe.result.id));
if (found == null)
continue inner;
const consumerIdx = ret.recipesToDo.indexOf(res1);
ret.recipesToDo.splice(consumerIdx, 1);
if (ret.recipesToDo.length <= 1)
break hey;
const producerIdx = ret.recipesToDo.indexOf(found);
ret.recipesToDo.splice(producerIdx, 1);
change++;
}
if (change === 0)
break hey;
}
}
else {
hey: while (ret.recipesToDo.length > 0) {
// remove single-level loops
let change = 0;
inner: for (const res1 of ret.recipesToDo) {
const res = res1.recipe.result;
const res2 = res1.recipe.delta.slice(0, 1);
const found = ret.recipesToDo.find((r1) => r1 !== res1 &&
r1.recipe.delta.length === res1.recipe.delta.length &&
!(r1.recipe.delta.find((i) => i.id !== r1.recipe.result.id && i.id === res.id) == null) &&
res2.find((i) => i.id === r1.recipe.result.id));
// console.log("found loop", !!res1, !!res, found);
if (found == null)
continue inner;
const consumerIdx = ret.recipesToDo.indexOf(res1);
ret.recipesToDo.splice(consumerIdx, 1);
change++;
if (ret.recipesToDo.length === 1)
break hey;
}
if (change === 0)
break hey;
}
}
// console.log(ret.recipesToDo.map((r) => r.recipe.delta.map((i) => [i.count, itemsMap[i.id].name])));
for (let i = 0; i < ret.recipesToDo.length; i++) {
const res = ret.recipesToDo[i];
const recipe = res.recipe;
const recipeApplications = res.recipeApplications;
const delta = recipe.delta;
for (let j = 0; j < delta.length; j++) {
const ing = delta[j];
const count = ing.count * recipeApplications;
const val = map[ing.id];
const nan = isNaN(val);
if (nan)
map[ing.id] = count;
else
map[ing.id] += count;
}
}
if (ret.recipesToDo.length > 1) {
for (let idx = 0; idx < ret.recipesToDo.length; idx++) {
const res = ret.recipesToDo[idx];
if (res.recipe.result.id === item.id)
continue;
const potentialShift = res.recipe.delta.slice(0, -1).some((i) => map[i.id] < 0);
if (!potentialShift)
continue;
const valid = res.recipe.delta.reduce((acc, ing) => (map[ing.id] < 0 ? true : map[ing.id] - ing.count >= 0 && acc), true);
if (valid) {
for (const ing of res.recipe.delta) {
map[ing.id] -= ing.count;
}
// removed this so users can know when intermediate items are crafted.
// uncomment to remove blanks.
// for (const ing of res.recipe.delta) {
// const val = map[ing.id];
// if (val === 0) delete map[ing.id];
// }
ret.recipesToDo.splice(idx, 1);
}
}
}
for (const [key, val] of Object.entries(map)) {
const key1 = Number(key);
if (key1 === item.id)
continue;
if (val >= 0)
continue;
const required = { id: key1, count: -val };
ret.itemsRequiredBase.push(required);
ret.itemsRequiredImmediate.push(required);
}
ret1.requiresCraftingTable = ret.recipesToDo.some((r) => r.recipe.requiresTable);
return ret1;
}
return newCraft;
}
async function buildStatic(Recipe) {
return _build(Recipe);
}