diff --git a/js/build.js b/js/build.js index fd3dd82..f079909 100644 --- a/js/build.js +++ b/js/build.js @@ -30,7 +30,6 @@ class Build{ this.level = level; } else if (typeof level === "string") { this.level = level; - errors.push(new IncorrectInput(level, "a number", "level-choice")); } else { errors.push("Level is not a string or number."); } diff --git a/js/builder_graph.js b/js/builder_graph.js index f836986..ffe2d08 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -407,7 +407,10 @@ class BuildAssembleNode extends ComputeNode { input_map.get('guildTome1-input') ]; let weapon = input_map.get('weapon-input'); - let level = input_map.get('level-input'); + let level = parseInt(input_map.get('level-input')); + if (isNaN(level)) { + level = 106; + } let all_none = weapon.statMap.has('NONE'); for (const item of equipments) { diff --git a/js/load_ing.js b/js/load_ing.js index ffc4c05..7652efb 100644 --- a/js/load_ing.js +++ b/js/load_ing.js @@ -9,13 +9,13 @@ let iload_complete = false; let ings; let recipes; -let ingMap; +let ingMap = new Map(); let ingList = []; let recipeMap; let recipeList = []; -let ingIDMap; +let ingIDMap = new Map(); let recipeIDMap; /* @@ -163,39 +163,41 @@ async function load_ing_init() { } function init_ing_maps() { - ingMap = new Map(); recipeMap = new Map(); - ingIDMap = new Map(); recipeIDMap = new Map(); - let ing = Object(); - ing.name = "No Ingredient"; - ing.displayName = "No Ingredient"; - ing.tier = 0; - ing.lvl = 0; - ing.skills = ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING", "COOKING", "ALCHEMISM", "SCRIBING"]; - ing.ids= {}; - ing.itemIDs = {"dura": 0, "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0,}; - ing.consumableIDs = {"dura": 0, "charges": 0}; - ing.posMods = {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0}; - ing.id = 4000; - ingMap.set(ing["displayName"], ing); - ingList.push(ing["displayName"]); - ingIDMap.set(ing["id"], ing["displayName"]); + let ing = { + name: "No Ingredient", + displayName: "No Ingredient", + tier: 0, + lvl: 0, + skills: ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING", "COOKING", "ALCHEMISM", "SCRIBING"], + ids: {}, + itemIDs: {"dura": 0, "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0,}, + consumableIDs: {"dura": 0, "charges": 0}, + posMods: {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0}, + id: 4000 + }; + ingMap.set(ing.displayName, ing); + ingList.push(ing.displayName); + ingIDMap.set(ing.id, ing.displayName); let numerals = new Map([[1, "I"], [2, "II"], [3, "III"], [4, "IV"], [5, "V"], [6, "VI"]]); for (let i = 0; i < 5; i ++) { for (const powderIng of powderIngreds) { - let ing = Object(); - ing.name = "" + damageClasses[i+1] + " Powder " + numerals.get(powderIngreds.indexOf(powderIng) + 1); - ing.displayName = ing.name - ing.tier = 0; - ing.lvl = 0; - ing.skills = ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING"]; - ing.ids = {}; - ing.isPowder = true; - ing.pid = 6*i + powderIngreds.indexOf(powderIng); + let ing = { + name: "" + damageClasses[i+1] + " Powder " + numerals.get(powderIngreds.indexOf(powderIng) + 1), + tier: 0, + lvl: 0, + skills: ["ARMOURING", "TAILORING", "WEAPONSMITHING", "WOODWORKING", "JEWELING"], + ids: {}, + isPowder: true, + pid: 6*i + powderIngreds.indexOf(powderIng), + itemIDs: {"dura": powderIng["durability"], "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0}, + consumableIDs: {"dura": 0, "charges": 0}, + posMods: {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0} + }; ing.id = 4001 + ing.pid; - ing.itemIDs = {"dura": powderIng["durability"], "strReq": 0, "dexReq": 0,"intReq": 0,"defReq": 0,"agiReq": 0,}; + ing.diplayName = ing.name; switch(i) { case 0: ing.itemIDs["strReq"] = powderIng["skpReq"]; @@ -213,24 +215,21 @@ function init_ing_maps() { ing.itemIDs["agiReq"] = powderIng["skpReq"]; break; } - ing.consumableIDs = {"dura": 0, "charges": 0}; - ing.posMods = {"left": 0, "right": 0, "above": 0, "under": 0, "touching": 0, "notTouching": 0}; - ingMap.set(ing["displayName"],ing); - ingList.push(ing["displayName"]); - ingIDMap.set(ing["id"], ing["displayName"]); + ingMap.set(ing.displayName, ing); + ingList.push(ing.displayName); + ingIDMap.set(ing.id, ing.displayName); } } for (const ing of ings) { - ingMap.set(ing["displayName"], ing); - ingList.push(ing["displayName"]); - ingIDMap.set(ing["id"], ing["displayName"]); + ingMap.set(ing.displayName, ing); + ingList.push(ing.displayName); + ingIDMap.set(ing.id, ing.displayName); } for (const recipe of recipes) { - recipeMap.set(recipe["name"], recipe); - recipeList.push(recipe["name"]); - recipeIDMap.set(recipe["id"],recipe["name"]); + recipeMap.set(recipe.name, recipe); + recipeList.push(recipe.name); + recipeIDMap.set(recipe.id, recipe.name); } - console.log(ingMap); } diff --git a/js/skillpoints.js b/js/skillpoints.js index 5748a00..f2fa858 100644 --- a/js/skillpoints.js +++ b/js/skillpoints.js @@ -1,98 +1,116 @@ +/** + * Apply skillpoint bonuses from an item. + * Also applies set deltas. + * Modifies the skillpoints array. + */ +function apply_skillpoints(skillpoints, item, activeSetCounts) { + for (let i = 0; i < 5; i++) { + skillpoints[i] += item.skillpoints[i]; + } + + const setName = item.set; + if (setName) { // undefined/null means no set. + let setCount = activeSetCounts.get(setName); + let old_bonus = {}; + if (setCount) { + old_bonus = sets.get(setName).bonuses[setCount-1]; + activeSetCounts.set(setName, setCount + 1); + } + else { + setCount = 0; + activeSetCounts.set(setName, 1); + } + const new_bonus = sets.get(setName).bonuses[setCount]; + //let skp_order = ["str","dex","int","def","agi"]; + for (const i in skp_order) { + const delta = (new_bonus[skp_order[i]] || 0) - (old_bonus[skp_order[i]] || 0); + skillpoints[i] += delta; + } + } +} + +/** + * Apply skillpoints until this item can be worn. + * Also applies set deltas. + * Confusingly, does not modify the skillpoints array. + * Instead, return an array of deltas. + */ +function apply_to_fit(skillpoints, item, skillpoint_min, activeSetCounts) { + let applied = [0, 0, 0, 0, 0]; + for (let i = 0; i < 5; i++) { + if (item.skillpoints[i] < 0 && skillpoint_min[i]) { + const unadjusted = skillpoints[i] + item.skillpoints[i]; + const delta = skillpoint_min[i] - unadjusted; + if (delta > 0) { + applied[i] += delta; + } + } + if (item.reqs[i] == 0) continue; + skillpoint_min[i] = Math.max(skillpoint_min[i], item.reqs[i] + item.skillpoints[i]); + const req = item.reqs[i]; + const cur = skillpoints[i]; + if (req > cur) { + const diff = req - cur; + applied[i] += diff; + } + } + + const setName = item.set; + if (setName) { // undefined/null means no set. + const setCount = activeSetCounts.get(setName); + if (setCount) { + const old_bonus = sets.get(setName).bonuses[setCount-1]; + const new_bonus = sets.get(setName).bonuses[setCount]; + //let skp_order = ["str","dex","int","def","agi"]; + for (const i in skp_order) { + const set_delta = (new_bonus[skp_order[i]] || 0) - (old_bonus[skp_order[i]] || 0); + if (set_delta < 0 && skillpoint_min[i]) { + const unadjusted = skillpoints[i] + set_delta; + const delta = skillpoint_min[i] - unadjusted; + if (delta > 0) { + applied[i] += delta; + } + } + } + } + } + return applied; +} + function calculate_skillpoints(equipment, weapon) { + // const start = performance.now(); // Calculate equipment equipping order and required skillpoints. // Return value: [equip_order, best_skillpoints, final_skillpoints, best_total]; let fixed = []; let consider = []; let noboost = []; let crafted = []; + weapon.skillpoints = weapon.get('skillpoints'); + weapon.reqs = weapon.get('reqs'); + weapon.set = weapon.get('set'); for (const item of equipment) { + item.skillpoints = item.get('skillpoints'); + item.reqs = item.get('reqs'); + item.set = item.get('set'); if (item.get("crafted")) { crafted.push(item); } - else if (item.get("reqs").every(x => x === 0) && item.get("skillpoints").every(x => x >= 0)) { + // TODO hack: We will treat ALL set items as unsafe :( + else if (item.set !== null) { + consider.push(item); + } + else if (item.get("reqs").every(x => x === 0) && item.skillpoints.every(x => x >= 0)) { // All reqless item without -skillpoints. fixed.push(item); } - // TODO hack: We will treat ALL set items as unsafe :( - else if (item.get("skillpoints").every(x => x === 0) && item.get("set") === null) { + else if (item.skillpoints.every(x => x <= 0)) { noboost.push(item); } else { consider.push(item); } } - function apply_skillpoints(skillpoints, item, activeSetCounts) { - for (let i = 0; i < 5; i++) { - skillpoints[i] += item.get("skillpoints")[i]; - } - const setName = item.get("set"); - if (setName) { // undefined/null means no set. - let setCount = activeSetCounts.get(setName); - let old_bonus = {}; - if (setCount) { - old_bonus = sets.get(setName).bonuses[setCount-1]; - activeSetCounts.set(setName, setCount + 1); - } - else { - setCount = 0; - activeSetCounts.set(setName, 1); - } - const new_bonus = sets.get(setName).bonuses[setCount]; - //let skp_order = ["str","dex","int","def","agi"]; - for (const i in skp_order) { - const delta = (new_bonus[skp_order[i]] || 0) - (old_bonus[skp_order[i]] || 0); - skillpoints[i] += delta; - } - } - } - - function apply_to_fit(skillpoints, item, skillpoint_min, activeSetCounts) { - let applied = [0, 0, 0, 0, 0]; - let total = 0; - for (let i = 0; i < 5; i++) { - if (item.get("skillpoints")[i] < 0 && skillpoint_min[i]) { - const unadjusted = skillpoints[i] + item.get("skillpoints")[i]; - const delta = skillpoint_min[i] - unadjusted; - if (delta > 0) { - applied[i] += delta; - total += delta; - } - } - if (item.get("reqs")[i] == 0) continue; - skillpoint_min[i] = Math.max(skillpoint_min[i], item.get("reqs")[i] + item.get("skillpoints")[i]); - const req = item.get("reqs")[i]; - const cur = skillpoints[i]; - if (req > cur) { - const diff = req - cur; - applied[i] += diff; - total += diff; - } - } - - const setName = item.get("set"); - if (setName) { // undefined/null means no set. - const setCount = activeSetCounts.get(setName); - if (setCount) { - const old_bonus = sets.get(setName).bonuses[setCount-1]; - const new_bonus = sets.get(setName).bonuses[setCount]; - //let skp_order = ["str","dex","int","def","agi"]; - for (const i in skp_order) { - const set_delta = (new_bonus[skp_order[i]] || 0) - (old_bonus[skp_order[i]] || 0); - if (set_delta < 0 && skillpoint_min[i]) { - const unadjusted = skillpoints[i] + set_delta; - const delta = skillpoint_min[i] - unadjusted; - if (delta > 0) { - applied[i] += delta; - total += delta; - } - } - } - } - } - - return [applied, total]; - } // Separate out the no req items and add them to the static skillpoint base. let static_skillpoints_base = [0, 0, 0, 0, 0] @@ -101,7 +119,7 @@ function calculate_skillpoints(equipment, weapon) { apply_skillpoints(static_skillpoints_base, item, static_activeSetCounts); } - let best = consider.concat(noboost); + let best = consider; let final_skillpoints = static_skillpoints_base.slice(); let best_skillpoints = [0, 0, 0, 0, 0]; let best_total = Infinity; @@ -110,100 +128,175 @@ function calculate_skillpoints(equipment, weapon) { let allFalse = [0, 0, 0, 0, 0]; if (consider.length > 0 || noboost.length > 0 || crafted.length > 0) { // Try every combination and pick the best one. - for (let permutation of perm(consider)) { - let activeSetCounts = new Map(static_activeSetCounts); + const [root, terminal, sccs] = construct_scc_graph(consider); + const end_checks = crafted.concat(noboost); + end_checks.push(weapon); - let has_skillpoint = allFalse.slice(); - - permutation = permutation.concat(noboost); - - let skillpoints_applied = [0, 0, 0, 0, 0]; - // Complete slice is a shallow copy. - let skillpoints = static_skillpoints_base.slice(); - - let total_applied = 0; - - let result; - let needed_skillpoints; - let total_diff; - for (const item of permutation) { - result = apply_to_fit(skillpoints, item, has_skillpoint, activeSetCounts); - needed_skillpoints = result[0]; - total_diff = result[1]; - - for (let i = 0; i < 5; ++i) { - skillpoints_applied[i] += needed_skillpoints[i]; - skillpoints[i] += needed_skillpoints[i]; - } - apply_skillpoints(skillpoints, item, activeSetCounts); - total_applied += total_diff; - if (total_applied >= best_total) { - break; - } - } - + function check_end(skillpoints_applied, skillpoints, activeSetCounts, total_applied) { // Crafted skillpoint does not count initially. - for (const item of crafted) { - //console.log(item) - result = apply_to_fit(skillpoints, item, allFalse.slice(), activeSetCounts); - //console.log(result) - needed_skillpoints = result[0]; - total_diff = result[1]; + for (const item of end_checks) { + const needed_skillpoints = apply_to_fit(skillpoints, item, + [false, false, false, false, false], activeSetCounts); for (let i = 0; i < 5; ++i) { - skillpoints_applied[i] += needed_skillpoints[i]; - skillpoints[i] += needed_skillpoints[i]; + const skp = needed_skillpoints[i] + skillpoints_applied[i] += skp; + skillpoints[i] += skp; + total_applied += skp; } - total_applied += total_diff; - } - - if (total_applied >= best_total) { - continue; - } - - let pre = skillpoints.slice(); - result = apply_to_fit(skillpoints, weapon, allFalse.slice(), activeSetCounts); - needed_skillpoints = result[0]; - total_diff = result[1]; - for (let i = 0; i < 5; ++i) { - skillpoints_applied[i] += needed_skillpoints[i]; - skillpoints[i] += needed_skillpoints[i]; - } - - apply_skillpoints(skillpoints, weapon, activeSetCounts); - total_applied += total_diff; - - // Applying crafted item skill points last. - for (const item of crafted) { - apply_skillpoints(skillpoints, item, activeSetCounts); - //total_applied += total_diff; - } - - if (total_applied < best_total) { - best = permutation; - final_skillpoints = skillpoints; - best_skillpoints = skillpoints_applied; - best_total = total_applied; - best_activeSetCounts = activeSetCounts; + if (best_total < total_applied) { return -1; } } + return total_applied; } + function permute_check(idx, _applied, _skillpoints, _sets, _has, _total_applied, order) { + const {nodes, children} = sccs[idx]; + if (nodes[0] === terminal) { + const total = check_end(_applied, _skillpoints, _sets, _total_applied); + if (total !== -1 && total < best_total) { + final_skillpoints = _skillpoints; + best_skillpoints = _applied; + best_total = total; + best_activeSetCounts = _sets; + best = order; + } + return; + } + for (let permutation of perm(nodes)) { + const skillpoints_applied = _applied.slice(); + const skillpoints = _skillpoints.slice(); + const activeSetCounts = new Map(_sets); + const has_skillpoint = _has.slice(); + let total_applied = _total_applied; + let short_circuit = false; + for (const {item} of permutation) { + needed_skillpoints = apply_to_fit(skillpoints, item, has_skillpoint, activeSetCounts); + for (let i = 0; i < 5; ++i) { + skp = needed_skillpoints[i]; + skillpoints_applied[i] += skp; + skillpoints[i] += skp; + total_applied += skp; + } + if (total_applied >= best_total) { + short_circuit = true; + break; // short circuit failure + } + apply_skillpoints(skillpoints, item, activeSetCounts); + } + if (short_circuit) { continue; } + permute_check(idx+1, skillpoints_applied, skillpoints, activeSetCounts, has_skillpoint, total_applied, order.concat(permutation.map(x => x.item))); + } + } + // skip root. + permute_check(1, best_skillpoints, final_skillpoints, best_activeSetCounts, allFalse.slice(), 0, []); + + // add extra sp bonus + apply_skillpoints(final_skillpoints, weapon, best_activeSetCounts); + // Applying crafted item skill points last. + for (const item of crafted) { + apply_skillpoints(final_skillpoints, item, best_activeSetCounts); + } } else { best_total = 0; - result = apply_to_fit(final_skillpoints, weapon, allFalse.slice(), best_activeSetCounts); - needed_skillpoints = result[0]; - total_diff = result[1]; + needed_skillpoints = apply_to_fit(final_skillpoints, weapon, allFalse.slice(), best_activeSetCounts); for (let i = 0; i < 5; ++i) { - best_skillpoints[i] += needed_skillpoints[i]; - final_skillpoints[i] += needed_skillpoints[i]; + const skp = needed_skillpoints[i]; + best_skillpoints[i] += skp; + final_skillpoints[i] += skp; + best_skillpoints += skp; } apply_skillpoints(final_skillpoints, weapon, best_activeSetCounts); - best_total += total_diff; } - let equip_order = fixed.concat(best).concat(crafted); + let equip_order = fixed.concat(best).concat(noboost).concat(crafted); // best_skillpoints: manually assigned (before any gear) // final_skillpoints: final totals (5 individ) // best_total: total skillpoints assigned (number) + // const end = performance.now(); + // const output_msg = `skillpoint calculation took ${(end-start)/ 1000} seconds.`; + // console.log(output_msg); return [equip_order, best_skillpoints, final_skillpoints, best_total, best_activeSetCounts]; } + +function construct_scc_graph(items_to_consider) { + let nodes = []; + let terminal_node = { + item: null, + children: [], + parents: nodes, + visited: false, + assigned: false, + scc: null + }; + let root_node = { + item: null, + children: nodes, + parents: [], + visited: false, + assigned: false, + scc: null + }; + for (const item of items_to_consider) { + nodes.push({item: item, children: [terminal_node], parents: [root_node], visited: false, assigned: false, scc: null}); + } + // Dependency graph construction. + for (const node_a of nodes) { + const {item: a, children: a_children} = node_a; + for (const node_b of nodes) { + const {item: b, parents: b_parents} = node_b; + for (let i = 0; i < 5; ++i) { + if (a.skillpoints[i] > 0 && (a.reqs[i] < b.reqs[i] || b.skillpoints[i] < 0)) { + a_children.push(node_b); + b_parents.push(node_a); + break; + } + } + } + } + const res = [] + /* + * SCC graph construction. + * https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm + */ + function visit(u, res) { + if (u.visited) { return; } + u.visited = true; + for (const child of u.children) { + if (!child.visited) { visit(child, res); } + } + res.push(u); + } + visit(root_node, res); + res.reverse(); + const sccs = []; + function assign(node, cur_scc) { + if (node.assigned) { return; } + cur_scc.nodes.push(node); + node.scc = cur_scc; + node.assigned = true; + for (const parent of node.parents) { + assign(parent, cur_scc); + } + } + for (const node of res) { + if (node.assigned) { continue; } + const cur_scc = { + nodes: [], + children: new Set(), + parents: new Set() + }; + assign(node, cur_scc); + sccs.push(cur_scc); + } + for (const scc of sccs) { + for (const node of scc.nodes) { + for (const child of node.children) { + scc.children.add(child.scc); + } + for (const parent of node.parents) { + scc.parents.add(parent.scc); + } + } + } + return [root_node, terminal_node, sccs]; +}