diff --git a/TODO.txt b/TODO.txt index 86770f5..df9e4f7 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,5 +1,6 @@ Item identifications - Base max/min, rounding correctly, and handling pre-ID + - Appears to be working correctly. Need to continue checking. Damage calculation - General calculation framework @@ -17,3 +18,1393 @@ Build encoding LATER STUFF: - Custom items integration - wynndata parse? And/or google sheets +- Crafted Items + +"use strict"; +$(function () { + // some globals + let globalItemDb = {}; + let globalHashItemDb = {}; + let dropdowns = [ + ["helmet_select", "helmet"], + ["chestplate_select", "chestplate"], + ["leggings_select", "leggings"], + ["boots_select", "boots"], + ["ring0_select", "ring"], + ["ring1_select", "ring"], + ["bracelet_select", "bracelet"], + ["necklace_select", "necklace"], + ["weapon_select", "weapon"] + ]; + let correctOrder = ["helmet", "chestplate", "leggings", "boots", "ring", "ring", "bracelet", "necklace", "weapon"]; + // [suffix, id] + let idMap = { + damage: { + spellPercent: ["%", "Spell Damage"], + spellRaw: ["", "Neutral Spell Damage"], + meleePercent: ["%", "Main Attack Damage"], + meleeRaw: ["", "Main Attack Neutral Damage"], + earth: ["%", "Earth Damage"], + thunder: ["%", "Thunder Damage"], + water: ["%", "Water Damage"], + fire: ["%", "Fire Damage"], + air: ["%", "Air Damage"], + }, + regen: { + healthPercent: ["%", "Health Regen"], + healthRaw: ["", "Health Regen"], + mana: ["/4s", "Mana Regen"], + soulPoint: ["%", "Soul Pint Regen"] + }, + steal: { + mana: ["/4s", "Mana Steal"], + health: ["/4s", "Life Steal"] + }, + others: { + attackSpeedBonus: [" Tier", "Attack Speed"], + exploding: ["%", "Exploding"], + healthBonus: ["", "Health"], + jumpHeight: ["", "Jump Height"], + lootBonus: ["%", "Loot Bonus"], + lootQuality: ["%", "Loot Quality"], + poison: ["/3s", "Poison"], + reflection: ["%", "Reflection"], + sprint: ["%", "Sprint"], + sprintRegen: ["%", "Sprint Regen"], + stealing: ["%", "Stealing"], + thorns: ["%", "Thorns"], + walkSpeed: ["%", "Walk Speed"], + xpBonus: ["%", "XP Bonus"] + } + }; + // min dmg, max dmg, conversion, +def, -def, powder code + let powderStats = { + E: [["earth","air"],[3,6,17,2,1,'0'],[6,9,21,4,2,'1'],[8,14,25,8,3,'2'],[11,16,31,14,5,'3'],[15,18,38,22,9,'4'],[18,22,46,30,13,'5']], + T: [["thunder","earth"],[1,8,9,3,1,'6'],[1,13,11,5,1,'7'],[2,18,14,9,2,'8'],[3,24,17,14,4,'9'],[3,32,22,20,7,'A'],[5,40,28,28,10,'B']], + W: [["water","thunder"],[3,4,13,3,1,'C'],[4,7,15,6,1,'D'],[6,10,17,11,2,'E'],[8,12,21,18,4,'F'],[11,14,26,28,7,'G'],[13,17,32,40,10,'H']], + F: [["fire","water"],[2,5,14,3,1,'J'],[4,8,16,5,2,'K'],[6,10,19,9,3,'L'],[9,13,24,16,5,'M'],[12,16,30,25,9,'N'],[15,19,37,36,13,'P']], + A: [["air","fire"],[2,6,11,3,1,'Q'],[4,9,14,6,2,'R'],[7,10,17,10,3,'S'],[9,13,22,16,5,'T'],[13,18,28,24,9,'U'],[16,18,35,34,13,'W']] + }; + let skillBounsPct = [ + 0.0, 1.0, 2.0, 2.9, 3.9, 4.9, 5.8, 6.7, 7.7, 8.6, + 9.5,10.4,11.3,12.2,13.1,13.9,14.8,15.7,16.5,17.3, + 18.2,19.0,19.8,20.6,21.4,22.2,23.0,23.8,24.6,25.3, + 26.1,26.8,27.6,28.3,29.0,29.8,30.5,31.2,31.9,32.6, + 33.3,34.0,34.6,35.3,36.0,36.6,37.3,37.9,38.6,39.2, + 39.9,40.5,41.1,41.7,42.3,42.9,43.5,44.1,44.7,45.3, + 45.8,46.4,47.0,47.5,48.1,48.6,49.2,49.7,50.3,50.8, + 51.3,51.8,52.3,52.8,53.4,53.9,54.3,54.8,55.3,55.8, + 56.3,56.8,57.2,57.7,58.1,58.6,59.1,59.5,59.9,60.4, + 60.8,61.3,61.7,62.1,62.5,62.9,63.3,63.8,64.2,64.6, + 65.0,65.4,65.7,66.1,66.5,66.9,67.3,67.6,68.0,68.4, + 68.7,69.1,69.4,69.8,70.1,70.5,70.8,71.2,71.5,71.8, + 72.2,72.5,72.8,73.1,73.5,73.8,74.1,74.4,74.7,75.0, + 75.3,75.6,75.9,76.2,76.5,76.8,77.1,77.3,77.6,77.9, + 78.2,78.4,78.7,79.0,79.2,79.5,79.8,80.0,80.3,80.5,80.8]; + let elemIcons = { + earth: "✤", + thunder: "✦", + water: "❉", + fire: "✹", + air: "❋" + }; + let elemColours = { + earth: "dark_green", + thunder: "yellow", + water: "aqua", + fire: "red", + air: "white" + }; + let elementList = ["earth", "thunder", "water", "fire", "air"]; + let skillList = ["strength", "dexterity", "intelligence", "defense", "agility"]; + let itemListBox = $("#item_list_box"); + let currentReq = { + req: { + strength: 0, + dexterity: 0, + intelligence: 0, + defense: 0, + agility: 0 + }, + bonus: { + strength: 0, + dexterity: 0, + intelligence: 0, + defense: 0, + agility: 0 + }, + order: [] + }; // for the wearables + let realReq = { + req: { + strength: 0, + dexterity: 0, + intelligence: 0, + defense: 0, + agility: 0 + }, + bonus: { + strength: 0, + dexterity: 0, + intelligence: 0, + defense: 0, + agility: 0 + }, + order: [] + }; // for the wearables+wep + let powderList = {helmet: [], chestplate: [], leggings: [], boots: [], weapon: []}; + + console.log("window ready"); + // load item db + loadItemDb().then(itemDb => { + itemDb.forEach(item => globalItemDb[item.displayName || item.info.name] = item); + itemDb.forEach(item => globalHashItemDb[item.info.hash] = item); + + // add powder button handlers + $("span.powder").click(e => { + let type = e.target.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.id.replace("_powders", ""); + let powder = e.target.classList[1].substr(7).toUpperCase(); + let item = globalItemDb[$(`#${type}_select > select`).val()]; + let sockets = item.info.sockets; + let powderArray = powderList[type]; + for (let i = 0; i < sockets; i++) { + if (powderArray.length <= i) { + powderArray.push(powder); + break; + } + if (!powderArray[i]) { + powderArray[i] = powder; + break; + } + } + let powderBox = $(`#${type}_powders`); + let powderListBox = powderBox.find("div > div.powder_list"); + renderSockets(powderListBox, type, realReq); + }); + $('.sp_input').change(() => { + let build = dropdowns.map(dropdown => { + let select = $("#" + dropdown[0] + " > select"); + let name = select.val(); + return {name, powder: powderList[dropdown[1]]}; + }); + let total = $('.sp_input').map((i, v) => 1*v.value).toArray().reduce((a, b) => a + b); + $('#sp_remaining').html(`Assign Skill Points (${200 - total} remaining):`); + renderBuild(calculateBuild(build)); + }); + $('.reset_button').click(() => { + $('.sp_input').map((i, v) => v.value = v.getAttribute("min")); + $('.sp_input[data-slot=0]').change(); + }); + $('#copy_btn').click(function (e) { + let copyText = $('#link_box'); + copyText.select(); + document.execCommand("copy"); + $(e.target).html('Copied!'); + }); + + // load into dropdown menus + let readySelects = 0; + dropdowns.forEach((dropdown) => { + let select = $("#" + dropdown[0] + " > select"); + itemDb.filter((x) => (x.info.type || x.accessoryType).toLowerCase() === dropdown[1] || x.category.toLowerCase() === dropdown[1]) + .map((x) => x.displayName || x.info.name).sort().forEach(name => { + select.append(``); + }); + select.change((e, o) => { + let build = dropdowns.map(dropdown => { + let select = $("#" + dropdown[0] + " > select"); + let name = select.val(); + return {name, powder: powderList[dropdown[1]]}; + }); + let calculatedBuild = calculateBuild(build); + let parentId = e.target.parentElement.id; + let type = parentId.replace("_select", ""); + let index = correctOrder.indexOf(type.replace(/\d/, "")); + if (o && !o.deferCalc) { + if (type !== "weapon") { + currentReq = findStatReq(calculatedBuild.items); + realReq = JSON.parse(JSON.stringify(currentReq)); + } + if (calculatedBuild.items[8]) { + // take the skills we have and see what else do we need + let weaponItem = calculatedBuild.items[8]; + skillList.forEach(skill => { + let ownedPoints = currentReq.req[skill] + currentReq.bonus[skill]; + let diff; + if (!weaponItem.req[skill]) { + diff = 0; + } else if (weaponItem.req[skill] > ownedPoints) { + diff = weaponItem.req[skill] - ownedPoints; + } else { + diff = 0; + } + realReq.req[skill] = currentReq.req[skill] + diff; + }); + let bonus = {}; + skillList.forEach(skill => { + bonus[skill] = currentReq.bonus[skill] + weaponItem.base.skill[skill]; + }); + realReq.bonus = bonus; + realReq.order = currentReq.order; + } + skillList.forEach((skill, i) => { + let spInput = $(`.sp_input[data-slot=${i}]`); + spInput.attr("min", realReq.req[skill]); + spInput.val(realReq.req[skill]); + }); + } + let item = calculatedBuild.items[index]; + let box = $(`#${type}_div`).empty(); + let powderBox = $(`#${type}_powders`); + if (item && item.info.sockets) { + box.append(generateItemBox(item, false)); + powderBox.show(); + powderBox.find("div > p.large").text(item.displayName || item.info.name).attr("class", "large item_name " + item.info.tier.toLowerCase()); + let powderListBox = powderBox.find("div > div.powder_list"); + renderSockets(powderListBox, type); + } else { + powderBox.hide(); + renderBuild(calculatedBuild); + } + // $(`#${type}_div`).empty(); + }); + select.on("chosen:ready", () => { + ++readySelects; + if (readySelects == 9) { + let query = document.location.search.substr(1); + if (query.split('-').length === 14) { + let _s = query.split('-'); + let itemNames = _s.slice(0, 9).map(h => (globalHashItemDb[h] || {info: {name: ""}}).info.name); + let powders = []; + let selects = $('.item_select > select'); + for (let i = 0; i < 5; ++i) { + let _pows = _s[9 + i]; + let pows = []; + for (let j = 0; j < _pows.length; ++j) { + let idx = '0123456789ABCDEFGHJKLMNPQRSTUW'.indexOf(_pows[j]); + pows.push('ETWFA'[Math.floor(idx/6)]+(1+(idx%6))); + } + powders.push(pows); + } + for (let i = 0; i < 9; ++i) { + $(selects[i]).val(itemNames[i]).trigger('chosen:updated'); + } + powderList.helmet = powders[0]; + powderList.chestplate = powders[1]; + powderList.leggings = powders[2]; + powderList.boots = powders[3]; + powderList.weapon = powders[4]; + for (let i = 0; i < 5; ++i) { + for (let j = 0; j < powders[i].length; ++j) { + $(`.powder_choices:eq(${i})`).find(`.powder_${powders[i][j].toLowerCase()}`).click(); + } + } + $(selects[0]).trigger("change", {deferCalc: true}); + $(selects[1]).trigger("change", {deferCalc: true}); + $(selects[2]).trigger("change", {deferCalc: true}); + $(selects[3]).trigger("change", {deferCalc: true}); + $(selects[7]).trigger("change", {deferCalc: false}); + $(selects[8]).trigger("change", {deferCalc: false}); + + window.selects = selects; + $('.reset_button').click(); + } + } + }); + select.chosen({width: "100%"}).addClass("col-md-3 col-sm-4"); + }); + $(window).resize(resetPos); + }); + + function spellDamage(build, totalSkills, spellMultiplier, elemMultiplier) { + let weaponDamage = JSON.parse(JSON.stringify(build.base.damage)); + for (let i in weaponDamage) { + weaponDamage[i] = weaponDamage[i].split("-").map(x => 1*x); + } + // consider wep powders, move elemMultiplier.neutral to the respective elem + powderList.weapon.forEach(powder => { + if (!powder || !elemMultiplier.neutral) { + return; + } + let stat = powderStats[powder[0]][powder[1]]; + let conversion = stat[2]; + if (elemMultiplier.neutral > conversion) { + elemMultiplier.neutral -= conversion; + elemMultiplier[powderStats[powder[0]][0][0]] += conversion; + } else { + elemMultiplier[powderStats[powder[0]][0][0]] += elemMultiplier.neutral; + elemMultiplier.neutral = 0; + } + }); + // base damage is `neutral * multiplier + elemDamageOnWep` then multiply by attack speed multiplier, then spell multiplier + let base = {}; + let attackSpeedMultiplier = {SUPER_SLOW: 0.51, VERY_SLOW: 0.83, SLOW: 1.5, NORMAL: 2.05, FAST: 2.5, VERY_FAST: 3.1, SUPER_FAST: 4.3}[build.base.attackSpeed]; + base.neutral = weaponDamage.neutral.map(x => x * elemMultiplier.neutral / 100 * attackSpeedMultiplier * spellMultiplier / 100); + elementList.forEach(elem => { + base[elem] = [0, 1].map(i => (weaponDamage.neutral[i] * elemMultiplier[elem]) / 100 + weaponDamage[elem][i]) + .map(x => x * attackSpeedMultiplier * spellMultiplier / 100); + }); + // console.log(base); + // multiply by (1 + spellDamage + strengthPercentage + elemSkillPercentage + elemDamagePercentage) + let normal = {}; + normal.neutral = base.neutral.map(x => (x * (100 + build.identification.damage.spellPercent + skillBounsPct[totalSkills[0]]) + build.identification.damage.spellRaw * spellMultiplier) / 100).map(Math.floor); + normal.total = [normal.neutral[0], normal.neutral[1]]; + elementList.forEach((elem, i) => { + normal[elem] = base[elem].map(x => x * (100 + build.identification.damage.spellPercent + skillBounsPct[totalSkills[0]] + skillBounsPct[totalSkills[i]] + build.identification.damage[elem]) / 100).map(Math.floor); + normal.total = sum(normal.total, normal[elem]); + }); + // for crit hits change the 1 to 2 + let critical = {}; + critical.neutral = base.neutral.map(x => (x * (200 + build.identification.damage.spellPercent + skillBounsPct[totalSkills[0]]) + build.identification.damage.spellRaw * spellMultiplier) / 100).map(Math.floor); + critical.total = [critical.neutral[0], critical.neutral[1]]; + elementList.forEach((elem, i) => { + critical[elem] = base[elem].map(x => x * (200 + build.identification.damage.spellPercent + skillBounsPct[totalSkills[0]] + skillBounsPct[totalSkills[i]] + build.identification.damage[elem]) / 100).map(Math.floor); + critical.total = sum(critical.total, critical[elem]); + }); + let args = [build.identification.damage.spellPercent, elementList.map(elem => build.identification.damage[elem]), skillBounsPct[totalSkills[0]], totalSkills.map(x => skillBounsPct[x])]; + return {normal, critical, args}; + } + + function renderSockets(powderListBox, type) { + let box = $(`#${type}_div`).empty(); + let powderBox = $(`#${type}_powders`); + let index = correctOrder.indexOf(type); + + let build = dropdowns.map(dropdown => { + let select = $("#" + dropdown[0] + " > select"); + let name = select.val(); + return {name, powder: powderList[dropdown[1]]}; + }); + let calculatedBuild = calculateBuild(build); + let item = calculatedBuild.items[index]; + let sockets = item.info.sockets; + if (sockets) { + box.append(generateItemBox(item, false)); + powderBox.show(); + powderBox.find("div > p.large").text(item.displayName || item.info.name).attr("class", "large item_name " + item.info.tier.toLowerCase()); + let powderListBox = powderBox.find("div > div.powder_list"); + powderListBox.empty(); + for (let i = 0; i < sockets; i++) { + let powderBox; + if (powderList[type].length <= i || !powderList[type][i]) { + powderBox = $(`
`); + } else { + powderBox = $(`
`); + powderBox.click(() => { + powderList[type][i] = undefined; + renderSockets(powderListBox, type); + }); + } + powderListBox.append(powderBox); + } + } else { + powderBox.hide(); + } + renderBuild(calculatedBuild); + } + + async function loadItemDb() { + // load from server lol, maybe cached tho + return await fetch("/build/itemdb.json").then(data => data.json()).then(json => json.itemDB) + } + + /** + * Calculates the build"s stats according to the items and skill points allocated. + * Normally we use the item"s base stats as outlined in itemDB, but stats can be overridden, + * even if the override is outside of the normal range we won"t check + * @param items The items equipped as an array + */ + function calculateBuild(items) { + // check for the correct types [helm, chest, leggings, boots, ring, ring, bracelet, necklace, wep] + let totalIdentifications = {}; + let totalBase = {}; + let totalReq = {quest: []}; + let realItems = []; + let conversion = {neutral: 100, earth: 0, thunder: 0, water: 0, fire: 0, air: 0}; + for (let i = 0; i < 9; i++) { + if (!items[i]) { + realItems.push(null); + continue; + } + let name = items[i].name; + let item = globalItemDb[name]; + if (!item) { + realItems.push(null); + if (name.length) { + console.warn("Item " + name + " not found"); + } + continue; + } + if ((item.info.type || item.accessoryType).toLowerCase() !== correctOrder[i] && item.category.toLowerCase() !== correctOrder[i]) { + return null; + } + // must clone since we don"t want to override the base item + let realItem = JSON.parse(JSON.stringify(item)); + // this item passed the check, populate it with the real one + override(realItem.identification, items.override); + // powders + if (Array.isArray(items[i].powder)) { + let powders = items[i].powder.slice(0, realItem.info.sockets); + realItem.powders = powders; + + for (let j = 0; j < powders.length; j++) { + if (!powders[j]) { + continue; + } + let elem = powders[j][0]; + let tier = powders[j][1]; + let elemName = powderStats[elem][0][0]; + let weakElemName = powderStats[elem][0][1]; + let powder = powderStats[elem][tier]; + let conversionPct = Math.min(conversion.neutral, powder[2]); + if (i === 8) { + // weapon, modify conversion and damage + conversion.neutral -= conversionPct; + conversion[elemName] += conversionPct; + let originalDamage = realItem.base.damage[elemName] || "0-0"; + let [lower, upper] = originalDamage.split("-").map(x => parseInt(x)); + // console.log(lower, upper, originalDamage); + lower += powder[0]; + upper += powder[1]; + realItem.base.damage[elemName] = lower + "-" + upper; + switch (realItem.info.type) { + case "Relik": + realItem.req.class = "Shaman"; + break; + case "Wand": + realItem.req.class = "Mage"; + break; + case "Spear": + realItem.req.class = "Warrior"; + break; + case "Bow": + realItem.req.class = "Archer"; + break; + case "Dagger": + realItem.req.class = "Assassin"; + break; + } + } else { + // non weapon, modify defense + realItem.base.defense[elemName] += powder[3]; + realItem.base.defense[weakElemName] -= powder[4]; + } + } + } + // this goes after all the modifications + totalIdentifications = sum(totalIdentifications, realItem.identification); + totalBase = sum(totalBase, realItem.base); + totalReq = max(totalReq, realItem.req); + + realItems.push(realItem); + } + // adjust health according to lvl req + totalBase.health = (totalBase.health || 0) + Math.max((totalReq.level || 1), 101) * 5 + 5; + + if (realItems[8]) { + let totalConverted = [0, 0]; + let displayDamage = JSON.parse(JSON.stringify(totalBase.damage)); + for (let i in displayDamage) { + displayDamage[i] = displayDamage[i].split("-").map(x => 1*x); + } + elementList.forEach(elem => { + if (conversion[elem]) { + let converted = totalBase.damage.neutral.split("-").map(x => Math.round(x * conversion[elem] / 100)); + [0, 1].forEach(i => { + if (converted[i] + totalConverted[i] > displayDamage.neutral[i]) { + converted[i] = displayDamage.neutral[i] - totalConverted[i]; + } + }); + displayDamage[elem] = sum(displayDamage[elem], converted); + totalConverted[0] += converted[0]; + totalConverted[1] += converted[1]; + } + }); + displayDamage.neutral[0] -= totalConverted[0]; + displayDamage.neutral[1] -= totalConverted[1]; + realItems[8].displayDamage = displayDamage; + return { + identification: totalIdentifications, + base: totalBase, + req: totalReq, + items: realItems, + conversion, + displayDamage + }; + } + return { + identification: totalIdentifications, + base: totalBase, + req: totalReq, + items: realItems, + conversion + }; + } + + function renderBuild(build) { + if (!build.items.filter(x => x).length) { + // empty build + return; + } + let buildCode = build.items.map(x => x ? x.info.hash : ""); + [["helmet", 0], ["chestplate", 1], ["leggings", 2], ["boots", 3], ["weapon", 8]].forEach(v => { + let type = v[0]; + let index = v[1]; + let item = build.items[index]; + if (item) { + let powderCode = ""; + let sockets = Math.min(powderList[type].length, item.info.sockets); + for (let i = 0; i < sockets; i++) { + let powder = powderList[type][i]; + powderCode += powderStats[powder[0]][powder[1]][5]; + } + buildCode.push(powderCode); + } else { + buildCode.push(""); + } + }); + $("#link_box").val(`${location.protocol}//${location.host}${location.pathname}?${buildCode.join("-")}`); + history.replaceState({}, "", "?" + buildCode.join("-")); + itemListBox.empty(); + build.items.forEach(item => { + if (item !== null) { + itemListBox.append(generateItemBox(item, true)); + } + }); + resetPos(); + // requirements + let req = build.req; + let reqBox = $("#build_req > .build_content"); + reqBox.empty(); + if (req.quest.length) { + for (let i = 0; i < req.quest.length; i++) { + reqBox.append(`Quest Req: ${req.quest[i]}
`); + } + } + if (req.class) { + reqBox.append(`Class Req: ${req.class}
`); + } + if (req.level) { + reqBox.append(`Combat Lv. Min: ${req.level}
`); + } + skillList.forEach(skill => { + let value = req[skill]; + if (value) { + reqBox.append(`${capitalize(skill)} Min: ${value}
`); + } + }); + // how to wear + let howToWearBox = $("#build_howto > .build_content"); + howToWearBox.empty(); + howToWearBox.append("

Assign the follow Skill Points:

"); + skillList.forEach(skill => { + howToWearBox.append(`${capitalize(skill)}: ${realReq.req[skill]}
`); + }); + howToWearBox.append("
Then wear your items in the following order:
"); + realReq.order.forEach((index, i) => { + howToWearBox.append(`${i + 1}. ${capitalize(correctOrder[index])}
`); + }); + if (build.items[8]) { + howToWearBox.append(`${realReq.order.length + 1}. Weapon
`); + } + let {others, damage, regen, steal} = build.identification; + let {healthRaw, healthPercent} = regen; + // defenses + let defensesBox = $("#build_defs > .build_content"); + defensesBox.empty(); + defensesBox.empty(); + defensesBox.append("
"); + defensesBox = defensesBox.children("table"); + defensesBox.append(`❤ Health:${build.base.health}`); + defensesBox.append(`❤ Health Regen:${healthRaw}${nToString(healthPercent)}%=${Math.round(healthRaw + Math.abs(healthRaw) * healthPercent / 100)}`); + defensesBox.append(" "); + elementList.forEach(elem => { + let raw = build.base.defense[elem] || 0; + let percent = build.identification.defense[elem] || 0; + let final = Math.round(raw + Math.abs(raw) * percent / 100); + defensesBox.append(`${elemIcons[elem]} ${capitalize(elem)} Defense:${raw}${nToString(percent)}%=${final}`); + }); + // other ids + let othersBox = $("#build_ids > .build_content"); + othersBox.empty(); + othersBox.append("
"); + othersBox = othersBox.children("table"); + skillList.forEach(skill => { + if (realReq.bonus[skill]) { + let value = realReq.bonus[skill]; + othersBox.append(`${capitalize(skill)}${nToString(value)}`); + } + }); + for (let i in damage) { + if (damage.hasOwnProperty(i)) { + let value = damage[i]; + if (value) { + othersBox.append(`${idMap.damage[i][1]}${nToString(value)}${idMap.damage[i][0]}`); + } + } + } + for (let i in regen) { + if (regen.hasOwnProperty(i)) { + if (i.startsWith('health')) { + continue; + } + let value = regen[i]; + if (value) { + othersBox.append(`${idMap.regen[i][1]}${nToString(value)}${idMap.regen[i][0]}`); + } + } + } + for (let i in steal) { + if (steal.hasOwnProperty(i)) { + let value = steal[i]; + if (value) { + othersBox.append(`${idMap.steal[i][1]}${nToString(value)}${idMap.steal[i][0]}`); + } + } + } + let ordinals = ["1st", "2nd", "3rd", "4th"]; + let spellCostPct = build.identification.spellCost.percent; + let spellCostRaw = build.identification.spellCost.raw; + for (let i = 0; i < 4; i++) { + if (spellCostRaw[i]) { + othersBox.append(`${ordinals[i]} Spell Cost: ${nToString(spellCostRaw[i])}`); + } + if (spellCostPct[i]) { + othersBox.append(`${ordinals[i]} Spell Cost: ${nToString(spellCostPct[i])}%`); + } + } + for (let i in others) { + if (others.hasOwnProperty(i)) { + let value = others[i]; + if (value) { + othersBox.append(`${idMap.others[i][1]}${nToString(value)}${idMap.others[i][0]}`); + } + } + } + // get skills + let skills = $('input.sp_input').map((i, v) => 1*v.value).toArray(); + skillList.forEach((v, i) => { + skills[i] += realReq.bonus[v] || 0; + skills[i] = Math.max(0, Math.min(skills[i], 150)); + }); + let damagesBox = $('#build_dmg > .build_content'); + damagesBox.empty(); + if (build.items[8]) { + // calculate damages + // melee + let weaponDamage = JSON.parse(JSON.stringify(build.displayDamage)); + let {damage} = build.identification; + let meleeDamage = {normal: {}, critical: {}}; + for (let i in weaponDamage) { + if (!weaponDamage.hasOwnProperty(i)) continue; + meleeDamage.normal.neutral = build.displayDamage.neutral.map(x => x * (100 + skillBounsPct[skills[0]] + damage.meleePercent) / 100 + damage.meleeRaw).map(Math.floor); + meleeDamage.critical.neutral = build.displayDamage.neutral.map(x => x * (200 + skillBounsPct[skills[0]] + damage.meleePercent) / 100 + damage.meleeRaw).map(Math.floor); + meleeDamage.normal.total = [meleeDamage.normal.neutral[0], meleeDamage.normal.neutral[1]]; + meleeDamage.critical.total = [meleeDamage.critical.neutral[0], meleeDamage.critical.neutral[1]]; + elementList.forEach((elem, i) => { + meleeDamage.normal[elem] = build.displayDamage[elem].map(x => x * (100 + skillBounsPct[skills[0]] + skillBounsPct[skills[i]] + damage.meleePercent + damage[elem]) / 100).map(Math.floor); + meleeDamage.critical[elem] = build.displayDamage[elem].map(x => x * (200 + skillBounsPct[skills[0]] + skillBounsPct[skills[i]] + damage.meleePercent + damage[elem]) / 100).map(Math.floor); + meleeDamage.normal.total = sum(meleeDamage.normal.total, meleeDamage.normal[elem]); + meleeDamage.critical.total = sum(meleeDamage.critical.total, meleeDamage.critical[elem]); + }); + } + + // spell + let spells = []; + switch (build.items[8].info.type.toLowerCase()) { + case "spear": + // warrior + // bash + spells.push({spell: "Bash", subtitle: "First Explosion", damage: spellDamage(build, skills, 130, {neutral: 60, earth: 40, thunder: 0, water: 0, fire: 0, air: 0})}); + spells.push({spell: "Bash", subtitle: "Second Explosion", damage: spellDamage(build, skills, 130, {neutral: 100, earth: 0, thunder: 0, water: 0, fire: 0, air: 0})}); + spells.push({spell: "Bash", subtitle: "Total Damage", primary: true, damage: sum(spells[0].damage, spells[1].damage)}); + // charge + spells.push({spell: "Charge", subtitle: "", primary: true, damage: spellDamage(build, skills, 150, {neutral: 60, earth: 0, thunder: 0, water: 0, fire: 40, air: 0})}); + // uppercut + spells.push({spell: "Uppercut", subtitle: "First Damage", damage: spellDamage(build, skills, 300, {neutral: 85, earth: 15, thunder: 0, water: 0, fire: 0, air: 0})}); + spells.push({spell: "Uppercut", subtitle: "Fireworks Damage", damage: spellDamage(build, skills, 50, {neutral: 85, earth: 0, thunder: 15, water: 0, fire: 0, air: 0})}); + spells.push({spell: "Uppercut", subtitle: "Comet Damage", damage: spellDamage(build, skills, 50, {neutral: 100, earth: 0, thunder: 0, water: 0, fire: 0, air: 0})}); + spells.push({spell: "Uppercut", subtitle: "Total Damage", primary: true, damage: sum(sum(spells[4].damage, spells[5].damage), spells[6].damage)}); + // war scream + spells.push({spell: "War Scream", subtitle: "Per Hit", primary: true, damage: spellDamage(build, skills, 50, {neutral: 0, earth: 0, thunder: 0, water: 0, fire: 75, air: 25})}); + break; + case "bow": + // archer + // arrow storm + spells.push({spell: "Arrow Storm", subtitle: "Per Arrow", damage: spellDamage(build, skills, 10, {neutral: 60, earth: 0, thunder: 25, water: 0, fire: 15, air: 0})}); + spells.push({spell: "Arrow Storm", subtitle: "Per 60 Arrows", primary: true, damage: multiply(spells[0].damage, 60)}); + // escape + spells.push({spell: "Escape", subtitle: "", primary: true, damage: spellDamage(build, skills, 100, {neutral: 50, earth: 0, thunder: 0, water: 0, fire: 0, air: 50})}); + // bomb + spells.push({spell: "Bomb", subtitle: "", primary: true, damage: spellDamage(build, skills, 250, {neutral: 60, earth: 25, thunder: 0, water: 0, fire: 15, air: 0})}); + // arrow shield + spells.push({spell: "Arrow Shield", subtitle: "", primary: true, damage: spellDamage(build, skills, 100, {neutral: 70, earth: 0, thunder: 0, water: 30, fire: 0, air: 0})}); + spells.push({spell: "Arrow Shield", subtitle: "Arrow Rain Damage", damage: spellDamage(build, skills, 250, {neutral: 70, earth: 0, thunder: 0, water: 0, fire: 0, air: 30})}); + break; + case "wand": + // mage + // heal + spells.push({spell: "Heal", subtitle: "", primary: true, heal: [heal(build, 0.12), heal(build, 0.06), heal(build, 0.06)]}); + // teleport + spells.push({spell: "Teleport", subtitle: "", primary: true, damage: spellDamage(build, skills, 100, {neutral: 60, earth: 0, thunder: 40, water: 0, fire: 0, air: 0})}); + // meteor + spells.push({spell: "Meteor", subtitle: "Explosion Damage", primary: true, damage: spellDamage(build, skills, 500, {neutral: 40, earth: 30, thunder: 0, water: 0, fire: 30, air: 0})}); + spells.push({spell: "Meteor", subtitle: "Burning Damage", damage: spellDamage(build, skills, 125, {neutral: 100, earth: 0, thunder: 0, water: 0, fire: 0, air: 0})}); + // ice snake + spells.push({spell: "Ice Snake", subtitle: "", primary: true, damage: spellDamage(build, skills, 70, {neutral: 50, earth: 0, thunder: 0, water: 50, fire: 0, air: 0})}); + break; + case "dagger": + // assassin + // spin attack + spells.push({spell: "Spin Attack", subtitle: "", primary: true, damage: spellDamage(build, skills, 150, {neutral: 70, earth: 0, thunder: 30, water: 0, fire: 0, air: 0})}); + // multihit + spells.push({spell: "Multihit", subtitle: "First 10 Hits", damage: spellDamage(build, skills, 30, {neutral: 70, earth: 0, thunder: 30, water: 0, fire: 0, air: 0})}); + spells.push({spell: "Multihit", subtitle: "Last Hit", damage: spellDamage(build, skills, 30, {neutral: 40, earth: 0, thunder: 30, water: 30, fire: 0, air: 0})}); + spells.push({spell: "Multihit", subtitle: "Total Damage", primary: true, damage: sum(multiply(spells[1].damage, 10), spells[2].damage)}); + // smoke bomb + spells.push({spell: "Smoke Bomb", subtitle: "Total Damage", primary: true, damage: spellDamage(build, skills, 60, {neutral: 50, earth: 25, thunder: 0, water: 0, fire: 0, air: 25})}); + spells.push({spell: "Smoke Bomb", subtitle: "Per Second", damage: multiply(spells[4].damage, 5)}); + break; + case "relik": + // shaman + // totem + spells.push({spell: "Totem", subtitle: "Damage Per Second", primary: true, damage: spellDamage(build, skills, 20, {neutral: 60, earth: 0, thunder: 0, water: 0, fire: 20, air: 20})}); + spells.push({spell: "Totem", subtitle: "Heal Per Second", primary: true, heal: [heal(build, 0.04)]}); + spells.push({spell: "Totem", subtitle: "Landing Damage", damage: spellDamage(build, skills, 100, {neutral: 100, earth: 0, thunder: 0, water: 0, fire: 0, air: 0})}); + // haul + spells.push({spell: "Haul", subtitle: "", primary: true, damage: spellDamage(build, skills, 100, {neutral: 100, earth: 0, thunder: 0, water: 0, fire: 0, air: 0})}); + // explosive blender + spells.push({spell: "Aura", subtitle: "Center Damage", primary: true, damage: spellDamage(build, skills, 200, {neutral: 70, earth: 0, thunder: 0, water: 30, fire: 0, air: 0})}); + // uproot + spells.push({spell: "Uproot", subtitle: "", primary: true, damage: spellDamage(build, skills, 50, {neutral: 70, earth: 30, thunder: 0, water: 0, fire: 0, air: 0})}); + } + spells.sort((a,b) => (b.primary||0)-(a.primary||0)); + let div = $('
'); + div.append(`

Melee Damage

`); + let {normal, critical} = meleeDamage; + let normalAvg = (normal.total[0] + normal.total[1]) / 2; + let criticalAvg = (critical.total[0] + critical.total[1]) / 2; + let critChance = skillBounsPct[skills[1]]; + let avg = Math.round(normalAvg * (1 - critChance / 100) + criticalAvg * critChance / 100); + let attackSpeeds = ['SUPER_SLOW', 'VERY_SLOW', 'SLOW', 'NORMAL', 'FAST', 'VERY_FAST', 'SUPER_FAST']; + let hitsPerSec = [0.51, 0.83, 1.5, 2.05, 2.5, 3.4, 4.3]; + let speedTier = Math.min(6, Math.max(0, attackSpeeds.indexOf(build.items[8].base.attackSpeed) + build.identification.others.attackSpeedBonus)); + div.append(`${capitalize(attackSpeeds[speedTier])} Attack Speed`); + div.append(`DPS: ${Math.floor(avg * hitsPerSec[speedTier] + build.identification.others.poison * (1 + skillBounsPct[skills[0]] / 100) / 3)}`); + div.append(`Average Damage: ${avg}`); + let leftDamageBox = $(`
Normal Damage
Total: ${normal.total[0]} - ${normal.total[1]} (${Math.round(normalAvg)})
✤ ${normal.neutral[0]} - ${normal.neutral[1]}
`); + let rightDamageBox = $(`
Critical Damage
Total: ${critical.total[0]} - ${critical.total[1]} (${Math.round(criticalAvg)})
✤ ${critical.neutral[0]} - ${critical.neutral[1]}
`); + elementList.forEach(elem => { + if (normal[elem][1]) { + leftDamageBox.append(`${elemIcons[elem]} ${normal[elem][0]} - ${normal[elem][1]}
`); + rightDamageBox.append(`${elemIcons[elem]} ${critical[elem][0]} - ${critical[elem][1]}
`); + } + }); + div.append(leftDamageBox); + div.append(rightDamageBox); + damagesBox.append(div); + spells.forEach(spell => { + if (spell.damage) { + let div = $('
'); + div.append(`

${spell.spell}

`); + let {normal, critical} = spell.damage; + let normalAvg = (normal.total[0] + normal.total[1]) / 2; + let criticalAvg = (critical.total[0] + critical.total[1]) / 2; + let critChance = skillBounsPct[skills[1]]; + let avg = Math.round(normalAvg * (1 - critChance / 100) + criticalAvg * critChance / 100); + if (spell.subtitle.length) { + div.append(`${spell.subtitle}`); + } + div.append(`Average Damage: ${avg}`); + let leftDamageBox = $(`
Normal Damage
Total: ${normal.total[0]} - ${normal.total[1]} (${Math.round(normalAvg)})
✤ ${normal.neutral[0]} - ${normal.neutral[1]}
`); + let rightDamageBox = $(`
Critical Damage
Total: ${critical.total[0]} - ${critical.total[1]} (${Math.round(criticalAvg)})
✤ ${critical.neutral[0]} - ${critical.neutral[1]}
`); + elementList.forEach(elem => { + if (normal[elem][1]) { + leftDamageBox.append(`${elemIcons[elem]} ${normal[elem][0]} - ${normal[elem][1]}
`); + rightDamageBox.append(`${elemIcons[elem]} ${critical[elem][0]} - ${critical[elem][1]}
`); + } + }); + div.append(leftDamageBox); + div.append(rightDamageBox); + damagesBox.append(div); + } + if (spell.heal) { + let div = $('
'); + let {heal} = spell; + div.append(`

${spell.spell}

`); + if (spell.subtitle.length) { + div.append(`${spell.subtitle}`); + } + switch (spell.spell) { + case "Heal": + div.append(`Total Heal: ${heal[0] + heal[1] + heal[2]}`); + for (let i = 0; i < 3; i++) { + div.append(`${['1st', '2nd', '3rd'][i]} Pulse: ${heal[i]}`); + } + case "Totem": + div.append(`Heal: ${heal[0]}`); + break; + } + damagesBox.append(div); + } + }); + } else { + // no weapon to calculate damages + damagesBox.html('No weapon to selected.'); + } + } + + function heal(build, ratio) { + return Math.floor(build.base.health * ratio * (1 + build.identification.damage.water / 200)); + } + + function generateItemBox(item, floatLeft) { + let itemBox; + let newLine = false; + if (floatLeft) { + itemBox = $(`
`); + } else { + itemBox = $(`
`); + } + // 1. name coloured according to tier + itemBox.append(`${item.displayName || item.info.name}
`); + // 2. weapon attack speed + if (item.category === "weapon") { + itemBox.append(`${capitalize(item.base.attackSpeed)} Attack Speed
`); + } + itemBox.append("
"); + if (item.category !== "weapon") { + // 3. armour health + if (item.base.health) { + itemBox.append(`❤ Health: ${nToString(item.base.health)}
`); + newLine = true; + } + // 4. armour defenses + elementList.forEach(elem => { + let value = item.base.defense[elem]; + if (value) { + itemBox.append(`${elemIcons[elem]} ${capitalize(elem)} Defense: ${nToString(value)}
`); + newLine = true; + } + }); + } else { + // 5. weapon damage + if (item.displayDamage.neutral[1] !== 0) { + itemBox.append(`${elemIcons.earth} Neutral Damage: ${item.displayDamage.neutral[0]}-${item.displayDamage.neutral[1]}
`); + } + elementList.forEach(elem => { + let value = item.displayDamage[elem]; + if (value[1] !== 0) { + itemBox.append(`${elemIcons[elem]} ${capitalize(elem)} Damage: ${value[0]}-${value[1]}
`); + newLine = true; + } + }); + } + // 6. powder special + if (item.category !== "accessory"){ + let powders; + if (item.category === "weapon") { + powders = powderList.weapon; + } else { + powders = powderList[item.info.type.toLowerCase()]; + } + let count = {E: 0, T: 0, W: 0, F: 0, A: 0}; + let sum = {E: 0, T: 0, W: 0, F: 0, A: 0}; + let specialElem; + let specialTier = 0; + for (const powder of powders) { + if (!powder) { + continue; + } + let elem = powder[0]; + let tier = powder[1]; + if (tier < 4) { + continue; + } + ++count[elem]; + sum[elem] += 1*tier; + if (count[elem] == 2) { + specialElem = elem; + specialTier = sum[elem]; + break; + } + } + if (specialTier) { + if (item.category === "weapon") { + // weapon specials + switch (specialElem) { + case 'E': + itemBox.append(`  Quake
`); + itemBox.append(`  - Radius: ${specialTier / 2 + 1} blocks
`); + itemBox.append(`  - Damage: ${specialTier * 65 - 365}% ${elemIcons.earth}
`); + break; + case 'T': + itemBox.append(`  Chained Lightning
`); + itemBox.append(`  - Chains: ${specialTier - 3}
`); + itemBox.append(`  - Damage: ${specialTier * 40 - 240}% ${elemIcons.thunder}
`); + break; + case 'W': + itemBox.append(`  Curse
`); + itemBox.append(`  - Duration: ${specialTier - 3} seconds
`); + itemBox.append(`  - Damage Boost: +${specialTier * 30 - 150}%
`); + break; + case 'F': + itemBox.append(`  Courage
`); + itemBox.append(`  - Duration: ${specialTier / 2 + 2} seconds
`); + itemBox.append(`  - Damage: ${specialTier * 12.5 - 25}% ${elemIcons.fire}
`); + itemBox.append(`  - Damage Boost: +${specialTier * 20 - 90}%
`); + break; + case 'A': + itemBox.append(`  Wind Prison
`); + itemBox.append(`  - Duration: ${specialTier / 2 - 1} seconds
`); + itemBox.append(`  - Damage Boost: +${specialTier * 100 - 600}%
`); + itemBox.append(`  - Knockback: ${specialTier * 4 - 24} blocks
`); + break; + } + } else { + // armour specials + switch (specialElem) { + case 'E': + itemBox.append(`  Rage [% ❤ Missing]
`); + itemBox.append(`  - Damage: +${(specialTier - 4 + (specialTier == 12 ? 2 : 0)) / 10}% ${elemIcons.earth}
`); + break; + case 'T': + itemBox.append(`  Kill Streak [Mob Killed]
`); + itemBox.append(`  - Damage: +${specialTier * 1.5 - 9}% ${elemIcons.thunder}
`); + itemBox.append(`  - Duration: 5 seconds
`); + break; + case 'W': + itemBox.append(`  Concentration [Mana Used]
`); + itemBox.append(`  - Damage: ${specialTier - 7}% ${elemIcons.water} / Mana
`); + itemBox.append(`  - Duration: 1 Sec. / Mana
`); + break; + case 'F': + itemBox.append(`  Endurance [Hit Taken]
`); + itemBox.append(`  - Damage: ${specialTier - 6}% ${elemIcons.fire}
`); + itemBox.append(`  - Duration: 8 seconds
`); + break; + case 'A': + itemBox.append(`  Dodge [Near Mobs]
`); + itemBox.append(`  - Damage: ${specialTier - 6}% ${elemIcons.air}
`); + itemBox.append(`  - Duration: 6 seconds
`); + break; + } + } + } + } + if (newLine) { + itemBox.append("
"); + newLine = false; + } + // 7. requirements + // 7.1 quest req + if (item.req.quest) { + itemBox.append(`Quest Req: ${item.req.quest}
`); + newLine = true; + } + // 7.2 class req + if (item.req.class) { + itemBox.append(`Class Req: ${item.req.class}
`); + newLine = true; + } + // 7.3 combat lvl req + if (item.req.level) { + itemBox.append(`Combat Lv. Min: ${item.req.level}
`); + newLine = true; + } + // 7.4 skill req + skillList.forEach(skill => { + let value = item.req[skill]; + if (value) { + itemBox.append(`${capitalize(skill)} Min: ${value}
`); + newLine = true; + } + }); + if (newLine) { + itemBox.append("
"); + newLine = false; + } + // 8. bonus skills + skillList.forEach(skill => { + let value = item.base.skill[skill]; + if (value) { + itemBox.append(`${nToString(value)} ${capitalize(skill)}
`); + newLine = true; + } + }); + if (newLine) { + itemBox.append("
"); + newLine = false; + } + // 9. ids + { + // damage + { + elementList.forEach(elem => { + let value = item.identification.damage[elem]; + if (value) { + itemBox.append(`${nToString(value)}% ${capitalize(elem)} Damage
`); + newLine = true; + } + }); + for (let i in idMap.damage) { + if (idMap.damage.hasOwnProperty(i)) { + let value = item.identification.damage[i]; + let line = idMap.damage[i]; + if (value) { + itemBox.append(`${nToString(value)}${line[0]} ${line[1]}
`); + } + } + } + } + // defenses + { + elementList.forEach(elem => { + let value = item.identification.defense[elem]; + if (value) { + itemBox.append(`${nToString(value)}% ${capitalize(elem)} Defense
`); + newLine = true; + } + }); + } + // regen + { + for (let i in idMap.regen) { + if (idMap.regen.hasOwnProperty(i)) { + let value = item.identification.regen[i]; + let line = idMap.regen[i]; + if (value) { + itemBox.append(`${nToString(value)}${line[0]} ${line[1]}
`); + } + } + } + } + // spell cost + { + let ordinals = ["1st", "2nd", "3rd", "4th"]; + let spellCostPct = item.identification.spellCost.percent; + let spellCostRaw = item.identification.spellCost.raw; + for (let i = 0; i < 4; i++) { + if (spellCostRaw[i]) { + itemBox.append(`${nToString(spellCostRaw[i])} ${ordinals[i]} Spell Cost
`); + newLine = true; + } + if (spellCostPct[i]) { + itemBox.append(`${nToString(spellCostPct[i])}% ${ordinals[i]} Spell Cost
`); + newLine = true; + } + } + } + // steal + { + for (let i in idMap.steal) { + if (idMap.steal.hasOwnProperty(i)) { + let value = item.identification.steal[i]; + let line = idMap.steal[i]; + if (value) { + itemBox.append(`${nToString(value)}${line[0]} ${line[1]}
`); + newLine = true; + } + } + } + } + // others + { + for (let i in idMap.others) { + if (idMap.others.hasOwnProperty(i)) { + let value = item.identification.others[i]; + let line = idMap.others[i]; + if (value) { + itemBox.append(`${nToString(value)}${line[0]} ${line[1]}
`); + newLine = true; + } + } + } + } + } + if (newLine) { + itemBox.append("
"); + newLine = false; + } + // 10. powder slots + { + let type = (item.info.type || item.accessoryType).toLowerCase(); + let {sockets} = item.info; + if (type == "relik" || type == "wand" || type == "bow" || type == "spear" || type == "dagger") { + type = "weapon"; + } + let powderArray = (powderList[type] || []).filter(x => !!x); + let powderCount = Math.min(powderArray.length, sockets || 0); + if (sockets) { + if (!powderCount) { + itemBox.append(`[0/${sockets}] Powder Slots
`); + } else { + let powderString = ""; + let count = Math.min(powderArray.length, item.info.sockets); + for (let i = 0; i < count; i++) { + let elem = powderStats[powderArray[i][0].toUpperCase()][0][0]; + powderString += `${elemIcons[elem]}`; + } + itemBox.append(`[${powderCount}/${sockets}] Powder Slots [${powderString}]
`); + } + } + } + // 11. tier + itemBox.append(`${item.info.tier} Item
`); + // 12. restrictions + if (item.restrictions) { + itemBox.append(`${item.restrictions} Item
`); + } + // 13. lore + if (item.info.lore) { + itemBox.append(`${item.info.lore}
`); + } + return itemBox; + } + + function capitalize(s) { + return s.split("_").map(x => x.substr(0, 1).toUpperCase() + x.substr(1).toLowerCase()).join(" "); + } + + function resetPos() { + const CONTAINER = "#item_list_box"; + const SELECTOR = ".item.float_left"; + const COLUMN_WIDTH = 250; + const VERTICAL_MARGIN = 40; + const HORIZONTAL_MARGIN = 24; + + let container = $(CONTAINER); + let columns = Math.floor(container.width() / (HORIZONTAL_MARGIN + COLUMN_WIDTH)); + let height_occupied = []; + let elems = container.children(SELECTOR); + container.css("position", "relative"); + for (let i = 0; i < columns; i++) { + height_occupied.push(0); + } + for (let i = 0; i < elems.length; i++) { + // find the col with least occupied height + let idx = 0; + let min = height_occupied[0]; + for (let j = 1; j < columns; j++) { + if (height_occupied[j] < min) { + min = height_occupied[j]; + idx = j; + } + } + let $elem = $(elems[i]); + $elem.css("position", "absolute").css("top", min).css("left", COLUMN_WIDTH * idx + HORIZONTAL_MARGIN * idx); + height_occupied[idx] += $elem.height() + VERTICAL_MARGIN; + } + let h = Math.max.apply(this, height_occupied); + container.height(h); + } + + function nToString(n) { + return (n >= 0 ? "+" : "") + n; + } + + function findStatReq(items) { + items = items.slice(0, 8); // remove weapon since it always come last + let combinations = []; + for (let i = 0; i < 256; i++) { + let buildItems = [null, null, null, null, null, null, null, null, null]; + for (let j = 0; j < 8; j++) { + if (items[j] && (i & (1 << j))) { + buildItems[j] = {name: items[j].displayName || items[j].info.name}; + } + } + combinations.push(calculateBuild(buildItems)); + } + let currentOrder = [0, 1, 2, 3, 4, 5, 6, 7]; + currentOrder = currentOrder.filter(i => items[i]); // so that we don"t consider empty slots + // Absolute minimum, even if it means that you need to allocate 200 points in one skill + let currentMin; + let currentMinSum = 694201337; + // The minimum that is actually valid (<=200 in total, <=100 in any skill) + let currentValidMin; + let currentValidMinSum = 694201337; + do { + let bitSet = 0; + let currentReq = { + req: { + strength: 0, + dexterity: 0, + intelligence: 0, + defense: 0, + agility: 0 + }, + bonus: { + strength: 0, + dexterity: 0, + intelligence: 0, + defense: 0, + agility: 0 + } + }; + let sum = 0; + let valid = true; + for (let i = 0; i < 8; i++) { + bitSet |= 1 << currentOrder[i]; + let build = combinations[bitSet]; + let stageReq = build.req; + skillList.forEach(skill => { + let ownedPoints = currentReq.req[skill] + currentReq.bonus[skill]; + let diff; + if (!stageReq[skill]) { + diff = 0; + } else if (stageReq[skill] > ownedPoints) { + diff = stageReq[skill] - ownedPoints; + } else { + diff = 0; + } + currentReq.req[skill] += diff; + currentReq.bonus[skill] = build.base.skill[skill]; + sum += diff; + valid = sum <= 200 && currentReq[skill] <= 100; + }); + } + if (sum < currentMinSum) { + currentReq.order = currentOrder.slice(0); + currentMinSum = sum; + currentMin = currentReq; + if (valid) { + currentValidMinSum = sum; + currentValidMin = currentReq; + } + } + } while (nextPermutation(currentOrder)); + return currentValidMin || currentMin; + } + + // overrides source with data, overriding an object with a primitive won"t work + function override(source, data) { + if (data === undefined) { + return source; + } + for (let i in data) { + if (data.hasOwnProperty(i)) { + let obj = source[i]; + if (typeof obj === "object") { + override(obj, data[i]); + } else { + if (data[i] !== undefined) { + source[i] = data[i]; + } + } + } + } + } + + // similar to override(), but adds instead of assigns, and clones the source + function sum(left, right) { + return combine(left, right, (x, y) => { + if (Array.isArray(x)) { + return x.concat(y); + } + if (x !== undefined) { + return x + y; + } + return y; + + }); + } + + function multiply(obj, val) { + let result = {}; + for (let i in obj) { + if (obj.hasOwnProperty(i)) { + if ("object" !== typeof obj[i]) { + result[i] = val * obj[i]; + } else { + result[i] = multiply(obj[i], val); + } + } + } + return result; + } + + function max(left, right) { + return combine(left, right, (x, y) => { + if (Array.isArray(x)) { + return x.concat(y); + } + if (x !== undefined) { + return Math.max(x, y); + } else { + return y; + } + }); + } + + // similar to override(), but adds instead of assigns, and clones the source + function combine(left, right, combiner) { + if (right === undefined || right === null) { + return left; + } + if (left === undefined || left === null) { + return right; + } + let result = JSON.parse(JSON.stringify(left)); + if (typeof right === "string") { + return combiner(left, right); + } + for (let i in right) { + if (right.hasOwnProperty(i)) { + if (typeof left[i] === "object") { + result[i] = combine(left[i], right[i], combiner); + } else { + result[i] = combiner(left[i], right[i]); + } + } + } + return result; + } + + function nextPermutation(array) { + /* The algorithm: find the longest decreasing subsequence from the end, since we don"t have a larger permutation + for such a sequence, then find the smallest number that is larger than the one before the decreasing + subsequence, and swap the smallest with the one before. That way we increment the whole sequence the least. + However this way the decreasing subsequence that we"ve found is still decreasing, so we reverse it to become an + increasing one, just like 39+1=40, the last digit is reset from the highest possible value to the lowest. + 6 8 7 4 3 [(5) 2 1] -> 6 8 7 4 (5) [3 2 1] -> 6 8 7 4 5 1 2 3 + */ + // The last index of the array, our starting point. + let i = array.length - 1; + let last = i; + // `i--` returns the value before the decrement, so it is one larger than the decremented `i`. + // So this expands to `while (i--, array[i + 1] < array[i]);`, which continues iff the next item is is smaller + // than the last item i.e. true if the pair is decreasing. + // When `i` reaches -1, it"s comparing undefined and a number, which gives false. + // When this completes, `i` is pointing at the index right before the longest decreasing subsequence from the end. + while (array[i--] < array[i]); + // if `i` is -1, the whole sequence is decreasing. There"s no next permutation. + if (!(i+1)) return false; + // This is the number to be swapped out, 3 in the example. We are going to find a number in the subsequence that + // is slightly larger than this. + let n = array[i]; + // This is the starting point of our search for the slightly larger number. + let m = array[i+1]; + let mi = i+1; + for (let j = last; j > i; j--) { + let k = array[j]; + // This statements checks if the current item is larger than the number to be swapped out and smaller than + // the existing candidate because we want it to be as small as possible for minimum increment. + if (k > n && k < m) { + m = k; + mi = j; + } + } + // Usual swapping. + array[mi] = array[i]; + array[i] = m; + // Recall that `i` is pointing at the index right before the longest decreasing subsequence from the end. Plus + // one and it's the start of the decreasing sequence. Remove the subsequence from the array by using .splice(), + // reverse it and push it back to the array. + // [6 8 7 4 5 3 2 1] -> [6 8 7 4 5] [3 2 1] -> [6 8 7 4 5] [1 2 3] -> [6 8 7 4 5 1 2 3] + array.push(...array.splice(i+1).reverse()); + return true; + } + + window.calculateBuild = calculateBuild; + window.findStatReq = findStatReq; +}); \ No newline at end of file diff --git a/build.js b/build.js index 4961b26..e8f7d50 100644 --- a/build.js +++ b/build.js @@ -101,7 +101,7 @@ class Build{ } this.availableSkillpoints = levelToSkillPoints(this.level); this.equipment = [ helmet, chestplate, leggings, boots, ring1, ring2, bracelet, necklace ]; - + this.items = [helmet, chestplate, leggings, boots, ring1, ring2, bracelet, necklace, weapon]; // return [equip_order, best_skillpoints, final_skillpoints, best_total]; let result = calculate_skillpoints(this.equipment, weapon); console.log(result); @@ -118,6 +118,9 @@ class Build{ } /* Getters */ + + /* Get total health for build. + */ getHealth(){ health = parseInt(this.helmet.hp,10) + parseInt(this.helmet.hpBonus,10) + parseInt(this.chestplate.hp,10) + parseInt(this.chestplate.hpBonus,10) + parseInt(this.leggings.hp,10) + parseInt(this.leggings.hpBonus,10) + parseInt(this.boots.hp,10) + parseInt(this.boots.hpBonus,10) + parseInt(this.ring1.hp,10) + parseInt(this.ring1.hpBonus,10) + parseInt(this.ring2.hp,10) + parseInt(this.ring2.hpBonus,10) + parseInt(this.bracelet.hp,10) + parseInt(this.bracelet.hpBonus,10) + parseInt(this.necklace.hp,10) + parseInt(this.necklace.hpBonus,10) + parseInt(this.weapon.hp,10) + parseInt(this.weapon.hpBonus,10) + levelToHPBase(this.level); if(health<5){ @@ -126,6 +129,60 @@ class Build{ return health; } } + /* Get total melee dps for build. + */ + getMeleeDPS(){ + let meleeMult = { + "SUPER_SLOW":"0.51", + "VERY_SLOW":"0.83", + "SLOW":"1.5", + "NORMAL":"2.05", + "FAST":"2.5", + "VERY_FAST":"3.1", + "SUPER_FAST":"4.3", + } + let stats = this.getBuildStats(); + let nDam = stats.get("nDam"); + + return []; + } + + /* Get all stats for this build. Returns a map w/ sums of all IDs. + @dep test.js.item_fields + @dep test.js.rolledIDs + @dep test.js.nonRolledIDs + @dep test.js.expandItem() + @pre The build itself should be valid. No checking of validity of pieces is done here. + @post The map returned will contain non-stacking IDs w/ a value null. + */ + getBuildStats(){ + //Create a map of this build's stats + //This is universal for every possible build, so it's possible to move this elsewhere. + let statMap = new Map(); + for (const i in item_fields){ + let id = item_fields[i]; + if(stackingIDs.includes(id)){ //IDs stack - make it number + statMap.set(id,0); + }else if(standaloneIDs.includes(id)){ //IDs do not stack - string + statMap.set(id,""); + } + } + for (const i in this.items){ + let item = expandItem(this.items[i]); + console.log(item,type(item)); + if(item.has("fixID") && item.get("fixID")){//item has fixed IDs + for(const [key,value] in item.entries()){ + console.log(key,value); + } + }else{//item does not have fixed IDs + for (const i in item) { + console.log(entry,": ",item.get(entry)); + } + } + } + + return statMap; + } /* Setters */ diff --git a/test.js b/test.js index 7012970..8ae78bf 100644 --- a/test.js +++ b/test.js @@ -23,6 +23,8 @@ let weaponTypes = [ "wand", "spear", "bow", "dagger", "relik" ]; let item_fields = [ "name", "displayName", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "agiReq", "defReq", "hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "str", "dex", "int", "agi", "def", "thorns", "exploding", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "fixID", "category", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd", "id" ]; let nonRolledIDs = ["name", "displayName", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "agiReq", "defReq","str", "dex", "int", "agi", "def", "fixID", "category", "id"]; let rolledIDs = ["hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "thorns", "exploding", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd"]; +let stackingIDs = ["hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "thorns", "exploding", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd", "fDef", "wDef", "aDef", "tDef", "eDef", "str", "dex", "int", "agi", "def"]; +let standaloneIDs = ["name", "displayName", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "lvl", "classReq", "strReq", "dexReq", "intReq", "agiReq", "defReq", "fixID", "category", "id"]; let itemTypes = armorTypes.concat(accessoryTypes).concat(weaponTypes); let itemLists = new Map(); for (const it of itemTypes) { @@ -72,6 +74,8 @@ function init() { item.skillpoints = [0, 0, 0, 0, 0]; item.has_negstat = false; item.reqs = [0, 0, 0, 0, 0]; + item.fixID = true; + item.tier = " ";//do not get rid of this @hpp item.id = 10000 + i; noneItems[i] = item; @@ -289,7 +293,7 @@ function calculateBuild(){ setHTML("build-bracelet", expandedItemToString(expandItem(player_build.bracelet))); setHTML("build-necklace", expandedItemToString(expandItem(player_build.necklace))); setHTML("build-weapon", expandedItemToString(expandItem(player_build.weapon))); - + setHTML("build-cumulative-stats", player_build.getMeleeDPS()); //Incomplete function location.hash = encodeBuild(); } /* Helper function that gets stats ranges for wearable items. @@ -314,7 +318,6 @@ function expandItem(item){ } }else{ //The item does not have fixed IDs. for (const id in rolledIDs){ - console.log(id); if(item[rolledIDs[id]]){ if(item[rolledIDs[id]] > 0){ // positive rolled IDs minRolls.set(rolledIDs[id],idRound(item[rolledIDs[id]]*0.3)); @@ -336,7 +339,6 @@ function expandItem(item){ } expandedItem.set("minRolls",minRolls); expandedItem.set("maxRolls",maxRolls); - console.log(expandedItem) return expandedItem; } /* A second helper function that takes items from expandItem() and stringifies them. @@ -344,7 +346,6 @@ function expandItem(item){ TODO: write the function */ function expandedItemToString(item){ - console.log(item); let ids = ["lvl", "classReq","strReq", "dexReq", "intReq", "defReq","agiReq", "nDam", "eDam", "tDam", "wDam", "tDam", "aDam", "atkSpd", "hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "agi", "def", "hpBonus", "hprRaw", "hprPct", "sdRaw", "sdPct", "mdRaw", "mdPct", "mr", "ms", "ref", "ls", "poison", "thorns", "exploding", "spd", "atkTier", "eDamPct", "tDamPct", "wDamPct", "fDamPct", "aDamPct", "eDefPct", "tDefPct", "wDefPct", "fDefPct", "aDefPct", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "xpb", "lb", "lq", "spRegen", "eSteal", "gXp", "gSpd", "slots", "set", "quest", "restrict"]; let idPrefixes = {"lvl":"Combat Level Min: ", "classReq":"Class Req: ","strReq":"Strength Min: ","dexReq":"Dexterity Min: ","intReq":"Intelligence Min: ","defReq":"Defense Min: ","agiReq":"Agility Min: ", "nDam":"Neutral Damage: ", "eDam":"Earth Damage: ", "tDam":"Thunder Damage: ", "wDam":"Water Damage: ", "fDam":"Fire Damage: ", "aDam":"Air Damage: ", "atkSpd":"Attack Speed: ", "hp":"Health: ", "eDef":"Earth Defense: ", "tDef":"Thunder Defense: ", "wDef":"Water Defense: ", "fDef":"Fire Defense: ", "aDef":"Air Defense: ", "str":"Strength: ", "dex":"Dexterity: ", "int":"Intelligence: ", "def":"Defense: ","agi":"Agility: ", "hpBonus":"Health Bonus: ", "hprRaw":"Health Regen Raw: ", "hprPct":"Health Regen %: ", "sdRaw":"Raw Spell Damage: ", "sdPct":"Spell Damage %: ", "mdRaw":"Main Attack Neutral Damage: ", "mdPct":"Main Attack Damage %: ", "mr":"Mana Regen: ", "ms":"Mana Steal: ", "ref":"Reflection: ", "ls":"Life Steal: ", "poison":"Poison: ", "thorns":"Thorns: ", "exploding":"Expoding: ", "spd":"Walk Speed Bonus: ", "atkTier":"Attack Speed Bonus: ", "eDamPct":"Earth Damage %: ", "tDamPct":"Thunder Damage %: ", "wDamPct":"Water Damage %: ", "fDamPct":"Fire Damage %: ", "aDamPct":"Air Damage %: ", "eDefPct":"Earth Defense %: ", "tDefPct":"Thunder Defense %: ", "wDefPct":"Water Defense %: ", "fDefPct":"Fire Defense %: ", "aDefPct":"Air Defense %: ", "spPct1":"1st Spell Cost %: ", "spRaw1":"1st Spell Cost Raw: ", "spPct2":"2nd Spell Cost %: ", "spRaw2":"2nd Spell Cost Raw: ", "spPct3":"3rd Spell Cost %: ", "spRaw3":"3rd Spell Cost Raw: ", "spPct4":"4th Spell Cost %: ", "spRaw4":"4th Spell Cost Raw: ", "rainbowRaw":"Rainbow Spell Damage Raw: ", "sprint":"Sprint Bonus: ", "sprintReg":"Sprint Regen Bonus: ", "jh":"Jump Height: ", "xpb":"Combat XP Bonus: ", "lb":"Loot Bonus: ", "lq":"Loot Quality: ", "spRegen":"Soul Point Regen: ", "eSteal":"Stealing: ", "gXp":"Gathering XP Bonus: ", "gSpd":"Gathering Speed Bonus: ", "slots":"Powder Slots: ", "set":"This item belongs to the ", "quest":"This item is from the quest
", "restrict":""}; let idSuffixes = {"lvl":"", "classReq":"","strReq":"","dexReq":"","intReq":"","defReq":"","agiReq":"", "nDam":"", "eDam":"", "tDam":"", "wDam":"", "fDam":"", "aDam":"", "atkSpd":"", "hp":"", "eDef":"", "tDef":"", "wDef":"", "fDef":"", "aDef":"", "str":"", "dex":"", "int":"", "def":"","agi":"", "hpBonus":"", "hprRaw":"", "hprPct":"%", "sdRaw":"", "sdPct":"%", "mdRaw":"", "mdPct":"%", "mr":"/4s", "ms":"/4s", "ref":"%", "ls":"/4s", "poison":"/3s", "thorns":"%", "exploding":"%", "spd":"%", "atkTier":" tier", "eDamPct":"%", "tDamPct":"%", "wDamPct":"%", "fDamPct":"%", "aDamPct":"%", "eDefPct":"%", "tDefPct":"%", "wDefPct":"%", "fDefPct":"%", "aDefPct":"%", "spPct1":"%", "spRaw1":"", "spPct2":"%", "spRaw2":"", "spPct3":"%", "spRaw3":"", "spPct4":"%", "spRaw4":"", "rainbowRaw":"", "sprint":"%", "sprintReg":"%", "jh":"", "xpb":"%", "lb":"%", "lq":"%", "spRegen":"%", "eSteal":"%", "gXp":"%", "gSpd":"%", "slots":"", "set":" set.", "quest":".", "restrict":""}; @@ -357,7 +358,6 @@ function expandedItemToString(item){ itemString = itemString.concat(item.get(ids[i]), idSuffixes[ids[i]],"
"); } if(rolledIDs.includes(ids[i])&& item.get("minRolls").get(ids[i]) && item.get("maxRolls").get(ids[i]) ){//rolled ID & non-0/non-null/non-und ID - console.log("hi"); itemString = itemString.concat(idPrefixes[ids[i]]); itemString = itemString.concat(item.get("minRolls").get(ids[i]), idSuffixes[ids[i]],"
"); }//Just don't do anything if else @@ -369,7 +369,6 @@ function expandedItemToString(item){ itemString = itemString.concat(item.get(ids[i]), idSuffixes[ids[i]],"
"); } if(rolledIDs.includes(ids[i])&& item.get("minRolls").get(ids[i]) && item.get("maxRolls").get(ids[i]) ){//rolled ID & non-0/non-null/non-und ID - console.log("hi"); itemString = itemString.concat(idPrefixes[ids[i]]); itemString = itemString.concat(item.get("minRolls").get(ids[i]), idSuffixes[ids[i]], " -> ", idRound(item.get("maxRolls").get(ids[i])),idSuffixes[ids[i]],"
"); }//Just don't do anything if else @@ -390,6 +389,8 @@ function idRound(id){ } } + + function resetFields(){ setValue("helmet-choice", ""); setValue("helmet-powder", ""); @@ -413,3 +414,4 @@ function resetFields(){ } load_init(init); +