search: add --store pricing and total cost; stop at purchasable leaves; add nodeBudget guard; totals table shows Price and Total price [cai]
This commit is contained in:
parent
4217c02857
commit
f1e1c37b59
1 changed files with 144 additions and 7 deletions
151
sdfh.js
151
sdfh.js
|
|
@ -206,7 +206,7 @@ function search(index, rawQuery, limit = 5) {
|
|||
// ---------------- Fusion analysis ----------------
|
||||
|
||||
function parseFlags(input) {
|
||||
const flags = { recipeIndex: null, fuseRankLimit: null, full: false, doFuse: false };
|
||||
const flags = { recipeIndex: null, fuseRankLimit: null, full: false, doFuse: false, storeLevel: null };
|
||||
const parts = input.split(/\s+/).filter(Boolean);
|
||||
const kept = [];
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
|
|
@ -224,6 +224,8 @@ function parseFlags(input) {
|
|||
if (mRank) { flags.fuseRankLimit = parseInt(mRank, 10); continue; }
|
||||
const mDepthAsRank = p.match(/^--(?:depth)=(\d+)$/i)?.[1] || (p.match(/^--depth$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
if (mDepthAsRank) { flags.fuseRankLimit = parseInt(mDepthAsRank, 10); continue; }
|
||||
const mStore = p.match(/^--store=(\d+)$/i)?.[1] || (p.match(/^--store$/i) && parts[i + 1] && /^\d+$/.test(parts[i + 1]) ? parts[++i] : null);
|
||||
if (mStore) { flags.storeLevel = Math.max(1, Math.min(5, parseInt(mStore, 10))); continue; }
|
||||
kept.push(p);
|
||||
}
|
||||
return { flags, remaining: kept.join(' ') };
|
||||
|
|
@ -240,6 +242,12 @@ function getItemByExactName(index, name) {
|
|||
}
|
||||
|
||||
function expandIngredient(index, ingredient, opts, visited) {
|
||||
if (opts && typeof opts.nodeBudget === 'number') {
|
||||
if (opts.nodeBudget <= 0) {
|
||||
return { name: ingredient.name, rank: ingredient.rank, leaf: true, truncated: true, children: [] };
|
||||
}
|
||||
opts.nodeBudget -= 1;
|
||||
}
|
||||
const node = { name: ingredient.name, rank: ingredient.rank, children: [] };
|
||||
if (opts.fuseRankLimit != null && ingredient.rank <= opts.fuseRankLimit) {
|
||||
node.leaf = true;
|
||||
|
|
@ -251,6 +259,14 @@ function expandIngredient(index, ingredient, opts, visited) {
|
|||
node.missing = true;
|
||||
return node;
|
||||
}
|
||||
// If store level provided and this ingredient is purchasable at that store, stop here
|
||||
if (opts.storeLevel != null) {
|
||||
const priceInfo = getPriceAtStore(childItem, opts.storeLevel);
|
||||
if (priceInfo.purchasable) {
|
||||
node.leaf = true;
|
||||
return node;
|
||||
}
|
||||
}
|
||||
const key = `${childItem.name}|${childItem.rank}`;
|
||||
if (visited.has(key)) {
|
||||
node.leaf = true;
|
||||
|
|
@ -285,6 +301,32 @@ function expandIngredient(index, ingredient, opts, visited) {
|
|||
visited.delete(key);
|
||||
return node;
|
||||
}
|
||||
// If storeLevel given (and no fuseRankLimit), try to find any recipe that resolves to purchasable leaves
|
||||
if (opts.storeLevel != null) {
|
||||
const ordered = sortRecipes(recipes);
|
||||
for (const r of ordered) {
|
||||
const left = expandIngredient(index, r.ingredients[0], opts, new Set(visited));
|
||||
const right = expandIngredient(index, r.ingredients[1], opts, new Set(visited));
|
||||
const ok = checkAllLeavesPurchasable([left, right], index, opts.storeLevel);
|
||||
if (ok) {
|
||||
node.children.push(left, right);
|
||||
node.recipe = { ingredients: r.ingredients };
|
||||
visited.delete(key);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
// fallback: first recipe
|
||||
if (recipes[0]) {
|
||||
const left = expandIngredient(index, recipes[0].ingredients[0], opts, new Set(visited));
|
||||
const right = expandIngredient(index, recipes[0].ingredients[1], opts, new Set(visited));
|
||||
node.children.push(left, right);
|
||||
node.recipe = { ingredients: recipes[0].ingredients };
|
||||
} else {
|
||||
node.leaf = true;
|
||||
}
|
||||
visited.delete(key);
|
||||
return node;
|
||||
}
|
||||
// depth-limited expansion (one level per call)
|
||||
if (recipes[0]) {
|
||||
const left = expandIngredient(index, recipes[0].ingredients[0], opts, new Set(visited));
|
||||
|
|
@ -308,6 +350,21 @@ function checkAllLeavesWithinRank(nodes, limit) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function checkAllLeavesPurchasable(nodes, index, storeLevel) {
|
||||
const stack = [...nodes];
|
||||
while (stack.length) {
|
||||
const n = stack.pop();
|
||||
if (n.children && n.children.length) {
|
||||
stack.push(...n.children);
|
||||
} else {
|
||||
const it = getItemByExactName(index, n.name);
|
||||
const info = getPriceAtStore(it, storeLevel);
|
||||
if (!info.purchasable) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function printFusionTree(node, indent = '') {
|
||||
if (!node) return [];
|
||||
const lines = [];
|
||||
|
|
@ -360,6 +417,76 @@ function printTotalsTable(leaves) {
|
|||
return lines;
|
||||
}
|
||||
|
||||
function getPriceAtStore(item, storeLevel) {
|
||||
if (!item || storeLevel == null) return { purchasable: false, price: null };
|
||||
const p = item.price;
|
||||
if (p == null) return { purchasable: false, price: null };
|
||||
if (typeof p === 'number') return { purchasable: true, price: p };
|
||||
if (Array.isArray(p)) {
|
||||
let last = null;
|
||||
for (let s = 1; s <= storeLevel; s += 1) {
|
||||
const idx = s - 1;
|
||||
if (idx < p.length) {
|
||||
const v = p[idx];
|
||||
if (v != null) last = v;
|
||||
}
|
||||
}
|
||||
if (last != null) return { purchasable: true, price: last };
|
||||
}
|
||||
return { purchasable: false, price: null };
|
||||
}
|
||||
|
||||
function buildTotalsRowsWithPrice(leaves, index, storeLevel) {
|
||||
const countMap = new Map();
|
||||
for (const l of leaves) {
|
||||
const key = `${l.name}|${l.rank}`;
|
||||
countMap.set(key, (countMap.get(key) || 0) + 1);
|
||||
}
|
||||
const rows = [];
|
||||
for (const [key, count] of countMap.entries()) {
|
||||
const [name, rankStr] = key.split('|');
|
||||
const item = getItemByExactName(index, name);
|
||||
let unit = null;
|
||||
let purch = false;
|
||||
if (storeLevel != null) {
|
||||
const res = getPriceAtStore(item, storeLevel);
|
||||
unit = res.price;
|
||||
purch = res.purchasable;
|
||||
}
|
||||
rows.push({ count, name, rank: parseInt(rankStr, 10), price: unit, purchasable: purch });
|
||||
}
|
||||
rows.sort((a, b) => a.rank - b.rank || a.name.localeCompare(b.name));
|
||||
return rows;
|
||||
}
|
||||
|
||||
function printTotalsTableWithPrice(rows, fuseRankLimit) {
|
||||
const h1 = 'Count', h2 = 'Item', h3 = 'Rank', h4 = 'Price';
|
||||
const w1 = Math.max(h1.length, ...rows.map(r => String(r.count).length), 1);
|
||||
const w2 = Math.max(h2.length, ...rows.map(r => r.name.length), 4);
|
||||
const w3 = Math.max(h3.length, ...rows.map(r => String(r.rank).length), 4);
|
||||
const priceStrs = rows.map(r => (r.price != null ? String(r.price) : '-'));
|
||||
const w4 = Math.max(h4.length, ...priceStrs.map(s => s.length), 5);
|
||||
const hr = `+${'-'.repeat(w1 + 2)}+${'-'.repeat(w2 + 2)}+${'-'.repeat(w3 + 2)}+${'-'.repeat(w4 + 2)}+`;
|
||||
const header = `| ${h1.padEnd(w1)} | ${h2.padEnd(w2)} | ${h3.padEnd(w3)} | ${h4.padEnd(w4)} |`;
|
||||
const lines = [];
|
||||
lines.push(hr);
|
||||
lines.push(header);
|
||||
lines.push(hr);
|
||||
let total = 0;
|
||||
for (const r of rows) {
|
||||
const priceDisplay = r.price != null ? String(r.price) : '-';
|
||||
lines.push(`| ${String(r.count).padEnd(w1)} | ${r.name.padEnd(w2)} | ${String(r.rank).padEnd(w3)} | ${priceDisplay.padEnd(w4)} |`);
|
||||
if (r.price != null) total += r.count * r.price;
|
||||
else if (fuseRankLimit != null) total += 0; // treat as owned when depth/leaf rank is specified
|
||||
}
|
||||
lines.push(hr);
|
||||
const footerLabel = 'Total price';
|
||||
const footer = `| ${''.padEnd(w1)} | ${footerLabel.padEnd(w2)} | ${''.padEnd(w3)} | ${String(total).padEnd(w4)} |`;
|
||||
lines.push(footer);
|
||||
lines.push(hr);
|
||||
return lines;
|
||||
}
|
||||
|
||||
function summarizeItem(item) {
|
||||
const lines = [];
|
||||
const typeLabel = item.type ? ` [${item.type}]` : '';
|
||||
|
|
@ -488,8 +615,8 @@ function start() {
|
|||
console.log(summarizeItem(item));
|
||||
} else {
|
||||
const visited = new Set();
|
||||
const left = expandIngredient(index, recipe.ingredients[0], { fuseRankLimit: flags.fuseRankLimit }, visited);
|
||||
const right = expandIngredient(index, recipe.ingredients[1], { fuseRankLimit: flags.fuseRankLimit }, visited);
|
||||
const left = expandIngredient(index, recipe.ingredients[0], { fuseRankLimit: flags.fuseRankLimit, storeLevel: flags.storeLevel, nodeBudget: 5000 }, visited);
|
||||
const right = expandIngredient(index, recipe.ingredients[1], { fuseRankLimit: flags.fuseRankLimit, storeLevel: flags.storeLevel, nodeBudget: 5000 }, visited);
|
||||
console.log(`${item.name} (Rank ${item.rank})${item.type ? ` [${item.type}]` : ''}`);
|
||||
console.log(`Recipe ${idx + 1}: ${recipe.ingredients.map((i) => `${i.name} (R${i.rank})`).join(' + ')}`);
|
||||
const lines = [...printFusionTree(left), ...printFusionTree(right)];
|
||||
|
|
@ -499,7 +626,12 @@ function start() {
|
|||
collectLeaves(right, leaves);
|
||||
if (leaves.length) {
|
||||
console.log('Totals:');
|
||||
printTotalsTable(leaves).forEach((l) => console.log(l));
|
||||
if (flags.storeLevel != null) {
|
||||
const rows = buildTotalsRowsWithPrice(leaves, index, flags.storeLevel);
|
||||
printTotalsTableWithPrice(rows, flags.fuseRankLimit).forEach((l) => console.log(l));
|
||||
} else {
|
||||
printTotalsTable(leaves).forEach((l) => console.log(l));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -517,8 +649,8 @@ function start() {
|
|||
console.log(summarizeItem(item));
|
||||
} else {
|
||||
const visited = new Set();
|
||||
const left = expandIngredient(index, recipe.ingredients[0], { fuseRankLimit: flags.fuseRankLimit }, visited);
|
||||
const right = expandIngredient(index, recipe.ingredients[1], { fuseRankLimit: flags.fuseRankLimit }, visited);
|
||||
const left = expandIngredient(index, recipe.ingredients[0], { fuseRankLimit: flags.fuseRankLimit, storeLevel: flags.storeLevel, nodeBudget: 5000 }, visited);
|
||||
const right = expandIngredient(index, recipe.ingredients[1], { fuseRankLimit: flags.fuseRankLimit, storeLevel: flags.storeLevel, nodeBudget: 5000 }, visited);
|
||||
console.log(`${item.name} (Rank ${item.rank})${item.type ? ` [${item.type}]` : ''}`);
|
||||
console.log(`Recipe ${idx + 1}: ${recipe.ingredients.map((i) => `${i.name} (R${i.rank})`).join(' + ')}`);
|
||||
const lines = [...printFusionTree(left), ...printFusionTree(right)];
|
||||
|
|
@ -528,7 +660,12 @@ function start() {
|
|||
collectLeaves(right, leaves);
|
||||
if (leaves.length) {
|
||||
console.log('Totals:');
|
||||
printTotalsTable(leaves).forEach((l) => console.log(l));
|
||||
if (flags.storeLevel != null) {
|
||||
const rows = buildTotalsRowsWithPrice(leaves, index, flags.storeLevel);
|
||||
printTotalsTableWithPrice(rows, flags.fuseRankLimit).forEach((l) => console.log(l));
|
||||
} else {
|
||||
printTotalsTable(leaves).forEach((l) => console.log(l));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue