From 1e863fd01560966d5ec714d076adc118b5f24651 Mon Sep 17 00:00:00 2001 From: hppeng Date: Sat, 21 May 2022 22:38:43 -0700 Subject: [PATCH 01/68] Create common.js for more common stuff (Item class, expandItem func) --- js/common.js | 123 ++++++++++++++++++++++++++++++++++++++++ js/computation_graph.js | 48 ++++++++++++++-- js/display.js | 54 ------------------ js/display_constants.js | 95 ------------------------------- 4 files changed, 165 insertions(+), 155 deletions(-) create mode 100644 js/common.js diff --git a/js/common.js b/js/common.js new file mode 100644 index 0000000..079922c --- /dev/null +++ b/js/common.js @@ -0,0 +1,123 @@ +let nonRolledIDs = [ + "name", + "lore", + "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", "defReq", "agiReq", + "str", "dex", "int", "agi", "def", + "fixID", + "category", + "id", + "skillpoints", + "reqs", + "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", + "majorIds"]; +let rolledIDs = [ + "hprPct", + "mr", + "sdPct", + "mdPct", + "ls", + "ms", + "xpb", + "lb", + "ref", + "thorns", + "expd", + "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" +]; + +/** + * Take an item with id list and turn it into a set of minrolls and maxrolls. + */ +function expandItem(item) { + let minRolls = new Map(); + let maxRolls = new Map(); + let expandedItem = new Map(); + if (item.fixID) { //The item has fixed IDs. + expandedItem.set("fixID",true); + for (const id of rolledIDs) { //all rolled IDs are numerical + let val = (item[id] || 0); + minRolls.set(id,val); + maxRolls.set(id,val); + } + } else { //The item does not have fixed IDs. + for (const id of rolledIDs) { + let val = (item[id] || 0); + if (val > 0) { // positive rolled IDs + if (reversedIDs.includes(id)) { + maxRolls.set(id,idRound(val*0.3)); + minRolls.set(id,idRound(val*1.3)); + } else { + maxRolls.set(id,idRound(val*1.3)); + minRolls.set(id,idRound(val*0.3)); + } + } else if (val < 0) { //negative rolled IDs + if (reversedIDs.includes(id)) { + maxRolls.set(id,idRound(val*1.3)); + minRolls.set(id,idRound(val*0.7)); + } + else { + maxRolls.set(id,idRound(val*0.7)); + minRolls.set(id,idRound(val*1.3)); + } + } + else { // if val == 0 + // NOTE: DO NOT remove this case! idRound behavior does not round to 0! + maxRolls.set(id,0); + minRolls.set(id,0); + } + } + } + for (const id of nonRolledIDs) { + expandedItem.set(id,item[id]); + } + expandedItem.set("minRolls",minRolls); + expandedItem.set("maxRolls",maxRolls); + expandedItem.set("powders", powders); + return expandedItem; +} + +class Item { + constructor(item_obj) { + this.statMap = ; //can use the statMap as an expanded Item + this.atkSpd = attackSpeed; + this.hash = "CR-" + hash; + this.initCraftStats(); + this.statMap.set("hash", this.hash); + } +} diff --git a/js/computation_graph.js b/js/computation_graph.js index 4e193c6..975a22d 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -18,6 +18,7 @@ class ComputeNode { this.name = name; this.update_task = null; this.update_time = Date.now(); + this.fail_cb = false; // Set to true to force updates even if parent failed. } /*** @@ -33,9 +34,11 @@ class ComputeNode { for (const input of this.inputs) { value_map.set(input.name, input.get_value()); } - this.value = this.compute_func(); + this.value = this.compute_func(value_map); for (const child of this.children) { - child.update(); + if (this.value || child.fail_cb) { + child.update(); + } } } @@ -49,7 +52,7 @@ class ComputeNode { /*** * Abstract method for computing something. Return value is set into this.value */ - compute_func() { + compute_func(input_map) { throw "no compute func specified"; } @@ -94,10 +97,14 @@ class ItemStats extends ComputeNode { this.none_item = none_item; } - compute_func() { + compute_func(input_map) { // built on the assumption of no one will type in CI/CR letter by letter let item_text = this.input_field.value; + if (!item_text) { + return this.none_item; + } + let item; if (item_text.slice(0, 3) == "CI-") { @@ -113,10 +120,39 @@ class ItemStats extends ComputeNode { item = tomeMap.get(item_text); } - if (!item) { - return this.none_item; + if (!item || item.) { + return null; } return item; } } +/*** + * Node for updating item input fields from parsed items. + */ +class ItemInputDisplay extends ComputeNode { + + constructor(name, item_input_field, item_image) { + super(name); + this.input_field = item_input_field; + this.image = item_image; + this.fail_cb = true; + } + + compute_func(input_map) { + if (input_map.size !== 1) { + throw "ItemInputDisplay accepts exactly one input (item)"; + } + + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + this.input_field.classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom'); + this.input_field.classList.add("text-light"); + this.image.classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow'); + + if (!item) { + this.input_field.classList.add("is-invalid"); + return null; + } + } +} diff --git a/js/display.js b/js/display.js index fcf9f5e..707efe3 100644 --- a/js/display.js +++ b/js/display.js @@ -27,60 +27,6 @@ function applyArmorPowdersOnce(expandedItem, powders) { } } -/** - * Take an item with id list and turn it into a set of minrolls and maxrolls. - * Also applies powders to armor. - */ -function expandItem(item, powders) { - let minRolls = new Map(); - let maxRolls = new Map(); - let expandedItem = new Map(); - if (item.fixID) { //The item has fixed IDs. - expandedItem.set("fixID",true); - for (const id of rolledIDs) { //all rolled IDs are numerical - let val = (item[id] || 0); - minRolls.set(id,val); - maxRolls.set(id,val); - } - } else { //The item does not have fixed IDs. - for (const id of rolledIDs) { - let val = (item[id] || 0); - if (val > 0) { // positive rolled IDs - if (reversedIDs.includes(id)) { - maxRolls.set(id,idRound(val*0.3)); - minRolls.set(id,idRound(val*1.3)); - } else { - maxRolls.set(id,idRound(val*1.3)); - minRolls.set(id,idRound(val*0.3)); - } - } else if (val < 0) { //negative rolled IDs - if (reversedIDs.includes(id)) { - maxRolls.set(id,idRound(val*1.3)); - minRolls.set(id,idRound(val*0.7)); - } - else { - maxRolls.set(id,idRound(val*0.7)); - minRolls.set(id,idRound(val*1.3)); - } - } - else { // if val == 0 - // NOTE: DO NOT remove this case! idRound behavior does not round to 0! - maxRolls.set(id,0); - minRolls.set(id,0); - } - } - } - for (const id of nonRolledIDs) { - expandedItem.set(id,item[id]); - } - expandedItem.set("minRolls",minRolls); - expandedItem.set("maxRolls",maxRolls); - expandedItem.set("powders", powders); - if (item.category === "armor") { - applyArmorPowders(expandedItem, powders); - } - return expandedItem; -} /* Takes in an ingredient object and returns an equivalent Map(). */ diff --git a/js/display_constants.js b/js/display_constants.js index f686899..d8c26dd 100644 --- a/js/display_constants.js +++ b/js/display_constants.js @@ -1,98 +1,3 @@ -let nonRolledIDs = [ - "name", - "lore", - "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", - "defReq", - "agiReq","str", - "dex", - "int", - "agi", - "def", - "fixID", - "category", - "id", - "skillpoints", - "reqs", - "nDam_", - "fDam_", - "wDam_", - "aDam_", - "tDam_", - "eDam_", - "majorIds"]; -let rolledIDs = [ - "hprPct", - "mr", - "sdPct", - "mdPct", - "ls", - "ms", - "xpb", - "lb", - "ref", - "thorns", - "expd", - "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 reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ]; let colorMap = new Map( [ From 02341012628b52d46ab7b27f0c394368dcc6973a Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 22 May 2022 00:14:20 -0700 Subject: [PATCH 02/68] Remove common (we already have build_utils), edit instances of expandItems --- js/build_utils.js | 119 ++++++++++++++++++++++++++++++++++++++ js/common.js | 123 ---------------------------------------- js/computation_graph.js | 56 ++++++++++++------ js/customizer.js | 2 +- js/items_2.js | 2 +- js/sq2items.js | 2 +- 6 files changed, 160 insertions(+), 144 deletions(-) delete mode 100644 js/common.js diff --git a/js/build_utils.js b/js/build_utils.js index 16bacb5..e60474b 100644 --- a/js/build_utils.js +++ b/js/build_utils.js @@ -76,3 +76,122 @@ for (const [k, v] of translations) { reversetranslations.set(v, k); } +let nonRolledIDs = [ + "name", + "lore", + "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", "defReq", "agiReq", + "str", "dex", "int", "agi", "def", + "fixID", + "category", + "id", + "skillpoints", + "reqs", + "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", + "majorIds"]; +let rolledIDs = [ + "hprPct", + "mr", + "sdPct", + "mdPct", + "ls", + "ms", + "xpb", + "lb", + "ref", + "thorns", + "expd", + "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" +]; + +/** + * Take an item with id list and turn it into a set of minrolls and maxrolls. + */ +function expandItem(item) { + let minRolls = new Map(); + let maxRolls = new Map(); + let expandedItem = new Map(); + if (item.fixID) { //The item has fixed IDs. + expandedItem.set("fixID",true); + for (const id of rolledIDs) { //all rolled IDs are numerical + let val = (item[id] || 0); + minRolls.set(id,val); + maxRolls.set(id,val); + } + } else { //The item does not have fixed IDs. + for (const id of rolledIDs) { + let val = (item[id] || 0); + if (val > 0) { // positive rolled IDs + if (reversedIDs.includes(id)) { + maxRolls.set(id,idRound(val*0.3)); + minRolls.set(id,idRound(val*1.3)); + } else { + maxRolls.set(id,idRound(val*1.3)); + minRolls.set(id,idRound(val*0.3)); + } + } else if (val < 0) { //negative rolled IDs + if (reversedIDs.includes(id)) { + maxRolls.set(id,idRound(val*1.3)); + minRolls.set(id,idRound(val*0.7)); + } + else { + maxRolls.set(id,idRound(val*0.7)); + minRolls.set(id,idRound(val*1.3)); + } + } + else { // if val == 0 + // NOTE: DO NOT remove this case! idRound behavior does not round to 0! + maxRolls.set(id,0); + minRolls.set(id,0); + } + } + } + for (const id of nonRolledIDs) { + expandedItem.set(id,item[id]); + } + expandedItem.set("minRolls",minRolls); + expandedItem.set("maxRolls",maxRolls); + expandedItem.set("powders", powders); + return expandedItem; +} + +class Item { + constructor(item_obj) { + this.statMap = expandItem(item_obj); + } +} diff --git a/js/common.js b/js/common.js deleted file mode 100644 index 079922c..0000000 --- a/js/common.js +++ /dev/null @@ -1,123 +0,0 @@ -let nonRolledIDs = [ - "name", - "lore", - "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", "defReq", "agiReq", - "str", "dex", "int", "agi", "def", - "fixID", - "category", - "id", - "skillpoints", - "reqs", - "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", - "majorIds"]; -let rolledIDs = [ - "hprPct", - "mr", - "sdPct", - "mdPct", - "ls", - "ms", - "xpb", - "lb", - "ref", - "thorns", - "expd", - "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" -]; - -/** - * Take an item with id list and turn it into a set of minrolls and maxrolls. - */ -function expandItem(item) { - let minRolls = new Map(); - let maxRolls = new Map(); - let expandedItem = new Map(); - if (item.fixID) { //The item has fixed IDs. - expandedItem.set("fixID",true); - for (const id of rolledIDs) { //all rolled IDs are numerical - let val = (item[id] || 0); - minRolls.set(id,val); - maxRolls.set(id,val); - } - } else { //The item does not have fixed IDs. - for (const id of rolledIDs) { - let val = (item[id] || 0); - if (val > 0) { // positive rolled IDs - if (reversedIDs.includes(id)) { - maxRolls.set(id,idRound(val*0.3)); - minRolls.set(id,idRound(val*1.3)); - } else { - maxRolls.set(id,idRound(val*1.3)); - minRolls.set(id,idRound(val*0.3)); - } - } else if (val < 0) { //negative rolled IDs - if (reversedIDs.includes(id)) { - maxRolls.set(id,idRound(val*1.3)); - minRolls.set(id,idRound(val*0.7)); - } - else { - maxRolls.set(id,idRound(val*0.7)); - minRolls.set(id,idRound(val*1.3)); - } - } - else { // if val == 0 - // NOTE: DO NOT remove this case! idRound behavior does not round to 0! - maxRolls.set(id,0); - minRolls.set(id,0); - } - } - } - for (const id of nonRolledIDs) { - expandedItem.set(id,item[id]); - } - expandedItem.set("minRolls",minRolls); - expandedItem.set("maxRolls",maxRolls); - expandedItem.set("powders", powders); - return expandedItem; -} - -class Item { - constructor(item_obj) { - this.statMap = ; //can use the statMap as an expanded Item - this.atkSpd = attackSpeed; - this.hash = "CR-" + hash; - this.initCraftStats(); - this.statMap.set("hash", this.hash); - } -} diff --git a/js/computation_graph.js b/js/computation_graph.js index 975a22d..cdd1212 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -1,7 +1,7 @@ let _ALL_NODES = new Map(); class ComputeNode { - /*** + /** * Make a generic compute node. * Adds the node to the global map of nodenames to nodes (for calling from html listeners). * @@ -21,7 +21,7 @@ class ComputeNode { this.fail_cb = false; // Set to true to force updates even if parent failed. } - /*** + /** * Request update of this compute node. Pushes updates to children. */ update(timestamp) { @@ -42,14 +42,14 @@ class ComputeNode { } } - /*** + /** * Get value of this compute node. Can't trigger update cascades (push based update, not pull based.) */ get_value() { return this.value } - /*** + /** * Abstract method for computing something. Return value is set into this.value */ compute_func(input_map) { @@ -62,7 +62,7 @@ class ComputeNode { } } -/*** +/** * Schedule a ComputeNode to be updated. * * @param node_name : ComputeNode name to schedule an update for. @@ -79,11 +79,11 @@ function calcSchedule(node_name) { }, 500); } -/*** +/** * Node for getting an item's stats from an item input field. */ -class ItemStats extends ComputeNode { - /*** +class ItemInputNode extends ComputeNode { + /** * Make an item stat pulling compute node. * * @param name: Name of this node. @@ -94,7 +94,7 @@ class ItemStats extends ComputeNode { super(name); this.input_field.setAttribute("onInput", "calcSchedule('"+name+"');"); this.input_field = item_input_field; - this.none_item = none_item; + this.none_item = expandItem(none_item); } compute_func(input_map) { @@ -114,23 +114,23 @@ class ItemStats extends ComputeNode { item = getCraftFromHash(item_text); } else if (itemMap.has(item_text)) { - item = itemMap.get(item_text); + item = Item(itemMap.get(item_text)); } else if (tomeMap.has(item_text)) { - item = tomeMap.get(item_text); + item = Item(tomeMap.get(item_text)); } - if (!item || item.) { + if (!item || item.statMap.get('type') !== this.none_item.statMap.get('type')) { return null; } return item; } } -/*** +/** * Node for updating item input fields from parsed items. */ -class ItemInputDisplay extends ComputeNode { +class ItemInputDisplayNode extends ComputeNode { constructor(name, item_input_field, item_image) { super(name); @@ -140,10 +140,7 @@ class ItemInputDisplay extends ComputeNode { } compute_func(input_map) { - if (input_map.size !== 1) { - throw "ItemInputDisplay accepts exactly one input (item)"; - } - + if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; } const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element this.input_field.classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom'); @@ -154,5 +151,28 @@ class ItemInputDisplay extends ComputeNode { this.input_field.classList.add("is-invalid"); return null; } + + const tier = item.statMap.get('tier'); + this.input_field.classList.add(tier); + this.image.classList.add(tier + "-shadow"); + } +} + +/** + * Change the weapon to match correct type. + */ +class WeaponDisplayNode extends ComputeNode { + + constructor(name, image_field) { + super(name); + this.image = image_field; + } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "WeaponDisplayNode accepts exactly one input (item)"; } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + const type = item.statMap.get('type'); + this.image_field.setAttribute('src', '../media/items/new/generic-'+type+'.png'); } } diff --git a/js/customizer.js b/js/customizer.js index 84c665e..85ffbb0 100644 --- a/js/customizer.js +++ b/js/customizer.js @@ -369,7 +369,7 @@ function useBaseItem(elem) { //Check items db. for (const [name,itemObj] of itemMap) { if (itemName === name) { - baseItem = expandItem(itemObj, []); + baseItem = expandItem(itemObj); break; } } diff --git a/js/items_2.js b/js/items_2.js index a670ee3..220b447 100644 --- a/js/items_2.js +++ b/js/items_2.js @@ -239,7 +239,7 @@ function init_items2() { const itemListFooter = document.getElementById('item-list-footer'); // compile the search db from the item db - const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]); + const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i)]); // init item list elements const ITEM_LIST_SIZE = 64; diff --git a/js/sq2items.js b/js/sq2items.js index 59f16ba..8531721 100644 --- a/js/sq2items.js +++ b/js/sq2items.js @@ -250,7 +250,7 @@ function resetItemSearch() { } function init_items() { - items_expanded = items.filter( (i) => !("remapID" in i) ).map( (i) => expandItem(i, []) ); + items_expanded = items.filter( (i) => !("remapID" in i) ).map( (i) => expandItem(i) ); } load_init(init_items); From 06f745c158b8da65c0201bba1f85b3a6cbad47b5 Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 22 May 2022 00:57:47 -0700 Subject: [PATCH 03/68] Consolidate constants/utils files, start working on new builder file --- builder/index.html | 4 +- js/build_constants.js | 106 +++++++ js/build_utils.js | 3 +- js/builder.js | 607 +++++++++++++++++++--------------------- js/computation_graph.js | 13 +- js/display_constants.js | 1 - js/sq2bs.js | 14 +- js/sq2builder.js | 21 +- 8 files changed, 405 insertions(+), 364 deletions(-) create mode 100644 js/build_constants.js diff --git a/builder/index.html b/builder/index.html index c49ccb4..7a89a26 100644 --- a/builder/index.html +++ b/builder/index.html @@ -1379,6 +1379,7 @@ + @@ -1399,7 +1400,8 @@ - + + diff --git a/js/build_constants.js b/js/build_constants.js new file mode 100644 index 0000000..fbcbb3f --- /dev/null +++ b/js/build_constants.js @@ -0,0 +1,106 @@ +/** + * I kinda lied. Theres some listener stuff in here + * but its mostly constants for builder page specifically. + */ + +const url_tag = location.hash.slice(1); + +const BUILD_VERSION = "7.0.19"; + +let player_build; + + +// THIS IS SUPER DANGEROUS, WE SHOULD NOT BE KEEPING THIS IN SO MANY PLACES +let editable_item_fields = [ "sdPct", "sdRaw", "mdPct", "mdRaw", "poison", + "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", + "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", + "hprRaw", "hprPct", "hpBonus", "atkTier", + "spPct1", "spRaw1", "spPct2", "spRaw2", + "spPct3", "spRaw3", "spPct4", "spRaw4" ]; + +let editable_elems = []; + +for (let i of editable_item_fields) { + let elem = document.getElementById(i); + elem.addEventListener("change", (event) => { + elem.classList.add("highlight"); + }); + editable_elems.push(elem); +} + +for (let i of skp_order) { + let elem = document.getElementById(i+"-skp"); + elem.addEventListener("change", (event) => { + elem.classList.add("highlight"); + }); + editable_elems.push(elem); +} + +function clear_highlights() { + for (let i of editable_elems) { + i.classList.remove("highlight"); + } +} + + +let equipment_fields = [ + "helmet", + "chestplate", + "leggings", + "boots", + "ring1", + "ring2", + "bracelet", + "necklace", + "weapon" +]; +let tome_fields = [ + "weaponTome1", + "weaponTome2", + "armorTome1", + "armorTome2", + "armorTome3", + "armorTome4", + "guildTome1", +] +let equipment_names = [ + "Helmet", + "Chestplate", + "Leggings", + "Boots", + "Ring 1", + "Ring 2", + "Bracelet", + "Necklace", + "Weapon" +]; + +let tome_names = [ + "Weapon Tome", + "Weapon Tome", + "Armor Tome", + "Armor Tome", + "Armor Tome", + "Armor Tome", + "Guild Tome", +] +let equipmentInputs = equipment_fields.map(x => x + "-choice"); +let buildFields = equipment_fields.map(x => x+"-tooltip").concat(tome_fields.map(x => x + "-tooltip")); +let tomeInputs = tome_fields.map(x => x + "-choice"); + +let powderInputs = [ + "helmet-powder", + "chestplate-powder", + "leggings-powder", + "boots-powder", + "weapon-powder", +]; + +let weapon_keys = ['dagger', 'wand', 'bow', 'relik', 'spear']; +let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots']; +let accessory_keys= ['ring1', 'ring2', 'bracelet', 'necklace']; +let powderable_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'weapon']; +let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon'].concat(tome_keys); + +let spell_disp = ['spell0-info', 'spell1-info', 'spell2-info', 'spell3-info']; +let other_disp = ['build-order', 'set-info', 'int-info']; diff --git a/js/build_utils.js b/js/build_utils.js index e60474b..9381035 100644 --- a/js/build_utils.js +++ b/js/build_utils.js @@ -138,6 +138,7 @@ let rolledIDs = [ "gXp", "gSpd" ]; +let reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ]; /** * Take an item with id list and turn it into a set of minrolls and maxrolls. @@ -186,7 +187,7 @@ function expandItem(item) { } expandedItem.set("minRolls",minRolls); expandedItem.set("maxRolls",maxRolls); - expandedItem.set("powders", powders); + expandedItem.set("powders", []); return expandedItem; } diff --git a/js/builder.js b/js/builder.js index a91ef19..3455ee2 100644 --- a/js/builder.js +++ b/js/builder.js @@ -1,182 +1,3 @@ -const url_tag = location.hash.slice(1); -// console.log(url_base); -// console.log(url_tag); - - -const BUILD_VERSION = "7.0.19"; - -function setTitle() { - let text; - if (url_base.includes("hppeng-wynn")) { - text = "WynnBuilder UNSTABLE version "+BUILD_VERSION+" (db version "+DB_VERSION+")"; - } - else { - text = "WynnBuilder version "+BUILD_VERSION+" (db version "+DB_VERSION+")"; - document.getElementById("header").classList.add("funnynumber"); - } - document.getElementById("header").textContent = text; -} - -setTitle(); - -let player_build; - -let editable_item_fields = [ "sdPct", "sdRaw", "mdPct", "mdRaw", "poison", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "hprRaw", "hprPct", "hpBonus", "atkTier", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ]; -let editable_elems = []; - -for (let i of editable_item_fields) { - let elem = document.getElementById(i); - elem.addEventListener("change", (event) => { - elem.classList.add("highlight"); - }); - editable_elems.push(elem); -} - -for (let i of skp_order) { - let elem = document.getElementById(i+"-skp"); - elem.addEventListener("change", (event) => { - elem.classList.add("highlight"); - }); - editable_elems.push(elem); -} - -function clear_highlights() { - for (let i of editable_elems) { - i.classList.remove("highlight"); - } -} - - -let equipment_fields = [ - "helmet", - "chestplate", - "leggings", - "boots", - "ring1", - "ring2", - "bracelet", - "necklace", - "weapon" -]; -let equipment_names = [ - "Helmet", - "Chestplate", - "Leggings", - "Boots", - "Ring 1", - "Ring 2", - "Bracelet", - "Necklace", - "Weapon" -]; -let equipmentInputs = equipment_fields.map(x => x + "-choice"); -let buildFields = equipment_fields.map(x => "build-"+x); - -let powderInputs = [ - "helmet-powder", - "chestplate-powder", - "leggings-powder", - "boots-powder", - "weapon-powder", -]; - - - -/* - * Function that takes an item list and populates its corresponding dropdown. - * Used for armors and bracelet/necklace. - */ -function populateItemList(type) { - let item_list = document.getElementById(type+"-items"); - for (const item of itemLists.get(type)) { - let item_obj = itemMap.get(item); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - let el = document.createElement("option"); - el.value = item; - item_list.appendChild(el); - } -} - -/* - * Populate dropdowns, add listeners, etc. - */ -function init() { - console.log("builder.js init"); - - for (const armorType of armorTypes) { - populateItemList(armorType); - // Add change listener to update armor slots. - document.getElementById(armorType+"-choice").addEventListener("change", (event) => { - let item_name = event.target.value; - let nSlots = undefined; - if (itemMap.has(item_name)) { - let item = itemMap.get(item_name); - nSlots = item["slots"]; - //console.log(item); - } - else { - let crafted_custom_item = getCraftFromHash(item_name) !== undefined ? getCraftFromHash(item_name) : (getCustomFromHash(item_name) !== undefined ? getCustomFromHash(item_name) : undefined); - if (crafted_custom_item !== undefined) { - nSlots = crafted_custom_item.statMap.get("slots"); - } - } - if (nSlots !== undefined) { - document.getElementById(armorType+"-slots").textContent = nSlots + " slots"; - } - else { - document.getElementById(armorType+"-slots").textContent = "X slots"; - } - }); - } - - let ring1_list = document.getElementById("ring1-items"); - let ring2_list = document.getElementById("ring2-items"); - for (const ring of itemLists.get("ring")) { - let item_obj = itemMap.get(ring); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - let el1 = document.createElement("option"); - let el2 = document.createElement("option"); - el1.value = ring; - el2.value = ring; - ring1_list.appendChild(el1); - ring2_list.appendChild(el2); - } - - populateItemList("bracelet"); - populateItemList("necklace"); - - let weapon_list = document.getElementById("weapon-items"); - for (const weaponType of weaponTypes) { - for (const weapon of itemLists.get(weaponType)) { - let item_obj = itemMap.get(weapon); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - let el = document.createElement("option"); - el.value = weapon; - weapon_list.appendChild(el); - } - } - - // Add change listener to update weapon slots. - document.getElementById("weapon-choice").addEventListener("change", (event) => { - let item_name = event.target.value; - let item = itemMap.has(item_name) ? itemMap.get(item_name) : (getCraftFromHash(item_name) ? getCraftFromHash(item_name) : (getCustomFromHash(item_name) ? getCustomFromHash(item_name) : undefined)); - if (item !== undefined && event.target.value !== "") { - document.getElementById("weapon-slots").textContent = (item["slots"] ? item["slots"] : (item.statMap !== undefined ? ( item.statMap.has("slots") ? item.statMap.get("slots") : 0): 0) )+ " slots"; - } else { - document.getElementById("weapon-slots").textContent = "X slots"; - } - }); - - decodeBuild(url_tag); - - populateBuildList(); -} function getItemNameFromID(id) { if (redirectMap.has(id)) { @@ -185,13 +6,20 @@ function getItemNameFromID(id) { return idMap.get(id); } +function getTomeNameFromID(id) { + if (tomeRedirectMap.has(id)) { + return getTomeNameFromID(tomeRedirectMap.get(id)); + } + return tomeIDMap.get(id); +} + function parsePowdering(powder_info) { // TODO: Make this run in linear instead of quadratic time... ew let powdering = []; for (let i = 0; i < 5; ++i) { let powders = ""; let n_blocks = Base64.toInt(powder_info.charAt(0)); - console.log(n_blocks + " blocks"); + // console.log(n_blocks + " blocks"); powder_info = powder_info.slice(1); for (let j = 0; j < n_blocks; ++j) { let block = powder_info.slice(0,5); @@ -205,7 +33,7 @@ function parsePowdering(powder_info) { } powdering[i] = powders; } - return powdering; + return [powdering, powder_info]; } /* @@ -213,14 +41,20 @@ function parsePowdering(powder_info) { */ function decodeBuild(url_tag) { if (url_tag) { + //default values let equipment = [null, null, null, null, null, null, null, null, null]; + let tomes = [null, null, null, null, null, null, null]; let powdering = ["", "", "", "", ""]; let info = url_tag.split("_"); let version = info[0]; let save_skp = false; let skillpoints = [0, 0, 0, 0, 0]; let level = 106; - if (version === "0" || version === "1" || version === "2" || version === "3") { + + version_number = parseInt(version) + //equipment (items) + // TODO: use filters + if (version_number < 4) { let equipments = info[1]; for (let i = 0; i < 9; ++i ) { let equipment_str = equipments.slice(i*3,i*3+3); @@ -228,7 +62,7 @@ function decodeBuild(url_tag) { } info[1] = equipments.slice(27); } - if (version === "4") { + else if (version_number == 4) { let info_str = info[1]; let start_idx = 0; for (let i = 0; i < 9; ++i ) { @@ -244,7 +78,7 @@ function decodeBuild(url_tag) { } info[1] = info_str.slice(start_idx); } - if (version === "5") { + else if (version_number <= 6) { let info_str = info[1]; let start_idx = 0; for (let i = 0; i < 9; ++i ) { @@ -263,10 +97,17 @@ function decodeBuild(url_tag) { } info[1] = info_str.slice(start_idx); } - if (version === "1") { + //constant in all versions + for (let i in equipment) { + setValue(equipmentInputs[i], equipment[i]); + } + + //level, skill point assignments, and powdering + if (version_number == 1) { let powder_info = info[1]; - powdering = parsePowdering(powder_info); - } else if (version === "2") { + let res = parsePowdering(powder_info); + powdering = res[0]; + } else if (version_number == 2) { save_skp = true; let skillpoint_info = info[1].slice(0, 10); for (let i = 0; i < 5; ++i ) { @@ -274,8 +115,9 @@ function decodeBuild(url_tag) { } let powder_info = info[1].slice(10); - powdering = parsePowdering(powder_info); - } else if (version === "3" || version === "4" || version === "5"){ + let res = parsePowdering(powder_info); + powdering = res[0]; + } else if (version_number <= 6){ level = Base64.toInt(info[1].slice(10,12)); setValue("level-choice",level); save_skp = true; @@ -286,126 +128,101 @@ function decodeBuild(url_tag) { let powder_info = info[1].slice(12); - powdering = parsePowdering(powder_info); + let res = parsePowdering(powder_info); + powdering = res[0]; + info[1] = res[1]; + } + // Tomes. + if (version == 6) { + //tome values do not appear in anything before v6. + for (let i = 0; i < 7; ++i) { + let tome_str = info[1].charAt(i); + for (let i in tomes) { + setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); + } + } + info[1] = info[1].slice(7); } for (let i in powderInputs) { setValue(powderInputs[i], powdering[i]); } - for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); - } + calculateBuild(save_skp, skillpoints); } } -/* Stores the entire build in a string using B64 encryption and adds it to the URL. +/* Stores the entire build in a string using B64 encoding and adds it to the URL. */ function encodeBuild() { if (player_build) { let build_string; - if (player_build.customItems.length > 0) { //v5 encoding - build_string = "5_"; - let crafted_idx = 0; - let custom_idx = 0; - for (const item of player_build.items) { - - if (item.get("custom")) { - let custom = "CI-"+encodeCustom(player_build.customItems[custom_idx],true); - build_string += Base64.fromIntN(custom.length, 3) + custom; - custom_idx += 1; - } else if (item.get("crafted")) { - build_string += "CR-"+encodeCraft(player_build.craftedItems[crafted_idx]); - crafted_idx += 1; - } else { - build_string += Base64.fromIntN(item.get("id"), 3); - } - } - - for (const skp of skp_order) { - build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 - } - build_string += Base64.fromIntN(player_build.level, 2); - for (const _powderset of player_build.powders) { - let n_bits = Math.ceil(_powderset.length / 6); - build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. - // Slice copy. - let powderset = _powderset.slice(); - while (powderset.length != 0) { - let firstSix = powderset.slice(0,6).reverse(); - let powder_hash = 0; - for (const powder of firstSix) { - powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. - } - build_string += Base64.fromIntN(powder_hash, 5); - powderset = powderset.slice(6); - } - } - } else { //v4 encoding - build_string = "4_"; - let crafted_idx = 0; - for (const item of player_build.items) { - if (item.get("crafted")) { - build_string += "-"+encodeCraft(player_build.craftedItems[crafted_idx]); - crafted_idx += 1; - } else { - build_string += Base64.fromIntN(item.get("id"), 3); - } - } - - for (const skp of skp_order) { - build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 - } - build_string += Base64.fromIntN(player_build.level, 2); - for (const _powderset of player_build.powders) { - let n_bits = Math.ceil(_powderset.length / 6); - build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. - // Slice copy. - let powderset = _powderset.slice(); - while (powderset.length != 0) { - let firstSix = powderset.slice(0,6).reverse(); - let powder_hash = 0; - for (const powder of firstSix) { - powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. - } - build_string += Base64.fromIntN(powder_hash, 5); - powderset = powderset.slice(6); + + //V6 encoding - Tomes + build_version = 4; + build_string = ""; + tome_string = ""; + + let crafted_idx = 0; + let custom_idx = 0; + for (const item of player_build.items) { + + if (item.get("custom")) { + let custom = "CI-"+encodeCustom(player_build.customItems[custom_idx],true); + build_string += Base64.fromIntN(custom.length, 3) + custom; + custom_idx += 1; + build_version = Math.max(build_version, 5); + } else if (item.get("crafted")) { + build_string += "CR-"+encodeCraft(player_build.craftedItems[crafted_idx]); + crafted_idx += 1; + } else if (item.get("category") === "tome") { + let tome_id = item.get("id"); + if (tome_id <= 60) { + // valid normal tome. ID 61-63 is for NONE tomes. + build_version = Math.max(build_version, 6); } + tome_string += Base64.fromIntN(tome_id, 1); + } else { + build_string += Base64.fromIntN(item.get("id"), 3); } } - return build_string; + + for (const skp of skp_order) { + build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 + } + build_string += Base64.fromIntN(player_build.level, 2); + for (const _powderset of player_build.powders) { + let n_bits = Math.ceil(_powderset.length / 6); + build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. + // Slice copy. + let powderset = _powderset.slice(); + while (powderset.length != 0) { + let firstSix = powderset.slice(0,6).reverse(); + let powder_hash = 0; + for (const powder of firstSix) { + powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. + } + build_string += Base64.fromIntN(powder_hash, 5); + powderset = powderset.slice(6); + } + } + build_string += tome_string; + + return build_version.toString() + "_" + build_string; } - // this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ]; - // let build_string = "3_" + Base64.fromIntN(player_build.helmet.get("id"), 3) + - // Base64.fromIntN(player_build.chestplate.get("id"), 3) + - // Base64.fromIntN(player_build.leggings.get("id"), 3) + - // Base64.fromIntN(player_build.boots.get("id"), 3) + - // Base64.fromIntN(player_build.ring1.get("id"), 3) + - // Base64.fromIntN(player_build.ring2.get("id"), 3) + - // Base64.fromIntN(player_build.bracelet.get("id"), 3) + - // Base64.fromIntN(player_build.necklace.get("id"), 3) + - // Base64.fromIntN(player_build.weapon.get("id"), 3); - return ""; } function calculateBuild(save_skp, skp){ try { - let specialNames = ["Quake", "Chain_Lightning", "Curse", "Courage", "Wind_Prison"]; - for (const sName of specialNames) { - for (let i = 1; i < 6; i++) { - let elem = document.getElementById(sName + "-" + i); - let name = sName.replace("_", " "); - if (elem.classList.contains("toggleOn")) { //toggle the pressed button off - elem.classList.remove("toggleOn"); - } - } - } - if(player_build){ + resetEditableIDs(); + if (player_build) { + reset_powder_specials(); updateBoosts("skip", false); updatePowderSpecials("skip", false); } let weaponName = getValue(equipmentInputs[8]); + //bruh @hpp if (weaponName.startsWith("Morph-")) { let equipment = [ "Morph-Stardust", "Morph-Steel", "Morph-Iron", "Morph-Gold", "Morph-Topaz", "Morph-Emerald", "Morph-Amethyst", "Morph-Ruby", weaponName.substring(6) ]; for (let i in equipment) { @@ -440,7 +257,6 @@ function calculateBuild(save_skp, skp){ while (input) { let first = input.slice(0, 2); let powder = powderIDs.get(first); - console.log(powder); if (powder === undefined) { errorederrors.push(first); } else { @@ -457,11 +273,24 @@ function calculateBuild(save_skp, skp){ //console.log("POWDERING: " + powdering); powderings.push(powdering); } + let tomes = [ null, null, null, null, null, null, null]; + for (let i in tomes) { + let equip = getValue(tomeInputs[i]).trim(); + if (equip === "") { + equip = "No " + tome_names[i] + } + else { + setValue(tomeInputs[i], equip); + } + tomes[i] = equip; + } let level = document.getElementById("level-choice").value; - player_build = new Build(level, equipment, powderings, new Map(), errors); + player_build = new Build(level, equipment, powderings, new Map(), errors, tomes); console.log(player_build); + + //isn't this deprecated? for (let i of document.getElementsByClassName("hide-container-block")) { i.style.display = "block"; } @@ -470,21 +299,14 @@ function calculateBuild(save_skp, skp){ } console.log(player_build.toString()); - displayEquipOrder(document.getElementById("build-order"),player_build.equip_order); - - + displaysq2EquipOrder(document.getElementById("build-order"),player_build.equip_order); const assigned = player_build.base_skillpoints; const skillpoints = player_build.total_skillpoints; for (let i in skp_order){ //big bren - setText(skp_order[i] + "-skp-base", "Original Value: " + skillpoints[i]); + setText(skp_order[i] + "-skp-base", "Original: " + skillpoints[i]); } - for (let id of editable_item_fields) { - setValue(id, player_build.statMap.get(id)); - setText(id+"-base", "Original Value: " + player_build.statMap.get(id)); - } - if (save_skp) { // TODO: reduce duplicated code, @updateStats let skillpoints = player_build.total_skillpoints; @@ -499,14 +321,13 @@ function calculateBuild(save_skp, skp){ player_build.assigned_skillpoints += delta_total; } + updateEditableIDs(); calculateBuildStats(); - setTitle(); if (player_build.errored) throw new ListError(player_build.errors); - } catch (error) { - handleBuilderError(error); + console.log(error); } } @@ -586,7 +407,7 @@ function updateStats() { let delta_total = 0; for (let i in skp_order) { let value = document.getElementById(skp_order[i] + "-skp").value; - if (value === ""){value = "0"; setValue(skp_order[i] + "-skp", value)} + if (value === ""){value = 0; setValue(skp_order[i] + "-skp", value)} let manual_assigned = 0; if (value.includes("+")) { let skp = value.split("+"); @@ -604,15 +425,58 @@ function updateStats() { player_build.assigned_skillpoints += delta_total; if(player_build){ updatePowderSpecials("skip", false); + updateArmorPowderSpecials("skip", false); updateBoosts("skip", false); - } - for (let id of editable_item_fields) { - player_build.statMap.set(id, parseInt(getValue(id))); + for (let id of editable_item_fields) { + player_build.statMap.set(id, parseInt(getValue(id))); + } } player_build.aggregateStats(); console.log(player_build.statMap); calculateBuildStats(); } + + +/* Updates all IDs in the edit IDs section. Resets each input and original value text to the correct text according to the current build. +*/ +function updateEditableIDs() { + if (player_build) { + for (const id of editable_item_fields) { + let edit_input = document.getElementById(id); + let val = player_build.statMap.get(id); + edit_input.value = val; + edit_input.placeholder = val; + + let value_label = document.getElementById(id + "-base"); + value_label.textContent = "Original Value: " + val; + //a hack to make resetting easier + value_label.value = val; + } + } +} + +/* Resets all IDs in the edit IDs section to their "original" values. +*/ +function resetEditableIDs() { + if (player_build) { + for (const id of editable_item_fields) { + let edit_input = document.getElementById(id); + let value_label = document.getElementById(id + "-base"); + + edit_input.value = value_label.value; + edit_input.placeholder = value_label.value; + } + } else { + //no player build, reset to 0 + for (const id of editable_item_fields) { + let edit_input = document.getElementById(id); + + edit_input.value = 0; + edit_input.placeholder = 0; + } + } +} + /* Updates all spell boosts */ function updateBoosts(buttonId, recalcStats) { @@ -655,7 +519,7 @@ function updateBoosts(buttonId, recalcStats) { } } -/* Updates all powder special boosts +/* Updates ACTIVE powder special boosts (weapons) */ function updatePowderSpecials(buttonId, recalcStats) { //console.log(player_build.statMap); @@ -740,8 +604,76 @@ function updatePowderSpecials(buttonId, recalcStats) { if (recalcStats) { calculateBuildStats(); } - displayPowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build); + displaysq2PowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build, true); } + + +/* Updates PASSIVE powder special boosts (armors) +*/ +function updateArmorPowderSpecials(elem_id, recalc_stats) { + //we only update the powder special + external stats if the player has a build + if (elem_id !== "skip") { + if (player_build !== undefined && player_build.weapon !== undefined && player_build.weapon.get("name") !== "No Weapon") { + let wynn_elem = elem_id.split("_")[0]; //str, dex, int, def, agi + + //update the label associated w/ the slider + let elem = document.getElementById(elem_id); + let label = document.getElementById(elem_id + "_label"); + let prev_label = document.getElementById(elem_id + "_prev"); + + let value = elem.value; + + //for use in editing build stats + let prev_value = prev_label.value; + let value_diff = value - prev_value; + + //update the "previous" label + prev_label.value = value; + + label.textContent = label.textContent.split(":")[0] + ": " + value; + + let dmg_id = elem_chars[skp_names.indexOf(wynn_elem)] + "DamPct"; + let new_dmgboost = player_build.externalStats.get(dmg_id) + value_diff; + + //update build external stats - the second one is the relevant one for damage calc purposes + player_build.externalStats.set(dmg_id, new_dmgboost); + player_build.externalStats.get("damageBonus")[skp_names.indexOf(wynn_elem)] = new_dmgboost; + + //update the slider's graphics + let bg_color = elem_colors[skp_names.indexOf(wynn_elem)]; + let pct = Math.round(100 * value / powderSpecialStats[skp_names.indexOf(wynn_elem)].cap); + elem.style.background = `linear-gradient(to right, ${bg_color}, ${bg_color} ${pct}%, #AAAAAA ${pct}%, #AAAAAA 100%)`; + + } + } else { + if (player_build !== undefined) { + for (let i = 0; i < skp_names.length; ++i) { + skp_name = skp_names[i]; + skp_char = elem_chars[i]; + player_build.externalStats.set(skp_char + "DamPct", player_build.externalStats.get(skp_char + "DamPct") - document.getElementById(skp_name+"_boost_armor").value); + player_build.externalStats.get("damageBonus")[i] -= document.getElementById(skp_name+"_boost_armor").value; + } + } + } + + + if (recalc_stats && player_build) { + //calc build stats and display powder special + calculateBuildStats(); + // displaysq2PowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build, true); + } + +} + +function resetArmorPowderSpecials() { + for (const skp of skp_names) { + document.getElementById(skp + "_boost_armor").value = 0; + document.getElementById(skp + "_boost_armor_prev").value = 0; + document.getElementById(skp + "_boost_armor").style.background = `linear-gradient(to right, #AAAAAA, #AAAAAA 0%, #AAAAAA 100%)`; + document.getElementById(skp + "_boost_armor_label").textContent = `% ${capitalizeFirst(elem_names[skp_names.indexOf(skp)])} Damage Boost: 0` + } +} + /* Calculates all build statistics and updates the entire display. */ function calculateBuildStats() { @@ -749,27 +681,29 @@ function calculateBuildStats() { const skillpoints = player_build.total_skillpoints; let skp_effects = ["% more damage dealt.","% chance to crit.","% spell cost reduction.","% less damage taken.","% chance to dodge."]; for (let i in skp_order){ //big bren - setText(skp_order[i] + "-skp-assign", "Manually Assigned: " + assigned[i]); + setText(skp_order[i] + "-skp-assign", "Assign: " + assigned[i]); setValue(skp_order[i] + "-skp", skillpoints[i]); let linebreak = document.createElement("br"); linebreak.classList.add("itemp"); document.getElementById(skp_order[i] + "-skp-label"); setText(skp_order[i] + "-skp-pct", (skillPointsToPercentage(skillpoints[i])*100).toFixed(1).concat(skp_effects[i])); + document.getElementById(skp_order[i]+"-warnings").textContent = '' if (assigned[i] > 100) { let skp_warning = document.createElement("p"); skp_warning.classList.add("warning"); - skp_warning.textContent += "WARNING: Cannot assign " + assigned[i] + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually."; - document.getElementById(skp_order[i]+"-skp-pct").appendChild(skp_warning); + skp_warning.classList.add("small-text") + skp_warning.textContent += "Cannot assign " + assigned[i] + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually."; + document.getElementById(skp_order[i]+"-warnings").textContent = '' + document.getElementById(skp_order[i]+"-warnings").appendChild(skp_warning); } } let summarybox = document.getElementById("summary-box"); summarybox.textContent = ""; let skpRow = document.createElement("p"); - let td = document.createElement("p"); let remainingSkp = document.createElement("p"); - remainingSkp.classList.add("center"); + remainingSkp.classList.add("scaled-font"); let remainingSkpTitle = document.createElement("b"); remainingSkpTitle.textContent = "Assigned " + player_build.assigned_skillpoints + " skillpoints. Remaining skillpoints: "; let remainingSkpContent = document.createElement("b"); @@ -783,13 +717,12 @@ function calculateBuildStats() { summarybox.append(skpRow); summarybox.append(remainingSkp); if(player_build.assigned_skillpoints > levelToSkillPoints(player_build.level)){ - let skpWarning = document.createElement("p"); + let skpWarning = document.createElement("span"); //skpWarning.classList.add("itemp"); skpWarning.classList.add("warning"); - skpWarning.classList.add("itemp"); skpWarning.textContent = "WARNING: Too many skillpoints need to be assigned!"; let skpCount = document.createElement("p"); - skpCount.classList.add("itemp"); + skpCount.classList.add("warning"); skpCount.textContent = "For level " + (player_build.level>101 ? "101+" : player_build.level) + ", there are only " + levelToSkillPoints(player_build.level) + " skill points available."; summarybox.append(skpWarning); summarybox.append(skpCount); @@ -826,7 +759,6 @@ function calculateBuildStats() { const bonus = sets[setName].bonuses[count-1]; // console.log(setName); if (bonus["illegal"]) { - console.log("mmmm"); let setWarning = document.createElement("p"); setWarning.classList.add("itemp"); setWarning.classList.add("warning"); @@ -836,30 +768,32 @@ function calculateBuildStats() { } for (let i in player_build.items) { - displayExpandedItem(player_build.items[i], buildFields[i]); + displaysq2ExpandedItem(player_build.items[i], buildFields[i]); + collapse_element("#"+equipment_keys[i]+"-tooltip"); } - displayBuildStats("build-overall-stats",player_build); - displaySetBonuses("set-info",player_build); - displayNextCosts("int-info",player_build); + displaysq2ArmorStats(player_build); + displaysq2BuildStats('overall-stats', player_build, build_all_display_commands); + displaysq2BuildStats("offensive-stats",player_build, build_offensive_display_commands); + displaysq2SetBonuses("set-info",player_build); + displaysq2WeaponStats(player_build); let meleeStats = player_build.getMeleeStats(); - displayMeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); + displaysq2MeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); - displayDefenseStats(document.getElementById("build-defense-stats"),player_build); + displaysq2DefenseStats(document.getElementById("defensive-stats"),player_build); - displayPoisonDamage(document.getElementById("build-poison-stats"),player_build); + displaysq2PoisonDamage(document.getElementById("build-poison-stats"),player_build); let spells = spell_table[player_build.weapon.get("type")]; for (let i = 0; i < 4; ++i) { let parent_elem = document.getElementById("spell"+i+"-info"); let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); - displaySpellDamage(parent_elem, overallparent_elem, player_build, spells[i], i+1); + displaysq2SpellDamage(parent_elem, overallparent_elem, player_build, spells[i], i+1); } location.hash = encodeBuild(); clear_highlights(); - updateOGP(); } function copyBuild() { @@ -971,7 +905,21 @@ function toggleID() { } } +function toggleButton(button_id) { + let button = document.getElementById(button_id); + if (button) { + if (button.classList.contains("toggleOn")) { + button.classList.remove("toggleOn"); + } else { + button.classList.add("toggleOn"); + } + } +} + function optimizeStrDex() { + if (!player_build) { + return; + } const remaining = levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints; const base_skillpoints = player_build.base_skillpoints; const max_str_boost = 100 - base_skillpoints[0]; @@ -1061,7 +1009,6 @@ function optimizeStrDex() { try { calculateBuildStats(); - setTitle(); if (player_build.errored) throw new ListError(player_build.errors); } @@ -1071,8 +1018,20 @@ function optimizeStrDex() { } // TODO: Learn and use await +function init() { + console.log("builder.js init"); + init_autocomplete(); + decodeBuild(url_tag); + for (const i of equipment_keys) { + update_field(i); + } +} function init2() { load_ing_init(init); } -load_init(init2); -updateOGP(); \ No newline at end of file +function init3() { + load_tome_init(init2) +} + + +load_init(init3); diff --git a/js/computation_graph.js b/js/computation_graph.js index cdd1212..9039685 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -1,5 +1,3 @@ -let _ALL_NODES = new Map(); - class ComputeNode { /** * Make a generic compute node. @@ -8,10 +6,6 @@ class ComputeNode { * @param name : Name of the node (string). Must be unique. Must "fit in" a JS string (terminated by single quotes). */ constructor(name) { - if (_ALL_NODES.has(name)) { - throw 'Duplicate node name: ' + name; - } - _ALL_NODES.set(name, this) this.inputs = []; this.children = []; this.value = 0; @@ -65,10 +59,9 @@ class ComputeNode { /** * Schedule a ComputeNode to be updated. * - * @param node_name : ComputeNode name to schedule an update for. + * @param node : ComputeNode to schedule an update for. */ -function calcSchedule(node_name) { - node = _ALL_NODES.get(node_name); +function calcSchedule(node) { if (node.update_task !== null) { clearTimeout(node.update_task); } @@ -92,7 +85,7 @@ class ItemInputNode extends ComputeNode { */ constructor(name, item_input_field, none_item) { super(name); - this.input_field.setAttribute("onInput", "calcSchedule('"+name+"');"); + this.input_field.setAttribute("input", () => calcSchedule(this)); this.input_field = item_input_field; this.none_item = expandItem(none_item); } diff --git a/js/display_constants.js b/js/display_constants.js index d8c26dd..93f77e6 100644 --- a/js/display_constants.js +++ b/js/display_constants.js @@ -1,4 +1,3 @@ -let reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ]; let colorMap = new Map( [ ["Normal", "#fff"], diff --git a/js/sq2bs.js b/js/sq2bs.js index 25ac2d2..94f1c22 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -1,13 +1,3 @@ -let weapon_keys = ['dagger', 'wand', 'bow', 'relik', 'spear']; -let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots']; -let skp_keys = ['str', 'dex', 'int', 'def', 'agi']; -let accessory_keys= ['ring1', 'ring2', 'bracelet', 'necklace']; -let powderable_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'weapon']; -let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon'].concat(tome_keys); -let powder_keys = ['e', 't', 'w', 'f', 'a']; - -let spell_disp = ['spell0-info', 'spell1-info', 'spell2-info', 'spell3-info']; -let other_disp = ['build-order', 'set-info', 'int-info']; document.addEventListener('DOMContentLoaded', function() { @@ -202,7 +192,7 @@ function update_field(field) { document.querySelector("#"+field+"-powder").classList.add("is-invalid"); } else { for (i = 0; i < powder_string.length / 2; i++) { - if (powder_keys.includes(powder_string.substring(i*2, i*2+2).split("")[0]) == false || isNaN(powder_string.substring(i*2, i*2+2).split("")[1]) || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) < 1 || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) > 6) { + if (skp_elements.includes(powder_string.substring(i*2, i*2+2).split("")[0]) == false || isNaN(powder_string.substring(i*2, i*2+2).split("")[1]) || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) < 1 || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) > 6) { document.querySelector("#"+field+"-powder").classList.add("is-invalid"); } } @@ -239,7 +229,7 @@ function toggle_spell_tab(tab) { } function toggle_boost_tab(tab) { - for (const i of skp_keys) { + for (const i of skp_order) { document.querySelector("#"+i+"-boost").style.display = "none"; document.getElementById(i + "-boost-tab").classList.remove("selected-btn"); } diff --git a/js/sq2builder.js b/js/sq2builder.js index fc42f6d..e63cdb0 100644 --- a/js/sq2builder.js +++ b/js/sq2builder.js @@ -5,25 +5,16 @@ const url_tag = location.hash.slice(1); const BUILD_VERSION = "7.0.19"; -// function setTitle() { -// let text; -// if (url_base.includes("hppeng-wynn")) { -// text = "WynnBuilder UNSTABLE version "+BUILD_VERSION+" (db version "+DB_VERSION+")"; -// } -// else { -// text = "WynnBuilder version "+BUILD_VERSION+" (db version "+DB_VERSION+")"; -// document.getElementById("header").classList.add("funnynumber"); -// } -// document.getElementById("header").textContent = text; -// } -// -// setTitle(); - let player_build; // THIS IS SUPER DANGEROUS, WE SHOULD NOT BE KEEPING THIS IN SO MANY PLACES -let editable_item_fields = [ "sdPct", "sdRaw", "mdPct", "mdRaw", "poison", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "hprRaw", "hprPct", "hpBonus", "atkTier", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ]; +let editable_item_fields = [ "sdPct", "sdRaw", "mdPct", "mdRaw", "poison", + "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", + "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", + "hprRaw", "hprPct", "hpBonus", "atkTier", + "spPct1", "spRaw1", "spPct2", "spRaw2", + "spPct3", "spRaw3", "spPct4", "spRaw4" ]; let editable_elems = []; From 0f4dba258f3cc65ce07ff81c5ad0f80c84555f1a Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 22 May 2022 03:21:34 -0700 Subject: [PATCH 04/68] Proof of concept (compute nodes to get items from names and highlight) --- builder/index.html | 3 +- js/builder.js | 691 +--------------------------------------- js/builder_graph.js | 21 ++ js/computation_graph.js | 71 ++++- js/load.js | 86 ++--- js/optimize.js | 101 ++++++ js/utils.js | 27 +- 7 files changed, 227 insertions(+), 773 deletions(-) create mode 100644 js/builder_graph.js create mode 100644 js/optimize.js diff --git a/builder/index.html b/builder/index.html index 7a89a26..d7a802c 100644 --- a/builder/index.html +++ b/builder/index.html @@ -1402,10 +1402,11 @@ + - + diff --git a/js/builder.js b/js/builder.js index 3455ee2..1b18b96 100644 --- a/js/builder.js +++ b/js/builder.js @@ -1,4 +1,3 @@ - function getItemNameFromID(id) { if (redirectMap.has(id)) { return getItemNameFromID(redirectMap.get(id)); @@ -51,7 +50,7 @@ function decodeBuild(url_tag) { let skillpoints = [0, 0, 0, 0, 0]; let level = 106; - version_number = parseInt(version) + let version_number = parseInt(version) //equipment (items) // TODO: use filters if (version_number < 4) { @@ -147,8 +146,6 @@ function decodeBuild(url_tag) { for (let i in powderInputs) { setValue(powderInputs[i], powdering[i]); } - - calculateBuild(save_skp, skillpoints); } } @@ -213,589 +210,6 @@ function encodeBuild() { } } -function calculateBuild(save_skp, skp){ - try { - resetEditableIDs(); - if (player_build) { - reset_powder_specials(); - updateBoosts("skip", false); - updatePowderSpecials("skip", false); - } - let weaponName = getValue(equipmentInputs[8]); - //bruh @hpp - if (weaponName.startsWith("Morph-")) { - let equipment = [ "Morph-Stardust", "Morph-Steel", "Morph-Iron", "Morph-Gold", "Morph-Topaz", "Morph-Emerald", "Morph-Amethyst", "Morph-Ruby", weaponName.substring(6) ]; - for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); - } - } - - //updatePowderSpecials("skip"); //jank pt 1 - save_skp = (typeof save_skp !== 'undefined') ? save_skp : false; - /* TODO: implement level changing - Make this entire function prettier - */ - let equipment = [ null, null, null, null, null, null, null, null, null ]; - for (let i in equipment) { - let equip = getValue(equipmentInputs[i]).trim(); - if (equip === "") { - equip = "No " + equipment_names[i] - } - else { - setValue(equipmentInputs[i], equip); - } - equipment[i] = equip; - } - let powderings = []; - let errors = []; - for (const i in powderInputs) { - // read in two characters at a time. - // TODO: make this more robust. - let input = getValue(powderInputs[i]).trim(); - let powdering = []; - let errorederrors = []; - while (input) { - let first = input.slice(0, 2); - let powder = powderIDs.get(first); - if (powder === undefined) { - errorederrors.push(first); - } else { - powdering.push(powder); - } - input = input.slice(2); - } - if (errorederrors.length > 0) { - if (errorederrors.length > 1) - errors.push(new IncorrectInput(errorederrors.join(""), "t6w6", powderInputs[i])); - else - errors.push(new IncorrectInput(errorederrors[0], "t6 or e3", powderInputs[i])); - } - //console.log("POWDERING: " + powdering); - powderings.push(powdering); - } - let tomes = [ null, null, null, null, null, null, null]; - for (let i in tomes) { - let equip = getValue(tomeInputs[i]).trim(); - if (equip === "") { - equip = "No " + tome_names[i] - } - else { - setValue(tomeInputs[i], equip); - } - tomes[i] = equip; - } - - - let level = document.getElementById("level-choice").value; - player_build = new Build(level, equipment, powderings, new Map(), errors, tomes); - console.log(player_build); - - //isn't this deprecated? - for (let i of document.getElementsByClassName("hide-container-block")) { - i.style.display = "block"; - } - for (let i of document.getElementsByClassName("hide-container-grid")) { - i.style.display = "grid"; - } - - console.log(player_build.toString()); - displaysq2EquipOrder(document.getElementById("build-order"),player_build.equip_order); - - const assigned = player_build.base_skillpoints; - const skillpoints = player_build.total_skillpoints; - for (let i in skp_order){ //big bren - setText(skp_order[i] + "-skp-base", "Original: " + skillpoints[i]); - } - - if (save_skp) { - // TODO: reduce duplicated code, @updateStats - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; - for (let i in skp_order) { - let manual_assigned = skp[i]; - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; - } - player_build.assigned_skillpoints += delta_total; - } - - updateEditableIDs(); - calculateBuildStats(); - if (player_build.errored) - throw new ListError(player_build.errors); - } - catch (error) { - console.log(error); - } -} - -function handleBuilderError(error) { - if (error instanceof ListError) { - for (let i of error.errors) { - if (i instanceof ItemNotFound) { - i.element.textContent = i.message; - } else if (i instanceof IncorrectInput) { - if (document.getElementById(i.id) !== null) { - document.getElementById(i.id).parentElement.querySelectorAll("p.error")[0].textContent = i.message; - } - } else { - let msg = i.stack; - let lines = msg.split("\n"); - let header = document.getElementById("header"); - header.textContent = ""; - for (const line of lines) { - let p = document.createElement("p"); - p.classList.add("itemp"); - p.textContent = line; - header.appendChild(p); - } - let p2 = document.createElement("p"); - p2.textContent = "If you believe this is an error, contact hppeng on forums or discord."; - header.appendChild(p2); - } - } - } else { - let msg = error.stack; - let lines = msg.split("\n"); - let header = document.getElementById("header"); - header.textContent = ""; - for (const line of lines) { - let p = document.createElement("p"); - p.classList.add("itemp"); - p.textContent = line; - header.appendChild(p); - } - let p2 = document.createElement("p"); - p2.textContent = "If you believe this is an error, contact hppeng on forums or discord."; - header.appendChild(p2); - } -} - -/* Updates all build statistics based on (for now) the skillpoint input fields and then calculates build stats. -*/ -function updateStats() { - - let specialNames = ["Quake", "Chain_Lightning", "Curse", "Courage", "Wind_Prison"]; - for (const sName of specialNames) { - for (let i = 1; i < 6; i++) { - let elem = document.getElementById(sName + "-" + i); - let name = sName.replace("_", " "); - if (elem.classList.contains("toggleOn")) { //toggle the pressed button off - elem.classList.remove("toggleOn"); - let special = powderSpecialStats[specialNames.indexOf(sName)]; - console.log(special); - if (special["weaponSpecialEffects"].has("Damage Boost")) { - if (name === "Courage" || name === "Curse") { //courage is universal damage boost - //player_build.damageMultiplier -= special.weaponSpecialEffects.get("Damage Boost")[i-1]/100; - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.get("damageBonus")[4] -= special.weaponSpecialEffects.get("Damage Boost")[i-1]; - } - } - } - } - } - - - - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; - for (let i in skp_order) { - let value = document.getElementById(skp_order[i] + "-skp").value; - if (value === ""){value = 0; setValue(skp_order[i] + "-skp", value)} - let manual_assigned = 0; - if (value.includes("+")) { - let skp = value.split("+"); - for (const s of skp) { - manual_assigned += parseInt(s,10); - } - } else { - manual_assigned = parseInt(value,10); - } - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; - } - player_build.assigned_skillpoints += delta_total; - if(player_build){ - updatePowderSpecials("skip", false); - updateArmorPowderSpecials("skip", false); - updateBoosts("skip", false); - for (let id of editable_item_fields) { - player_build.statMap.set(id, parseInt(getValue(id))); - } - } - player_build.aggregateStats(); - console.log(player_build.statMap); - calculateBuildStats(); -} - - -/* Updates all IDs in the edit IDs section. Resets each input and original value text to the correct text according to the current build. -*/ -function updateEditableIDs() { - if (player_build) { - for (const id of editable_item_fields) { - let edit_input = document.getElementById(id); - let val = player_build.statMap.get(id); - edit_input.value = val; - edit_input.placeholder = val; - - let value_label = document.getElementById(id + "-base"); - value_label.textContent = "Original Value: " + val; - //a hack to make resetting easier - value_label.value = val; - } - } -} - -/* Resets all IDs in the edit IDs section to their "original" values. -*/ -function resetEditableIDs() { - if (player_build) { - for (const id of editable_item_fields) { - let edit_input = document.getElementById(id); - let value_label = document.getElementById(id + "-base"); - - edit_input.value = value_label.value; - edit_input.placeholder = value_label.value; - } - } else { - //no player build, reset to 0 - for (const id of editable_item_fields) { - let edit_input = document.getElementById(id); - - edit_input.value = 0; - edit_input.placeholder = 0; - } - } -} - -/* Updates all spell boosts -*/ -function updateBoosts(buttonId, recalcStats) { - let elem = document.getElementById(buttonId); - let name = buttonId.split("-")[0]; - if(buttonId !== "skip") { - if (elem.classList.contains("toggleOn")) { - player_build.damageMultiplier -= damageMultipliers.get(name); - if (name === "warscream") { - player_build.defenseMultiplier -= .20; - } - if (name === "vanish") { - player_build.defenseMultiplier -= .15; - } - elem.classList.remove("toggleOn"); - }else{ - player_build.damageMultiplier += damageMultipliers.get(name); - if (name === "warscream") { - player_build.defenseMultiplier += .20; - } - if (name === "vanish") { - player_build.defenseMultiplier += .15; - } - elem.classList.add("toggleOn"); - } - updatePowderSpecials("skip", false); //jank pt 1 - } else { - for (const [key, value] of damageMultipliers) { - let elem = document.getElementById(key + "-boost") - if (elem.classList.contains("toggleOn")) { - elem.classList.remove("toggleOn"); - player_build.damageMultiplier -= value; - if (key === "warscream") { player_build.defenseMultiplier -= .20 } - if (key === "vanish") { player_build.defenseMultiplier -= .15 } - } - } - } - if (recalcStats) { - calculateBuildStats(); - } -} - -/* Updates ACTIVE powder special boosts (weapons) -*/ -function updatePowderSpecials(buttonId, recalcStats) { - //console.log(player_build.statMap); - - let name = (buttonId).split("-")[0]; - let power = (buttonId).split("-")[1]; // [1, 5] - let specialNames = ["Quake", "Chain Lightning", "Curse", "Courage", "Wind Prison"]; - let powderSpecials = []; // [ [special, power], [special, power]] - - - if(name !== "skip"){ - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { //toggle the pressed button off - elem.classList.remove("toggleOn"); - let special = powderSpecialStats[specialNames.indexOf(name.replace("_", " "))]; - if (special.weaponSpecialEffects.has("Damage Boost")) { - name = name.replace("_", " "); - if (name === "Courage" || name === "Curse") { //courage and curse are universal damage boost - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - //poison? - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.get("damageBonus")[4] -= special.weaponSpecialEffects.get("Damage Boost")[power-1]; - } - } - } else { - for (let i = 1;i < 6; i++) { //toggle all pressed buttons of the same powder special off - //name is same, power is i - if(document.getElementById(name.replace(" ", "_") + "-" + i).classList.contains("toggleOn")) { - document.getElementById(name.replace(" ", "_") + "-" + i).classList.remove("toggleOn"); - let special = powderSpecialStats[specialNames.indexOf(name.replace("_", " "))]; - if (special.weaponSpecialEffects.has("Damage Boost")) { - name = name.replace("_", " "); //might be redundant - if (name === "Courage" || name === "Curse") { //courage is universal damage boost - //player_build.damageMultiplier -= special.weaponSpecialEffects.get("Damage Boost")[i-1]/100; - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.get("damageBonus")[4] -= special.weaponSpecialEffects.get("Damage Boost")[i-1]; - } - } - } - } - //toggle the pressed button on - elem.classList.add("toggleOn"); - } - } - - for (const sName of specialNames) { - for (let i = 1;i < 6; i++) { - if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) { - let powderSpecial = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))]; - powderSpecials.push([powderSpecial, i]); - break; - } - } - } - - - if (name !== "skip") { - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { - let special = powderSpecialStats[specialNames.indexOf(name.replace("_", " "))]; - if (special["weaponSpecialEffects"].has("Damage Boost")) { - let name = special["weaponSpecialName"]; - if (name === "Courage" || name === "Curse") { //courage and curse are is universal damage boost - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.get("damageBonus")[4] += special.weaponSpecialEffects.get("Damage Boost")[power-1]; - } - } - } - } - - if (recalcStats) { - calculateBuildStats(); - } - displaysq2PowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build, true); -} - - -/* Updates PASSIVE powder special boosts (armors) -*/ -function updateArmorPowderSpecials(elem_id, recalc_stats) { - //we only update the powder special + external stats if the player has a build - if (elem_id !== "skip") { - if (player_build !== undefined && player_build.weapon !== undefined && player_build.weapon.get("name") !== "No Weapon") { - let wynn_elem = elem_id.split("_")[0]; //str, dex, int, def, agi - - //update the label associated w/ the slider - let elem = document.getElementById(elem_id); - let label = document.getElementById(elem_id + "_label"); - let prev_label = document.getElementById(elem_id + "_prev"); - - let value = elem.value; - - //for use in editing build stats - let prev_value = prev_label.value; - let value_diff = value - prev_value; - - //update the "previous" label - prev_label.value = value; - - label.textContent = label.textContent.split(":")[0] + ": " + value; - - let dmg_id = elem_chars[skp_names.indexOf(wynn_elem)] + "DamPct"; - let new_dmgboost = player_build.externalStats.get(dmg_id) + value_diff; - - //update build external stats - the second one is the relevant one for damage calc purposes - player_build.externalStats.set(dmg_id, new_dmgboost); - player_build.externalStats.get("damageBonus")[skp_names.indexOf(wynn_elem)] = new_dmgboost; - - //update the slider's graphics - let bg_color = elem_colors[skp_names.indexOf(wynn_elem)]; - let pct = Math.round(100 * value / powderSpecialStats[skp_names.indexOf(wynn_elem)].cap); - elem.style.background = `linear-gradient(to right, ${bg_color}, ${bg_color} ${pct}%, #AAAAAA ${pct}%, #AAAAAA 100%)`; - - } - } else { - if (player_build !== undefined) { - for (let i = 0; i < skp_names.length; ++i) { - skp_name = skp_names[i]; - skp_char = elem_chars[i]; - player_build.externalStats.set(skp_char + "DamPct", player_build.externalStats.get(skp_char + "DamPct") - document.getElementById(skp_name+"_boost_armor").value); - player_build.externalStats.get("damageBonus")[i] -= document.getElementById(skp_name+"_boost_armor").value; - } - } - } - - - if (recalc_stats && player_build) { - //calc build stats and display powder special - calculateBuildStats(); - // displaysq2PowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build, true); - } - -} - -function resetArmorPowderSpecials() { - for (const skp of skp_names) { - document.getElementById(skp + "_boost_armor").value = 0; - document.getElementById(skp + "_boost_armor_prev").value = 0; - document.getElementById(skp + "_boost_armor").style.background = `linear-gradient(to right, #AAAAAA, #AAAAAA 0%, #AAAAAA 100%)`; - document.getElementById(skp + "_boost_armor_label").textContent = `% ${capitalizeFirst(elem_names[skp_names.indexOf(skp)])} Damage Boost: 0` - } -} - -/* Calculates all build statistics and updates the entire display. -*/ -function calculateBuildStats() { - const assigned = player_build.base_skillpoints; - const skillpoints = player_build.total_skillpoints; - let skp_effects = ["% more damage dealt.","% chance to crit.","% spell cost reduction.","% less damage taken.","% chance to dodge."]; - for (let i in skp_order){ //big bren - setText(skp_order[i] + "-skp-assign", "Assign: " + assigned[i]); - setValue(skp_order[i] + "-skp", skillpoints[i]); - let linebreak = document.createElement("br"); - linebreak.classList.add("itemp"); - document.getElementById(skp_order[i] + "-skp-label"); - setText(skp_order[i] + "-skp-pct", (skillPointsToPercentage(skillpoints[i])*100).toFixed(1).concat(skp_effects[i])); - document.getElementById(skp_order[i]+"-warnings").textContent = '' - if (assigned[i] > 100) { - let skp_warning = document.createElement("p"); - skp_warning.classList.add("warning"); - skp_warning.classList.add("small-text") - skp_warning.textContent += "Cannot assign " + assigned[i] + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually."; - document.getElementById(skp_order[i]+"-warnings").textContent = '' - document.getElementById(skp_order[i]+"-warnings").appendChild(skp_warning); - } - } - - let summarybox = document.getElementById("summary-box"); - summarybox.textContent = ""; - let skpRow = document.createElement("p"); - - let remainingSkp = document.createElement("p"); - remainingSkp.classList.add("scaled-font"); - let remainingSkpTitle = document.createElement("b"); - remainingSkpTitle.textContent = "Assigned " + player_build.assigned_skillpoints + " skillpoints. Remaining skillpoints: "; - let remainingSkpContent = document.createElement("b"); - remainingSkpContent.textContent = "" + (levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints); - remainingSkpContent.classList.add(levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints < 0 ? "negative" : "positive"); - - remainingSkp.appendChild(remainingSkpTitle); - remainingSkp.appendChild(remainingSkpContent); - - - summarybox.append(skpRow); - summarybox.append(remainingSkp); - if(player_build.assigned_skillpoints > levelToSkillPoints(player_build.level)){ - let skpWarning = document.createElement("span"); - //skpWarning.classList.add("itemp"); - skpWarning.classList.add("warning"); - skpWarning.textContent = "WARNING: Too many skillpoints need to be assigned!"; - let skpCount = document.createElement("p"); - skpCount.classList.add("warning"); - skpCount.textContent = "For level " + (player_build.level>101 ? "101+" : player_build.level) + ", there are only " + levelToSkillPoints(player_build.level) + " skill points available."; - summarybox.append(skpWarning); - summarybox.append(skpCount); - } - let lvlWarning; - for (const item of player_build.items) { - let item_lvl; - if (item.get("crafted")) { - //item_lvl = item.get("lvlLow") + "-" + item.get("lvl"); - item_lvl = item.get("lvlLow"); - } - else { - item_lvl = item.get("lvl"); - } - - if (player_build.level < item_lvl) { - if (!lvlWarning) { - lvlWarning = document.createElement("p"); - lvlWarning.classList.add("itemp"); - lvlWarning.classList.add("warning"); - lvlWarning.textContent = "WARNING: A level " + player_build.level + " player cannot use some piece(s) of this build." - } - let baditem = document.createElement("p"); - baditem.classList.add("nocolor"); - baditem.classList.add("itemp"); - baditem.textContent = item.get("displayName") + " requires level " + item.get("lvl") + " to use."; - lvlWarning.appendChild(baditem); - } - } - if(lvlWarning){ - summarybox.append(lvlWarning); - } - for (const [setName, count] of player_build.activeSetCounts) { - const bonus = sets[setName].bonuses[count-1]; - // console.log(setName); - if (bonus["illegal"]) { - let setWarning = document.createElement("p"); - setWarning.classList.add("itemp"); - setWarning.classList.add("warning"); - setWarning.textContent = "WARNING: illegal item combination: " + setName - summarybox.append(setWarning); - } - } - - for (let i in player_build.items) { - displaysq2ExpandedItem(player_build.items[i], buildFields[i]); - collapse_element("#"+equipment_keys[i]+"-tooltip"); - } - - displaysq2ArmorStats(player_build); - displaysq2BuildStats('overall-stats', player_build, build_all_display_commands); - displaysq2BuildStats("offensive-stats",player_build, build_offensive_display_commands); - displaysq2SetBonuses("set-info",player_build); - displaysq2WeaponStats(player_build); - - let meleeStats = player_build.getMeleeStats(); - displaysq2MeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); - - displaysq2DefenseStats(document.getElementById("defensive-stats"),player_build); - - displaysq2PoisonDamage(document.getElementById("build-poison-stats"),player_build); - - let spells = spell_table[player_build.weapon.get("type")]; - for (let i = 0; i < 4; ++i) { - let parent_elem = document.getElementById("spell"+i+"-info"); - let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); - displaysq2SpellDamage(parent_elem, overallparent_elem, player_build, spells[i], i+1); - } - - location.hash = encodeBuild(); - clear_highlights(); -} - function copyBuild() { if (player_build) { copyTextToClipboard(url_base+location.hash); @@ -916,107 +330,6 @@ function toggleButton(button_id) { } } -function optimizeStrDex() { - if (!player_build) { - return; - } - const remaining = levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints; - const base_skillpoints = player_build.base_skillpoints; - const max_str_boost = 100 - base_skillpoints[0]; - const max_dex_boost = 100 - base_skillpoints[1]; - if (Math.min(remaining, max_str_boost, max_dex_boost) < 0) return; // Unwearable - - const base_total_skillpoints = player_build.total_skillpoints; - let str_bonus = remaining; - let dex_bonus = 0; - let best_skillpoints = player_build.total_skillpoints; - let best_damage = 0; - for (let i = 0; i <= remaining; ++i) { - let total_skillpoints = base_total_skillpoints.slice(); - total_skillpoints[0] += Math.min(max_str_boost, str_bonus); - total_skillpoints[1] += Math.min(max_dex_boost, dex_bonus); - - // Calculate total 3rd spell damage - let spell = spell_table[player_build.weapon.get("type")][2]; - const stats = player_build.statMap; - let critChance = skillPointsToPercentage(total_skillpoints[1]); - let save_damages = []; - let spell_parts; - if (spell.parts) { - spell_parts = spell.parts; - } - else { - spell_parts = spell.variants.DEFAULT; - for (const majorID of stats.get("activeMajorIDs")) { - if (majorID in spell.variants) { - spell_parts = spell.variants[majorID]; - break; - } - } - } - let total_damage = 0; - for (const part of spell_parts) { - if (part.type === "damage") { - let _results = calculateSpellDamage(stats, part.conversion, - stats.get("sdRaw"), stats.get("sdPct") + player_build.externalStats.get("sdPct"), - part.multiplier / 100, player_build.weapon, total_skillpoints, - player_build.damageMultiplier, player_build.externalStats); - let totalDamNormal = _results[0]; - let totalDamCrit = _results[1]; - let results = _results[2]; - let tooltipinfo = _results[3]; - - for (let i = 0; i < 6; ++i) { - for (let j in results[i]) { - results[i][j] = results[i][j].toFixed(2); - } - } - let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0; - let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; - let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; - - save_damages.push(averageDamage); - if (part.summary == true) { - total_damage = averageDamage; - } - } else if (part.type === "total") { - total_damage = 0; - for (let i in part.factors) { - total_damage += save_damages[i] * part.factors[i]; - } - } - } // END Calculate total 3rd spell damage (total_damage) - if (total_damage > best_damage) { - best_damage = total_damage; - best_skillpoints = total_skillpoints.slice(); - } - - str_bonus -= 1; - dex_bonus += 1; - - } - // TODO: reduce duplicated code, @calculateBuild - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; - for (let i in skp_order) { - let manual_assigned = best_skillpoints[i]; - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; - } - player_build.assigned_skillpoints += delta_total; - - try { - calculateBuildStats(); - if (player_build.errored) - throw new ListError(player_build.errors); - } - catch (error) { - handleBuilderError(error); - } -} - // TODO: Learn and use await function init() { console.log("builder.js init"); @@ -1026,6 +339,7 @@ function init() { update_field(i); } } + function init2() { load_ing_init(init); } @@ -1033,5 +347,4 @@ function init3() { load_tome_init(init2) } - load_init(init3); diff --git a/js/builder_graph.js b/js/builder_graph.js new file mode 100644 index 0000000..7b652ba --- /dev/null +++ b/js/builder_graph.js @@ -0,0 +1,21 @@ + + +let item_nodes = []; + +document.addEventListener('DOMContentLoaded', function() { + for (const [eq, none_item] of zip(equipment_fields, none_items)) { + let input_field = document.getElementById(eq+"-choice"); + let item_image = document.getElementById(eq+"-img"); + + let item_input = new ItemInputNode(eq+'-input', input_field, none_item); + item_nodes.push(item_input); + new ItemInputDisplayNode(eq+'-display', input_field, item_image).link_to(item_input); + new PrintNode(eq+'-debug').link_to(item_input); + //document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); + + } + let weapon_image = document.getElementById("weapon-img"); + new WeaponDisplayNode('weapon-type', weapon_image).link_to(item_nodes[8]); + console.log("Set up graph"); + +}); diff --git a/js/computation_graph.js b/js/computation_graph.js index 9039685..417c9bf 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -13,6 +13,7 @@ class ComputeNode { this.update_task = null; this.update_time = Date.now(); this.fail_cb = false; // Set to true to force updates even if parent failed. + this.calc_inputs = new Map(); } /** @@ -23,19 +24,40 @@ class ComputeNode { return; } this.update_time = timestamp; + this.set_value(this.compute_func(this.calc_inputs)); + } - let value_map = Map(); - for (const input of this.inputs) { - value_map.set(input.name, input.get_value()); - } - this.value = this.compute_func(value_map); + /** + * Set this node's value directly. Notifies children. + */ + set_value(value) { + let timestamp = Date.now(); + this.update_time = timestamp; + this.value = value; for (const child of this.children) { - if (this.value || child.fail_cb) { - child.update(); + child.set_input(this.name, this.value, timestamp); + } + } + + /** + * Set an input value. Propagates calculation if all inputs are present. + */ + set_input(input_name, value, timestamp) { + if (value || this.fail_cb) { + this.calc_inputs.set(input_name, value) + if (this.calc_inputs.size === this.inputs.length) { + this.update(timestamp) } } } + /** + * Remove cached input values to this calculation. + */ + clear_cache() { + this.calc_inputs = new Map(); + } + /** * Get value of this compute node. Can't trigger update cascades (push based update, not pull based.) */ @@ -72,6 +94,19 @@ function calcSchedule(node) { }, 500); } +class PrintNode extends ComputeNode { + + constructor(name) { + super(name); + this.fail_cb = true; + } + + compute_func(input_map) { + console.log([this.name, input_map]); + return null; + } +} + /** * Node for getting an item's stats from an item input field. */ @@ -85,9 +120,9 @@ class ItemInputNode extends ComputeNode { */ constructor(name, item_input_field, none_item) { super(name); - this.input_field.setAttribute("input", () => calcSchedule(this)); this.input_field = item_input_field; - this.none_item = expandItem(none_item); + this.input_field.addEventListener("input", () => calcSchedule(this)); + this.none_item = new Item(none_item); } compute_func(input_map) { @@ -107,16 +142,22 @@ class ItemInputNode extends ComputeNode { item = getCraftFromHash(item_text); } else if (itemMap.has(item_text)) { - item = Item(itemMap.get(item_text)); + item = new Item(itemMap.get(item_text)); } else if (tomeMap.has(item_text)) { - item = Item(tomeMap.get(item_text)); + item = new Item(tomeMap.get(item_text)); } - if (!item || item.statMap.get('type') !== this.none_item.statMap.get('type')) { - return null; + if (item) { + let type_match; + if (this.none_item.statMap.get('category') === 'weapon') { + type_match = item.statMap.get('category') === 'weapon'; + } else { + type_match = item.statMap.get('type') === this.none_item.statMap.get('type'); + } + if (type_match) { return item; } } - return item; + return null; } } @@ -166,6 +207,6 @@ class WeaponDisplayNode extends ComputeNode { const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element const type = item.statMap.get('type'); - this.image_field.setAttribute('src', '../media/items/new/generic-'+type+'.png'); + this.image.setAttribute('src', '../media/items/new/generic-'+type+'.png'); } } diff --git a/js/load.js b/js/load.js index 265cc3c..461e1d7 100644 --- a/js/load.js +++ b/js/load.js @@ -201,59 +201,61 @@ function load_init(init_func) { } } +// List of 'raw' "none" items (No Helmet, etc), in order helmet, chestplate... ring1, ring2, brace, neck, weapon. +for (const it of itemTypes) { + itemLists.set(it, []); +} + +let none_items = [ + ["armor", "helmet", "No Helmet"], + ["armor", "chestplate", "No Chestplate"], + ["armor", "leggings", "No Leggings"], + ["armor", "boots", "No Boots"], + ["accessory", "ring", "No Ring 1"], + ["accessory", "ring", "No Ring 2"], + ["accessory", "bracelet", "No Bracelet"], + ["accessory", "necklace", "No Necklace"], + ["weapon", "dagger", "No Weapon"], +]; +for (let i = 0; i < none_items.length; i++) { + let item = Object(); + item.slots = 0; + item.category = none_items[i][0]; + item.type = none_items[i][1]; + item.name = none_items[i][2]; + item.displayName = item.name; + item.set = null; + item.quest = null; + item.skillpoints = [0, 0, 0, 0, 0]; + item.has_negstat = false; + item.reqs = [0, 0, 0, 0, 0]; + item.fixID = true; + item.tier = "Normal"; + item.id = 10000 + i; + item.nDam = "0-0"; + item.eDam = "0-0"; + item.tDam = "0-0"; + item.wDam = "0-0"; + item.fDam = "0-0"; + item.aDam = "0-0"; + clean_item(item); + + none_items[i] = item; +} + function init_maps() { //warp itemMap = new Map(); /* Mapping from item names to set names. */ idMap = new Map(); redirectMap = new Map(); - for (const it of itemTypes) { - itemLists.set(it, []); - } - - let noneItems = [ - ["armor", "helmet", "No Helmet"], - ["armor", "chestplate", "No Chestplate"], - ["armor", "leggings", "No Leggings"], - ["armor", "boots", "No Boots"], - ["accessory", "ring", "No Ring 1"], - ["accessory", "ring", "No Ring 2"], - ["accessory", "bracelet", "No Bracelet"], - ["accessory", "necklace", "No Necklace"], - ["weapon", "dagger", "No Weapon"], - ]; - for (let i = 0; i < noneItems.length; i++) { - let item = Object(); - item.slots = 0; - item.category = noneItems[i][0]; - item.type = noneItems[i][1]; - item.name = noneItems[i][2]; - item.displayName = item.name; - item.set = null; - item.quest = null; - item.skillpoints = [0, 0, 0, 0, 0]; - item.has_negstat = false; - item.reqs = [0, 0, 0, 0, 0]; - item.fixID = true; - item.tier = "Normal"; - item.id = 10000 + i; - item.nDam = "0-0"; - item.eDam = "0-0"; - item.tDam = "0-0"; - item.wDam = "0-0"; - item.fDam = "0-0"; - item.aDam = "0-0"; - clean_item(item); - - noneItems[i] = item; - } - items = items.concat(noneItems); + items = items.concat(none_items); //console.log(items); for (const item of items) { if (item.remapID === undefined) { itemLists.get(item.type).push(item.displayName); itemMap.set(item.displayName, item); - if (noneItems.includes(item)) { + if (none_items.includes(item)) { idMap.set(item.id, ""); } else { diff --git a/js/optimize.js b/js/optimize.js new file mode 100644 index 0000000..6185edd --- /dev/null +++ b/js/optimize.js @@ -0,0 +1,101 @@ +function optimizeStrDex() { + if (!player_build) { + return; + } + const remaining = levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints; + const base_skillpoints = player_build.base_skillpoints; + const max_str_boost = 100 - base_skillpoints[0]; + const max_dex_boost = 100 - base_skillpoints[1]; + if (Math.min(remaining, max_str_boost, max_dex_boost) < 0) return; // Unwearable + + const base_total_skillpoints = player_build.total_skillpoints; + let str_bonus = remaining; + let dex_bonus = 0; + let best_skillpoints = player_build.total_skillpoints; + let best_damage = 0; + for (let i = 0; i <= remaining; ++i) { + let total_skillpoints = base_total_skillpoints.slice(); + total_skillpoints[0] += Math.min(max_str_boost, str_bonus); + total_skillpoints[1] += Math.min(max_dex_boost, dex_bonus); + + // Calculate total 3rd spell damage + let spell = spell_table[player_build.weapon.get("type")][2]; + const stats = player_build.statMap; + let critChance = skillPointsToPercentage(total_skillpoints[1]); + let save_damages = []; + let spell_parts; + if (spell.parts) { + spell_parts = spell.parts; + } + else { + spell_parts = spell.variants.DEFAULT; + for (const majorID of stats.get("activeMajorIDs")) { + if (majorID in spell.variants) { + spell_parts = spell.variants[majorID]; + break; + } + } + } + let total_damage = 0; + for (const part of spell_parts) { + if (part.type === "damage") { + let _results = calculateSpellDamage(stats, part.conversion, + stats.get("sdRaw"), stats.get("sdPct") + player_build.externalStats.get("sdPct"), + part.multiplier / 100, player_build.weapon, total_skillpoints, + player_build.damageMultiplier, player_build.externalStats); + let totalDamNormal = _results[0]; + let totalDamCrit = _results[1]; + let results = _results[2]; + let tooltipinfo = _results[3]; + + for (let i = 0; i < 6; ++i) { + for (let j in results[i]) { + results[i][j] = results[i][j].toFixed(2); + } + } + let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0; + let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; + let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; + + save_damages.push(averageDamage); + if (part.summary == true) { + total_damage = averageDamage; + } + } else if (part.type === "total") { + total_damage = 0; + for (let i in part.factors) { + total_damage += save_damages[i] * part.factors[i]; + } + } + } // END Calculate total 3rd spell damage (total_damage) + if (total_damage > best_damage) { + best_damage = total_damage; + best_skillpoints = total_skillpoints.slice(); + } + + str_bonus -= 1; + dex_bonus += 1; + + } + // TODO: reduce duplicated code, @calculateBuild + let skillpoints = player_build.total_skillpoints; + let delta_total = 0; + for (let i in skp_order) { + let manual_assigned = best_skillpoints[i]; + let delta = manual_assigned - skillpoints[i]; + skillpoints[i] = manual_assigned; + player_build.base_skillpoints[i] += delta; + delta_total += delta; + } + player_build.assigned_skillpoints += delta_total; + + try { + calculateBuildStats(); + if (player_build.errored) + throw new ListError(player_build.errors); + } + catch (error) { + handleBuilderError(error); + } +} + diff --git a/js/utils.js b/js/utils.js index fed7df8..164543d 100644 --- a/js/utils.js +++ b/js/utils.js @@ -3,31 +3,6 @@ const url_base = getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.sp const zip = (a, b) => a.map((k, i) => [k, b[i]]); -//updates all the OGP tags for a webpage. Should be called when build changes -function updateOGP() { - //update the embed URL - let url_elem = document.getElementById("ogp-url"); - if (url_elem) { - url_elem.content = url_base+location.hash; - } - - //update the embed text content - let build_elem = document.getElementById("ogp-build-list"); - if (build_elem && player_build) { - let text = "WynnBuilder build:\n"+ - "> "+player_build.helmet.get("displayName")+"\n"+ - "> "+player_build.chestplate.get("displayName")+"\n"+ - "> "+player_build.leggings.get("displayName")+"\n"+ - "> "+player_build.boots.get("displayName")+"\n"+ - "> "+player_build.ring1.get("displayName")+"\n"+ - "> "+player_build.ring2.get("displayName")+"\n"+ - "> "+player_build.bracelet.get("displayName")+"\n"+ - "> "+player_build.necklace.get("displayName")+"\n"+ - "> "+player_build.weapon.get("displayName")+" ["+player_build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; - build_elem.content = text; - } -} - function clamp(num, low, high){ return Math.min(Math.max(num, low), high); } @@ -412,4 +387,4 @@ async function hardReload() { function capitalizeFirst(str) { return str[0].toUpperCase() + str.substring(1); -} \ No newline at end of file +} From adcf1ee3621393fb191acffa71b9c2ba816933bb Mon Sep 17 00:00:00 2001 From: reschan Date: Mon, 23 May 2022 13:20:03 +0700 Subject: [PATCH 05/68] re-enable melee breakdown --- builder/index.html | 2 +- js/sq2bs.js | 2 ++ js/sq2display.js | 26 +++++++------------------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/builder/index.html b/builder/index.html index 2569df8..806d3e5 100644 --- a/builder/index.html +++ b/builder/index.html @@ -1240,7 +1240,7 @@
-
melee
+
melee
diff --git a/js/sq2bs.js b/js/sq2bs.js index dce7eba..cb571d3 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -29,6 +29,8 @@ document.addEventListener('DOMContentLoaded', function() { document.querySelector("#"+i+"Avg").setAttribute("onclick", "toggle_spell_tab('"+i+"')"); } + document.querySelector("#build-melee-statsAvg").setAttribute("onclick", "toggle_spell_tab('build-melee-stats')"); + document.querySelector("#level-choice").setAttribute("oninput", "calcBuildSchedule()") document.querySelector("#weapon-choice").setAttribute("oninput", document.querySelector("#weapon-choice").getAttribute("oninput") + "resetArmorPowderSpecials();"); // document.querySelector("#edit-IDs-button").setAttribute("onclick", "toggle_edit_id_tab()"); diff --git a/js/sq2display.js b/js/sq2display.js index 07715d5..7dd1d27 100644 --- a/js/sq2display.js +++ b/js/sq2display.js @@ -692,7 +692,6 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ title_elem.classList.add("title"); title_elem.textContent = "Melee Stats"; parent_elem.append(title_elem); - parent_elem.append(document.createElement("br")); //overall title let title_elemavg = document.createElement("b"); @@ -703,9 +702,6 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ let averageDamage = document.createElement("p"); averageDamage.classList.add("left"); averageDamage.textContent = "Average DPS: " + stats[10]; - tooltiptext = `= ((${stats[8]} * ${(stats[6][2]).toFixed(2)}) + (${stats[9]} * ${(stats[7][2]).toFixed(2)}))` - tooltip = createTooltip(tooltip, "p", tooltiptext, averageDamage, ["melee-tooltip"]); - averageDamage.appendChild(tooltip); parent_elem.append(averageDamage); //overall average DPS @@ -751,8 +747,6 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ dmg.textContent = stats[i][0] + " \u2013 " + stats[i][1]; dmg.classList.add(damageClasses[i]); dmg.classList.add("itemp"); - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(0,2).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, dmg, ["melee-tooltip"]); nonCritStats.append(dmg); } } @@ -767,15 +761,11 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ arr2.push(stats[i][1]); } } - tooltiptext = tooltiparr[0] + arr.join(" + ") + "\n" + tooltiparr[1] + arr2.join(" + "); - tooltip = createTooltip(tooltip, "p", tooltiptext, normalDamage, ["melee-tooltip"]); nonCritStats.append(normalDamage); let normalDPS = document.createElement("p"); normalDPS.textContent = "Normal DPS: " + stats[8]; normalDPS.classList.add("tooltip"); - tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, normalDPS, ["melee-tooltip"]); nonCritStats.append(normalDPS); //overall average DPS @@ -785,9 +775,6 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ let singleHitDamageSecond = document.createElement("span"); singleHitDamageSecond.classList.add("Damage"); singleHitDamageSecond.textContent = stats[12].toFixed(2); - tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${stats[6][2].toFixed(2)} + ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${stats[7][2].toFixed(2)}`; - // tooltip = createTooltip(tooltip, "p", tooltiptext, singleHitDamage, ["melee-tooltip", "summary-tooltip"]); - singleHitDamage.appendChild(singleHitDamageFirst); singleHitDamage.appendChild(singleHitDamageSecond); overallparent_elem.append(singleHitDamage); @@ -812,8 +799,6 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ dmg.textContent = stats[i][2] + " \u2013 " + stats[i][3]; dmg.classList.add(damageClasses[i]); dmg.classList.add("itemp"); - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(2,4).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, dmg, ["melee-tooltip"]); critStats.append(dmg); } } @@ -827,15 +812,11 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ arr2.push(stats[i][3]); } } - tooltiptext = tooltiparr[0] + arr.join(" + ") + "\n" + tooltiparr[1] + arr2.join(" + "); - tooltip = createTooltip(tooltip, "p", tooltiptext, critDamage, ["melee-tooltip"]); critStats.append(critDamage); let critDPS = document.createElement("p"); critDPS.textContent = "Crit DPS: " + stats[9]; - tooltiptext = ` = ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, critDPS, ["melee-tooltip"]); critStats.append(critDPS); let critChance = document.createElement("p"); @@ -845,6 +826,13 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ critStats.append(critChance); parent_elem.append(critStats); + + //up and down arrow - done ugly + let arrow = document.createElement("img"); + arrow.id = "arrow_" + overallparent_elem.id; + arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem"; + arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png"; + overallparent_elem.appendChild(arrow); } function displaysq2ArmorStats(build) { From 4f7f0f9cfc20a48b51bcb7f9aa4c1e63d753ffd4 Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 12 Jun 2022 07:45:48 -0700 Subject: [PATCH 06/68] Clean up loading code, much cleaner invocation during init (and less prone to races) --- js/builder.js | 17 ++-- js/builder_graph.js | 194 ++++++++++++++++++++++++++++++++++++++++++++ js/load.js | 166 ++++++++++++++++--------------------- js/load_ing.js | 143 +++++++++++++++++--------------- js/load_tome.js | 128 ++++++++++++++--------------- 5 files changed, 411 insertions(+), 237 deletions(-) diff --git a/js/builder.js b/js/builder.js index 1b18b96..1e51e31 100644 --- a/js/builder.js +++ b/js/builder.js @@ -335,16 +335,11 @@ function init() { console.log("builder.js init"); init_autocomplete(); decodeBuild(url_tag); - for (const i of equipment_keys) { - update_field(i); - } } -function init2() { - load_ing_init(init); -} -function init3() { - load_tome_init(init2) -} - -load_init(init3); +//load_init(init3); +(async function() { + let load_promises = [ load_init(), load_ing_init(), load_tome_init() ]; + await Promise.all(load_promises); + init(); +})(); diff --git a/js/builder_graph.js b/js/builder_graph.js index 7b652ba..fd0de13 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -19,3 +19,197 @@ document.addEventListener('DOMContentLoaded', function() { console.log("Set up graph"); }); + +// autocomplete initialize +function init_autocomplete() { + console.log("autocomplete init"); + console.log(itemLists) + let dropdowns = new Map(); + for (const eq of equipment_keys) { + if (tome_keys.includes(eq)) { + continue; + } + // build dropdown + let item_arr = []; + if (eq == 'weapon') { + for (const weaponType of weapon_keys) { + for (const weapon of itemLists.get(weaponType)) { + let item_obj = itemMap.get(weapon); + if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { + continue; + } + if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { + continue; + } + item_arr.push(weapon); + } + } + } else { + for (const item of itemLists.get(eq.replace(/[0-9]/g, ''))) { + let item_obj = itemMap.get(item); + if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { + continue; + } + if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { + continue; + } + item_arr.push(item) + } + } + + // create dropdown + dropdowns.set(eq, new autoComplete({ + data: { + src: item_arr + }, + selector: "#"+ eq +"-choice", + wrapper: false, + resultsList: { + maxResults: 1000, + tabSelect: true, + noResults: true, + class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", + element: (list, data) => { + // dynamic result loc + let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); + list.style.top = position.bottom + window.scrollY +"px"; + list.style.left = position.x+"px"; + list.style.width = position.width+"px"; + list.style.maxHeight = position.height * 2 +"px"; + + if (!data.results.length) { + message = document.createElement('li'); + message.classList.add('scaled-font'); + message.textContent = "No results found!"; + list.prepend(message); + } + }, + }, + resultItem: { + class: "scaled-font search-item", + selected: "dark-5", + element: (item, data) => { + item.classList.add(itemMap.get(data.value).tier); + }, + }, + events: { + input: { + selection: (event) => { + if (event.detail.selection.value) { + event.target.value = event.detail.selection.value; + } + }, + }, + } + })); + } + + for (const eq of tome_keys) { + // build dropdown + let tome_arr = []; + for (const tome of tomeLists.get(eq.replace(/[0-9]/g, ''))) { + let tome_obj = tomeMap.get(tome); + if (tome_obj["restrict"] && tome_obj["restrict"] === "DEPRECATED") { + continue; + } + //this should suffice for tomes - jank + if (tome_obj["name"].includes('No ' + eq.charAt(0).toUpperCase())) { + continue; + } + let tome_name = tome; + tome_arr.push(tome_name); + } + + // create dropdown + dropdowns.set(eq, new autoComplete({ + data: { + src: tome_arr + }, + selector: "#"+ eq +"-choice", + wrapper: false, + resultsList: { + maxResults: 1000, + tabSelect: true, + noResults: true, + class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", + element: (list, data) => { + // dynamic result loc + let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); + list.style.top = position.bottom + window.scrollY +"px"; + list.style.left = position.x+"px"; + list.style.width = position.width+"px"; + list.style.maxHeight = position.height * 2 +"px"; + + if (!data.results.length) { + message = document.createElement('li'); + message.classList.add('scaled-font'); + message.textContent = "No results found!"; + list.prepend(message); + } + }, + }, + resultItem: { + class: "scaled-font search-item", + selected: "dark-5", + element: (tome, data) => { + tome.classList.add(tomeMap.get(data.value).tier); + }, + }, + events: { + input: { + selection: (event) => { + if (event.detail.selection.value) { + event.target.value = event.detail.selection.value; + } + }, + }, + } + })); + } + + let filter_loc = ["filter1", "filter2", "filter3", "filter4"]; + for (const i of filter_loc) { + dropdowns.set(i+"-choice", new autoComplete({ + data: { + src: sq2ItemFilters, + }, + selector: "#"+i+"-choice", + wrapper: false, + resultsList: { + tabSelect: true, + noResults: true, + class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", + element: (list, data) => { + // dynamic result loc + console.log(i); + list.style.zIndex = "100"; + let position = document.getElementById(i+"-dropdown").getBoundingClientRect(); + window_pos = document.getElementById("search-container").getBoundingClientRect(); + list.style.top = position.bottom - window_pos.top + 5 +"px"; + list.style.left = position.x - window_pos.x +"px"; + list.style.width = position.width+"px"; + + if (!data.results.length) { + message = document.createElement('li'); + message.classList.add('scaled-font'); + message.textContent = "No filters found!"; + list.prepend(message); + } + }, + }, + resultItem: { + class: "scaled-font search-item", + selected: "dark-5", + }, + events: { + input: { + selection: (event) => { + if (event.detail.selection.value) { + event.target.value = event.detail.selection.value; + } + }, + }, + } + })); + } +} diff --git a/js/load.js b/js/load.js index 461e1d7..4f6e4b2 100644 --- a/js/load.js +++ b/js/load.js @@ -14,42 +14,35 @@ let itemLists = new Map(); /* * Load item set from local DB. Calls init() on success. */ -async function load_local(init_func) { - let get_tx = db.transaction(['item_db', 'set_db'], 'readonly'); - let sets_store = get_tx.objectStore('set_db'); - let get_store = get_tx.objectStore('item_db'); - let request = get_store.getAll(); - request.onerror = function(event) { - console.log("Could not read local item db..."); - } - request.onsuccess = function(event) { - console.log("Successfully read local item db."); - items = request.result; - //console.log(items); - let request2 = sets_store.openCursor(); +async function load_local() { + return new Promise(function(resolve, reject) { + let get_tx = db.transaction(['item_db', 'set_db'], 'readonly'); + let sets_store = get_tx.objectStore('set_db'); + let get_store = get_tx.objectStore('item_db'); + let request = get_store.getAll(); + let request2 = sets_store.getAll(); + request.onerror = function(event) { + reject("Could not read local item db..."); + } + request.onsuccess = function(event) { + console.log("Successfully read local item db."); + } - sets = {}; request2.onerror = function(event) { - console.log("Could not read local set db..."); + reject("Could not read local set db..."); } - request2.onsuccess = function(event) { - let cursor = event.target.result; - if (cursor) { - sets[cursor.primaryKey] = cursor.value; - cursor.continue(); - } - else { - console.log("Successfully read local set db."); - //console.log(sets); - init_maps(); - init_func(); - load_complete = true; - } + console.log("Successfully read local set db."); } - } - await get_tx.complete; - db.close(); + get_tx.oncomplete = function(event) { + items = request.result; + sets = request2.result; + init_maps(); + load_complete = true; + db.close(); + resolve(); + } + }); } /* @@ -91,7 +84,7 @@ function clean_item(item) { /* * Load item set from remote DB (aka a big json file). Calls init() on success. */ -async function load(init_func) { +async function load() { let getUrl = window.location; let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1]; @@ -101,16 +94,6 @@ async function load(init_func) { items = result.items; sets = result.sets; - - -// let clear_tx = db.transaction(['item_db', 'set_db'], 'readwrite'); -// let clear_items = clear_tx.objectStore('item_db'); -// let clear_sets = clear_tx.objectStore('item_db'); -// -// await clear_items.clear(); -// await clear_sets.clear(); -// await clear_tx.complete; - let add_tx = db.transaction(['item_db', 'set_db'], 'readwrite'); add_tx.onabort = function(e) { console.log(e); @@ -131,74 +114,67 @@ async function load(init_func) { add_promises.push(sets_store.add(sets[set], set)); } add_promises.push(add_tx.complete); - Promise.all(add_promises).then((values) => { - init_maps(); - init_func(); - load_complete = true; - }); - // DB not closed? idfk man + + await Promise.all(add_promises); + init_maps(); + load_complete = true; + db.close(); } -function load_init(init_func) { - if (load_complete) { - console.log("Item db already loaded, skipping load sequence"); - init_func(); - return; - } - let request = window.indexedDB.open('item_db', DB_VERSION); +async function load_init() { + return new Promise((resolve, reject) => { + let request = window.indexedDB.open('item_db', DB_VERSION); - request.onerror = function() { - console.log("DB failed to open..."); - }; + request.onerror = function() { + reject("DB failed to open..."); + }; - request.onsuccess = function() { - (async function() { + request.onsuccess = async function() { db = request.result; - if (!reload) { - console.log("Using stored data...") - load_local(init_func); + if (load_in_progress) { + while (!load_complete) { + await sleep(100); + } + console.log("Skipping load...") } else { - if (load_in_progress) { - while (!load_complete) { - await sleep(100); - } - console.log("Skipping load...") - init_func(); + load_in_progress = true + if (reload) { + console.log("Using new data...") + await load(); } else { - // Not 100% safe... whatever! - load_in_progress = true - console.log("Using new data...") - load(init_func); + console.log("Using stored data...") + await load_local(); } } - })() - } + resolve(); + }; - request.onupgradeneeded = function(e) { - reload = true; + request.onupgradeneeded = function(e) { + reload = true; - let db = e.target.result; - - try { - db.deleteObjectStore('item_db'); - } - catch (error) { - console.log("Could not delete item DB. This is probably fine"); - } - try { - db.deleteObjectStore('set_db'); - } - catch (error) { - console.log("Could not delete set DB. This is probably fine"); - } + let db = e.target.result; + + try { + db.deleteObjectStore('item_db'); + } + catch (error) { + console.log("Could not delete item DB. This is probably fine"); + } + try { + db.deleteObjectStore('set_db'); + } + catch (error) { + console.log("Could not delete set DB. This is probably fine"); + } - db.createObjectStore('item_db'); - db.createObjectStore('set_db'); + db.createObjectStore('item_db'); + db.createObjectStore('set_db'); - console.log("DB setup complete..."); - } + console.log("DB setup complete..."); + }; + }); } // List of 'raw' "none" items (No Helmet, etc), in order helmet, chestplate... ring1, ring2, brace, neck, weapon. diff --git a/js/load_ing.js b/js/load_ing.js index 1a6144e..ffc4c05 100644 --- a/js/load_ing.js +++ b/js/load_ing.js @@ -4,6 +4,7 @@ const ING_DB_VERSION = 13; let idb; let ireload = false; +let iload_in_progress = false; let iload_complete = false; let ings; let recipes; @@ -20,32 +21,34 @@ let recipeIDMap; /* * Load item set from local DB. Calls init() on success. */ -async function ing_load_local(init_func) { - console.log("IngMap is: \n " + ingMap); - let get_tx = idb.transaction(['ing_db', 'recipe_db'], 'readonly'); - let ings_store = get_tx.objectStore('ing_db'); - let recipes_store = get_tx.objectStore('recipe_db'); - let request3 = ings_store.getAll(); - request3.onerror = function(event) { - console.log("Could not read local ingredient db..."); - } - request3.onsuccess = function(event) { - console.log("Successfully read local ingredient db."); - ings = request3.result; +async function ing_load_local() { + return new Promise(function(resolve, reject) { + let get_tx = idb.transaction(['ing_db', 'recipe_db'], 'readonly'); + let ings_store = get_tx.objectStore('ing_db'); + let recipes_store = get_tx.objectStore('recipe_db'); + let request3 = ings_store.getAll(); + request3.onerror = function(event) { + reject("Could not read local ingredient db..."); + } + request3.onsuccess = function(event) { + console.log("Successfully read local ingredient db."); + } let request4 = recipes_store.getAll(); request4.onerror = function(event) { - console.log("Could not read local recipe db..."); + reject("Could not read local recipe db..."); } request4.onsuccess = function(event) { console.log("Successfully read local recipe db."); + } + get_tx.oncomplete = function(event) { + ings = request3.result; recipes = request4.result; init_ing_maps(); - init_func(); iload_complete = true; + idb.close(); + resolve() } - } - await get_tx.complete; - idb.close(); + }); } function clean_ing(ing) { @@ -59,11 +62,12 @@ function clean_ing(ing) { /* * Load item set from remote DB (aka a big json file). Calls init() on success. */ -async function load_ings(init_func) { +async function load_ings() { let getUrl = window.location; - let baseUrl = getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.split('/')[1]; - let url = baseUrl + "/ingreds_compress.json"; + let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1]; + // "Random" string to prevent caching! + let url = baseUrl + "/ingreds_compress.json?"+new Date(); url = url.replace(/\w+.html/, "") ; let result = await (await fetch(url)).json(); @@ -97,59 +101,65 @@ async function load_ings(init_func) { } add_promises.push(add_tx2.complete); add_promises.push(add_tx3.complete); - Promise.all(add_promises).then((values) => { - init_ing_maps(); - init_func(); - iload_complete = true; - }); - // DB not closed? idfk man + + await Promise.all(add_promises); + init_ing_maps(); + iload_complete = true; + idb.close(); } -function load_ing_init(init_func) { - if (iload_complete) { - console.log("Ingredient db already loaded, skipping load sequence"); - init_func(); - return; - } - let request = window.indexedDB.open("ing_db", ING_DB_VERSION) - request.onerror = function() { - console.log("DB failed to open..."); - } +async function load_ing_init() { + return new Promise((resolve, reject) => { + let request = window.indexedDB.open("ing_db", ING_DB_VERSION) + request.onerror = function() { + reject("DB failed to open..."); + } - request.onsuccess = function() { - idb = request.result; - if (!ireload) { - console.log("Using stored data...") - ing_load_local(init_func); + request.onsuccess = async function() { + idb = request.result; + if (iload_in_progress) { + while (!iload_complete) { + await sleep(100); + } + console.log("Skipping load...") + } + else { + iload_in_progress = true + if (ireload) { + console.log("Using new data...") + await load_ings(); + } + else { + console.log("Using stored data...") + await ing_load_local(); + } + } + resolve(); } - else { - console.log("Using new data...") - load_ings(init_func); - } - } - request.onupgradeneeded = function(e) { - ireload = true; + request.onupgradeneeded = function(e) { + ireload = true; - let idb = e.target.result; - - try { - idb.deleteObjectStore('ing_db'); - } - catch (error) { - console.log("Could not delete ingredient DB. This is probably fine"); - } - try { - idb.deleteObjectStore('recipe_db'); - } - catch (error) { - console.log("Could not delete recipe DB. This is probably fine"); - } - idb.createObjectStore('ing_db'); - idb.createObjectStore('recipe_db'); + let idb = e.target.result; + + try { + idb.deleteObjectStore('ing_db'); + } + catch (error) { + console.log("Could not delete ingredient DB. This is probably fine"); + } + try { + idb.deleteObjectStore('recipe_db'); + } + catch (error) { + console.log("Could not delete recipe DB. This is probably fine"); + } + idb.createObjectStore('ing_db'); + idb.createObjectStore('recipe_db'); - console.log("DB setup complete..."); - } + console.log("DB setup complete..."); + } + }); } function init_ing_maps() { @@ -222,4 +232,5 @@ function init_ing_maps() { recipeList.push(recipe["name"]); recipeIDMap.set(recipe["id"],recipe["name"]); } + console.log(ingMap); } diff --git a/js/load_tome.js b/js/load_tome.js index afd9315..2186a60 100644 --- a/js/load_tome.js +++ b/js/load_tome.js @@ -13,29 +13,33 @@ let tomeLists = new Map(); /* * Load tome set from local DB. Calls init() on success. */ -async function load_tome_local(init_func) { - let get_tx = tdb.transaction(['tome_db'], 'readonly'); - let get_store = get_tx.objectStore('tome_db'); - let request = get_store.getAll(); - request.onerror = function(event) { - console.log("Could not read local tome db..."); - } - request.onsuccess = function(event) { - console.log("Successfully read local tome db."); - tomes = request.result; - - init_tome_maps(); - init_func(); - tload_complete = true; - } - await get_tx.complete; - tdb.close(); +async function load_tome_local() { + return new Promise(function(resolve, reject) { + let get_tx = tdb.transaction(['tome_db'], 'readonly'); + let get_store = get_tx.objectStore('tome_db'); + let request = get_store.getAll(); + request.onerror = function(event) { + reject("Could not read local tome db..."); + } + request.onsuccess = function(event) { + console.log("Successfully read local tome db."); + } + get_tx.oncomplete = function(event) { + console.log('b'); + console.log(request.readyState); + tomes = request.result; + init_tome_maps(); + tload_complete = true; + tdb.close(); + resolve(); + } + }); } /* * Load tome set from remote DB (json). Calls init() on success. */ -async function load_tome(init_func) { +async function load_tome() { let getUrl = window.location; let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1]; @@ -60,67 +64,61 @@ async function load_tome(init_func) { }; add_promises.push(req); } - Promise.all(add_promises).then((values) => { - init_tome_maps(); - init_func(); - tload_complete = true; - }); - // DB not closed? idfk man + add_promises.push(add_tx.complete); + + await Promise.all(add_promises); + init_tome_maps(); + tload_complete = true; + tdb.close(); } -function load_tome_init(init_func) { - if (tload_complete) { - console.log("Tome db already loaded, skipping load sequence"); - init_func(); - return; - } - let request = window.indexedDB.open('tome_db', TOME_DB_VERSION); +async function load_tome_init() { + return new Promise((resolve, reject) => { + let request = window.indexedDB.open('tome_db', TOME_DB_VERSION); - request.onerror = function() { - console.log("DB failed to open..."); - }; + request.onerror = function() { + reject("DB failed to open..."); + }; - request.onsuccess = function() { - (async function() { + request.onsuccess = async function() { tdb = request.result; - if (!treload) { - console.log("Using stored data...") - load_tome_local(init_func); + if (tload_in_progress) { + while (!tload_complete) { + await sleep(100); + } + console.log("Skipping load...") } else { - if (tload_in_progress) { - while (!tload_complete) { - await sleep(100); - } - console.log("Skipping load...") - init_func(); + tload_in_progress = true + if (treload) { + console.log("Using new data...") + await load_tome(); } else { - // Not 100% safe... whatever! - tload_in_progress = true - console.log("Using new data...") - load_tome(init_func); + console.log("Using stored data...") + await load_tome_local(); } } - })() - } - - request.onupgradeneeded = function(e) { - treload = true; - - let tdb = e.target.result; - - try { - tdb.deleteObjectStore('tome_db'); - } - catch (error) { - console.log("Could not delete tome DB. This is probably fine"); + resolve(); } - tdb.createObjectStore('tome_db'); + request.onupgradeneeded = function(e) { + treload = true; - console.log("DB setup complete..."); - } + let tdb = e.target.result; + + try { + tdb.deleteObjectStore('tome_db'); + } + catch (error) { + console.log("Could not delete tome DB. This is probably fine"); + } + + tdb.createObjectStore('tome_db'); + + console.log("DB setup complete..."); + } + }); } function init_tome_maps() { From 8108f033e5e36d0dd9f1e13faeaff830f90ee1f9 Mon Sep 17 00:00:00 2001 From: reschan Date: Mon, 13 Jun 2022 16:21:01 +0700 Subject: [PATCH 07/68] atree initial commit --- builder/index.html | 23 +++++++++-- css/sq2bs.css | 27 ++++++++++++- js/atree_constants.js | 70 ++++++++++++++++++++++++++++++++++ js/sq2bs.js | 66 ++++++++++++++++++++++++++++++++ media/atree/connect_angle.png | Bin 0 -> 1066 bytes media/atree/connect_line.png | Bin 0 -> 1184 bytes media/atree/node.png | Bin 0 -> 5645 bytes 7 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 js/atree_constants.js create mode 100644 media/atree/connect_angle.png create mode 100644 media/atree/connect_line.png create mode 100644 media/atree/node.png diff --git a/builder/index.html b/builder/index.html index 9fe56ed..634bc22 100644 --- a/builder/index.html +++ b/builder/index.html @@ -418,17 +418,22 @@
-
+
-
+
+ +
+
-
+
@@ -607,6 +612,16 @@
+
+
+
+ +
+
+ Active: +
+
+
-
+
-
+
-
+
Active:
diff --git a/css/sq2bs.css b/css/sq2bs.css index 7bad703..6790cb9 100644 --- a/css/sq2bs.css +++ b/css/sq2bs.css @@ -473,3 +473,20 @@ a:hover { -o-transform: rotate(270deg); transform: rotate(270deg); } + +/* atree hover */ +.atree-node { + opacity: 75%; +} + +.atree-node:hover { + opacity: 100%; +} + +.hide-scroll { + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ +} +.hide-scroll::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} diff --git a/js/sq2bs.js b/js/sq2bs.js index 5a8c677..8b97a88 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -559,7 +559,6 @@ function construct_AT(elem, tree) { // create node let node_elem = document.createElement('div') - node_elem.className = "atree-node"; node_elem.style = "background-image: url('" + node.image + "'); background-size: cover; width: 100%; height: 100%;"; if (node.connector && node.rotate != 0) { @@ -571,8 +570,8 @@ function construct_AT(elem, tree) { node_elem.addEventListener('mouseover', function(e) { if (e.target !== this) {return;} let tooltip = this.children[0]; - tooltip.style.top = this.getBoundingClientRect().bottom + window.scrollY + "px"; - tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + "px"; + tooltip.style.top = this.getBoundingClientRect().bottom + window.scrollY * 1.02 + "px"; + tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + (elem.getBoundingClientRect().width * .05 / 2) + "px"; tooltip.style.display = "block"; }); @@ -582,15 +581,50 @@ function construct_AT(elem, tree) { tooltip.style.display = "none"; }); - let node_tooltip = document.createElement('div'); - node_tooltip.addEventListener('mouseover', function() {}); - node_tooltip.style.backgroundColor = "#444444"; - node_tooltip.style.color = "#ffffff"; + + + node_elem.classList.add("atree-node"); + + let active_tooltip = document.createElement('div'); + active_tooltip.classList.add("rounded-bottom", "dark-7", "border"); + active_tooltip.style.width = elem.getBoundingClientRect().width * .95 + "px"; + active_tooltip.style.display = "none"; + + // tooltip text formatting + + let active_tooltip_title = document.createElement('b'); + active_tooltip_title.classList.add("scaled-font"); + active_tooltip_title.textContent = node.title; + + let active_tooltip_text = document.createElement('p'); + active_tooltip_text.classList.add("scaled-font-sm"); + active_tooltip_text.textContent = node.desc; + + active_tooltip.appendChild(active_tooltip_title); + active_tooltip.appendChild(active_tooltip_text); + + node_tooltip = active_tooltip.cloneNode(true); + + active_tooltip.id = "atree-ab-" + node.title.replaceAll(" ", ""); + node_tooltip.style.position = "absolute"; - node_tooltip.style.width = elem.getBoundingClientRect().width + "px"; - node_tooltip.style.display = "none"; - node_tooltip.textContent = node.title; + node_tooltip.style.zIndex = "100"; + node_elem.appendChild(node_tooltip); + document.getElementById("atree-active").appendChild(active_tooltip); + + node_elem.addEventListener('click', function(e) { + if (e.target !== this) {return;} + let tooltip = document.getElementById("atree-ab-" + node.title.replaceAll(" ", "")); + if (tooltip.style.display == "block") { + tooltip.style.display = "none"; + this.classList.add("atree-node"); + } + else { + tooltip.style.display = "block"; + this.classList.remove("atree-node"); + } + }); }; document.getElementById("atree-row-" + node.row).children[node.col].appendChild(node_elem); From 0166b7814d90f64d9c6ca2bfb1afd3f5e3261ac8 Mon Sep 17 00:00:00 2001 From: ferricles Date: Wed, 15 Jun 2022 12:53:51 -0700 Subject: [PATCH 09/68] crafted item version 4 build decode bug fix, crafted page slight bug fixes --- crafter/index.html | 2 +- js/crafter.js | 2 +- js/sq2builder.js | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crafter/index.html b/crafter/index.html index 645d2b6..76f6cf3 100644 --- a/crafter/index.html +++ b/crafter/index.html @@ -211,7 +211,7 @@
-
diff --git a/js/crafter.js b/js/crafter.js index dbbbfd6..66b9087 100644 --- a/js/crafter.js +++ b/js/crafter.js @@ -264,7 +264,7 @@ function populateFields() { */ function copyRecipeHash() { if (player_craft) { - copyTextToClipboard("CR-"+location.hash); + copyTextToClipboard("CR-"+location.hash.slice(1)); document.getElementById("copy-hash-button").textContent = "Copied!"; } } diff --git a/js/sq2builder.js b/js/sq2builder.js index b758e3b..2dc8e91 100644 --- a/js/sq2builder.js +++ b/js/sq2builder.js @@ -142,6 +142,7 @@ function parsePowdering(powder_info) { * Populate fields based on url, and calculate build. */ function decodeBuild(url_tag) { + console.log("decoding build"); if (url_tag) { //default values let equipment = [null, null, null, null, null, null, null, null, null]; @@ -168,11 +169,12 @@ function decodeBuild(url_tag) { let info_str = info[1]; let start_idx = 0; for (let i = 0; i < 9; ++i ) { - if (info_str.charAt(start_idx) === "-") { - equipment[i] = "CR-"+info_str.slice(start_idx+1, start_idx+18); - start_idx += 18; - } - else { + console.log(info_str.slice(start_idx,start_idx+3)); + if (info_str.slice(start_idx,start_idx+3) === "CR-") { + console.log(info_str.slice(start_idx, start_idx+20)); + equipment[i] = info_str.slice(start_idx, start_idx+20); + start_idx += 20; + } else { let equipment_str = info_str.slice(start_idx, start_idx+3); equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); start_idx += 3; From db7981024f04f2c7a1c938288f40b67e57b19167 Mon Sep 17 00:00:00 2001 From: ferricles Date: Wed, 15 Jun 2022 15:25:47 -0700 Subject: [PATCH 10/68] removed printouts --- js/craft.js | 1 - js/sq2builder.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/js/craft.js b/js/craft.js index 81d632c..1d3ae41 100644 --- a/js/craft.js +++ b/js/craft.js @@ -190,7 +190,6 @@ class Craft{ let amounts = this.recipe.get("materials").map(x=> x.get("amount")); //Mat Multipliers - should work! matmult = (tierToMult[tiers[0]]*amounts[0] + tierToMult[tiers[1]]*amounts[1]) / (amounts[0]+amounts[1]); - console.log(matmult); let low = this.recipe.get("healthOrDamage")[0]; let high = this.recipe.get("healthOrDamage")[1]; diff --git a/js/sq2builder.js b/js/sq2builder.js index 2dc8e91..22f5f18 100644 --- a/js/sq2builder.js +++ b/js/sq2builder.js @@ -142,7 +142,6 @@ function parsePowdering(powder_info) { * Populate fields based on url, and calculate build. */ function decodeBuild(url_tag) { - console.log("decoding build"); if (url_tag) { //default values let equipment = [null, null, null, null, null, null, null, null, null]; @@ -169,9 +168,7 @@ function decodeBuild(url_tag) { let info_str = info[1]; let start_idx = 0; for (let i = 0; i < 9; ++i ) { - console.log(info_str.slice(start_idx,start_idx+3)); if (info_str.slice(start_idx,start_idx+3) === "CR-") { - console.log(info_str.slice(start_idx, start_idx+20)); equipment[i] = info_str.slice(start_idx, start_idx+20); start_idx += 20; } else { From 5216686040e6747c78bb775d2add578d5a0e3567 Mon Sep 17 00:00:00 2001 From: ferricles Date: Wed, 15 Jun 2022 17:07:54 -0700 Subject: [PATCH 11/68] ability trees for different classes + selection based on weapon --- builder/index.html | 8 +-- js/atree_constants.js | 110 +++++++++++++++++++++++++++++++++++++++++- js/build_utils.js | 1 + js/sq2bs.js | 8 ++- 4 files changed, 120 insertions(+), 7 deletions(-) diff --git a/builder/index.html b/builder/index.html index 919c603..3539c4b 100644 --- a/builder/index.html +++ b/builder/index.html @@ -272,7 +272,7 @@
- +
@@ -612,12 +612,12 @@
-
+
-
+
-
+
Active:
diff --git a/js/atree_constants.js b/js/atree_constants.js index 6161f9e..94dea24 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -1,3 +1,111 @@ +const atrees = +{ + "Assassin": [ + { + "title": "Spin Attack", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 0, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 1, + "col": 4 + }, + { + "title": "Dagger Proficiency I", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 2, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 2, + "col": 3 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 2, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 3, + "col": 4 + }, + { + "title": "Double Spin", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 4, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 5, + "col": 4 + }, + { + "title": "Dash", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 6, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 6, + "col": 3 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 6, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 5, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 6, + "col": 6 + }, + ], + "Warrior": [], + "Mage": [], + "Archer": [], + "Shaman": [] +} + const atree_example = [ { "title": "skill", @@ -10,7 +118,7 @@ const atree_example = [ { "image": "../media/atree/connect_angle.png", "connector": true, - "rotate": "270", + "rotate": 270, "row": 4, "col": 3, }, diff --git a/js/build_utils.js b/js/build_utils.js index 16bacb5..7bbcafd 100644 --- a/js/build_utils.js +++ b/js/build_utils.js @@ -56,6 +56,7 @@ const attackSpeeds = ["SUPER_SLOW", "VERY_SLOW", "SLOW", "NORMAL", "FAST", "VERY const baseDamageMultiplier = [ 0.51, 0.83, 1.5, 2.05, 2.5, 3.1, 4.3 ]; //0.51, 0.82, 1.50, 2.05, 2.50, 3.11, 4.27 const classes = ["Warrior", "Assassin", "Mage", "Archer", "Shaman"]; +const wep_to_class = new Map([["dagger", "Assassin"], ["spear", "Warrior"], ["wand", "Mage"], ["bow", "Archer"], ["relik", "Shaman"]]) const tiers = ["Normal", "Unique", "Rare", "Legendary", "Fabled", "Mythic", "Set", "Crafted"] //I'm not sure why you would make a custom crafted but if you do you should be able to use it w/ the correct powder formula const types = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(consumableTypes).concat(tomeTypes).map(x => x.substring(0,1).toUpperCase() + x.substring(1)); //weaponTypes.push("sword"); diff --git a/js/sq2bs.js b/js/sq2bs.js index 8b97a88..092df9a 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -73,7 +73,7 @@ document.addEventListener('DOMContentLoaded', function() { }; }); - construct_AT(document.getElementById("atree-ui"), atree_example); + construct_AT(document.getElementById("atree-ui"), atrees["Assassin"]); //dagger is default atree document.getElementById("atree-dropdown").style.display = "none"; }); @@ -232,9 +232,13 @@ function update_field(field) { }; } - // set weapon img + // set weapon img and set ability tree if (category == 'weapon') { document.querySelector("#weapon-img").setAttribute('src', '../media/items/new/generic-'+type+'.png'); + construct_AT(document.getElementById("atree-ui"), atrees[wep_to_class[type.toLowerCase()]]); //dagger is default atree + document.getElementById("atree-dropdown").style.display = "none"; + + //TODO: reset chosen abilities (once ability implementation is here) } } /* tabulars | man i hate this code but too lazy to fix /shrug */ From 4f43a81313f9b77ab4293556b545979af3106547 Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 16 Jun 2022 07:18:29 +0700 Subject: [PATCH 12/68] change columns to 9, adjust sizes, fix selecteds --- builder/index.html | 2 +- css/sq2bs.css | 9 +++------ js/sq2bs.js | 18 ++++++++---------- media/atree/connect-3.png | Bin 0 -> 7142 bytes 4 files changed, 12 insertions(+), 17 deletions(-) create mode 100644 media/atree/connect-3.png diff --git a/builder/index.html b/builder/index.html index 919c603..c307134 100644 --- a/builder/index.html +++ b/builder/index.html @@ -617,7 +617,7 @@
-
+
Active:
diff --git a/css/sq2bs.css b/css/sq2bs.css index 6790cb9..27fb6dd 100644 --- a/css/sq2bs.css +++ b/css/sq2bs.css @@ -475,12 +475,9 @@ a:hover { } /* atree hover */ -.atree-node { - opacity: 75%; -} - -.atree-node:hover { - opacity: 100%; +.atree-selected { + border: 5px solid blue; + border-radius: 50%; } .hide-scroll { diff --git a/js/sq2bs.js b/js/sq2bs.js index 8b97a88..cf57510 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -544,10 +544,10 @@ function construct_AT(elem, tree) { let row = document.createElement('div'); row.classList.add("row"); row.id = "atree-row-" + j; - row.style.height = elem.getBoundingClientRect().width / 5 + "px"; + row.style.height = elem.getBoundingClientRect().width / 9 + "px"; - for (let k = 0; k < 5; k++) { + for (let k = 0; k < 9; k++) { col = document.createElement('div'); col.classList.add('col', 'px-0'); row.appendChild(col); @@ -571,7 +571,7 @@ function construct_AT(elem, tree) { if (e.target !== this) {return;} let tooltip = this.children[0]; tooltip.style.top = this.getBoundingClientRect().bottom + window.scrollY * 1.02 + "px"; - tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + (elem.getBoundingClientRect().width * .05 / 2) + "px"; + tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + (elem.getBoundingClientRect().width * .2 / 2) + "px"; tooltip.style.display = "block"; }); @@ -581,13 +581,11 @@ function construct_AT(elem, tree) { tooltip.style.display = "none"; }); - - - node_elem.classList.add("atree-node"); + node_elem.classList.add("fake-button"); let active_tooltip = document.createElement('div'); - active_tooltip.classList.add("rounded-bottom", "dark-7", "border"); - active_tooltip.style.width = elem.getBoundingClientRect().width * .95 + "px"; + active_tooltip.classList.add("rounded-bottom", "dark-7", "border", "mb-2", "mx-auto"); + active_tooltip.style.width = elem.getBoundingClientRect().width * .80 + "px"; active_tooltip.style.display = "none"; // tooltip text formatting @@ -618,11 +616,11 @@ function construct_AT(elem, tree) { let tooltip = document.getElementById("atree-ab-" + node.title.replaceAll(" ", "")); if (tooltip.style.display == "block") { tooltip.style.display = "none"; - this.classList.add("atree-node"); + this.classList.remove("atree-selected"); } else { tooltip.style.display = "block"; - this.classList.remove("atree-node"); + this.classList.add("atree-selected"); } }); }; diff --git a/media/atree/connect-3.png b/media/atree/connect-3.png new file mode 100644 index 0000000000000000000000000000000000000000..736036614e8594d8f6ea71c1300c387407a62b4f GIT binary patch literal 7142 zcmeHMc{J2*`yXqvL?MI97?N$w3IQI;$v z(I$FQM2m>Dh-OevDV5)M)YEd__x+vIbAIpduVy)Of3M|pUDxNn?)%*LnRItIM;S>K zNeBcYOlW&PaiRf`{wE+TTSO*Got3( z^{#krfg3bEJ3mqodc17paQNJbQ;dt{f5mNk|74EW+Ok!=ZYgb0Eq}+G=D~L{9pSAb zyXoS(PdGh%wZ6J@WfszG7X{ZN@lsgvPt)(`jBKlPuDUL40ewPm^g+w9tE z^uWC$E6Vnd#n68F_M6c5%*H)@sI1uGIBtfFlJc(xSPxHO{O2g};%{ zZO@dd*JmobJo^>LaK~z@G7>|#k#!sB!?JJL-k%La+Os>q0F~3XxQjqC|!g|;7~TQ10!r>s8^KX1#RHlvS+Mv zU42uTu%N>p6NrZ@@E*Cc`qx9PDYaDXm`>J6V-C(ZoVtcxz&P zC`Wvc1t;IbF%RY;XQe5%K1Y}KVX^K_W9qZc+HPDCH&mrqYxT_={&k@(hojp*Onedw z{cMfUKkBl#x^l$eH6XzlNs=~#EZ;I?eNaYN4hN-dp4Jhw{S);tG~U-eK9^jWZhARB z=z!(k4^q(vlW8iA((%b&dD_mHx`#^HOTBqzQ;?Z^dZ{kV7+TEskM*t z`-`o5R5;=}t3qbt(>Lxr*gmE1tG(EFoVZtbTp43RbMrssTwhmNsAyJ~uKh4r_3h?7 z~ekizb$qz-O>qX?JVkk)mVI^Ns-MklP&6Fv4uvo>~arQ(w68+ZH^KOs3~C;kej9+Y@h3 z zQ`^r^drLi!$Ow6+mKqbkvt`%g(D$iu;b|1ZdS_>Y@#;-dMD@XxQ|rQdy=OJelx-g> zC6tP$zMR8p?K2~9p_FinykvU$tvX`eTWT5=B)y=m>RELa#B-jBP-htpX=>(y(fGW- zcp*_j8&2;Sq|6=6rcB*Zp~T%Hz>QNF4A4p(Lxt^)**gQhapj%0<-l+8U_~Jna`l5`#6es+VNv zmw%#nK5r!sJ5RKxrvzW>iIbPP74lp#?qdzxb=39~8+@$FH=J0K8@+V0RMTly4ORU$ zam@Q5m+b(^uNP`u>s~9j_pS0ATI1BX-deg{L*(|OF8LG(!z6a~GV!VFm#n=r7gKo4 zxB6=&AdBA)mip7(w}b%ijgD8i;rRrRCY$dZ6HOe>yHk^`o#IH42jWHmau zx#jAlBZgmJH>RnIQPMf#ziZ{HRdsb;r`i-GH1Z5xL{mNb+~Y!2yTVIvZkRt}+WhL1 zFMn?QQT6+BgEzVbwTdrFhAa%?ufGRcQxy+}LOumn%C2p#ZyFhFU1oW?lw&bZFkAryC0_4cE~75R0?U?#--hI za~gkcy{^Kh%}x%d<|g&w%I95A>Le;MouoRl@QTZW%oiR0sGtzK`>1X3P@KLj)Y8ag z@r%Thk7gLN{0I75x~01!>yAEH5pm2m9s2|txaP=kMLe%S$m+~+S07(~uE|&Y+^ z{@g&=ToYsQY>3r6gwWDjO!{GKd&P7)UP=%y|6Yy*VN7P5Y=EMbSk&_EW)+9?9Ymci zLds9jY{i;pmUb5@Nox;ysBC73XvBmYUE0z@Or%XkJ3wa?LY(x~HSjh~(2*NU(c)3V zDO~2{h-}p%v3o=Cm)GtD<=2(L>UwO8{K$j7%fg>ZNb2=@d;GDNj@h2UGUOaM?c@-6 zqC4GVkht#X>}DE^<_=w(anHuu#fRIFB7xNk$(`2A44FPnLSjD2D;Dn^i43*-SR}^Vt9jI6AMbM3iZa;y@na;>+jpm>G(U@r zN+Y%H2^=5ZLnL&kdt+szUdT&=tos8gWH_muXc|eCP){Cj(o$YrU`iNH{M@>qra6>* zjh>UI)^HnkD`!o8Q43<|<>kIJCPz)u)Ow@y!-JbnagSV&tf|m6rwfmD8g@2MBw5Wq zz-?_IKoH?}%=8h&b3g}TwJyk~o;N1`qi z*YWNrS~d7)GzCAUUtBL-Qn1}=S$(`EqNVs+GWXa=l4H-Yx6JM^NB&Oa4hNSmc~s|z zV4*bqY>lX`y_?m^B{DBGwBc|{N%DG>vqNrBs*k^^rhj4n?)~gb(_Q^|B~IYDe|t2x zj8&cWLXMB%dY=_LYHLIU#LeD0E9_7deYD17%6q~xom z7QAeay@>aA;yHDoj`H<1-zEt(D!t2>*^H&g+8z636LzJ`Fn)THRVX!$5LbQ2; z3gP5~bBgY9$M7W-qr&YoY5MQ3lwQ$4mXZkg-d^Icn|d~@8P##V`*FWnm_iNqRcq}r z=!QdNO>1cR1m*I~++!XtcAOs|`m$_tk_m_G-~4N-Jb@M=Y@MQs_U09kt-G_W?cWDQ@OXGIE(h<_VZGu?;Kq7*0X$kR^16G}8JO;-AiM4 zWXqgwb4N$D8<(&6=ywv=Xg2Zkv91JH4VLBRw%fpSbUt@&22?g{Zkjjrgtrpb*X4pS zzbfQio9ShtMPca?A1ek(sHa(GuIuiTA5WIHxHf>_vExFWD{Jd&hGE;9Ww#6; z)uauiSC}G-Ut2~N7HB5V9^`bFN-eWu21LK#!@5mK2u|7K5}IY^qQCs1AD!f5=ryV{ znszueA4v}IF=Isfbb0rmU$cvVszy{axv?Z7_2CJccGoM3B7k4oo#Qz~xcwYjcE0q< zhTQd91L9EZGmiwx&gsPYOMLpx_OYZQg-oGhWWy)4v}kmK!_9_^*|e5O;n_n8dZ(UN z_a*$X;Z7I=!4yquNXVGjr^OfAZ{F6HXP}&`@t!J~EIhT-VK`sp10$it``o!_mTqeC z(oywnkrgY1HSBL}ea(r9+0K|Rp}u`5Ld#~Bhg_9!1yA%)DtL1Db9KRy7&HR{nLz{$ zIJ5}xWDkK@SaBi5L)h#H91wWdY;^$st4_ z(FQ0ajfVWugT=Ow20^|i^lv>_Uf=}?=?SnHQA`qG9}Upi8b3miN#FbEeufvk}(QJBqK}+02X0Pz+w?ZtRVqGMv(v_ z28|+{m=S(}a;CG`1Ud;2K!M-}R1k-RrI;8K(V(M=2?k+|CZiBo3<`xXLZi?aBa)dJ z8bJR5v4KeiyOI$8V^#tvG6-dChyhFtDW(XD3Bed)Y-VbRz?zbX2rLRi#$qr;QzL@u z0u-5qb6_xO1aLa3G(s?djGzZEC3*sU@(SQjIjyU6ooQ0HT-Vm129=&FA7l6CfOUA#u>;H@{$?w-4fDWF3V!+Fc2fs@eya_@ai6cminO?p*5i||ci-rZdO;Esm+m0IBcDkk|_ z+9$K4hMwZ$WR)N=NIZxGk@(fYKlS@xqW!lZi#pQ^DUffae)P30c|%Z)5c(%&|A+Kd zyRPL;)U8vDyZ1fL`5S2=6m7zR?~;Ed`5zGfA2Rrc{r?YHbDF|6JT@=&m0`8P+kR5H TO{u6v2>5cgce6XaHi-K_$P=Ah literal 0 HcmV?d00001 From 9036de536ba25469e9b0518ef62b02e06cf480be Mon Sep 17 00:00:00 2001 From: ferricles Date: Wed, 15 Jun 2022 22:03:20 -0700 Subject: [PATCH 13/68] modified atree graphics, added full assassin ability tree --- css/sq2bs.css | 6 +- js/atree_constants.js | 1148 ++++++++++++++++++++++++++++++++- js/sq2bs.js | 2 + media/atree/connect-3.png | Bin 7142 -> 0 bytes media/atree/connect_angle.png | Bin 1066 -> 1081 bytes media/atree/connect_line.png | Bin 1184 -> 1270 bytes media/atree/connect_t.png | Bin 718 -> 694 bytes media/atree/node-blocked.png | Bin 0 -> 2416 bytes media/atree/node-selected.png | Bin 0 -> 2418 bytes media/atree/node.png | Bin 5645 -> 2806 bytes 10 files changed, 1150 insertions(+), 6 deletions(-) delete mode 100644 media/atree/connect-3.png create mode 100644 media/atree/node-blocked.png create mode 100644 media/atree/node-selected.png diff --git a/css/sq2bs.css b/css/sq2bs.css index 27fb6dd..0532fc0 100644 --- a/css/sq2bs.css +++ b/css/sq2bs.css @@ -474,11 +474,7 @@ a:hover { transform: rotate(270deg); } -/* atree hover */ -.atree-selected { - border: 5px solid blue; - border-radius: 50%; -} + .hide-scroll { -ms-overflow-style: none; /* Internet Explorer 10+ */ diff --git a/js/atree_constants.js b/js/atree_constants.js index 94dea24..8ed02d6 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -88,7 +88,7 @@ const atrees = "image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, - "row": 5, + "row": 6, "col": 5 }, { @@ -99,6 +99,1152 @@ const atrees = "row": 6, "col": 6 }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 7, + "col": 2 + }, + { + "title": "Smoke Bomb", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 8, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 7, + "col": 6 + }, + { + "title": "Multihit", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 8, + "col": 6 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 8, + "col": 3 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 8, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 8, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 8, + "col": 1 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 8, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 9, + "col": 0 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 10, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 9, + "col": 2 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 10, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 9, + "col": 6 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 10, + "col": 6 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 8, + "col": 7 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 8, + "col": 8 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 9, + "col": 8 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 10, + "col": 8 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 10, + "col": 1 + }, + { + "title": "Backstab", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 11, + "col": 1 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 9, + "col": 4 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 90, + "row": 10, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 10, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 11, + "col": 4 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 10, + "col": 7 + }, + { + "title": "Fatality", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 11, + "col": 7 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 11, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 12, + "col": 0 + }, + { + "title": "Violent Vortex", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 13, + "col": 0 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 11, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 12, + "col": 2 + }, + { + "title": "Vanish", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 13, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 12, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 13, + "col": 3 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 13, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 13, + "col": 6 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 14, + "col": 2 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 15, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 14, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 15, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 12, + "col": 7 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 13, + "col": 7 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 14, + "col": 7 + }, + { + "title": "Lacerate", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 15, + "col": 7 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 15, + "col": 1 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 16, + "col": 1 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 15, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 16, + "col": 5 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 15, + "col": 8 + }, + { + "title": "Wall of Smoke", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 16, + "col": 8 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 16, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 17, + "col": 0 + }, + { + "title": "Silent Killer", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 18, + "col": 0 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 16, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 17, + "col": 2 + }, + { + "title": "Shadow Travel", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 18, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 17, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 18, + "col": 5 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 17, + "col": 8 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 18, + "col": 8 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 18, + "col": 4 + }, + { + "title": "Exploding Clones", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 19, + "col": 4 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 18, + "col": 3 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 19, + "col": 0 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 20, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 19, + "col": 3 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 20, + "col": 3 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 18, + "col": 6 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 18, + "col": 7 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 19, + "col": 7 + }, + { + "title": "Weightless", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 20, + "col": 7 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 20, + "col": 1 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 20, + "col": 2 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 21, + "col": 1 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 20, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 21, + "col": 4 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 20, + "col": 6 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 21, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 21, + "col": 6 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 20, + "col": 8 + }, + { + "title": "Dancing Blade", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 21, + "col": 8 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 21, + "col": 0 + }, + { + "title": "Spin Attack Damage", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 22, + "col": 0 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 21, + "col": 3 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 22, + "col": 3 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 22, + "col": 1 + }, + { + "title": "Marked", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 23, + "col": 1 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 22, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 23, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 23, + "col": 5 + }, + { + "title": "Shurikens", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 23, + "col": 6 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 23, + "col": 7 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 22, + "col": 8 + }, + { + "title": "Far Reach", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 23, + "col": 8 + }, + { + "title": "Stronger Multihit", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 24, + "col": 5 + }, + { + "title": "Psithurism", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 24, + "col": 7 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 24, + "col": 1 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 25, + "col": 1 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 25, + "col": 3 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 24, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 25, + "col": 4 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 25, + "col": 5 + }, + { + "title": "Choke Bomb", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 25, + "col": 6 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 25, + "col": 7 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 25, + "col": 8 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 26, + "col": 5 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 25, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 26, + "col": 0 + }, + { + "title": "Death Magnet", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 27, + "col": 0 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 25, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 26, + "col": 2 + }, + { + "title": "Cheaper Multihit", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 27, + "col": 2 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 26, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 27, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 26, + "col": 7 + }, + { + "title": "Parry", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 27, + "col": 7 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 27, + "col": 1 + }, + { + "title": "Fatal Spin", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 28, + "col": 1 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 27, + "col": 3 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 28, + "col": 3 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 27, + "col": 6 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 28, + "col": 6 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 27, + "col": 8 + }, + { + "title": "Wall Jump", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 28, + "col": 8 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 28, + "col": 0 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 29, + "col": 0 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 29, + "col": 1 + }, + { + "title": "Harvester", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 30, + "col": 1 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 28, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 29, + "col": 4 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 30, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 28, + "col": 7 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 29, + "col": 7 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 30, + "col": 7 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 30, + "col": 2 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 31, + "col": 2 + }, + { + "image": "../media/atree/connect_t.png", + "connector": true, + "rotate": 180, + "row": 30, + "col": 5 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 90, + "row": 30, + "col": 6 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 31, + "col": 5 + }, + { + "title": "Ricochet", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 31, + "col": 8 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 31, + "col": 1 + }, + { + "title": "Satsujin", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 32, + "col": 1 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 31, + "col": 4 + }, + { + "title": "Forbidden Art", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 32, + "col": 4 + }, + { + "image": "../media/atree/connect_line.png", + "connector": true, + "rotate": 0, + "row": 31, + "col": 7 + }, + { + "title": "Jasmine Bloom", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 32, + "col": 7 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 180, + "row": 32, + "col": 0 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 33, + "col": 0 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 32, + "col": 2 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 33, + "col": 2 + }, + { + "image": "../media/atree/connect_angle.png", + "connector": true, + "rotate": 270, + "row": 32, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 33, + "col": 5 + }, + { + "title": "Text", + "desc": "desc", + "image": "../media/atree/node.png", + "connector": false, + "row": 33, + "col": 8 + }, ], "Warrior": [], "Mage": [], diff --git a/js/sq2bs.js b/js/sq2bs.js index 67bbef3..a1de216 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -621,10 +621,12 @@ function construct_AT(elem, tree) { if (tooltip.style.display == "block") { tooltip.style.display = "none"; this.classList.remove("atree-selected"); + this.style.backgroundImage = 'url("../media/atree/node.png")'; } else { tooltip.style.display = "block"; this.classList.add("atree-selected"); + this.style.backgroundImage = 'url("../media/atree/node-selected.png")'; } }); }; diff --git a/media/atree/connect-3.png b/media/atree/connect-3.png deleted file mode 100644 index 736036614e8594d8f6ea71c1300c387407a62b4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7142 zcmeHMc{J2*`yXqvL?MI97?N$w3IQI;$v z(I$FQM2m>Dh-OevDV5)M)YEd__x+vIbAIpduVy)Of3M|pUDxNn?)%*LnRItIM;S>K zNeBcYOlW&PaiRf`{wE+TTSO*Got3( z^{#krfg3bEJ3mqodc17paQNJbQ;dt{f5mNk|74EW+Ok!=ZYgb0Eq}+G=D~L{9pSAb zyXoS(PdGh%wZ6J@WfszG7X{ZN@lsgvPt)(`jBKlPuDUL40ewPm^g+w9tE z^uWC$E6Vnd#n68F_M6c5%*H)@sI1uGIBtfFlJc(xSPxHO{O2g};%{ zZO@dd*JmobJo^>LaK~z@G7>|#k#!sB!?JJL-k%La+Os>q0F~3XxQjqC|!g|;7~TQ10!r>s8^KX1#RHlvS+Mv zU42uTu%N>p6NrZ@@E*Cc`qx9PDYaDXm`>J6V-C(ZoVtcxz&P zC`Wvc1t;IbF%RY;XQe5%K1Y}KVX^K_W9qZc+HPDCH&mrqYxT_={&k@(hojp*Onedw z{cMfUKkBl#x^l$eH6XzlNs=~#EZ;I?eNaYN4hN-dp4Jhw{S);tG~U-eK9^jWZhARB z=z!(k4^q(vlW8iA((%b&dD_mHx`#^HOTBqzQ;?Z^dZ{kV7+TEskM*t z`-`o5R5;=}t3qbt(>Lxr*gmE1tG(EFoVZtbTp43RbMrssTwhmNsAyJ~uKh4r_3h?7 z~ekizb$qz-O>qX?JVkk)mVI^Ns-MklP&6Fv4uvo>~arQ(w68+ZH^KOs3~C;kej9+Y@h3 z zQ`^r^drLi!$Ow6+mKqbkvt`%g(D$iu;b|1ZdS_>Y@#;-dMD@XxQ|rQdy=OJelx-g> zC6tP$zMR8p?K2~9p_FinykvU$tvX`eTWT5=B)y=m>RELa#B-jBP-htpX=>(y(fGW- zcp*_j8&2;Sq|6=6rcB*Zp~T%Hz>QNF4A4p(Lxt^)**gQhapj%0<-l+8U_~Jna`l5`#6es+VNv zmw%#nK5r!sJ5RKxrvzW>iIbPP74lp#?qdzxb=39~8+@$FH=J0K8@+V0RMTly4ORU$ zam@Q5m+b(^uNP`u>s~9j_pS0ATI1BX-deg{L*(|OF8LG(!z6a~GV!VFm#n=r7gKo4 zxB6=&AdBA)mip7(w}b%ijgD8i;rRrRCY$dZ6HOe>yHk^`o#IH42jWHmau zx#jAlBZgmJH>RnIQPMf#ziZ{HRdsb;r`i-GH1Z5xL{mNb+~Y!2yTVIvZkRt}+WhL1 zFMn?QQT6+BgEzVbwTdrFhAa%?ufGRcQxy+}LOumn%C2p#ZyFhFU1oW?lw&bZFkAryC0_4cE~75R0?U?#--hI za~gkcy{^Kh%}x%d<|g&w%I95A>Le;MouoRl@QTZW%oiR0sGtzK`>1X3P@KLj)Y8ag z@r%Thk7gLN{0I75x~01!>yAEH5pm2m9s2|txaP=kMLe%S$m+~+S07(~uE|&Y+^ z{@g&=ToYsQY>3r6gwWDjO!{GKd&P7)UP=%y|6Yy*VN7P5Y=EMbSk&_EW)+9?9Ymci zLds9jY{i;pmUb5@Nox;ysBC73XvBmYUE0z@Or%XkJ3wa?LY(x~HSjh~(2*NU(c)3V zDO~2{h-}p%v3o=Cm)GtD<=2(L>UwO8{K$j7%fg>ZNb2=@d;GDNj@h2UGUOaM?c@-6 zqC4GVkht#X>}DE^<_=w(anHuu#fRIFB7xNk$(`2A44FPnLSjD2D;Dn^i43*-SR}^Vt9jI6AMbM3iZa;y@na;>+jpm>G(U@r zN+Y%H2^=5ZLnL&kdt+szUdT&=tos8gWH_muXc|eCP){Cj(o$YrU`iNH{M@>qra6>* zjh>UI)^HnkD`!o8Q43<|<>kIJCPz)u)Ow@y!-JbnagSV&tf|m6rwfmD8g@2MBw5Wq zz-?_IKoH?}%=8h&b3g}TwJyk~o;N1`qi z*YWNrS~d7)GzCAUUtBL-Qn1}=S$(`EqNVs+GWXa=l4H-Yx6JM^NB&Oa4hNSmc~s|z zV4*bqY>lX`y_?m^B{DBGwBc|{N%DG>vqNrBs*k^^rhj4n?)~gb(_Q^|B~IYDe|t2x zj8&cWLXMB%dY=_LYHLIU#LeD0E9_7deYD17%6q~xom z7QAeay@>aA;yHDoj`H<1-zEt(D!t2>*^H&g+8z636LzJ`Fn)THRVX!$5LbQ2; z3gP5~bBgY9$M7W-qr&YoY5MQ3lwQ$4mXZkg-d^Icn|d~@8P##V`*FWnm_iNqRcq}r z=!QdNO>1cR1m*I~++!XtcAOs|`m$_tk_m_G-~4N-Jb@M=Y@MQs_U09kt-G_W?cWDQ@OXGIE(h<_VZGu?;Kq7*0X$kR^16G}8JO;-AiM4 zWXqgwb4N$D8<(&6=ywv=Xg2Zkv91JH4VLBRw%fpSbUt@&22?g{Zkjjrgtrpb*X4pS zzbfQio9ShtMPca?A1ek(sHa(GuIuiTA5WIHxHf>_vExFWD{Jd&hGE;9Ww#6; z)uauiSC}G-Ut2~N7HB5V9^`bFN-eWu21LK#!@5mK2u|7K5}IY^qQCs1AD!f5=ryV{ znszueA4v}IF=Isfbb0rmU$cvVszy{axv?Z7_2CJccGoM3B7k4oo#Qz~xcwYjcE0q< zhTQd91L9EZGmiwx&gsPYOMLpx_OYZQg-oGhWWy)4v}kmK!_9_^*|e5O;n_n8dZ(UN z_a*$X;Z7I=!4yquNXVGjr^OfAZ{F6HXP}&`@t!J~EIhT-VK`sp10$it``o!_mTqeC z(oywnkrgY1HSBL}ea(r9+0K|Rp}u`5Ld#~Bhg_9!1yA%)DtL1Db9KRy7&HR{nLz{$ zIJ5}xWDkK@SaBi5L)h#H91wWdY;^$st4_ z(FQ0ajfVWugT=Ow20^|i^lv>_Uf=}?=?SnHQA`qG9}Upi8b3miN#FbEeufvk}(QJBqK}+02X0Pz+w?ZtRVqGMv(v_ z28|+{m=S(}a;CG`1Ud;2K!M-}R1k-RrI;8K(V(M=2?k+|CZiBo3<`xXLZi?aBa)dJ z8bJR5v4KeiyOI$8V^#tvG6-dChyhFtDW(XD3Bed)Y-VbRz?zbX2rLRi#$qr;QzL@u z0u-5qb6_xO1aLa3G(s?djGzZEC3*sU@(SQjIjyU6ooQ0HT-Vm129=&FA7l6CfOUA#u>;H@{$?w-4fDWF3V!+Fc2fs@eya_@ai6cminO?p*5i||ci-rZdO;Esm+m0IBcDkk|_ z+9$K4hMwZ$WR)N=NIZxGk@(fYKlS@xqW!lZi#pQ^DUffae)P30c|%Z)5c(%&|A+Kd zyRPL;)U8vDyZ1fL`5S2=6m7zR?~;Ed`5zGfA2Rrc{r?YHbDF|6JT@=&m0`8P+kR5H TO{u6v2>5cgce6XaHi-K_$P=Ah diff --git a/media/atree/connect_angle.png b/media/atree/connect_angle.png index 3ea2c1650b9085635c3fd31d4868c18582e7df5f..46c5e81aa19b176dafaba4aa04be9b0359cc4982 100644 GIT binary patch delta 562 zcmZ3*v6ExNX2yD^0#6smkcv5PZ*1gsG8AAvX#DH%_8cyTMvn~%=}zwYQFHgS#c!0C z`*!%UVfNbP=kMP;n4Y(so#8nLLklP6sGf2oC^Dg*8lE801+m7P{reft6)-x?V5kRT z5W2B-yS>ATB9IWx(T!O%YpYo5v**ek$mY^x;1GtO$s3uICm&#D6K^PgcP5cbcWP+v QJO&`}boFyt=akR{0C4?~VE_OC delta 550 zcmdnVv5I5EX2yC3wj^(N7l!{JxM1({$qWn(oCO|{#S9F3${@^GvDCf{D9B#o>Fdh= zjE#p~P~SvC;5AT4vcxr_#5q4VH#M(>!MP|ku_QG`p**uBL&4qCHz2%`Pn>~)iOJK& zF{I+w+iM4T8x#aw5B4wQSiRX|^g zE}peR2rGbfdS zL1SX=L|c!;4l+mMgSRS)vRijpq-4z1>FBcZ675b{xJ5Lo)L`cq?pY=(ntFXr*#{5y zA6?bly?Gtqx+aAmjE`PCS@J?j^`E}xmX41PcF4bfXZ!9RqtoI;A)B*h0!&YP98FF# z+pfhHcd=RF&kRSGq-FCw^wS<#?tAy}@$1+r>6P<;7k|;NoXlu9v7+x~o9iA&o4uJY z=X~#UUZbLU?N9FJ%3Rg6rL&Yfk~uk56rJ37Q$rFIy;i$*b&9+_Zu94GpS`Ev z%$QVzBAo?XqLUu8|8Ks3cUJ33PIFBO=3VhQzm=I-${N$|Zrk6VyStL<$NWa^wQtv( zHQe;S^g36!hJ`1GS^Rpg?>%O{@`i=#u|_KHpDv4Vu|D|Qvrm(q=N?mSc3%G4+BtR% zlCQFUvRPQLxOGq2`|jhrd*|-`v%UQ4J^RYoqP;J+FVkaSVAl0?aSW-L^Y+R@!3GBb z7RSCn#dS;!%@4b08@dqXV?!=X_$8UiCQ1oT|4 WvN7~ca0~>NXAGXMelF{r5}E+Vf})lH literal 1184 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$5xX)7d$|)7e>}peR2rGbfdS zL1SX=L|c!;4l+mMgSRS)vRijpq-4z1>FBcZ675b{xJ5Lo)L`cq?pY=(ntFXr*#{5y zA6?bly?Gtqx+aAmjE`PCS@J?j^`E}xmX41PcF4bfXZ!9RqtoI;A)B*h0!&YP98FF# z+pfhHcd=RF&kRSGq-FCw^wS<#?tAy}@$1+r>6P<;7k|;NoXlu9v7+x~o9iA&o4uJY z=X~#UUZbLU?N9FJ%3Rg6rL&Yfk~uk56rJ37Q$rFIy;i$*b&9+_Zu94GpS`Ev z%$QVzBAo?XqLUu8|8Ks3cUJ33PIFBO=3VhQzm=I-${N$|Zrk6VyStL<$NWa^wQtv( zHQe;S^g36!hJ`1GS^Rpg?>%O{@`i=#u|_KHpDv4Vu|D|Qvrm(q=N?mSc3%G4+BtR% zlCQFUvRPQLxOGq2`|jhrd*|-`v%UQ4J^RYoqP;J+FVh1?3tN)6y9>jA5L~c#`DCC7 zXMsm#F$061G6*wPEVVBK3bL1Y`ns||W8+~LwAg-nst!;{vcxr_#5q4VH#M(>!MP|k zu_QG`p**uBL&4qCHz2%`Pn>~)slwC6F{I+w+iMqj83cG797UaHoGVzinbrBn#T~O3 zWIlBj*>r<5oiDvQdMlFv!vp8DTtc&7&y92|=eK8Y0Hqxk1_4qT?mj0a*K_z@Z2X*f@bh^^>bP0l+XkK2ay&| diff --git a/media/atree/connect_t.png b/media/atree/connect_t.png index 8aa4f12c0fc15a07c8cc5e033756bc830c956e71..14d3c16b69250a233cd1de4ca7f2a0b44f98b040 100644 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=B*Ko-U3d z6?5L+Fyv!0Y`KQ`W_}XlrNx5dCd*WBNtI?6u1u`@cU}owr<^ z;hZ1?hwvyOJp^{2KV!F-rT+BoUqyOIx}&S}bP0l+XkKd~%Vu diff --git a/media/atree/node-blocked.png b/media/atree/node-blocked.png new file mode 100644 index 0000000000000000000000000000000000000000..42892b60f352bc10fd6c924985cdbaf9472d12ba GIT binary patch literal 2416 zcmZ`*dpy(YAAi2Hu`$f0QYoB%E~BhYC0cEB8AWWltU?+^Ds_s~(%6PbDRmNEu=)wd zWvE}mDV7FzSkvc0DjyfThIeaUr~*^eV*JC@xpVw0!4ABv zW&2KElCOV!=`4GOP`0Mj6MC{`r_}aB&iGVGn}IZ)=FrRYT0c98q(?sIF$AmGS;K>+ z2)V>?`fLTVjD$2z`gggCF3&(ze@WNh`0)6QmX0(%|L_*mCvgj`%7%?zS!Av1x#lAq z3v~K|fG5XUI-1#cUxZ5%j+DwEU)5f~444p5MiJF>?hq}qcyODQlK$f_Ay5byq1_iy zN@1W`f4oz2)PyT{UwT?CXfj3(tl&sW%MDF3)$%LEmFNUCI$yZo;dl|KP)i;?{BMff z-P*byk#57yiyQ0z7qY_nT33V#_pL=iz+qkIuUK(M(%SRiR3Ub&q)x8uNZv5`@ga9r2q-6GhhMLUho1PUJ>m}wVYq%Yz~DqTU&k|P&~8;Y zy~GB_YldQM`WL|-b5l;DblZiM(H%}I3eK!|*t9wbv~tY)?qb9W!=uDoJ41k3|Hlm? z2E2-9&LEk}bdvdtvNhnjJhqIpz#ZxPfM>p60M`A4pqTd;&qqRLnsD4%o0)nO?u12a zq#n4hbQ*F1=$J5&G`I;nyf@7k;BFweEcMOlMF3ehN7Q`QgkwYYI6)MqE{J%E#{=}( zs-HB?@^{}JMR3KY09gzyD=UEIS`^^H;ID5TpnhGk4;Pn?0f>O#)&6At$=Cr=EIVdq z*)b|FW{eF$!!<1ZPt<>cKG!1@U)!>R+n_+^A2}Z6{RT&d)BN9Op?RJGK46h2ZUD>k zyDm2T9T_H_@Lhnn;e9&{6Y#j!2RVmK{_0=ni&uq!0xj{Kx(6|T_Y`pIlonMaMN1qQ z>21K?{>$q8P`yC?0Bq6Wx_*99`Ny`~>M@!i=tpgeY&z;467a?xK>(Nxw=VCuna_Tw zH!x(O-5Q|>cq{BdFNp5FIzcWPLsaW?o8V8yOxF`TfuB#FRMJKhVGXg^?ui}$wZ2N)COK&yw;?tX`owHf1_-3p=DN6Nu+jqKL zZ*PGbzk-dv5jTwmKX1r?A-hhwjTi{G-lfT|Y{W({<2vPaMod_se@%prXAvI+MJ%dc z#JzoerAnl@%9K=f=&>>otTVVe1idwXkNrl-4q`E`rpxPqq`F~A_iMt?8B6rJzNvxl zDkqUl2QXv*IJy@>Nht{4@FigrUjXtZpO0{QZS!^_j!UjdX{FB=i@$>CwtUn8L9Yp- zRjKj61j4_aCP`hm3&l)dCKai!b+J|ghr{Q{)&woLpxP2?iLRIdE}uEDnKS2_F97T^ zmpQWfTvRrY*5cIJ%M!GiM2e{(m0YwLRh{s?H@Ca?RMva;!_E!?#k)=`bJSN+e&GIe z!&Ek~sxecS?PGaj*48kZ_o*&+*+FSNv5MQy+@;oHdaxnK`;z(dOk*Z`E3B4gaKIBI zsSSI^_usFxz7RR$PlGqmq;+d2;$%_1yrgHu^_3m;Sh8Oca9AOZV)$5EQz?m!Y2>0- z!((vgy@F=GzmAS`Au_u4vZ&_`weGQV`%-I4d?74;+2CZlfu}RNcYj_YgzF(uj9a*5cyxOWbZpMHqm>0!q>6$&ug4OW>0>#OFmLrF^v}_SQ>{Ajern^owRX(c84MdAV z(aI$=6Wbj_U2-i}Q}L+?9_>YANa{sa);F}9UiefuxK;({=_QDMCAicEV^Jwj2T6?? zdh-=tVVtLhmdCI&4&xo^LP`7?_m`r+i;ILKW9J##gQ4E#!UVnu3kc` literal 0 HcmV?d00001 diff --git a/media/atree/node-selected.png b/media/atree/node-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..f67fef90e09f0bb730dde7b5736b9d7587c9beb0 GIT binary patch literal 2418 zcmZ`*dpy(YAAi0xY?I8Tq1>wD)<{LlPuX(6Z&}IRbP`3U4yO{b&25RIlcFNZEtjGa zJ5ifR7r$(Zt}?6T7PUf~o!QP;o!5E&{`j50p6C7fyg%>v`}6$u(0wy5p32NOgyw^DCx2 zd(O60xaD0ksPICoTQG7wab)yeh?`<1qRD4HPI0H-G_rN)HaW74jYrZ66-i@0S#5>Oyzkwa z1G;lgMK1i1hul@<#h;#CU^PXDW9BUww9%c*Sutt}Ya1O6r3=t`Ig4hK0C61+QW^YF z*X68yQi9;5!>!k9@*|IjBUV2@UNZf_&W}c$|JbKs^NJ3(pm2N-oDQeOr9)R5V_>Bu zmZ<2CQhJY^uO7#X+A?fr4jNiyH0Ds{M;DVInXkz4U^7b6abL1Uq1dDy{aZDOr#WmY;^liRELMAm8(Nvt3X#4bVz5*vG}~S-vf`@yV<0l zBK6jBSD*5sc4=#;`sV~8D@qGQiC)cGdgHt7J8dKeU#d~rkDj2+cMgo=G^-gyBu*W7 z%Kf5BvQv^^bZy01Vx#J6*`{B*w8o~&mCGjFSEXNE94Oh-ZDGpmVPFVP^e-WkH+l+c z_AGPbrQQu8a0WzbPS)eMkReVR1vU81XNmQyX0qRETeV`plxLP*o85-EYFHq$&5ZCa zugcI&%YN#DZcZ?{$1Mo4Vc-@!OY}o}Ov!(O%rb0b=I}+Q2VB>0`uc%Kh4%WSS(7ssd)|-c35~i3_Z2ZR|-)D$i zOif!6Ofz~(;uQZUWW&bO+@0F=0pp95y_k*Di#R=LYmTjGLuhQpzn=|eZ?1U3H=_^A)Jz>$vQ(O4xKR6rA8CnQK_a}isT5RVTODYKpnvWva zn8E$#5AkOFC9sEd)}GBYU#N`x+RqcQdT&N_nzn;(n%>|gG)JPb&++`$P@u=3uww_p zRTNc>q{^6129N5jL6<0@j<&!Z8yvEvzF26ieT5(z@T>ofhV&Fs*c3Byt2X_Mad)&D zxGuB%fCs4f2#_Y+uxKPa+Yexi5%eVaxo=AVvPJ5sm{de93Jtc0h*S*N`PkADpr%&7 zQgn)*uiJ;9b94Z*8kk&b048gc0RwKn8^A++49AC#WugIs@?Gt3*53?Yh-lIyo=c6N z_n^j`0Thg3>3^gCTWhigA(?u?k26#yGkzF7OnAKG|JY52LP@sRt|$cEB*OaB{~C3z zhx1OZtg^Evj8zf!dP1syEDy)+va6ce4<#)I8LuDT_-BtK`KzzAcImJ#7b@TXd0qTJ z`=2dqk@g@&20-PydE1Yb4EScU9m>>Yp!Cg!U~Tv*+kh{`W1eY9yD&)!v7r~y^z?GA z0rJol5U*6e&!byW*rLX<2P!dYfMK{AyaBP#`o0jV#0wfqj<=N8C=)|_8|0Q#ct8P* zCT3iS6woa2#))xsOCY_0zgZNaVy8?D-(j0n`Q7Sm@Ew`E2?g|TU6Im;F&S-_IwBd> zzBk+NDmu;Y64Pco2ny-@t{Oz|%bDdGoAu>|tKy+e8uUYh!9ackYap6|)A=bEB+5UJ z5|X!rLsvBAZwPaFqfpc3KndmRI+JbNQ3u-EUKCn)$~8y@?Q%mo97~->>VTSE(GBpO=Xtr53RBF zrmvpWkryuARIYDI+>xwC^FCqJpQlx>wmF+nm8}uK`xY`F0NMW{4xW?^!;^j!4l>Jw z2k7wR?n#Ny=I~whxfvx}5!*JvQ76WTG4?(trf!_0?iiYPso`Y$9{PhkoXjTNXeLi< z_)rC`dQu~``z$?lQZ~LhJm$*BjL0?WWZz6tjU5MVRjk5_7oMW+Db_x>6;YxUJy5o6 zVi!&aYBP{{-Rb-g&C(7bBjS&qVnU!hkx{8{^3m#$8S2~trk(xvV`jIOLig(SXf{d8M= zI=89Ym51(Nci)#3CSI)G6QaW3#w*vpL>ixth@5j`Rsmc>FWdLo4EqB!Kg}qb{QMEG zMrgzrzH4~sK!fYzubm!*{1vQ`$9v13PKZyDFvBI3m6hvWKG>@;(49Q*lj4;n>dfNM zel;!=-aC^x*AY<&VGB$d6D30SXrF6_{R95zi?VqbSv3q9v<5LBc Y>09jeIpAbuf8TfS@80izznOF9oZXyZ z;~gAAT)*AZEg-4%w-?le*WO037{7vYD6g`tclgOwDV2x6(le+KlS(YL-!VUKo)qqopbc;5`<;BLuyH1wYL>G5J0xIf`gg;B$fEE*e z8JEfpgadei|F*L{v(Q{KJfeF>QY15WlWSEI=1y}y-0yTiQ=jj{A4t?yJcMcHUh?28 zD~&h?BKLxosh@F0d6C=nGJCsameIq9{b$yh%)hQ%c&Fcr_x|z=k+k5Hm*XgrNGtTT zP0RO8IBhadz-|IP{{%-U_jDfIkr6Kn_ z_M0#y`JFBoZMtom!$z2=GI*|CV1of;k}1*)7aW0B5HB5HWw#r=RA`m8 zEjWr~WOD1zX%#s<@o8!cv`1U`mRyEVsjdWCfM0@b92@GTJX86&3(q*s+qvNODkmt_ zS9wkY;K4hs466?|ykWEQz_*5J`{8dBRSSth2z$)#W~}K8r;$Yuj|Nwr&9~vkg)Wx; zR*l95kNtc2@VoWcG$h#!o;91}5_%b$Gr%M?kHtV39?~E6VL#@9REEb+rz9=6O8O%P z$@sKeU;E-hVAfKO$g~*bs>Ll;pEkohMZNFzk>xHp&IH|vtSi566rz(HY8Lh^cl&=U zgO2-PfXT)6;GyE{ws0e!!c6nl;RJ+=Gf^K1Z2`J{O#cpuC;SDLY~yyZUPg}KL7FuJ zPD%OE;W#?0RxmxFGV5-<6uA=MuLAR{r57Nc*!6;vMv7fO_)kFCiIKFyt0&q3@&~#_ z5YIfUU0+@g9^eV_MZnw<#^nBF{lO3#0Q_wZf1u|l1m7KnnPW93{WmJ|m)ieg|An|V zym79&`KNeb_=l5S4d?&)br|wCX}GP%rhPU1s^RZWp%*cGS3kz!hm0`o%v*Jlqs#FD zvK(utKSOQAlLE(U^>!tH9>VsrUfokm<|j_etY+^YxOLYG@p}pYXqXRaVk%L5_=WTrL_N`!i?}L6&IIBy&ZVa@U9`Rb5|; zy36+2Y{XgNl7jMczzizX~6C&gC!0+A{_Ik%KAkBph(qRRTcK$=d79#C@&5q ztpT<%Su_g=4fnw2nu!{1VGuZfGGYs|;8s=-I5#K@G*wnq0hnmkRTZpC)?`uX2KAY= z>Md|%GID{z`?2|W-ZRdf{=RxTqeUb4jSpHIacl1WYynu?WTLtep5#q*YzpcMSDqcP z!etL7pe37mpIQia1(OlRxz!JI^;v|im|XTJ5#B^`b66+e?~s;L6<+4Y#1e=!ig!cb zRNs}?49VPnu)?*8525}ss4yyMhq;9p9Z7A^rd4;7k77r!ly~v{w6r!?;o05i1w(^g zEp3~7KeeDAtHRXJfAUS#9lW0(e(dlSOzYnw`y$Px@7W^t(@8gMtfQM^Q2)26@bFi} zdyo40F9(lhx4EM5Ab8|SKo}aO!t`ifpZA*i!fZ-DHoNuiAZ2T%Z7ddZ%`o&`w2b z?{!YD9V0qerMnSTt_PSLH$9TOJdcRrqK+5R8D}asT5^AFNTIb7j_kyj=`I}!&1F)g z(ZyFI1_OeuWq`_Gh4_c;3? z4m8fpv{m|%hPpZHdal|070^%F=x&pdS~oWkFKTFQDhwddLJ3R89~Il}Ln(!3c3#$* zP#W`bdr0Qlq`Y;^%iF9o-LYC3T&j~Sc;!H84h$y286B^ee>xWHwuK=bRpv)YA8n`l za_)(bo%DDj7&~itPxjlXAkC?EP0o-juQ#DzeOdWy6FScJrU~qoW`QL|Sro<&1;C!| MUcR0U?yU6x0PlfHm;e9( delta 5259 zcmZ`-XH-+$vp)$044@zoP>KO=kSakyP+E|p6p;=hNP=h(rMDm@Cjn`4m7>y=A|ePP zMXCfy4F0cjMG=tR!W9Lig(8HOym;69@YZ|lt+V!*DZiQ7v(N0=vx&H;URw_V0lup? z=gk4m@2|MM`~d&}@L)^#FaXf_?bHBlv~ikUMW>Ywt!rBnbdw->uBgI!BEzO@RIbzuMog&h8Gc z@=xmfpuDUI9Jvut)4eG|)cwl43SR3$K{Lk;*G!k{2ieFqn@a{&u%psp@du!-D)HZv zX-D3A^wC@NPdXNy{-JZ!{WV56p3r-mkDxw;jG4l)7#98?&n#`P?yp9pQ0L~hSIfL{*l(;6gkdt+Cr{HEm3~AXYz^LG%5#X?PC2C-tlOabW8_VaPM=Q;^by zT&6T5B~NxhO?{2yP5nCYaw0C3LU46+;~$8s7{IoE-Z4_3I%+U%XPo^$MUwX6GXbPGf!4p!ko^_m`jWlSjLpd6<*4 zn@-$#*4|%kH^sB4Tkkabk2Bx^XWzj6lY7$VCTL(_fOcaDdK?%S7*LXyl8T&&91Ae` zh7;gUp6-WQejwW@0S184q);FQ;=oddbHZc zszyShpkzng)&JZr0_<`}b}t(tOzht3;q*0)au{5`^1pls4-d+8>q7My4qyGq3GxKV zHXt9@-p!9MT2#1k>sHM5J&?7!psJ>(cWc1W3t0Bz>DH+~DL`z!i~IF+K(^ku$Lkh; zNtLt#ql)GR;16;+X}E>KSJDeVe;%`4s8RL!J05$e8}cJ}1R>CI7bq?(`_X=T{fANL z#}mzlSMlX8K$j559Ou;(G_S&%SBqy3PFrh_RX{B-GBLv4di61&cx+eMfyl+-Ro1&y zF{Exi<^}MNEZdJ3hu3v}o8x&GH(2HVzGHtW2&3aaT&v^)tO~O2aSlyN+u}sTSv$HF z9f6NjPwkJJ5XD4aMD<+_YQlnY#s{=9DV*_-C^7i%b_qNUL)@R>b19C#EFd<4s-$bg*f)OM|r1I_e66Gl1sAj9^i@!)k`r*o8ZObE%m2M->M@csN`Tatjiia+lRyh-Hn6@ThJr^*sd zR+y{i#dqhP+(dF%?@b6n+Vwfkz(^u^ZFoK1o~5{~Sc`ibpSeQiut}P58E44GF?O$a z57g+}}nJtt{XVA3ZW+EOVq2#S*S79^%y2&JMvbRs(0q5FWmUH@hW{RErym z+%OqWCnsm!Sc>rIojImYy3wYmlT&}(^R`Kxdnz47NN#7ObDb&T1%HPT$)J%r-cyW zuTnAmBE%x_UY^QcNf=ZzkaOiq{oObt@`b~l82QD01hE(OLoxwwYR)7 zZ*43oCrirnYW$S!fsz2@Le=@@Zh zri?HQ2&C?BW}AWs-QCG_Oo~@Wwav8TcKC6PC%j-&;DkPF^j1wk$kr6%r=XQ!#G&Fh z%^z>4T1HB!U(#F?#Ij7aHPI5Yz<8WMt@|^orB@ICxF5d9d`HHFb&zTyK(!?H@OKws zKnHmZ?ZDd_r1I{nv(u7Y98_(@!r;AMzkU%cLZQK1ekKWC`kC|>H8nw}LuWoTd_a&W zAb70Bj*ICn3@aq=%%vnms<#d9Q~oJ4!t(eTj24WsEJlvm2u&4#UuyMW z8U98s0MZh3M)S9Fh%hW$icx$&2xa1_PWmjGMJ zvu8+@hAqj|NYl-=Np+i@#mZr?1NHlarH!Ry+`fSD{?h>MGh?;)hI;n(i_Ti2m4 z7Bu!0M1`+hxxynXtM5ECWJclKRC8(Ey2%YCvgPokvr=|`;Dp(c|d@;w9i zCQJ?-S8|b^it4QIF*Z24UK;d->$P`h((1%MNWtwPg=CrorA)q2En_{Sv})Vh#_bC9UA*y1(T{<@`g(!{ zMKkBl#O*XB#%@I)uz$9SMX_5S4-dWaaO?lZ^$H8$YtFk;!X@8Mh9D3K5V^nI1G3$; zW11=OVyPDMkH@nvyiZ3`Q8R60W~5#zjzw175oUeSzJItKOjC7C`fz5{&7dIUuL|e^ zNp{QSkAGzOk2MET^=J0Q#Au%`OJ#m-*``BTNyK$KlP|Y7l8Kr|~QCBKozO>69=@nvDe&$IjU0{LSl3zm~X@*c5 z%t3{DE$%H>9R_2*&Nf3ucp19^XbAGAk$Io@?>;tZ{K3h3Di8XnA<0tzK1W(w8Y}dM z!+J)e_Bp1OcE-a5jX&?cfkt_KRLGL*0ZV}HMGNh60)U&hy39pa#Li%-fId0B!C0Zo*}-Ae;lp;hAgy~6 zwmHAVjqg6^)2C0nrk^PV05IAZuF%D8d^aj$v6p}zNMXRU6idTR_V@4KkF8CFL2^gn zxFA9@L_ri=ez9OwdTnBX7pOh|Q8DZE#E(`Abl6r{65i`gvfTTN^YbV8^MU-3BpT2* zfGzfMiCW>|;fn%qKHr1_!xh)Xto}T;?Jx-8PyzvkN`Yw4iB% zn(W~0uLP|ut;DF20%%8VAquPGyKXKn1@JSU+qs)@y4J#pWN9dC92;fxA*ARyK=KXs zlR2FF*Cgla_NPh!q$l!h`U!}nN)o0B>_p6tJ_PXZ6PFXEv~$Y*iR=;>4sSrx;NsSe z>=F{kH>p5;xehGl9e#!5+|-;I05OEOJykJ5jY?>Evd(?D<*J@sclCH}86_<#UPDGz zG_jpZ>Cvih+%5!wLSXVsUz&OJ3ddXn=YU|2XOcdoD8+t*X4?e2`a#7+@cIbPSe%$o6Nrn#l4iPSi1uFNU2K|IcB9TF(B^x z2kg9n+(X($MhMp{3+a8N@SA5dFcCu`87DaRdgHz=?kBc=PyD`+G#)3#bSe-LSDBmS zV7eE(J|Ik@g6$v&RUw)2cfe<0-o&Gx-M?qH>uP|>zNaKhVkahOvaQ*)W9{#?xP)b6 z*zkD2c(Ij9sa*`m;L~DW>=d>`37{uds5}yK3*W?+4Nft{)=<5B+Dr}7K;U;m%LsuCH#IN2W4nzy7e z=u>yqcr`7Kl3@*+^hvQZ(w}q;C`lv(k}`*?VA;Pm<>h4$Sz32etA?l1JG>%=bH1bj zlV`^v4y?BmLp)pgBmDm%dgfi7@{Y=~AqC{~non;9(6SuwP*E@uYXr_vKN>l3_1?ctVvol_D`Tb8xTCkiR)hb)5IClwyv zNdm!#$LcKt4o}3Pn1H6Kq8wcR2UhgC5qM2dPJQ^Gx9E`(5BQ4%D86aTkVFuVkIt8JGy0rYq*zj|4Y;JwNNrC;6(kNxBI z*{gG9C?xj2#E_(tXF$}+U^J-=qH>ny=JZZ5suE}g595IoG@LDuV`(O5TyN7*Vr%xM zRn*(@{cL;aA1+WwMmzhh2%BR~4{IuIi<%a+dQ5&ZA|uM(f*eM2fIotnU`li=0tJdW z7Mie2ys-|7a<@kpsIV&KuWsQlfi*1D%nYt`&50WqjG7bR=W~ElhpJ9c+_s3^;yzDl z_6YLKkv$)p8GiFx)=*~_JD##TOWvj#jTB1TA1;j^=Z}(!8t!IiY@DiVXk#rKAI{}> zzU~24jo(;Y?bPjqRq50h_<`<7oBqET%in2Fn6m;1ay+srZHsiCbiR8rJb$pOuO;Mn z*D8JE!}nSfi;NZqk3a9RQ}xgMJkd7mfBmz3igp2aK|`@vA%(!~yfp=>I#s`^Q}fub zA3~st-GFwKrOK_o1!0$b@sAs$RY~i5LOf;380_c91I~QC2jimRz$ri%knZkoysJto z)O`q{5lEEX%e<-yH}0_87#!XxdiUgzbtUk6B3O&2!_X+Uwh(#QZe$|K6la#^?K^R& zA9Hp`FF?znQ;W9_zYx0nx|x5N-(Ra<_Ocbiq$3T-s|jn7Ju*C`U@t$T#GGx{@Fks~ zY2za;)hfAjlf$4h5J9`f7-e68`KjNDjqSQMJVrg_&)<|c5)zQ7N^*So_6APDUeL3! zM$6JserCa=z--PAN+A_E?3UOfOj^?zS~#fYu_J`QfOQwX5alG(xHw9YE!ztxTszi% z(J##jV*W;mk2^<_`doOrtQ64#Q*>R From a1227f6e7510054b6c18ae8e88f9b64c666b401a Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 16 Jun 2022 16:14:16 +0700 Subject: [PATCH 14/68] add archer tree --- js/atree_constants.js | 177 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 8ed02d6..38ca0a2 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -1,5 +1,181 @@ const atrees = { + "Archer": [ + {"title": "Arrow Bomb", "desc": "Throw a long-ranged arrow that explodes and deal high damage in a large area (self-damage for 30% of DPS) Mana cost: 50 Total damage: 180% (of DPS) - Neutral: 160% - Fire: 20% Range: 26 blocks AoE: 2.5 blocks (circular)", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 1, "col": 4, "rotate": 0}, + {"title": "Bow Proficiency", "desc": "Improve Main Attack damage and range w/ bow Main attack damage: +5% Main attack range: +6 blocks AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 2, "col": 5, "rotate": 90}, + {"title": "Cheaper Arrow Bomb", "desc": "Reduce Mana cost of Arrow Bomb Mana cost: -10 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 3, "col": 4, "rotate": 0}, + {"title": "Heart Shatter", "desc": "If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage Total damage: +100% (of DPS) - Neutral: 100% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 4, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 5, "col": 4, "rotate": 0}, + {"title": "Escape", "desc": "Throw yourself backward to avoid danger (hold shift to cancel) Mana cost: 25 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 3, "rotate": 90}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 5, "rotate": 90}, + {"title": "Double Shots Ability\nBoltslinger Archetype", "desc": "Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies) Blocks: - Power Shots AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 2}, + {"title": "Power Shots Ability\nSharpshooter Archetype", "desc": "Main Attack arrows have increased speed and knockback Blocks: - Double Shots AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 6, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 2, "rotate": 0}, + {"title": "Arrow Storm", "desc": "Shoots 2 streams of 8 arrows, dealing damage to close mobs and pushing them back Mana cost: 40 Total damage: 40% (of DPS per arrow) - Neutral: 30% - Thunder: 10% Range: 16 blocks AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 2}, + {"title": "Cheaper Escape", "desc": "Reduce mana cost of Escape Mana cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 4}, + {"title": "Arrow Shield", "desc": "Create a shield around you that deal damage and knockback mobs when triggered (2 charges) Mana cost: 30 Total damage: 100% (of DPS) - Neutral: 90% - Air: 10% Duration: 60s AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 3, "rotate": 90}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 5, "rotate": 90}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 8, "col": 1, "rotate": 180}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 0, "rotate": 180}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 7, "rotate": 90}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 8, "rotate": 270}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 9, "col": 0, "rotate": 0}, + {"title": "Windy Feet Ability\nBoltslinger Archetype", "desc": "When casting Escape, give speed to yourself and nearby allies Effect: +20% Walk Speed Duration: 120s AoE: 8 blocks (circular) AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 9, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 2, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 6, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 8, "rotate": 0}, + {"title": "Air Mastery Ability\nBoltslinger Archetype", "desc": "Increases base damage from all Air attacks Air Damage: +3-4 Air Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 0}, + {"title": "Thunder Mastery Ability\nBoltslinger Archetype", "desc": "Increases base damage from all Thunder attacks Thunder Damage: +1-8 Thunder Damage: +10% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_c.png", "connector": true, "row": 10, "col": 4, "rotate": 0}, + {"title": "Fire Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Fire attacks Fire Damage: +3-5 Fire Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 6}, + {"title": "Earth Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Earth attacks Earth Damage: +2-4 Earth Damage: +20% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 0, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 2, "rotate": 0}, + {"title": "Water Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Water attacks Water Damage: +2-4 Water Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 6, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 8, "rotate": 0}, + {"title": "Arrow Rain", "desc": "When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies Total Damage: 200% (of DPS per arrow) - Neutral: 120% - Air: 80% AP: 2 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 12, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 1, "rotate": 90}, + {"title": "Nimble String", "desc": "Arrow Storm throws out +8 arrows per stream and shoot twice as fast Total Damage: -15% (of DPS per arrow) - Neutral: -15% Blocks: - Phantom Ray AP: 2 Req: Arrow Storm", "image": "../media/atree/node.png", "connector": false, "row": 12, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 6, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 8, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 0, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 2, "rotate": 0}, + {"title": "Phantom Ray", "desc": "Condense Arrow Storm into a single ray that damages enemies 10 times per second Mana cost: -10 Total Damage: 30% (of DPS per hit) - Neutral: 25% - Water: 5% Range: 12 blocks Blocks: - Windstorm - Nimble String - Arrow Hurricane AP: 2 Req: Arrow Storm Min Sharpshooter: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 5, "rotate": 90}, + {"title": "Fire Creep\nSharpshooter Archetype", "desc": "Arrow Bomb will leak a trail of fire for 6s, damaging enemies that walk into it every 0.4s Total Damage: 50% (of DPS) - Neutral: 30% - Fire: 20% Duration: 6s AoE: 2 blocks (linear) AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 7, "rotate": 90}, + {"title": "Bryophyte Roots\nTrapper Archetype", "desc": "When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s Total Damage: 60% (of DPS) - Neutral: 40% - Earth: 20% Effect: 40% Slowness to Enemies Duration: 5s AoE: 2 blocks (circular) AP: 2 Req: Arrow Storm Min Trapper: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 8}, + {"title": "Triple Shot\nBoltslinger Archetype", "desc": "Triple Main Attack arrows, but they deal -20% damage per arrow AP: 1 Req: Double Shots", "image": "../media/atree/node.png", "connector": false, "row": 14, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 14, "col": 1, "rotate": 180}, + {"title": "Frenzy\nBoltslinger Archetype", "desc": "Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 14, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 8, "rotate": 0}, + {"title": "Guardian Angels Ability", "desc": "Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies (Arrow Shield will no longer push nearby enemies) Total Damage: 60% (of DPS per arrow) - Neutral: 40% - Air: 20% Range: 4 Blocks Duration: 60s AP: 2 Req: Arrow Shield Min Boltslinger: 0/3", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 3, "rotate": 180}, + {"title": "Focus Ability\nSharpshooter Archetype", "desc": "When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once Damage Bonus: +35% (per focus) AP: 2 Min Sharpshooter: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 5, "rotate": 270}, + {"title": "Basaltic Trap Ability\nTrapper Archetype", "desc": "When you hit the ground with Arrow Bomb, leave a Trap that damages enemies (Max 2 Traps) Total Damage: 200% (of DPS) - Neutral: 140% - Earth: 30% - Fire: 30% AoE: 7 Blocks (Circular) AP: 2 Min Trapper: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 7, "rotate": 180}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 0, "rotate": 180}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 16, "col": 2, "rotate": 90}, + {"title": "Cheaper Arrow Storm", "desc": "Reduces the Mana cost of Arrow Storm. Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 3}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 4, "rotate": 180}, + {"title": "Grappling Hook Ability\nTrapper Archetype", "desc": "When casting Escape, you throw a hook that pulls you when hitting a block. If you hit a mob, pull them towards you instead. (Escape will not throw you backward anymore) Range: 20 blocks Blocks: - Escape Artist AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 5}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 6, "rotate": 180}, + {"title": "More Shields Ability", "desc": "Give +2 charges to Arrow Shield AP: 1 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 7}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 8, "rotate": 270}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 0, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 1, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 4, "rotate": 0}, + {"title": "Implosion Ability\nTrapper Archetype", "desc": "Arrow Bomb will pull enemies towards you. If a Trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage Total Damage: +40% (of DPS) - Neutral: +40% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 17, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 7, "rotate": 0}, + {"title": "Patient Hunter Ability\nTrapper Archetype", "desc": "Your Traps will deal +20% more damage for every second they are active (Max +80%) AP: 2 Req: Basaltic Trap", "image": "../media/atree/node.png", "connector": false, "row": 17, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 0, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 1}, + {"title": "More Focus Ability\nSharpshooter Archetype", "desc": "Add +2 max Focus Damage Bonus: -5% (per focus) AP: 1 Req: Focus", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 7, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 0, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 7, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 1, "rotate": 180}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 20, "col": 3, "rotate": 90}, + {"title": "Twain's Arc Ability\nSharpshooter Archetype", "desc": "If you have 2+ Focus, holding shift will summon Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage) Total Damage: 200% (of DPS) - Neutral: 200% Range: 64 blocks AP: 2 Min Sharpshooter: 0/4 Req: Focus", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 5, "rotate": 180}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 6, "rotate": 270}, + {"title": "Bouncing Bomb Ability", "desc": "Arrow Bomb will bounce once when hitting a block or mob AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 7}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 8, "rotate": 270}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 0, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 4, "rotate": 0}, + {"title": "Scorched Earth Ability\nSharpshooter Archetype", "desc": "Fire Creep becomes much stronger Total Damage: +15% (of DPS) - Neutral: +10% - Fire: +5% Duration: 2s AoE: +0.4 Blocks (linear) AP: 1 Req: Fire Creep", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 5}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 6, "rotate": 0}, + {"title": "More Traps Ability\nTrapper Archetype", "desc": "Increase the maximum amount of active Traps you can have by +2 AP: 1 Req: Basaltic Trap", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 0, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 6, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 8, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 1, "rotate": 180}, + {"title": "Homing Shots Ability", "desc": "Your Main Attack arrows will follow nearby enemies and not be affected by gravity AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 3, "rotate": 90}, + {"title": "Shocking Bomb Ability\nSharpshooter Archetype", "desc": "Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder AP: 2 Min Sharpshooter: 0/5 Req: Arrow Bomb", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 5, "rotate": 180}, + {"title": "Better Arrow Shield Ability", "desc": "Arrow Shield will gain additonal AoE, knockback, and damage Total Damage: +40% (of DPS) - Neutral: +40% AoE: +1 Blocks (Circular) AP: 1 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 7, "rotate": 90}, + {"title": "Mana Trap Ability\nTrapper Archetype", "desc": "Your Traps will give you 4 Mana per second when you stay close to them Mana Cost: +10 Range: 12 Blocks AP: 2 Min Trapper: 0/5", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 0, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 2, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 5, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 8, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 1, "rotate": 90}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 25, "col": 4, "rotate": 180}, + {"title": "Initiator Ability\nSharpshooter Archetype", "desc": "If you do not damage an enemy for 5s for more, your next successful hit will deal +50% damage and add +1 Focus AP: 2 Req: Focus Min Sharpshooter: 0/5", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 5}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 6, "rotate": 90}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 25, "col": 7, "rotate": 180}, + {"title": "Cheaper Arrow Storm Ability", "desc": "Reduce the Mana cost of Arrow Storm Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 0, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 2, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 4, "rotate": 0}, + {"title": "Call of the Hound Ability\nTrapper Archetype", "desc": "Arrow Shield summons a Hound that will attack and drag aggressive mobs towards your traps Total Damage: 40% (of DPS) - Neutral: 40% AP: 2 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 26, "col": 7}, + {"title": "Arrow Hurricane Ability\nBoltslinger Archetype", "desc": "Arrow Storm will shoot +2 stream of arrows Blocks: - Phantom Ray AP: 2 Min Boltslinger: 0/8", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 27, "col": 1, "rotate": 180}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 3, "rotate": 90}, + {"title": "Cheaper Arrow Shield Ability", "desc": "Reduce the Mana cost of Arrow Shield Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 27, "col": 5, "rotate": 270}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 7, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 4, "rotate": 0}, + {"title": "Decimator Ability\nSharpshooter Archetype", "desc": "Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%) AP: 2 Req: Phantom Ray", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 5}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 6, "rotate": 90}, + {"title": "Cheaper Escape Ability", "desc": "Reduce the Mana cost of Escape Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 7}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 28, "col": 8, "rotate": 270}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 1, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 7, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 29, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 0, "rotate": 180}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 1}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 2, "rotate": 270}, + {"title": "Crepuscular Ray Ability\nSharpshooter Archetype", "desc": "If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus While in that state, you will lose 1 Focus per second Total Damage: 15% (of DPS per arrrow) - Neutral: 10% - Water: 5% AP: 2 Req: Arrow Storm Min Sharpshooter: 0/8", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 6, "rotate": 180}, + {"title": "Grape Bomb Ability", "desc": "Arrow Bomb will throw 3 additional smaller bombs when exploding Total Damage: 40% (of DPS) - Neutral: 30% - Fire: 10% AoE: 2 Blocks (Circular) AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 7}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 8, "rotate": 270}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 2, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 4, "rotate": 0}, + {"title": "Tangled Traps Ability\nTrapper Archetype", "desc": "Your Traps will be connected by a rope that deals damage to enemies every 0.2s Total Damage: 40% (of DPS) - Neutral: 20% - Air: 20% AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 6}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 7, "rotate": 0}, + {"title": "Stringer Patient Hunter Ability\nTrapper Archetype", "desc": "Add +80% Max Damage to Patient Hunter AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 8}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 2, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 4, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 7, "rotate": 0}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 1, "rotate": 180}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 2}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 3, "rotate": 90}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 4}, + {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 5, "rotate": 270}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 7, "rotate": 0}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 1}, + {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 5}, + {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 34, "col": 6, "rotate": 90}, + {"title": "Minefield Ability\nTrapper Archetype", "desc": "Allow you to place +6 Traps, but with reduced damage and range Total Damage: -80% (of DPS) - Neutral: -80% AoE: -2 Blocks (Circular) AP: 2 Req: Basaltic Trap Min Trapper: 0/10", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 7}, + ], "Assassin": [ { "title": "Spin Attack", @@ -1248,7 +1424,6 @@ const atrees = ], "Warrior": [], "Mage": [], - "Archer": [], "Shaman": [] } From a1179911abb2ef56268932a034c6b6d45dd3e00c Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 16 Jun 2022 16:14:56 +0700 Subject: [PATCH 15/68] parse newlines to line breaks --- js/sq2bs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/sq2bs.js b/js/sq2bs.js index a1de216..4c25ad4 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -596,7 +596,7 @@ function construct_AT(elem, tree) { let active_tooltip_title = document.createElement('b'); active_tooltip_title.classList.add("scaled-font"); - active_tooltip_title.textContent = node.title; + active_tooltip_title.innerHTML = node.title.replaceAll("\n", "
"); let active_tooltip_text = document.createElement('p'); active_tooltip_text.classList.add("scaled-font-sm"); From 441300ec100ed22d6d732a9cd7411c1e589ba6fe Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 16 Jun 2022 16:15:08 +0700 Subject: [PATCH 16/68] add cross --- media/atree/connect_c.png | Bin 0 -> 6254 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 media/atree/connect_c.png diff --git a/media/atree/connect_c.png b/media/atree/connect_c.png new file mode 100644 index 0000000000000000000000000000000000000000..c274306f9ef6e69b70ab50b9f34a5f0b67d333c7 GIT binary patch literal 6254 zcmeHLX;f3!7QUej0wPfnq>6;#fMDhXk}yLUA|xPCWiH9hO^A?zBtU?o5wVIq2ZZ`m zKq<(eqD+D)h$t#5q6I4`76h#mBA|7wsP6`FTJQaMu4Vt3tR&~2^X+f%{hfWzJvSNt zY%c@7@AUuxFkpH!0ssI3Hxa-TZTKf#R>lVa?fuC?!HNJ*B1$Tg@P)AuN|7XmP>@mx z4=Q`wgG1^bT1_46wocaFux!ct$9a9OxD!6t9=9p%*N%9#F8VoY$zE&bhTmS^8)2>< zxSekBYU}*Fvkjb_UdQUwxpjUaiMFeHaprv^cDEG@Oz8S9k>UJdNyOP#p8pz1i>#k9 zU$oj^V$M5JyL?#l`oXyUS6KZp80EaDpL|EzPjpV zzADl%xHZInuKn5ud4mS~#P)F4MYk?txoFTWWJ}je>{OLQ%Y&D^Xz~v~yi;_B#YQA6 zE^j~b+9**+=Fn4m`}Dx@8vCxQZkrU^AAj=a4n5B_?=7gfNo%+Wk;<){GzRPE#1o)r z_zJFWMCg?L9@(P?$gHkk;&dC2RUPW`J#&>>qZN_un22>qDzA%MyRmOtwXRuAZhVY? zc=dJL(y^rD%AP-~AF`WyVTV%$h7n3kE~}_^SJa(LJ64*W3Vw6zN~TjPhi#z6x71nw zTU)5soRwDc{`eN7Ng2s;vY61j8eH*nk@|-L3*6uc(wTP(p!|P+L@eF)NQ6a zrfjI!6j$4z+Fk?in34VN3+nsbrx5m{EY{D6cuF`uJMj0t!P|Ux2Y3^T=Pxcrvl}%t zwlU`WoVT@~-@X-OhsZ7!N40C5*OHj{S{3&U?yp$0vV7mt_F8hi-(Q%+*8;M)hdrok z*gVz&`b8uUL!&Ai1Lsp4EDcPPa&%YZZZ6rwIlHsuqC?xs9`?hQXNZ`_8oo&&onThZ zAs1&(=~B$dAtCwaNwfWyBIfp33p}r-9JshxurQ3_QRz>#66F6vz~9%ofq!+%?E0o< z(6EstXUstIJnXLbzSmoidiCwD>WZJUGP4S!H!J8iGF9ocH+b=}>yC$?j=5sA30eHM zdBkM53se1tY;zw!@z3IImANgJt;}+{e+@pC?P7MOzC#ewS?Mmul;Z>GB`&MHDT+r| zHGl10Xjz0H;Od&?a}4|+2y%1fQ^TAm!!Y!*8Yy_t@n zqw0ami(Xk$gXU^;UZiD#{*M+eH$xL{cIF}*6#yNeJ^&7XC+$Jp*IZ)Pb*1)|V6N&A zih2i7?0$K$_z2j)?z`V+0i|A^8~PU5ZY@oGEN7kSe&2w93p+sBWIS z)-gjnJvX##Ws7IWa)$=vWlc$8+;;g!v!tC1Ow$x5$w-f*W1Fo4YFD`%iHvufc$idP z+F0mZzk1Q&t?Z}d`5~o+hghAJyEQAE=&bwVS0;n~yQ-`_ON^*epEdro3X#*#ZM-@_ zK54a_n`^A|0@Y+>gf)Iv^)|dVbWYpB+y?#FWL&M6-4Nf}`gHh{*s|kbUZ|GEA)k@o zecD=DwXTE8cKat%=xQA zYB{%Um50&3&=VFLZS^l_7U~BJJUYz(xSUZKqi3O~B^H=Jt=K-g%x6#2sIzf;V2{Sk zh78o2f+s6{v!+Y?%50GJ#w@~Oba~#XO~NU+svmvdnQ~!c(SXIAq1qCT?i;b{EkoH# zsXbG5r%>h3FDEic0c~~IfS-L3N85D309sDxR9g3*nMzKe-i>GZ0q15lw%;%!?ZOQk z6%}^}#06g5!J_!~1=$;|#rjZs=l@$~x4(lDB^WJ?X};9&(XD!uzQSu^*WP-?P|@Cj zSUW455_dNb=gmtI!XLO#YeMt(WyeQGxjZ#qzdY8WcRBAj^UNa$lM$ZzgF!F!{`R}u zRbs2Gu4v&iAQASqmE+^xETHKkY3Zys zJ}?b?=@`9UFRkBjLs52iVP=l<$AIYRx7W74rjF#dq`pA79lsb<18VqF+Pu#eau+sO zYe#mhTV);P%PpDbhbb92+B7_H`*lRRAnE37`7>);Ho_@Ox%t7*9*wRJHNppZFEZTA ztrnlYclx$A#e3{r{PjC_m$cSv?KUNLo=>`ktO||>k8TR-@XsjCXxX@bS<}|Ikb%ZC zA(#~*HGXeTMX|$}arrA}6P8$P)?}P3D@-RYVMKR(6SDUHRknWhxyOsnEyzBg>A6`e zsK8A&*1vi*rH;R9WhYC|bIm1_8*gVLG#Xrr_&090*7Z^mW0#wM>3Xtauz6_FgWB!J zP8xsLt;u}2ec=e=-}cS@*;vzUmaq1u|1?kOTqL=m@!b)Z%e(B4+@-P2Rpj=Sue7OD zO;7!t=(v z4$ev`2aJIfC@v%riXG7-4JXhjA2O=!jYFfxA&MABbTG>wqr`~imTHO# z4hAF#WkRV!C=sL7oE)wsUg3yF!{ey;@rk4?)(3jAd{PBi51f)C#SyT0oJfTG)IzTC zNPtNu1Nxs9@*wyyh6{k?l6V;ic_ct$h2^IdJn)0PG+q`vo(>PhL9vhshRR{DgwI@h zF2}2g4{9i_VaM@#^rH z3`exuKb7EjqhyzcjP{a{HIH_1L z9;RAwx~o6a5lzJ6Klb>?auj^n0A2$^F;9{x|2PyR6hVOsj#^KGErmjY^CsHSs6;zE z>POIWNG6ACQO!!gV@VUt>WQJl>A=!*)RhVoj9b9j(A{MaMoC~o3vViCj&$A?g;@xrBZzzHXEP6EUmcLdu_41v)cu>gWs z_+&}F?-%}$=^}8sGzymhPd16h!;ozu5JRPK$QT}%%i$3T1P+%-`dPR;?!&UeYOs`|I))@ z9Pr5|0NYLU!J7-bSK&TvR+DziC( zq`((}zg5>)lS}X8;|?T-Pe6(A!_3iO;5_^wq|Np5VgT>d?}Fo{*>I1p)H_5D0Qxi4 z9|W)~Z#LYiqhPW;bna>+wKa57{8C_?Mi!Id8l>!bbgfFyjeulym?gb?*9KZNdg{;N zSl-Lo=QacDL(D6b=s()kzTj2;P)|Y%RRamQsQ>HL1in_|>oI+Z=&MkwM#4#ft<77M zVGG}<9E=jB{;tyfYN;cE{_Q*Wfz-f;dxP8Rkuw1lpn~TGSQ6j<&~ob=_E`C|I^T(| rDeuSrpN0nT|De7I^UqT@|CppPdI&jIb0BaFT(1DrgUvYP7M1oNXi3{| literal 0 HcmV?d00001 From 112dd1b61931044553c110b2d2a2e5f4aa381b27 Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 16 Jun 2022 16:15:25 +0700 Subject: [PATCH 17/68] add script to convert atree csv to json --- py_script/atree_csv_to_json.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 py_script/atree_csv_to_json.py diff --git a/py_script/atree_csv_to_json.py b/py_script/atree_csv_to_json.py new file mode 100644 index 0000000..4c63d06 --- /dev/null +++ b/py_script/atree_csv_to_json.py @@ -0,0 +1,24 @@ +import csv +import json +import re + +with open('atree.csv', newline='') as csvfile: + res = "" + reader = csv.DictReader(csvfile) + for row in reader: + if not row["connector"]: + row["connector"] = False + else: + row["connector"] = True + row["row"] = int(row["row"]) + row["col"] = int(row["col"]) + if row["rotate"].isdigit(): + row["rotate"] = int(row["rotate"]) + else: + row.pop("rotate") + row["desc"] = re.sub("\n", " ", row["desc"]) + + resjson = json.dumps(row) + res += str(resjson) + ",\n" + + print(res) From e2eaf489849522f83bd78555561e504885e79ecc Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 16 Jun 2022 16:53:59 +0700 Subject: [PATCH 18/68] remove empty title and desc --- js/atree_constants.js | 212 +++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 104 deletions(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 38ca0a2..2dea7bc 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -2,178 +2,182 @@ const atrees = { "Archer": [ {"title": "Arrow Bomb", "desc": "Throw a long-ranged arrow that explodes and deal high damage in a large area (self-damage for 30% of DPS) Mana cost: 50 Total damage: 180% (of DPS) - Neutral: 160% - Fire: 20% Range: 26 blocks AoE: 2.5 blocks (circular)", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 1, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 1, "col": 4, "rotate": 0}, {"title": "Bow Proficiency", "desc": "Improve Main Attack damage and range w/ bow Main attack damage: +5% Main attack range: +6 blocks AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 2, "col": 5, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 2, "col": 5, "rotate": 90}, {"title": "Cheaper Arrow Bomb", "desc": "Reduce Mana cost of Arrow Bomb Mana cost: -10 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 3, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 3, "col": 4, "rotate": 0}, {"title": "Heart Shatter", "desc": "If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage Total damage: +100% (of DPS) - Neutral: 100% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 4, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 5, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 5, "col": 4, "rotate": 0}, {"title": "Escape", "desc": "Throw yourself backward to avoid danger (hold shift to cancel) Mana cost: 25 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 3, "rotate": 90}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 5, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 3, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 5, "rotate": 90}, {"title": "Double Shots Ability\nBoltslinger Archetype", "desc": "Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies) Blocks: - Power Shots AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 2}, {"title": "Power Shots Ability\nSharpshooter Archetype", "desc": "Main Attack arrows have increased speed and knockback Blocks: - Double Shots AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 6, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 6, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 2, "rotate": 0}, {"title": "Arrow Storm", "desc": "Shoots 2 streams of 8 arrows, dealing damage to close mobs and pushing them back Mana cost: 40 Total damage: 40% (of DPS per arrow) - Neutral: 30% - Thunder: 10% Range: 16 blocks AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 2}, {"title": "Cheaper Escape", "desc": "Reduce mana cost of Escape Mana cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 4}, {"title": "Arrow Shield", "desc": "Create a shield around you that deal damage and knockback mobs when triggered (2 charges) Mana cost: 30 Total damage: 100% (of DPS) - Neutral: 90% - Air: 10% Duration: 60s AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 3, "rotate": 90}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 5, "rotate": 90}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 8, "col": 1, "rotate": 180}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 0, "rotate": 180}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 7, "rotate": 90}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 8, "rotate": 270}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 9, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 3, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 5, "rotate": 90}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 8, "col": 1, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 0, "rotate": 180}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 7, "rotate": 90}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 8, "rotate": 270}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 0, "rotate": 0}, {"title": "Windy Feet Ability\nBoltslinger Archetype", "desc": "When casting Escape, give speed to yourself and nearby allies Effect: +20% Walk Speed Duration: 120s AoE: 8 blocks (circular) AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 9, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 2, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 6, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 8, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 6, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 8, "rotate": 0}, {"title": "Air Mastery Ability\nBoltslinger Archetype", "desc": "Increases base damage from all Air attacks Air Damage: +3-4 Air Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 0}, {"title": "Thunder Mastery Ability\nBoltslinger Archetype", "desc": "Increases base damage from all Thunder attacks Thunder Damage: +1-8 Thunder Damage: +10% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_c.png", "connector": true, "row": 10, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 10, "col": 3, "rotate": 90}, + + {"image": "../media/atree/connect_c.png", "connector": true, "row": 10, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 10, "col": 5, "rotate": 90}, + {"title": "Fire Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Fire attacks Fire Damage: +3-5 Fire Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 6}, {"title": "Earth Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Earth attacks Earth Damage: +2-4 Earth Damage: +20% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 0, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 2, "rotate": 0}, {"title": "Water Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Water attacks Water Damage: +2-4 Water Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 6, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 8, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 6, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 8, "rotate": 0}, {"title": "Arrow Rain", "desc": "When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies Total Damage: 200% (of DPS per arrow) - Neutral: 120% - Air: 80% AP: 2 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 12, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 1, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 1, "rotate": 90}, {"title": "Nimble String", "desc": "Arrow Storm throws out +8 arrows per stream and shoot twice as fast Total Damage: -15% (of DPS per arrow) - Neutral: -15% Blocks: - Phantom Ray AP: 2 Req: Arrow Storm", "image": "../media/atree/node.png", "connector": false, "row": 12, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 6, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 8, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 0, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 6, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 8, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 2, "rotate": 0}, {"title": "Phantom Ray", "desc": "Condense Arrow Storm into a single ray that damages enemies 10 times per second Mana cost: -10 Total Damage: 30% (of DPS per hit) - Neutral: 25% - Water: 5% Range: 12 blocks Blocks: - Windstorm - Nimble String - Arrow Hurricane AP: 2 Req: Arrow Storm Min Sharpshooter: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 5, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 5, "rotate": 90}, {"title": "Fire Creep\nSharpshooter Archetype", "desc": "Arrow Bomb will leak a trail of fire for 6s, damaging enemies that walk into it every 0.4s Total Damage: 50% (of DPS) - Neutral: 30% - Fire: 20% Duration: 6s AoE: 2 blocks (linear) AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 7, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 7, "rotate": 90}, {"title": "Bryophyte Roots\nTrapper Archetype", "desc": "When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s Total Damage: 60% (of DPS) - Neutral: 40% - Earth: 20% Effect: 40% Slowness to Enemies Duration: 5s AoE: 2 blocks (circular) AP: 2 Req: Arrow Storm Min Trapper: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 8}, {"title": "Triple Shot\nBoltslinger Archetype", "desc": "Triple Main Attack arrows, but they deal -20% damage per arrow AP: 1 Req: Double Shots", "image": "../media/atree/node.png", "connector": false, "row": 14, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 14, "col": 1, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 14, "col": 1, "rotate": 180}, {"title": "Frenzy\nBoltslinger Archetype", "desc": "Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 14, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 8, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 8, "rotate": 0}, {"title": "Guardian Angels Ability", "desc": "Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies (Arrow Shield will no longer push nearby enemies) Total Damage: 60% (of DPS per arrow) - Neutral: 40% - Air: 20% Range: 4 Blocks Duration: 60s AP: 2 Req: Arrow Shield Min Boltslinger: 0/3", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 3, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 3, "rotate": 180}, {"title": "Focus Ability\nSharpshooter Archetype", "desc": "When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once Damage Bonus: +35% (per focus) AP: 2 Min Sharpshooter: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 5, "rotate": 270}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 5, "rotate": 270}, {"title": "Basaltic Trap Ability\nTrapper Archetype", "desc": "When you hit the ground with Arrow Bomb, leave a Trap that damages enemies (Max 2 Traps) Total Damage: 200% (of DPS) - Neutral: 140% - Earth: 30% - Fire: 30% AoE: 7 Blocks (Circular) AP: 2 Min Trapper: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 7, "rotate": 180}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 0, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 7, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 0, "rotate": 180}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 16, "col": 2, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 16, "col": 2, "rotate": 90}, {"title": "Cheaper Arrow Storm", "desc": "Reduces the Mana cost of Arrow Storm. Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 3}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 4, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 4, "rotate": 180}, {"title": "Grappling Hook Ability\nTrapper Archetype", "desc": "When casting Escape, you throw a hook that pulls you when hitting a block. If you hit a mob, pull them towards you instead. (Escape will not throw you backward anymore) Range: 20 blocks Blocks: - Escape Artist AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 5}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 6, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 6, "rotate": 180}, {"title": "More Shields Ability", "desc": "Give +2 charges to Arrow Shield AP: 1 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 7}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 8, "rotate": 270}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 0, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 1, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 8, "rotate": 270}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 1, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 4, "rotate": 0}, {"title": "Implosion Ability\nTrapper Archetype", "desc": "Arrow Bomb will pull enemies towards you. If a Trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage Total Damage: +40% (of DPS) - Neutral: +40% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 17, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 7, "rotate": 0}, {"title": "Patient Hunter Ability\nTrapper Archetype", "desc": "Your Traps will deal +20% more damage for every second they are active (Max +80%) AP: 2 Req: Basaltic Trap", "image": "../media/atree/node.png", "connector": false, "row": 17, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 0, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 1}, {"title": "More Focus Ability\nSharpshooter Archetype", "desc": "Add +2 max Focus Damage Bonus: -5% (per focus) AP: 1 Req: Focus", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 7, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 0, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 7, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 1, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 1, "rotate": 180}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 20, "col": 3, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 20, "col": 3, "rotate": 90}, {"title": "Twain's Arc Ability\nSharpshooter Archetype", "desc": "If you have 2+ Focus, holding shift will summon Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage) Total Damage: 200% (of DPS) - Neutral: 200% Range: 64 blocks AP: 2 Min Sharpshooter: 0/4 Req: Focus", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 5, "rotate": 180}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 6, "rotate": 270}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 5, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 6, "rotate": 270}, {"title": "Bouncing Bomb Ability", "desc": "Arrow Bomb will bounce once when hitting a block or mob AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 7}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 8, "rotate": 270}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 8, "rotate": 270}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 0, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 4, "rotate": 0}, {"title": "Scorched Earth Ability\nSharpshooter Archetype", "desc": "Fire Creep becomes much stronger Total Damage: +15% (of DPS) - Neutral: +10% - Fire: +5% Duration: 2s AoE: +0.4 Blocks (linear) AP: 1 Req: Fire Creep", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 5}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 6, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 6, "rotate": 0}, {"title": "More Traps Ability\nTrapper Archetype", "desc": "Increase the maximum amount of active Traps you can have by +2 AP: 1 Req: Basaltic Trap", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 0, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 6, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 8, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 6, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 8, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 1, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 1, "rotate": 180}, {"title": "Homing Shots Ability", "desc": "Your Main Attack arrows will follow nearby enemies and not be affected by gravity AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 3, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 3, "rotate": 90}, {"title": "Shocking Bomb Ability\nSharpshooter Archetype", "desc": "Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder AP: 2 Min Sharpshooter: 0/5 Req: Arrow Bomb", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 5, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 5, "rotate": 180}, {"title": "Better Arrow Shield Ability", "desc": "Arrow Shield will gain additonal AoE, knockback, and damage Total Damage: +40% (of DPS) - Neutral: +40% AoE: +1 Blocks (Circular) AP: 1 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 7, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 7, "rotate": 90}, {"title": "Mana Trap Ability\nTrapper Archetype", "desc": "Your Traps will give you 4 Mana per second when you stay close to them Mana Cost: +10 Range: 12 Blocks AP: 2 Min Trapper: 0/5", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 0, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 2, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 5, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 8, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 5, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 8, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 1, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 1, "rotate": 90}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 25, "col": 4, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 25, "col": 4, "rotate": 180}, {"title": "Initiator Ability\nSharpshooter Archetype", "desc": "If you do not damage an enemy for 5s for more, your next successful hit will deal +50% damage and add +1 Focus AP: 2 Req: Focus Min Sharpshooter: 0/5", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 5}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 6, "rotate": 90}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 25, "col": 7, "rotate": 180}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 6, "rotate": 90}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 25, "col": 7, "rotate": 180}, {"title": "Cheaper Arrow Storm Ability", "desc": "Reduce the Mana cost of Arrow Storm Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 0, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 2, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 0, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 4, "rotate": 0}, {"title": "Call of the Hound Ability\nTrapper Archetype", "desc": "Arrow Shield summons a Hound that will attack and drag aggressive mobs towards your traps Total Damage: 40% (of DPS) - Neutral: 40% AP: 2 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 26, "col": 7}, {"title": "Arrow Hurricane Ability\nBoltslinger Archetype", "desc": "Arrow Storm will shoot +2 stream of arrows Blocks: - Phantom Ray AP: 2 Min Boltslinger: 0/8", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_t.png", "connector": true, "row": 27, "col": 1, "rotate": 180}, + {"image": "../media/atree/connect_t.png", "connector": true, "row": 27, "col": 1, "rotate": 180}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 3, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 3, "rotate": 90}, {"title": "Cheaper Arrow Shield Ability", "desc": "Reduce the Mana cost of Arrow Shield Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 27, "col": 5, "rotate": 270}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 27, "col": 5, "rotate": 270}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 7, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 4, "rotate": 0}, {"title": "Decimator Ability\nSharpshooter Archetype", "desc": "Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%) AP: 2 Req: Phantom Ray", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 5}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 6, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 6, "rotate": 90}, {"title": "Cheaper Escape Ability", "desc": "Reduce the Mana cost of Escape Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 7}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 28, "col": 8, "rotate": 270}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 1, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 28, "col": 8, "rotate": 270}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 1, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 7, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 29, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 0, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 0, "rotate": 180}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 1}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 2, "rotate": 270}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 2, "rotate": 270}, {"title": "Crepuscular Ray Ability\nSharpshooter Archetype", "desc": "If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus While in that state, you will lose 1 Focus per second Total Damage: 15% (of DPS per arrrow) - Neutral: 10% - Water: 5% AP: 2 Req: Arrow Storm Min Sharpshooter: 0/8", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 6, "rotate": 180}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 6, "rotate": 180}, {"title": "Grape Bomb Ability", "desc": "Arrow Bomb will throw 3 additional smaller bombs when exploding Total Damage: 40% (of DPS) - Neutral: 30% - Fire: 10% AoE: 2 Blocks (Circular) AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 7}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 8, "rotate": 270}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 8, "rotate": 270}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 2, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 4, "rotate": 0}, {"title": "Tangled Traps Ability\nTrapper Archetype", "desc": "Your Traps will be connected by a rope that deals damage to enemies every 0.2s Total Damage: 40% (of DPS) - Neutral: 20% - Air: 20% AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 6}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 7, "rotate": 0}, {"title": "Stringer Patient Hunter Ability\nTrapper Archetype", "desc": "Add +80% Max Damage to Patient Hunter AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 8}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 2, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 4, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 7, "rotate": 0}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 1, "rotate": 180}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 2, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 4, "rotate": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 1, "rotate": 180}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 2}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 3, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 3, "rotate": 90}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 4}, - {"title": "", "desc": "", "image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 5, "rotate": 270}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 7, "rotate": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 5, "rotate": 270}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 7, "rotate": 0}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 1}, {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 5}, - {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 34, "col": 6, "rotate": 90}, + {"image": "../media/atree/connect_line.png", "connector": true, "row": 34, "col": 6, "rotate": 90}, {"title": "Minefield Ability\nTrapper Archetype", "desc": "Allow you to place +6 Traps, but with reduced damage and range Total Damage: -80% (of DPS) - Neutral: -80% AoE: -2 Blocks (Circular) AP: 2 Req: Basaltic Trap Min Trapper: 0/10", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 7}, ], "Assassin": [ From 1c3ef6d545369bd4b26a8f5fc2337aaaffab811a Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 16 Jun 2022 04:38:09 -0700 Subject: [PATCH 19/68] Add warrior abilities (71) --- js/atree_constants.js | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/js/atree_constants.js b/js/atree_constants.js index 38ca0a2..dda9834 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -176,6 +176,78 @@ const atrees = {"title": "", "desc": "", "image": "../media/atree/connect_line.png", "connector": true, "row": 34, "col": 6, "rotate": 90}, {"title": "Minefield Ability\nTrapper Archetype", "desc": "Allow you to place +6 Traps, but with reduced damage and range Total Damage: -80% (of DPS) - Neutral: -80% AoE: -2 Blocks (Circular) AP: 2 Req: Basaltic Trap Min Trapper: 0/10", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 7}, ], + "Warrior": [ + {"row": 0, "col": 4, "name": "Bash", "desc": "Violently bash the ground, dealing high damage in a large area"}, + {"row": 2, "col": 2, "name": "Cheaper Bash", "desc": "Reduce the Mana cost of Bash"}, + {"row": 2, "col": 4, "name": "Spear Proficiency 1", "desc": "Improve your Main Attack's damage and range w/ spear"}, + {"row": 4, "col": 4, "name": "Double Bash", "desc": "Bash will hit a second time at a farther range"}, + {"row": 6, "col": 2, "name": "Heavy Impact", "desc": "After using Charge, violently crash down into the ground and deal damage. (Does not work if Flying Kick is unlocked)"}, + {"row": 6, "col": 4, "name": "Charge", "desc": "Charge forward at high speed (hold shift to cancel)"}, + {"row": 6, "col": 6, "name": "Tougher Skin", "desc": "Harden your skin and become permanently +5% more resistant. For every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)"}, + {"row": 7, "col": 0, "name": "Vehement", "desc": "For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%). Damage Bonus: +5 (Raw)"}, + {"row": 8, "col": 2, "name": "Uppercut", "desc": "Rocket enemies in the air and deal massive damage"}, + {"row": 8, "col": 4, "name": "Cheaper Charge", "desc": "Reduce the Mana cost of Charge"}, + {"row": 8, "col": 6, "name": "War Scream", "desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies"}, + {"row": 10, "col": 0, "name": "Earth Mastery", "desc": "Increases base damage from all Earth attacks"}, + {"row": 10, "col": 2, "name": "Thunder Mastery", "desc": "Increases base damage from all Thunder attacks"}, + {"row": 10, "col": 6, "name": "Air Mastery", "desc": "Increases base damage from all Air attacks"}, + {"row": 10, "col": 8, "name": "Fire Mastery", "desc": "Increases base damage from all Fire attacks"}, + {"row": 11, "col": 4, "name": "Water Mastery", "desc": "Increases base damage from all Water attacks"}, + {"row": 12, "col": 0, "name": "Quadruple Bash", "desc": "Bash will hit 4 times at an even larger range"}, + {"row": 12, "col": 2, "name": "Fireworks", "desc": "Mobs hit by Uppercut will explode mid-air and receive additional damage"}, + {"row": 12, "col": 6, "name": "Flyby Jab", "desc": "Damage enemies in your way when using Charge"}, + {"row": 12, "col": 8, "name": "Flaming Uppercut", "desc": "Uppercut will light mobs on fire, dealing damage every 0.6 seconds"}, + {"row": 13, "col": 4, "name": "Half-Moon Swipe", "desc": "Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water"}, + {"row": 13, "col": 7, "name": "Iron Lungs", "desc": "War scream deals more damage"}, + {"row": 15, "col": 2, "name": "Generalist", "desc": "After casting 3 different spells in a row, your next spell will cost 5 mana"}, + {"row": 15, "col": 4, "name": "Counter", "desc": "When dodging a nearby enemy attack, get 30% chance to instantly attack back"}, + {"row": 15, "col": 7, "name": "Mantle of the Bovemists", "desc": "When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)"}, + {"row": 16, "col": 1, "name": "Bak'al's Grasp", "desc": "After casting war Scream, become Corrupted (15s Cooldown). You cannot heal while in that state. While Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)."}, + {"row": 17, "col": 0, "name": "Spear Proficiency 2", "desc": "Improve your Main Attack's damage and range w/ spear"}, + {"row": 17, "col": 3, "name": "Cheaper Uppercut", "desc": "Reduce the Mana Cost of Uppercut"}, + {"row": 17, "col": 5, "name": "Aerodynamics", "desc": "During Charge, you can steer and change direction"}, + {"row": 17, "col": 7, "name": "Provoke", "desc": "Mobs damaged by War Scream will target only you for at least 5s. Reduce the Mana cost of War Scream."}, + {"row": 18, "col": 2, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 18, "col": 6, "name": "Air Shout", "desc": "War Scream will fire a projectile that can go through walls and deal damage multiple times."}, + {"row": 20, "col": 0, "name": "Enraged Blow", "desc": "While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)"}, + {"row": 20, "col": 3, "name": "Flying Kick", "desc": "While using Charge, mobs hit will halt your momentum and get knocked back"}, + {"row": 20, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 20, "col": 8, "name": "Manachism", "desc": "If you receive a hit that's less than 5% of your max HP, gain 10 mana (1s Cooldown)"}, + {"row": 22, "col": 0, "name": "Boiling Blood", "desc": "Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds"}, + {"row": 22, "col": 2, "name": "Ragnarokkr", "desc": "War Scream becomes deafening, increasing its range and giving damage bonus to players"}, + {"row": 22, "col": 4, "name": "Ambidextrous", "desc": "Increase your change to attack with Counter by 30%"}, + {"row": 22, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 22, "col": 8, "name": "Stronger Bash", "desc": "Increase the damage of Bash"}, + {"row": 23, "col": 1, "name": "Massacre", "desc": "While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar"}, + {"row": 23, "col": 5, "name": "Collide", "desc": "Mobs thrown into walls from Flying Kick will explode and receive additonal damage"}, + {"row": 23, "col": 7, "name": "Rejuvenating Skin", "desc": "Regain back 30% of the damage you take as healing over 30s"}, + {"row": 24, "col": 2, "name": "Comet", "desc": "After being hit by Fireworks, enemies will crash into the ground and receive more damage"}, + {"row": 26, "col": 0, "name": "Uncontainable Corruption", "desc": "Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1"}, + {"row": 26, "col": 2, "name": "Radiant Devotee", "desc": "For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)"}, + {"row": 26, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 26, "col": 7, "name": "Mythril Skin", "desc": "Gain +5% Base Resistance and become immune to knockback"}, + {"row": 27, "col": 1, "name": "Armour Breaker", "desc": "While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage"}, + {"row": 27, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 27, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 27, "col": 8, "name": "Sparking Hope", "desc": "Everytime you heal 5% of your max health, deal damage to all nearby enemies"}, + {"row": 28, "col": 0, "name": "Massive Bash", "desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)"}, + {"row": 28, "col": 2, "name": "Tempest", "desc": "War Scream will ripple the ground and deal damage 3 times in a large area"}, + {"row": 28, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 29, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 29, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 29, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 31, "col": 0, "name": "Cheaper War Scream", "desc": "Reduce the Mana cost of War Scream"}, + {"row": 31, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 32, "col": 1, "name": "Blood KO", "desc": "Gonna have to rewrite this one chief"}, + {"row": 32, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 32, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 32, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 34, "col": 1, "name": "Blood Pact", "desc": "If you do not have enough mana to cast a spell, spend health instead (1% health per mana)"}, + {"row": 34, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 34, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 34, "col": 8, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 35, "col": 0, "name": "TEXT", "desc": "IDFK MMMM"} + ] "Assassin": [ { "title": "Spin Attack", From 0417a68e6591f014137b3fe6eea2245244c4c53c Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 16 Jun 2022 04:39:20 -0700 Subject: [PATCH 20/68] Remove duplicate warrior tag --- js/atree_constants.js | 145 +++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 195a192..5b90a40 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -180,78 +180,6 @@ const atrees = {"image": "../media/atree/connect_line.png", "connector": true, "row": 34, "col": 6, "rotate": 90}, {"title": "Minefield Ability\nTrapper Archetype", "desc": "Allow you to place +6 Traps, but with reduced damage and range Total Damage: -80% (of DPS) - Neutral: -80% AoE: -2 Blocks (Circular) AP: 2 Req: Basaltic Trap Min Trapper: 0/10", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 7}, ], - "Warrior": [ - {"row": 0, "col": 4, "name": "Bash", "desc": "Violently bash the ground, dealing high damage in a large area"}, - {"row": 2, "col": 2, "name": "Cheaper Bash", "desc": "Reduce the Mana cost of Bash"}, - {"row": 2, "col": 4, "name": "Spear Proficiency 1", "desc": "Improve your Main Attack's damage and range w/ spear"}, - {"row": 4, "col": 4, "name": "Double Bash", "desc": "Bash will hit a second time at a farther range"}, - {"row": 6, "col": 2, "name": "Heavy Impact", "desc": "After using Charge, violently crash down into the ground and deal damage. (Does not work if Flying Kick is unlocked)"}, - {"row": 6, "col": 4, "name": "Charge", "desc": "Charge forward at high speed (hold shift to cancel)"}, - {"row": 6, "col": 6, "name": "Tougher Skin", "desc": "Harden your skin and become permanently +5% more resistant. For every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)"}, - {"row": 7, "col": 0, "name": "Vehement", "desc": "For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%). Damage Bonus: +5 (Raw)"}, - {"row": 8, "col": 2, "name": "Uppercut", "desc": "Rocket enemies in the air and deal massive damage"}, - {"row": 8, "col": 4, "name": "Cheaper Charge", "desc": "Reduce the Mana cost of Charge"}, - {"row": 8, "col": 6, "name": "War Scream", "desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies"}, - {"row": 10, "col": 0, "name": "Earth Mastery", "desc": "Increases base damage from all Earth attacks"}, - {"row": 10, "col": 2, "name": "Thunder Mastery", "desc": "Increases base damage from all Thunder attacks"}, - {"row": 10, "col": 6, "name": "Air Mastery", "desc": "Increases base damage from all Air attacks"}, - {"row": 10, "col": 8, "name": "Fire Mastery", "desc": "Increases base damage from all Fire attacks"}, - {"row": 11, "col": 4, "name": "Water Mastery", "desc": "Increases base damage from all Water attacks"}, - {"row": 12, "col": 0, "name": "Quadruple Bash", "desc": "Bash will hit 4 times at an even larger range"}, - {"row": 12, "col": 2, "name": "Fireworks", "desc": "Mobs hit by Uppercut will explode mid-air and receive additional damage"}, - {"row": 12, "col": 6, "name": "Flyby Jab", "desc": "Damage enemies in your way when using Charge"}, - {"row": 12, "col": 8, "name": "Flaming Uppercut", "desc": "Uppercut will light mobs on fire, dealing damage every 0.6 seconds"}, - {"row": 13, "col": 4, "name": "Half-Moon Swipe", "desc": "Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water"}, - {"row": 13, "col": 7, "name": "Iron Lungs", "desc": "War scream deals more damage"}, - {"row": 15, "col": 2, "name": "Generalist", "desc": "After casting 3 different spells in a row, your next spell will cost 5 mana"}, - {"row": 15, "col": 4, "name": "Counter", "desc": "When dodging a nearby enemy attack, get 30% chance to instantly attack back"}, - {"row": 15, "col": 7, "name": "Mantle of the Bovemists", "desc": "When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)"}, - {"row": 16, "col": 1, "name": "Bak'al's Grasp", "desc": "After casting war Scream, become Corrupted (15s Cooldown). You cannot heal while in that state. While Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)."}, - {"row": 17, "col": 0, "name": "Spear Proficiency 2", "desc": "Improve your Main Attack's damage and range w/ spear"}, - {"row": 17, "col": 3, "name": "Cheaper Uppercut", "desc": "Reduce the Mana Cost of Uppercut"}, - {"row": 17, "col": 5, "name": "Aerodynamics", "desc": "During Charge, you can steer and change direction"}, - {"row": 17, "col": 7, "name": "Provoke", "desc": "Mobs damaged by War Scream will target only you for at least 5s. Reduce the Mana cost of War Scream."}, - {"row": 18, "col": 2, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 18, "col": 6, "name": "Air Shout", "desc": "War Scream will fire a projectile that can go through walls and deal damage multiple times."}, - {"row": 20, "col": 0, "name": "Enraged Blow", "desc": "While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)"}, - {"row": 20, "col": 3, "name": "Flying Kick", "desc": "While using Charge, mobs hit will halt your momentum and get knocked back"}, - {"row": 20, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 20, "col": 8, "name": "Manachism", "desc": "If you receive a hit that's less than 5% of your max HP, gain 10 mana (1s Cooldown)"}, - {"row": 22, "col": 0, "name": "Boiling Blood", "desc": "Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds"}, - {"row": 22, "col": 2, "name": "Ragnarokkr", "desc": "War Scream becomes deafening, increasing its range and giving damage bonus to players"}, - {"row": 22, "col": 4, "name": "Ambidextrous", "desc": "Increase your change to attack with Counter by 30%"}, - {"row": 22, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 22, "col": 8, "name": "Stronger Bash", "desc": "Increase the damage of Bash"}, - {"row": 23, "col": 1, "name": "Massacre", "desc": "While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar"}, - {"row": 23, "col": 5, "name": "Collide", "desc": "Mobs thrown into walls from Flying Kick will explode and receive additonal damage"}, - {"row": 23, "col": 7, "name": "Rejuvenating Skin", "desc": "Regain back 30% of the damage you take as healing over 30s"}, - {"row": 24, "col": 2, "name": "Comet", "desc": "After being hit by Fireworks, enemies will crash into the ground and receive more damage"}, - {"row": 26, "col": 0, "name": "Uncontainable Corruption", "desc": "Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1"}, - {"row": 26, "col": 2, "name": "Radiant Devotee", "desc": "For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)"}, - {"row": 26, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 26, "col": 7, "name": "Mythril Skin", "desc": "Gain +5% Base Resistance and become immune to knockback"}, - {"row": 27, "col": 1, "name": "Armour Breaker", "desc": "While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage"}, - {"row": 27, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 27, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 27, "col": 8, "name": "Sparking Hope", "desc": "Everytime you heal 5% of your max health, deal damage to all nearby enemies"}, - {"row": 28, "col": 0, "name": "Massive Bash", "desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)"}, - {"row": 28, "col": 2, "name": "Tempest", "desc": "War Scream will ripple the ground and deal damage 3 times in a large area"}, - {"row": 28, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 29, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 29, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 29, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 31, "col": 0, "name": "Cheaper War Scream", "desc": "Reduce the Mana cost of War Scream"}, - {"row": 31, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 32, "col": 1, "name": "Blood KO", "desc": "Gonna have to rewrite this one chief"}, - {"row": 32, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 32, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 32, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 34, "col": 1, "name": "Blood Pact", "desc": "If you do not have enough mana to cast a spell, spend health instead (1% health per mana)"}, - {"row": 34, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 34, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 34, "col": 8, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 35, "col": 0, "name": "TEXT", "desc": "IDFK MMMM"} - ] "Assassin": [ { "title": "Spin Attack", @@ -1498,7 +1426,78 @@ const atrees = "col": 8 }, ], - "Warrior": [], + "Warrior": [ + {"row": 0, "col": 4, "name": "Bash", "desc": "Violently bash the ground, dealing high damage in a large area"}, + {"row": 2, "col": 2, "name": "Cheaper Bash", "desc": "Reduce the Mana cost of Bash"}, + {"row": 2, "col": 4, "name": "Spear Proficiency 1", "desc": "Improve your Main Attack's damage and range w/ spear"}, + {"row": 4, "col": 4, "name": "Double Bash", "desc": "Bash will hit a second time at a farther range"}, + {"row": 6, "col": 2, "name": "Heavy Impact", "desc": "After using Charge, violently crash down into the ground and deal damage. (Does not work if Flying Kick is unlocked)"}, + {"row": 6, "col": 4, "name": "Charge", "desc": "Charge forward at high speed (hold shift to cancel)"}, + {"row": 6, "col": 6, "name": "Tougher Skin", "desc": "Harden your skin and become permanently +5% more resistant. For every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)"}, + {"row": 7, "col": 0, "name": "Vehement", "desc": "For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%). Damage Bonus: +5 (Raw)"}, + {"row": 8, "col": 2, "name": "Uppercut", "desc": "Rocket enemies in the air and deal massive damage"}, + {"row": 8, "col": 4, "name": "Cheaper Charge", "desc": "Reduce the Mana cost of Charge"}, + {"row": 8, "col": 6, "name": "War Scream", "desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies"}, + {"row": 10, "col": 0, "name": "Earth Mastery", "desc": "Increases base damage from all Earth attacks"}, + {"row": 10, "col": 2, "name": "Thunder Mastery", "desc": "Increases base damage from all Thunder attacks"}, + {"row": 10, "col": 6, "name": "Air Mastery", "desc": "Increases base damage from all Air attacks"}, + {"row": 10, "col": 8, "name": "Fire Mastery", "desc": "Increases base damage from all Fire attacks"}, + {"row": 11, "col": 4, "name": "Water Mastery", "desc": "Increases base damage from all Water attacks"}, + {"row": 12, "col": 0, "name": "Quadruple Bash", "desc": "Bash will hit 4 times at an even larger range"}, + {"row": 12, "col": 2, "name": "Fireworks", "desc": "Mobs hit by Uppercut will explode mid-air and receive additional damage"}, + {"row": 12, "col": 6, "name": "Flyby Jab", "desc": "Damage enemies in your way when using Charge"}, + {"row": 12, "col": 8, "name": "Flaming Uppercut", "desc": "Uppercut will light mobs on fire, dealing damage every 0.6 seconds"}, + {"row": 13, "col": 4, "name": "Half-Moon Swipe", "desc": "Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water"}, + {"row": 13, "col": 7, "name": "Iron Lungs", "desc": "War scream deals more damage"}, + {"row": 15, "col": 2, "name": "Generalist", "desc": "After casting 3 different spells in a row, your next spell will cost 5 mana"}, + {"row": 15, "col": 4, "name": "Counter", "desc": "When dodging a nearby enemy attack, get 30% chance to instantly attack back"}, + {"row": 15, "col": 7, "name": "Mantle of the Bovemists", "desc": "When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)"}, + {"row": 16, "col": 1, "name": "Bak'al's Grasp", "desc": "After casting war Scream, become Corrupted (15s Cooldown). You cannot heal while in that state. While Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)."}, + {"row": 17, "col": 0, "name": "Spear Proficiency 2", "desc": "Improve your Main Attack's damage and range w/ spear"}, + {"row": 17, "col": 3, "name": "Cheaper Uppercut", "desc": "Reduce the Mana Cost of Uppercut"}, + {"row": 17, "col": 5, "name": "Aerodynamics", "desc": "During Charge, you can steer and change direction"}, + {"row": 17, "col": 7, "name": "Provoke", "desc": "Mobs damaged by War Scream will target only you for at least 5s. Reduce the Mana cost of War Scream."}, + {"row": 18, "col": 2, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 18, "col": 6, "name": "Air Shout", "desc": "War Scream will fire a projectile that can go through walls and deal damage multiple times."}, + {"row": 20, "col": 0, "name": "Enraged Blow", "desc": "While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)"}, + {"row": 20, "col": 3, "name": "Flying Kick", "desc": "While using Charge, mobs hit will halt your momentum and get knocked back"}, + {"row": 20, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 20, "col": 8, "name": "Manachism", "desc": "If you receive a hit that's less than 5% of your max HP, gain 10 mana (1s Cooldown)"}, + {"row": 22, "col": 0, "name": "Boiling Blood", "desc": "Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds"}, + {"row": 22, "col": 2, "name": "Ragnarokkr", "desc": "War Scream becomes deafening, increasing its range and giving damage bonus to players"}, + {"row": 22, "col": 4, "name": "Ambidextrous", "desc": "Increase your change to attack with Counter by 30%"}, + {"row": 22, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 22, "col": 8, "name": "Stronger Bash", "desc": "Increase the damage of Bash"}, + {"row": 23, "col": 1, "name": "Massacre", "desc": "While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar"}, + {"row": 23, "col": 5, "name": "Collide", "desc": "Mobs thrown into walls from Flying Kick will explode and receive additonal damage"}, + {"row": 23, "col": 7, "name": "Rejuvenating Skin", "desc": "Regain back 30% of the damage you take as healing over 30s"}, + {"row": 24, "col": 2, "name": "Comet", "desc": "After being hit by Fireworks, enemies will crash into the ground and receive more damage"}, + {"row": 26, "col": 0, "name": "Uncontainable Corruption", "desc": "Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1"}, + {"row": 26, "col": 2, "name": "Radiant Devotee", "desc": "For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)"}, + {"row": 26, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 26, "col": 7, "name": "Mythril Skin", "desc": "Gain +5% Base Resistance and become immune to knockback"}, + {"row": 27, "col": 1, "name": "Armour Breaker", "desc": "While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage"}, + {"row": 27, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 27, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 27, "col": 8, "name": "Sparking Hope", "desc": "Everytime you heal 5% of your max health, deal damage to all nearby enemies"}, + {"row": 28, "col": 0, "name": "Massive Bash", "desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)"}, + {"row": 28, "col": 2, "name": "Tempest", "desc": "War Scream will ripple the ground and deal damage 3 times in a large area"}, + {"row": 28, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 29, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 29, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 29, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 31, "col": 0, "name": "Cheaper War Scream", "desc": "Reduce the Mana cost of War Scream"}, + {"row": 31, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 32, "col": 1, "name": "Blood KO", "desc": "Gonna have to rewrite this one chief"}, + {"row": 32, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 32, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 32, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 34, "col": 1, "name": "Blood Pact", "desc": "If you do not have enough mana to cast a spell, spend health instead (1% health per mana)"}, + {"row": 34, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 34, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 34, "col": 8, "name": "TEXT", "desc": "IDFK MMMM"}, + {"row": 35, "col": 0, "name": "TEXT", "desc": "IDFK MMMM"} + ], "Mage": [], "Shaman": [] } From ec13bdea1ed50e1974cd305268eba135f37a0e85 Mon Sep 17 00:00:00 2001 From: ferricles Date: Fri, 17 Jun 2022 20:46:52 -0700 Subject: [PATCH 21/68] small bug fixes for atree swapping - some unwanted behavior still exists --- js/atree_constants.js | 1411 +++++------------------------------------ js/sq2bs.js | 13 +- 2 files changed, 176 insertions(+), 1248 deletions(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 5b90a40..31c22eb 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -181,1250 +181,173 @@ const atrees = {"title": "Minefield Ability\nTrapper Archetype", "desc": "Allow you to place +6 Traps, but with reduced damage and range Total Damage: -80% (of DPS) - Neutral: -80% AoE: -2 Blocks (Circular) AP: 2 Req: Basaltic Trap Min Trapper: 0/10", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 7}, ], "Assassin": [ - { - "title": "Spin Attack", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 0, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 1, - "col": 4 - }, - { - "title": "Dagger Proficiency I", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 2, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 2, - "col": 3 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 2, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 3, - "col": 4 - }, - { - "title": "Double Spin", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 4, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 5, - "col": 4 - }, - { - "title": "Dash", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 6, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 6, - "col": 3 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 6, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 6, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 6, - "col": 6 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 7, - "col": 2 - }, - { - "title": "Smoke Bomb", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 8, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 7, - "col": 6 - }, - { - "title": "Multihit", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 8, - "col": 6 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 8, - "col": 3 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 8, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 8, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 8, - "col": 1 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 8, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 9, - "col": 0 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 10, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 9, - "col": 2 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 10, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 9, - "col": 6 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 10, - "col": 6 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 8, - "col": 7 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 8, - "col": 8 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 9, - "col": 8 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 10, - "col": 8 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 10, - "col": 1 - }, - { - "title": "Backstab", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 11, - "col": 1 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 9, - "col": 4 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 90, - "row": 10, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 10, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 11, - "col": 4 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 10, - "col": 7 - }, - { - "title": "Fatality", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 11, - "col": 7 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 11, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 12, - "col": 0 - }, - { - "title": "Violent Vortex", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 13, - "col": 0 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 11, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 12, - "col": 2 - }, - { - "title": "Vanish", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 13, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 12, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 13, - "col": 3 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 13, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 13, - "col": 6 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 14, - "col": 2 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 15, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 14, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 15, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 12, - "col": 7 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 13, - "col": 7 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 14, - "col": 7 - }, - { - "title": "Lacerate", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 15, - "col": 7 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 15, - "col": 1 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 16, - "col": 1 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 15, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 16, - "col": 5 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 15, - "col": 8 - }, - { - "title": "Wall of Smoke", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 16, - "col": 8 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 16, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 17, - "col": 0 - }, - { - "title": "Silent Killer", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 18, - "col": 0 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 16, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 17, - "col": 2 - }, - { - "title": "Shadow Travel", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 18, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 17, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 18, - "col": 5 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 17, - "col": 8 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 18, - "col": 8 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 18, - "col": 4 - }, - { - "title": "Exploding Clones", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 19, - "col": 4 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 18, - "col": 3 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 19, - "col": 0 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 20, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 19, - "col": 3 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 20, - "col": 3 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 18, - "col": 6 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 18, - "col": 7 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 19, - "col": 7 - }, - { - "title": "Weightless", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 20, - "col": 7 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 20, - "col": 1 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 20, - "col": 2 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 21, - "col": 1 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 20, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 21, - "col": 4 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 20, - "col": 6 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 21, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 21, - "col": 6 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 20, - "col": 8 - }, - { - "title": "Dancing Blade", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 21, - "col": 8 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 21, - "col": 0 - }, - { - "title": "Spin Attack Damage", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 22, - "col": 0 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 21, - "col": 3 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 22, - "col": 3 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 22, - "col": 1 - }, - { - "title": "Marked", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 23, - "col": 1 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 22, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 23, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 23, - "col": 5 - }, - { - "title": "Shurikens", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 23, - "col": 6 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 23, - "col": 7 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 22, - "col": 8 - }, - { - "title": "Far Reach", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 23, - "col": 8 - }, - { - "title": "Stronger Multihit", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 24, - "col": 5 - }, - { - "title": "Psithurism", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 24, - "col": 7 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 24, - "col": 1 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 25, - "col": 1 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 25, - "col": 3 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 24, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 25, - "col": 4 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 25, - "col": 5 - }, - { - "title": "Choke Bomb", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 25, - "col": 6 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 25, - "col": 7 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 25, - "col": 8 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 26, - "col": 5 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 25, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 26, - "col": 0 - }, - { - "title": "Death Magnet", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 27, - "col": 0 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 25, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 26, - "col": 2 - }, - { - "title": "Cheaper Multihit", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 27, - "col": 2 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 26, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 27, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 26, - "col": 7 - }, - { - "title": "Parry", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 27, - "col": 7 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 27, - "col": 1 - }, - { - "title": "Fatal Spin", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 28, - "col": 1 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 27, - "col": 3 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 28, - "col": 3 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 27, - "col": 6 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 28, - "col": 6 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 27, - "col": 8 - }, - { - "title": "Wall Jump", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 28, - "col": 8 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 28, - "col": 0 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 29, - "col": 0 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 29, - "col": 1 - }, - { - "title": "Harvester", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 30, - "col": 1 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 28, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 29, - "col": 4 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 30, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 28, - "col": 7 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 29, - "col": 7 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 30, - "col": 7 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 30, - "col": 2 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 31, - "col": 2 - }, - { - "image": "../media/atree/connect_t.png", - "connector": true, - "rotate": 180, - "row": 30, - "col": 5 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 90, - "row": 30, - "col": 6 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 31, - "col": 5 - }, - { - "title": "Ricochet", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 31, - "col": 8 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 31, - "col": 1 - }, - { - "title": "Satsujin", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 32, - "col": 1 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 31, - "col": 4 - }, - { - "title": "Forbidden Art", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 32, - "col": 4 - }, - { - "image": "../media/atree/connect_line.png", - "connector": true, - "rotate": 0, - "row": 31, - "col": 7 - }, - { - "title": "Jasmine Bloom", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 32, - "col": 7 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 180, - "row": 32, - "col": 0 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 33, - "col": 0 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 32, - "col": 2 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 33, - "col": 2 - }, - { - "image": "../media/atree/connect_angle.png", - "connector": true, - "rotate": 270, - "row": 32, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 33, - "col": 5 - }, - { - "title": "Text", - "desc": "desc", - "image": "../media/atree/node.png", - "connector": false, - "row": 33, - "col": 8 - }, + {"title": "Spin Attack", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 1, "col": 4}, + {"title": "Dagger Proficiency I", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 2, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 3, "col": 4}, + {"title": "Double Spin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 4, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 5, "col": 4}, + {"title": "Dash", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 6, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 6, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 7, "col": 2}, + {"title": "Smoke Bomb", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 7, "col": 6}, + {"title": "Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 1}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 8, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 6}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 8, "col": 8}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 8}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 8}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 10, "col": 1}, + {"title": "Backstab", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 90, "row": 10, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 10, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 10, "col": 7}, + {"title": "Fatality", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 11, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 0}, + {"title": "Violent Vortex", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 11, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 2}, + {"title": "Vanish", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 13, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 13, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 7}, + {"title": "Lacerate", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 15, "col": 1}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 1}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 15, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 5}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 15, "col": 8}, + {"title": "Wall of Smoke", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 8}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 16, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 0}, + {"title": "Silent Killer", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 16, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 2}, + {"title": "Shadow Travel", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 5}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 8}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 8}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 4}, + {"title": "Exploding Clones", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 19, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 18, "col": 6}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 7}, + {"title": "Weightless", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 7}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 20, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 20, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 1}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 20, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 4}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 20, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 21, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 6}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 20, "col": 8}, + {"title": "Dancing Blade", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 8}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 21, "col": 0}, + {"title": "Spin Attack Damage", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 22, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 21, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 22, "col": 3}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 22, "col": 1}, + {"title": "Marked", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 22, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 23, "col": 5}, + {"title": "Shurikens", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 23, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 22, "col": 8}, + {"title": "Far Reach", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 8}, + {"title": "Stronger Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 5}, + {"title": "Psithurism", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 24, "col": 1}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 1}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 24, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 25, "col": 5}, + {"title": "Choke Bomb", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 6}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 25, "col": 7}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 8}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 26, "col": 5}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 25, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 0}, + {"title": "Death Magnet", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 25, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 2}, + {"title": "Cheaper Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 7}, + {"title": "Parry", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 7}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 27, "col": 1}, + {"title": "Fatal Spin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 1}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 27, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 3}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 27, "col": 6}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 6}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 27, "col": 8}, + {"title": "Wall Jump", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 8}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 28, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 29, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 1}, + {"title": "Harvester", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 28, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 28, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 7}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 7 }, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 30, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 2 }, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 30, "col": 5}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 30, "col": 6}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 5}, + {"title": "Ricochet", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 8}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 1}, + {"title": "Satsujin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 4}, + {"title": "Forbidden Art", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 7}, + {"title": "Jasmine Bloom", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 32, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 32, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 2}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 32, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 8}, ], "Warrior": [ {"row": 0, "col": 4, "name": "Bash", "desc": "Violently bash the ground, dealing high damage in a large area"}, diff --git a/js/sq2bs.js b/js/sq2bs.js index 4c25ad4..3d8161e 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -73,11 +73,9 @@ document.addEventListener('DOMContentLoaded', function() { }; }); - construct_AT(document.getElementById("atree-ui"), atrees["Assassin"]); //dagger is default atree document.getElementById("atree-dropdown").style.display = "none"; }); -// phanta scheduler let calcBuildTask = null; let updateStatTask = null; let doSearchTask = null; @@ -235,8 +233,13 @@ function update_field(field) { // set weapon img and set ability tree if (category == 'weapon') { document.querySelector("#weapon-img").setAttribute('src', '../media/items/new/generic-'+type+'.png'); - construct_AT(document.getElementById("atree-ui"), atrees[wep_to_class[type.toLowerCase()]]); //dagger is default atree - document.getElementById("atree-dropdown").style.display = "none"; + //for some reason we have to cast to string + construct_AT(document.getElementById("atree-ui"), atrees[wep_to_class.get(type)]); + + if (document.getElementById("toggle-atree").classList.contains("toggleOn")) { + toggle_tab('atree-dropdown'); + toggleButton('toggle-atree'); + } //TODO: reset chosen abilities (once ability implementation is here) } @@ -538,6 +541,8 @@ function init_autocomplete() { // atree parsing function construct_AT(elem, tree) { + console.log("constructing ability tree UI"); + elem.innerHTML = ""; //reset the atree in the DOM for (let i = 0; i < tree.length; i++) { let node = tree[i]; From 6277b50d12343252c7e55f29c8daeaeeb71af182 Mon Sep 17 00:00:00 2001 From: ferricles Date: Sat, 18 Jun 2022 21:00:32 -0700 Subject: [PATCH 22/68] fixed atree display bugs | functional atree toggle on/off + switching based on inferred class type --- builder/index.html | 3 +-- js/sq2bs.js | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/builder/index.html b/builder/index.html index 443bd8b..398110c 100644 --- a/builder/index.html +++ b/builder/index.html @@ -617,8 +617,7 @@
-
- Active: +
diff --git a/js/sq2bs.js b/js/sq2bs.js index 3d8161e..41612bd 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -233,6 +233,13 @@ function update_field(field) { // set weapon img and set ability tree if (category == 'weapon') { document.querySelector("#weapon-img").setAttribute('src', '../media/items/new/generic-'+type+'.png'); + + //as of now, we NEED to have the dropdown tab visible/not hidden in order to properly display atree stuff. + if (!document.getElementById("toggle-atree").classList.contains("toggleOn")) { + toggle_tab('atree-dropdown'); + toggleButton('toggle-atree'); + } + //for some reason we have to cast to string construct_AT(document.getElementById("atree-ui"), atrees[wep_to_class.get(type)]); @@ -542,7 +549,15 @@ function init_autocomplete() { // atree parsing function construct_AT(elem, tree) { console.log("constructing ability tree UI"); + document.getElementById("atree-active").innerHTML = ""; //reset all atree actives - should be done in a more general way later elem.innerHTML = ""; //reset the atree in the DOM + + // add in the "Active" title to atree + let active_row = document.createElement("div"); + active_row.classList.add("row", "item-title", "mx-auto", "justify-content-center"); + active_row.textContent = "Active:"; + document.getElementById("atree-active").appendChild(active_row); + for (let i = 0; i < tree.length; i++) { let node = tree[i]; @@ -553,12 +568,16 @@ function construct_AT(elem, tree) { let row = document.createElement('div'); row.classList.add("row"); row.id = "atree-row-" + j; - row.style.height = elem.getBoundingClientRect().width / 9 + "px"; + //was causing atree rows to be 0 height + console.log(elem.scrollWidth / 9); + row.style.minHeight = elem.scrollWidth / 9 + "px"; + //row.style.minHeight = elem.getBoundingClientRect().width / 9 + "px"; for (let k = 0; k < 9; k++) { col = document.createElement('div'); col.classList.add('col', 'px-0'); + col.style.minHeight = elem.scrollWidth / 9 + "px"; row.appendChild(col); }; elem.appendChild(row); @@ -594,7 +613,8 @@ function construct_AT(elem, tree) { let active_tooltip = document.createElement('div'); active_tooltip.classList.add("rounded-bottom", "dark-7", "border", "mb-2", "mx-auto"); - active_tooltip.style.width = elem.getBoundingClientRect().width * .80 + "px"; + //was causing active element boxes to be 0 width + // active_tooltip.style.width = elem.getBoundingClientRect().width * .80 + "px"; active_tooltip.style.display = "none"; // tooltip text formatting From 554de7ea87da38470ec45352ee3438f9c9b0d35a Mon Sep 17 00:00:00 2001 From: ferricles Date: Sat, 18 Jun 2022 21:40:43 -0700 Subject: [PATCH 23/68] fixed asymmetric connector line images --- media/atree/connect_angle.png | Bin 1081 -> 1079 bytes media/atree/connect_c.png | Bin 6254 -> 1316 bytes media/atree/connect_line.png | Bin 1270 -> 1270 bytes media/atree/connect_t.png | Bin 694 -> 692 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/media/atree/connect_angle.png b/media/atree/connect_angle.png index 46c5e81aa19b176dafaba4aa04be9b0359cc4982..b1b03fd5193027c1a6854b70ffbf65cbde6be8c4 100644 GIT binary patch delta 115 zcmV-(0F3{+2)78Zv;jG8Nkl3cX`LRxdy}sNa~J@Cm+pA4 V3M)Je{Jj7G002ovPDHLkV1iVPFeLy0 delta 111 zcmV-#0FeK;2)PKbv;jGANkl}peR2rGbfdS zL1SX=L|c!;0V1vb!CRF?S*<%PQda0i35XU>(Gol8l&QVtRl=uB_8FHZEZJefwT_j& zUTig=-nm^?JGh!2@OLbpJb6*~l#2ME48h0Er+-_#pL@9WJEPOyGa;LknF35tdmLMo zq_$g&E$(7#omPTR(lR-J-DMAV?tAy}@$cA{^ve6ci@$1FPG+>5Skd>g&2^8X&F0LP zbG~;vuhGyv^8CnSHdBv3CrWQVH9yDN&N@%h=+eo`M*wAJtE& zPF~I8*2fyMz^UlZgZdBE+tTW?F8Ok~HJ%f{{ZE3CX}81TwA=TOZ@X*3_~-nAsn)WuMyFld#FtCZfd$$D2s{?$Js}E-!31lv$9G`tF!(0+Op#Q^_Pqd#Vibm zMiChyAeX(iwuibu0ISc}7Fl1JpKi^a!mAS!yk+x__pO Bg>3); literal 6254 zcmeHLX;f3!7QUej0wPfnq>6;#fMDhXk}yLUA|xPCWiH9hO^A?zBtU?o5wVIq2ZZ`m zKq<(eqD+D)h$t#5q6I4`76h#mBA|7wsP6`FTJQaMu4Vt3tR&~2^X+f%{hfWzJvSNt zY%c@7@AUuxFkpH!0ssI3Hxa-TZTKf#R>lVa?fuC?!HNJ*B1$Tg@P)AuN|7XmP>@mx z4=Q`wgG1^bT1_46wocaFux!ct$9a9OxD!6t9=9p%*N%9#F8VoY$zE&bhTmS^8)2>< zxSekBYU}*Fvkjb_UdQUwxpjUaiMFeHaprv^cDEG@Oz8S9k>UJdNyOP#p8pz1i>#k9 zU$oj^V$M5JyL?#l`oXyUS6KZp80EaDpL|EzPjpV zzADl%xHZInuKn5ud4mS~#P)F4MYk?txoFTWWJ}je>{OLQ%Y&D^Xz~v~yi;_B#YQA6 zE^j~b+9**+=Fn4m`}Dx@8vCxQZkrU^AAj=a4n5B_?=7gfNo%+Wk;<){GzRPE#1o)r z_zJFWMCg?L9@(P?$gHkk;&dC2RUPW`J#&>>qZN_un22>qDzA%MyRmOtwXRuAZhVY? zc=dJL(y^rD%AP-~AF`WyVTV%$h7n3kE~}_^SJa(LJ64*W3Vw6zN~TjPhi#z6x71nw zTU)5soRwDc{`eN7Ng2s;vY61j8eH*nk@|-L3*6uc(wTP(p!|P+L@eF)NQ6a zrfjI!6j$4z+Fk?in34VN3+nsbrx5m{EY{D6cuF`uJMj0t!P|Ux2Y3^T=Pxcrvl}%t zwlU`WoVT@~-@X-OhsZ7!N40C5*OHj{S{3&U?yp$0vV7mt_F8hi-(Q%+*8;M)hdrok z*gVz&`b8uUL!&Ai1Lsp4EDcPPa&%YZZZ6rwIlHsuqC?xs9`?hQXNZ`_8oo&&onThZ zAs1&(=~B$dAtCwaNwfWyBIfp33p}r-9JshxurQ3_QRz>#66F6vz~9%ofq!+%?E0o< z(6EstXUstIJnXLbzSmoidiCwD>WZJUGP4S!H!J8iGF9ocH+b=}>yC$?j=5sA30eHM zdBkM53se1tY;zw!@z3IImANgJt;}+{e+@pC?P7MOzC#ewS?Mmul;Z>GB`&MHDT+r| zHGl10Xjz0H;Od&?a}4|+2y%1fQ^TAm!!Y!*8Yy_t@n zqw0ami(Xk$gXU^;UZiD#{*M+eH$xL{cIF}*6#yNeJ^&7XC+$Jp*IZ)Pb*1)|V6N&A zih2i7?0$K$_z2j)?z`V+0i|A^8~PU5ZY@oGEN7kSe&2w93p+sBWIS z)-gjnJvX##Ws7IWa)$=vWlc$8+;;g!v!tC1Ow$x5$w-f*W1Fo4YFD`%iHvufc$idP z+F0mZzk1Q&t?Z}d`5~o+hghAJyEQAE=&bwVS0;n~yQ-`_ON^*epEdro3X#*#ZM-@_ zK54a_n`^A|0@Y+>gf)Iv^)|dVbWYpB+y?#FWL&M6-4Nf}`gHh{*s|kbUZ|GEA)k@o zecD=DwXTE8cKat%=xQA zYB{%Um50&3&=VFLZS^l_7U~BJJUYz(xSUZKqi3O~B^H=Jt=K-g%x6#2sIzf;V2{Sk zh78o2f+s6{v!+Y?%50GJ#w@~Oba~#XO~NU+svmvdnQ~!c(SXIAq1qCT?i;b{EkoH# zsXbG5r%>h3FDEic0c~~IfS-L3N85D309sDxR9g3*nMzKe-i>GZ0q15lw%;%!?ZOQk z6%}^}#06g5!J_!~1=$;|#rjZs=l@$~x4(lDB^WJ?X};9&(XD!uzQSu^*WP-?P|@Cj zSUW455_dNb=gmtI!XLO#YeMt(WyeQGxjZ#qzdY8WcRBAj^UNa$lM$ZzgF!F!{`R}u zRbs2Gu4v&iAQASqmE+^xETHKkY3Zys zJ}?b?=@`9UFRkBjLs52iVP=l<$AIYRx7W74rjF#dq`pA79lsb<18VqF+Pu#eau+sO zYe#mhTV);P%PpDbhbb92+B7_H`*lRRAnE37`7>);Ho_@Ox%t7*9*wRJHNppZFEZTA ztrnlYclx$A#e3{r{PjC_m$cSv?KUNLo=>`ktO||>k8TR-@XsjCXxX@bS<}|Ikb%ZC zA(#~*HGXeTMX|$}arrA}6P8$P)?}P3D@-RYVMKR(6SDUHRknWhxyOsnEyzBg>A6`e zsK8A&*1vi*rH;R9WhYC|bIm1_8*gVLG#Xrr_&090*7Z^mW0#wM>3Xtauz6_FgWB!J zP8xsLt;u}2ec=e=-}cS@*;vzUmaq1u|1?kOTqL=m@!b)Z%e(B4+@-P2Rpj=Sue7OD zO;7!t=(v z4$ev`2aJIfC@v%riXG7-4JXhjA2O=!jYFfxA&MABbTG>wqr`~imTHO# z4hAF#WkRV!C=sL7oE)wsUg3yF!{ey;@rk4?)(3jAd{PBi51f)C#SyT0oJfTG)IzTC zNPtNu1Nxs9@*wyyh6{k?l6V;ic_ct$h2^IdJn)0PG+q`vo(>PhL9vhshRR{DgwI@h zF2}2g4{9i_VaM@#^rH z3`exuKb7EjqhyzcjP{a{HIH_1L z9;RAwx~o6a5lzJ6Klb>?auj^n0A2$^F;9{x|2PyR6hVOsj#^KGErmjY^CsHSs6;zE z>POIWNG6ACQO!!gV@VUt>WQJl>A=!*)RhVoj9b9j(A{MaMoC~o3vViCj&$A?g;@xrBZzzHXEP6EUmcLdu_41v)cu>gWs z_+&}F?-%}$=^}8sGzymhPd16h!;ozu5JRPK$QT}%%i$3T1P+%-`dPR;?!&UeYOs`|I))@ z9Pr5|0NYLU!J7-bSK&TvR+DziC( zq`((}zg5>)lS}X8;|?T-Pe6(A!_3iO;5_^wq|Np5VgT>d?}Fo{*>I1p)H_5D0Qxi4 z9|W)~Z#LYiqhPW;bna>+wKa57{8C_?Mi!Id8l>!bbgfFyjeulym?gb?*9KZNdg{;N zSl-Lo=QacDL(D6b=s()kzTj2;P)|Y%RRamQsQ>HL1in_|>oI+Z=&MkwM#4#ft<77M zVGG}<9E=jB{;tyfYN;cE{_Q*Wfz-f;dxP8Rkuw1lpn~TGSQ6j<&~ob=_E`C|I^T(| rDeuSrpN0nT|De7I^UqT@|CppPdI&jIb0BaFT(1DrgUvYP7M1oNXi3{| diff --git a/media/atree/connect_line.png b/media/atree/connect_line.png index 02771e5dbeb70cba490f7f1011a6266174b75b61..41ee50731e02a10dae99c90a20decc01234064cf 100644 GIT binary patch delta 52 zcmeyy`HgeKEk<53gTe~ HDWM4fG*=SK diff --git a/media/atree/connect_t.png b/media/atree/connect_t.png index 14d3c16b69250a233cd1de4ca7f2a0b44f98b040..8ed976eb9f0d5d3c8d87891a0c058fa5598b8603 100644 GIT binary patch literal 692 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=B+po-U3d z6?5L+Fyv!0$Te-*MEOd-G7;z z;W-CG3+E^zJp^pi)8@xD?SB$`w}G4C5DN@lyLUUC*JUSzf)9;R&e=0|dszzFkSqr# d3HpEsi^UU5cvNTD9S{Lo=;`X`vd$@?2>@Frgo6M8 literal 694 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=B*Ko-U3d z6?5L+Fyv!0Y`KQ`W_}XlrNx5dCd*WBNtI?6u1u`@cU}owr<^ z;hZ1?hwvyOJp^{2KV!F-rT+BoUqyOIx}&S} Date: Sun, 19 Jun 2022 00:42:49 -0700 Subject: [PATCH 24/68] Compute graph cleanup, prepping for full build calc (currently broke) --- builder/index.html | 7 +- js/build.js | 262 ++------------------------------------ js/build_encode_decode.js | 201 +++++++++++++++++++++++++++++ js/builder.js | 211 ++---------------------------- js/builder_graph.js | 56 ++++++++ js/computation_graph.js | 81 +++++++++--- 6 files changed, 346 insertions(+), 472 deletions(-) create mode 100644 js/build_encode_decode.js diff --git a/builder/index.html b/builder/index.html index d7a802c..d9c3e8f 100644 --- a/builder/index.html +++ b/builder/index.html @@ -313,10 +313,10 @@
- +
- +
@@ -1399,8 +1399,9 @@ - + + diff --git a/js/build.js b/js/build.js index 84ff86a..447162d 100644 --- a/js/build.js +++ b/js/build.js @@ -100,238 +100,12 @@ class Build{ * @param {Number[]} powders : Powder application. List of lists of integers (powder IDs). * In order: boots, Chestplate, Leggings, Boots, Weapon. * @param {Object[]} inputerrors : List of instances of error-like classes. + * + * @param {Object[]} tomes: List of tomes. + * In order: 2x Weapon Mastery Tome, 4x Armor Mastery Tome, 1x Guild Tome. + * 2x Slaying Mastery Tome, 2x Dungeoneering Mastery Tome, 2x Gathering Mastery Tome are in game, but do not have "useful" stats (those that affect damage calculations or building) */ - constructor(level,equipment, powders, externalStats, inputerrors=[]){ - - let errors = inputerrors; - //this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem). - this.craftedItems = []; - this.customItems = []; - // NOTE: powders is just an array of arrays of powder IDs. Not powder objects. - this.powders = powders; - if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") { - const helmet = itemMap.get(equipment[0]); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - } else { - try { - let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined); - if (helmet.statMap.get("type") !== "helmet") { - throw new Error("Not a helmet"); - } - this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots")); - helmet.statMap.set("powders",this.powders[0].slice()); - this.helmet = helmet.statMap; - applyArmorPowders(this.helmet, this.powders[0]); - if (this.helmet.get("custom")) { - this.customItems.push(helmet); - } else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(helmet); - } - - } catch (Error) { - const helmet = itemMap.get("No Helmet"); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - errors.push(new ItemNotFound(equipment[0], "helmet", true)); - } - } - if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") { - const chestplate = itemMap.get(equipment[1]); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - } else { - try { - let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined); - if (chestplate.statMap.get("type") !== "chestplate") { - throw new Error("Not a chestplate"); - } - this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots")); - chestplate.statMap.set("powders",this.powders[1].slice()); - this.chestplate = chestplate.statMap; - applyArmorPowders(this.chestplate, this.powders[1]); - if (this.chestplate.get("custom")) { - this.customItems.push(chestplate); - } else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(chestplate); - } - } catch (Error) { - console.log(Error); - const chestplate = itemMap.get("No Chestplate"); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - errors.push(new ItemNotFound(equipment[1], "chestplate", true)); - } - } - if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") { - const leggings = itemMap.get(equipment[2]); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - } else { - try { - let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined); - if (leggings.statMap.get("type") !== "leggings") { - throw new Error("Not a leggings"); - } - this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots")); - leggings.statMap.set("powders",this.powders[2].slice()); - this.leggings = leggings.statMap; - applyArmorPowders(this.leggings, this.powders[2]); - if (this.leggings.get("custom")) { - this.customItems.push(leggings); - } else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(leggings); - } - } catch (Error) { - const leggings = itemMap.get("No Leggings"); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - errors.push(new ItemNotFound(equipment[2], "leggings", true)); - } - } - if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") { - const boots = itemMap.get(equipment[3]); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - } else { - try { - let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined); - if (boots.statMap.get("type") !== "boots") { - throw new Error("Not a boots"); - } - this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots")); - boots.statMap.set("powders",this.powders[3].slice()); - this.boots = boots.statMap; - applyArmorPowders(this.boots, this.powders[3]); - if (this.boots.get("custom")) { - this.customItems.push(boots); - } else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(boots); - } - } catch (Error) { - const boots = itemMap.get("No Boots"); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - errors.push(new ItemNotFound(equipment[3], "boots", true)); - } - } - if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") { - const ring = itemMap.get(equipment[4]); - this.ring1 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring1 = ring.statMap; - if (this.ring1.get("custom")) { - this.customItems.push(ring); - } else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 1"); - this.ring1 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring")); - } - } - if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") { - const ring = itemMap.get(equipment[5]); - this.ring2 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring2 = ring.statMap; - if (this.ring2.get("custom")) { - this.customItems.push(ring); - } else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 2"); - this.ring2 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring")); - } - } - if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") { - const bracelet = itemMap.get(equipment[6]); - this.bracelet = expandItem(bracelet, []); - }else{ - try { - let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined); - if (bracelet.statMap.get("type") !== "bracelet") { - throw new Error("Not a bracelet"); - } - this.bracelet = bracelet.statMap; - if (this.bracelet.get("custom")) { - this.customItems.push(bracelet); - } else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(bracelet); - } - } catch (Error) { - const bracelet = itemMap.get("No Bracelet"); - this.bracelet = expandItem(bracelet, []); - errors.push(new ItemNotFound(equipment[6], "bracelet", true)); - } - } - if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") { - const necklace = itemMap.get(equipment[7]); - this.necklace = expandItem(necklace, []); - }else{ - try { - let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined); - if (necklace.statMap.get("type") !== "necklace") { - throw new Error("Not a necklace"); - } - this.necklace = necklace.statMap; - if (this.necklace.get("custom")) { - this.customItems.push(necklace); - } else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(necklace); - } - } catch (Error) { - const necklace = itemMap.get("No Necklace"); - this.necklace = expandItem(necklace, []); - errors.push(new ItemNotFound(equipment[7], "necklace", true)); - } - } - if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") { - const weapon = itemMap.get(equipment[8]); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - if (equipment[8] !== "No Weapon") { - document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } else { - document.getElementsByClassName("powder-specials")[0].style.display = "none"; - } - }else{ - try { - let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined); - if (weapon.statMap.get("category") !== "weapon") { - throw new Error("Not a weapon"); - } - this.weapon = weapon.statMap; - if (this.weapon.get("custom")) { - this.customItems.push(weapon); - } else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(weapon); - } - this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots")); - this.weapon.set("powders",this.powders[4].slice()); - document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } catch (Error) { - const weapon = itemMap.get("No Weapon"); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - document.getElementsByClassName("powder-specials")[0].style.display = "none"; - errors.push(new ItemNotFound(equipment[8], "weapon", true)); - } - } - //console.log(this.craftedItems) + constructor(level, items, tomes, weapon){ if (level < 1) { //Should these be constants? this.level = 1; @@ -348,10 +122,12 @@ class Build{ document.getElementById("level-choice").value = this.level; this.availableSkillpoints = levelToSkillPoints(this.level); - this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ]; - this.items = this.equipment.concat([this.weapon]); + this.equipment = items; + this.tomes = tomes; + this.weapon = weapon; + this.items = this.equipment.concat([this.weapon]).concat(this.tomes); // return [equip_order, best_skillpoints, final_skillpoints, best_total]; - let result = calculate_skillpoints(this.equipment, this.weapon); + let result = calculate_skillpoints(this.equipment.concat(this.tomes), this.weapon); console.log(result); this.equip_order = result[0]; // How many skillpoints the player had to assign (5 number) @@ -361,28 +137,14 @@ class Build{ // How many skillpoints assigned (1 number, sum of base_skillpoints) this.assigned_skillpoints = result[3]; this.activeSetCounts = result[4]; - - // For strength boosts like warscream, vanish, etc. - this.damageMultiplier = 1.0; - this.defenseMultiplier = 1.0; - - // For other external boosts ;-; - this.externalStats = externalStats; this.initBuildStats(); - - // Remove every error before adding specific ones - for (let i of document.getElementsByClassName("error")) { - i.textContent = ""; - } - this.errors = errors; - if (errors.length > 0) this.errored = true; } /*Returns build in string format */ toString(){ - return [this.equipment,this.weapon].flat(); + return [this.equipment,this.weapon,this.tomes].flat(); } /* Getters */ @@ -392,7 +154,7 @@ class Build{ } getBaseSpellCost(spellIdx, cost) { - cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); + // old intelligence: cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); cost += this.statMap.get("spRaw"+spellIdx); return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100)); } diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js new file mode 100644 index 0000000..9b168b1 --- /dev/null +++ b/js/build_encode_decode.js @@ -0,0 +1,201 @@ + +/* + * Populate fields based on url, and calculate build. + */ +function decodeBuild(url_tag) { + if (url_tag) { + //default values + let equipment = [null, null, null, null, null, null, null, null, null]; + let tomes = [null, null, null, null, null, null, null]; + let powdering = ["", "", "", "", ""]; + let info = url_tag.split("_"); + let version = info[0]; + let save_skp = false; + let skillpoints = [0, 0, 0, 0, 0]; + let level = 106; + + let version_number = parseInt(version) + //equipment (items) + // TODO: use filters + if (version_number < 4) { + let equipments = info[1]; + for (let i = 0; i < 9; ++i ) { + let equipment_str = equipments.slice(i*3,i*3+3); + equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); + } + info[1] = equipments.slice(27); + } + else if (version_number == 4) { + let info_str = info[1]; + let start_idx = 0; + for (let i = 0; i < 9; ++i ) { + if (info_str.charAt(start_idx) === "-") { + equipment[i] = "CR-"+info_str.slice(start_idx+1, start_idx+18); + start_idx += 18; + } + else { + let equipment_str = info_str.slice(start_idx, start_idx+3); + equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); + start_idx += 3; + } + } + info[1] = info_str.slice(start_idx); + } + else if (version_number <= 6) { + let info_str = info[1]; + let start_idx = 0; + for (let i = 0; i < 9; ++i ) { + if (info_str.slice(start_idx,start_idx+3) === "CR-") { + equipment[i] = info_str.slice(start_idx, start_idx+20); + start_idx += 20; + } else if (info_str.slice(start_idx+3,start_idx+6) === "CI-") { + let len = Base64.toInt(info_str.slice(start_idx,start_idx+3)); + equipment[i] = info_str.slice(start_idx+3,start_idx+3+len); + start_idx += (3+len); + } else { + let equipment_str = info_str.slice(start_idx, start_idx+3); + equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); + start_idx += 3; + } + } + info[1] = info_str.slice(start_idx); + } + //constant in all versions + for (let i in equipment) { + setValue(equipmentInputs[i], equipment[i]); + } + + //level, skill point assignments, and powdering + if (version_number == 1) { + let powder_info = info[1]; + let res = parsePowdering(powder_info); + powdering = res[0]; + } else if (version_number == 2) { + save_skp = true; + let skillpoint_info = info[1].slice(0, 10); + for (let i = 0; i < 5; ++i ) { + skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); + } + + let powder_info = info[1].slice(10); + let res = parsePowdering(powder_info); + powdering = res[0]; + } else if (version_number <= 6){ + level = Base64.toInt(info[1].slice(10,12)); + setValue("level-choice",level); + save_skp = true; + let skillpoint_info = info[1].slice(0, 10); + for (let i = 0; i < 5; ++i ) { + skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); + } + + let powder_info = info[1].slice(12); + + let res = parsePowdering(powder_info); + powdering = res[0]; + info[1] = res[1]; + } + // Tomes. + if (version == 6) { + //tome values do not appear in anything before v6. + for (let i = 0; i < 7; ++i) { + let tome_str = info[1].charAt(i); + for (let i in tomes) { + setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); + } + } + info[1] = info[1].slice(7); + } + + for (let i in powderInputs) { + setValue(powderInputs[i], powdering[i]); + } + } +} + +/* Stores the entire build in a string using B64 encoding and adds it to the URL. +*/ +function encodeBuild(build) { + + if (build) { + let build_string; + + //V6 encoding - Tomes + build_version = 4; + build_string = ""; + tome_string = ""; + + let crafted_idx = 0; + let custom_idx = 0; + for (const item of build.items) { + + if (item.get("custom")) { + let custom = "CI-"+encodeCustom(build.customItems[custom_idx],true); + build_string += Base64.fromIntN(custom.length, 3) + custom; + custom_idx += 1; + build_version = Math.max(build_version, 5); + } else if (item.get("crafted")) { + build_string += "CR-"+encodeCraft(build.craftedItems[crafted_idx]); + crafted_idx += 1; + } else if (item.get("category") === "tome") { + let tome_id = item.get("id"); + if (tome_id <= 60) { + // valid normal tome. ID 61-63 is for NONE tomes. + build_version = Math.max(build_version, 6); + } + tome_string += Base64.fromIntN(tome_id, 1); + } else { + build_string += Base64.fromIntN(item.get("id"), 3); + } + } + + for (const skp of skp_order) { + build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 + } + build_string += Base64.fromIntN(build.level, 2); + for (const _powderset of build.powders) { + let n_bits = Math.ceil(_powderset.length / 6); + build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. + // Slice copy. + let powderset = _powderset.slice(); + while (powderset.length != 0) { + let firstSix = powderset.slice(0,6).reverse(); + let powder_hash = 0; + for (const powder of firstSix) { + powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. + } + build_string += Base64.fromIntN(powder_hash, 5); + powderset = powderset.slice(6); + } + } + build_string += tome_string; + + return build_version.toString() + "_" + build_string; + } +} + +function copyBuild(build) { + if (build) { + copyTextToClipboard(url_base+location.hash); + document.getElementById("copy-button").textContent = "Copied!"; + } +} + +function shareBuild(build) { + if (build) { + let text = url_base+location.hash+"\n"+ + "WynnBuilder build:\n"+ + "> "+build.helmet.get("displayName")+"\n"+ + "> "+build.chestplate.get("displayName")+"\n"+ + "> "+build.leggings.get("displayName")+"\n"+ + "> "+build.boots.get("displayName")+"\n"+ + "> "+build.ring1.get("displayName")+"\n"+ + "> "+build.ring2.get("displayName")+"\n"+ + "> "+build.bracelet.get("displayName")+"\n"+ + "> "+build.necklace.get("displayName")+"\n"+ + "> "+build.weapon.get("displayName")+" ["+build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; + copyTextToClipboard(text); + document.getElementById("share-button").textContent = "Copied!"; + } +} + diff --git a/js/builder.js b/js/builder.js index 1e51e31..249bf32 100644 --- a/js/builder.js +++ b/js/builder.js @@ -35,206 +35,6 @@ function parsePowdering(powder_info) { return [powdering, powder_info]; } -/* - * Populate fields based on url, and calculate build. - */ -function decodeBuild(url_tag) { - if (url_tag) { - //default values - let equipment = [null, null, null, null, null, null, null, null, null]; - let tomes = [null, null, null, null, null, null, null]; - let powdering = ["", "", "", "", ""]; - let info = url_tag.split("_"); - let version = info[0]; - let save_skp = false; - let skillpoints = [0, 0, 0, 0, 0]; - let level = 106; - - let version_number = parseInt(version) - //equipment (items) - // TODO: use filters - if (version_number < 4) { - let equipments = info[1]; - for (let i = 0; i < 9; ++i ) { - let equipment_str = equipments.slice(i*3,i*3+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - } - info[1] = equipments.slice(27); - } - else if (version_number == 4) { - let info_str = info[1]; - let start_idx = 0; - for (let i = 0; i < 9; ++i ) { - if (info_str.charAt(start_idx) === "-") { - equipment[i] = "CR-"+info_str.slice(start_idx+1, start_idx+18); - start_idx += 18; - } - else { - let equipment_str = info_str.slice(start_idx, start_idx+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - start_idx += 3; - } - } - info[1] = info_str.slice(start_idx); - } - else if (version_number <= 6) { - let info_str = info[1]; - let start_idx = 0; - for (let i = 0; i < 9; ++i ) { - if (info_str.slice(start_idx,start_idx+3) === "CR-") { - equipment[i] = info_str.slice(start_idx, start_idx+20); - start_idx += 20; - } else if (info_str.slice(start_idx+3,start_idx+6) === "CI-") { - let len = Base64.toInt(info_str.slice(start_idx,start_idx+3)); - equipment[i] = info_str.slice(start_idx+3,start_idx+3+len); - start_idx += (3+len); - } else { - let equipment_str = info_str.slice(start_idx, start_idx+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - start_idx += 3; - } - } - info[1] = info_str.slice(start_idx); - } - //constant in all versions - for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); - } - - //level, skill point assignments, and powdering - if (version_number == 1) { - let powder_info = info[1]; - let res = parsePowdering(powder_info); - powdering = res[0]; - } else if (version_number == 2) { - save_skp = true; - let skillpoint_info = info[1].slice(0, 10); - for (let i = 0; i < 5; ++i ) { - skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); - } - - let powder_info = info[1].slice(10); - let res = parsePowdering(powder_info); - powdering = res[0]; - } else if (version_number <= 6){ - level = Base64.toInt(info[1].slice(10,12)); - setValue("level-choice",level); - save_skp = true; - let skillpoint_info = info[1].slice(0, 10); - for (let i = 0; i < 5; ++i ) { - skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); - } - - let powder_info = info[1].slice(12); - - let res = parsePowdering(powder_info); - powdering = res[0]; - info[1] = res[1]; - } - // Tomes. - if (version == 6) { - //tome values do not appear in anything before v6. - for (let i = 0; i < 7; ++i) { - let tome_str = info[1].charAt(i); - for (let i in tomes) { - setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); - } - } - info[1] = info[1].slice(7); - } - - for (let i in powderInputs) { - setValue(powderInputs[i], powdering[i]); - } - } -} - -/* Stores the entire build in a string using B64 encoding and adds it to the URL. -*/ -function encodeBuild() { - - if (player_build) { - let build_string; - - //V6 encoding - Tomes - build_version = 4; - build_string = ""; - tome_string = ""; - - let crafted_idx = 0; - let custom_idx = 0; - for (const item of player_build.items) { - - if (item.get("custom")) { - let custom = "CI-"+encodeCustom(player_build.customItems[custom_idx],true); - build_string += Base64.fromIntN(custom.length, 3) + custom; - custom_idx += 1; - build_version = Math.max(build_version, 5); - } else if (item.get("crafted")) { - build_string += "CR-"+encodeCraft(player_build.craftedItems[crafted_idx]); - crafted_idx += 1; - } else if (item.get("category") === "tome") { - let tome_id = item.get("id"); - if (tome_id <= 60) { - // valid normal tome. ID 61-63 is for NONE tomes. - build_version = Math.max(build_version, 6); - } - tome_string += Base64.fromIntN(tome_id, 1); - } else { - build_string += Base64.fromIntN(item.get("id"), 3); - } - } - - for (const skp of skp_order) { - build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 - } - build_string += Base64.fromIntN(player_build.level, 2); - for (const _powderset of player_build.powders) { - let n_bits = Math.ceil(_powderset.length / 6); - build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. - // Slice copy. - let powderset = _powderset.slice(); - while (powderset.length != 0) { - let firstSix = powderset.slice(0,6).reverse(); - let powder_hash = 0; - for (const powder of firstSix) { - powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. - } - build_string += Base64.fromIntN(powder_hash, 5); - powderset = powderset.slice(6); - } - } - build_string += tome_string; - - return build_version.toString() + "_" + build_string; - } -} - -function copyBuild() { - if (player_build) { - copyTextToClipboard(url_base+location.hash); - document.getElementById("copy-button").textContent = "Copied!"; - } -} - -function shareBuild() { - if (player_build) { - let text = url_base+location.hash+"\n"+ - "WynnBuilder build:\n"+ - "> "+player_build.helmet.get("displayName")+"\n"+ - "> "+player_build.chestplate.get("displayName")+"\n"+ - "> "+player_build.leggings.get("displayName")+"\n"+ - "> "+player_build.boots.get("displayName")+"\n"+ - "> "+player_build.ring1.get("displayName")+"\n"+ - "> "+player_build.ring2.get("displayName")+"\n"+ - "> "+player_build.bracelet.get("displayName")+"\n"+ - "> "+player_build.necklace.get("displayName")+"\n"+ - "> "+player_build.weapon.get("displayName")+" ["+player_build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; - copyTextToClipboard(text); - document.getElementById("share-button").textContent = "Copied!"; - } -} - function populateBuildList() { const buildList = document.getElementById("build-choice"); const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); @@ -250,7 +50,7 @@ function saveBuild() { if (player_build) { const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); const saveName = document.getElementById("build-name").value; - const encodedBuild = encodeBuild(); + const encodedBuild = encodeBuild(player_build); if ((!Object.keys(savedBuilds).includes(saveName) || document.getElementById("saved-error").textContent !== "") && encodedBuild !== "") { savedBuilds[saveName] = encodedBuild.replace("#", ""); @@ -330,6 +130,15 @@ function toggleButton(button_id) { } } +// toggle tab +function toggle_tab(tab) { + if (document.querySelector("#"+tab).style.display == "none") { + document.querySelector("#"+tab).style.display = ""; + } else { + document.querySelector("#"+tab).style.display = "none"; + } +} + // TODO: Learn and use await function init() { console.log("builder.js init"); diff --git a/js/builder_graph.js b/js/builder_graph.js index fd0de13..111cd92 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -1,5 +1,52 @@ +class BuildEncodeNode extends ComputeNode { + constructor() { + super("builder-encode"); + } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "BuildEncodeNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + return encodeBuild(build); + } +} + +class URLUpdateNode extends ComputeNode { + constructor() { + super("builder-url-update"); + } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "URLUpdateNode accepts exactly one input (build_str)"; } + const [build_str] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + location.hash = build_str; + } +} + +class BuildAssembleNode extends ComputeNode { + constructor() { + super("builder-make-build"); + } + + compute_func(input_map) { + let equipments = [ + input_map.get('helmet-input'), + input_map.get('chestplate-input'), + input_map.get('leggings-input'), + input_map.get('boots-input'), + input_map.get('ring1-input'), + input_map.get('ring2-input'), + input_map.get('bracelet-input'), + input_map.get('necklace-input') + ]; + let weapon = input_map.get('weapon-input'); + let level = input_map.get('level-input'); + console.log('build node run'); + return new Build(level, equipments, [], weapon); + } +} + let item_nodes = []; document.addEventListener('DOMContentLoaded', function() { @@ -16,6 +63,14 @@ document.addEventListener('DOMContentLoaded', function() { } let weapon_image = document.getElementById("weapon-img"); new WeaponDisplayNode('weapon-type', weapon_image).link_to(item_nodes[8]); + let level_input = new InputNode('level-input', document.getElementById('level-choice')); + new PrintNode('lvl-debug').link_to(level_input); + + let build_node = new BuildAssembleNode(); + for (const input of item_nodes) { + build_node.link_to(input); + } + build_node.link_to(level_input); console.log("Set up graph"); }); @@ -98,6 +153,7 @@ function init_autocomplete() { if (event.detail.selection.value) { event.target.value = event.detail.selection.value; } + event.target.dispatchEvent(new Event('input')); }, }, } diff --git a/js/computation_graph.js b/js/computation_graph.js index 417c9bf..155d95a 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -6,14 +6,16 @@ class ComputeNode { * @param name : Name of the node (string). Must be unique. Must "fit in" a JS string (terminated by single quotes). */ constructor(name) { - this.inputs = []; + this.inputs = []; // parent nodes this.children = []; this.value = 0; this.name = name; this.update_task = null; this.update_time = Date.now(); this.fail_cb = false; // Set to true to force updates even if parent failed. - this.calc_inputs = new Map(); + this.dirty = false; + this.inputs_dirty = new Map(); + this.inputs_dirty_count = 0; } /** @@ -24,7 +26,19 @@ class ComputeNode { return; } this.update_time = timestamp; - this.set_value(this.compute_func(this.calc_inputs)); + + if (this.inputs_dirty_count != 0) { + return; + } + let calc_inputs = new Map(); + for (const input of this.inputs) { + calc_inputs.set(input.name, input.value); + } + this.value = this.compute_func(calc_inputs); + this.dirty = false; + for (const child of this.children) { + child.mark_input_clean(this.name, this.value, timestamp); + } } /** @@ -40,22 +54,35 @@ class ComputeNode { } /** - * Set an input value. Propagates calculation if all inputs are present. + * Mark parent as not dirty. Propagates calculation if all inputs are present. */ - set_input(input_name, value, timestamp) { - if (value || this.fail_cb) { - this.calc_inputs.set(input_name, value) - if (this.calc_inputs.size === this.inputs.length) { - this.update(timestamp) + mark_input_clean(input_name, value, timestamp) { + if (value !== null || this.fail_cb) { + if (this.inputs_dirty.get(input_name)) { + this.inputs_dirty.set(input_name, false); + this.inputs_dirty_count -= 1; + } + if (this.inputs_dirty_count === 0) { + this.update(timestamp); } } } - /** - * Remove cached input values to this calculation. - */ - clear_cache() { - this.calc_inputs = new Map(); + mark_input_dirty(input_name) { + if (!this.inputs_dirty.get(input_name)) { + this.inputs_dirty.set(input_name, true); + this.inputs_dirty_count += 1; + } + } + + mark_dirty() { + if (!this.dirty) { + this.dirty = true; + for (const child of this.children) { + child.mark_input_dirty(this.name); + child.mark_dirty(); + } + } } /** @@ -74,6 +101,10 @@ class ComputeNode { link_to(parent_node) { this.inputs.push(parent_node) + this.inputs_dirty.set(parent_node.name, parent_node.dirty); + if (parent_node.dirty) { + this.inputs_dirty_count += 1; + } parent_node.children.push(this); } } @@ -87,6 +118,7 @@ function calcSchedule(node) { if (node.update_task !== null) { clearTimeout(node.update_task); } + node.mark_dirty(); node.update_task = setTimeout(function() { const timestamp = Date.now(); node.update(timestamp); @@ -107,10 +139,25 @@ class PrintNode extends ComputeNode { } } +/** + * Node for getting an input from an input field. + */ +class InputNode extends ComputeNode { + constructor(name, input_field) { + super(name); + this.input_field = input_field; + this.input_field.addEventListener("input", () => calcSchedule(this)); + } + + compute_func(input_map) { + return this.input_field.value; + } +} + /** * Node for getting an item's stats from an item input field. */ -class ItemInputNode extends ComputeNode { +class ItemInputNode extends InputNode { /** * Make an item stat pulling compute node. * @@ -119,9 +166,7 @@ class ItemInputNode extends ComputeNode { * @param none_item: Item object to use as the "none" for this field. */ constructor(name, item_input_field, none_item) { - super(name); - this.input_field = item_input_field; - this.input_field.addEventListener("input", () => calcSchedule(this)); + super(name, item_input_field); this.none_item = new Item(none_item); } From 8db34d68a75f6743308d19d928a756325c4426e8 Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 09:49:04 -0700 Subject: [PATCH 25/68] Almost working spell damage calculation --- js/build.js | 45 +++++++-------- js/build_constants.js | 2 +- js/build_encode_decode.js | 26 ++++++++- js/builder.js | 38 +++++-------- js/builder_graph.js | 113 +++++++++++++++++++++++++++++++++++--- js/computation_graph.js | 29 +++++----- js/damage_calc.js | 19 +------ js/load.js | 21 +++++-- js/skillpoints.js | 9 +-- js/sq2display.js | 24 ++++---- 10 files changed, 212 insertions(+), 114 deletions(-) diff --git a/js/build.js b/js/build.js index 447162d..00300c1 100644 --- a/js/build.js +++ b/js/build.js @@ -127,7 +127,9 @@ class Build{ this.weapon = weapon; this.items = this.equipment.concat([this.weapon]).concat(this.tomes); // return [equip_order, best_skillpoints, final_skillpoints, best_total]; - let result = calculate_skillpoints(this.equipment.concat(this.tomes), this.weapon); + + // calc skillpoints requires statmaps only + let result = calculate_skillpoints(this.equipment.concat(this.tomes).map((x) => x.statMap), this.weapon.statMap); console.log(result); this.equip_order = result[0]; // How many skillpoints the player had to assign (5 number) @@ -165,8 +167,9 @@ class Build{ */ getMeleeStats(){ const stats = this.statMap; - if (this.weapon.get("tier") === "Crafted") { - stats.set("damageBases", [this.weapon.get("nDamBaseHigh"),this.weapon.get("eDamBaseHigh"),this.weapon.get("tDamBaseHigh"),this.weapon.get("wDamBaseHigh"),this.weapon.get("fDamBaseHigh"),this.weapon.get("aDamBaseHigh")]); + const weapon_stats = this.weapon.statMap; + if (weapon_stats.get("tier") === "Crafted") { + stats.set("damageBases", [weapon_stats.get("nDamBaseHigh"),weapon_stats.get("eDamBaseHigh"),weapon_stats.get("tDamBaseHigh"),weapon_stats.get("wDamBaseHigh"),weapon_stats.get("fDamBaseHigh"),weapon_stats.get("aDamBaseHigh")]); } let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier"); if(adjAtkSpd > 6){ @@ -176,13 +179,13 @@ class Build{ } let damage_mult = 1; - if (this.weapon.get("type") === "relik") { + if (weapon_stats.get("type") === "relik") { damage_mult = 0.99; // CURSE YOU WYNNCRAFT //One day we will create WynnWynn and no longer have shaman 99% melee injustice. //In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams. } // 0spellmult for melee damage. - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct") + this.externalStats.get("mdPct"), 0, this.weapon, this.total_skillpoints, damage_mult * this.damageMultiplier, this.externalStats); + let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct"), 0, this.weapon.statMap, this.total_skillpoints, damage_mult * this.damageMultiplier); let dex = this.total_skillpoints[1]; @@ -217,8 +220,8 @@ class Build{ defenseStats.push(totalHp); //EHP let ehp = [totalHp, totalHp]; - let defMult = classDefenseMultipliers.get(this.weapon.get("type")); - ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); + let defMult = classDefenseMultipliers.get(this.weapon.statMap.get("type")); + ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); defenseStats.push(ehp); //HPR @@ -259,26 +262,27 @@ class Build{ let major_ids = new Set(); for (const item of this.items){ - for (let [id, value] of item.get("maxRolls")) { + const item_stats = item.statMap; + for (let [id, value] of item_stats.get("maxRolls")) { if (staticIDs.includes(id)) { continue; } statMap.set(id,(statMap.get(id) || 0)+value); } for (const staticID of staticIDs) { - if (item.get(staticID)) { - statMap.set(staticID, statMap.get(staticID) + item.get(staticID)); + if (item_stats.get(staticID)) { + statMap.set(staticID, statMap.get(staticID) + item_stats.get(staticID)); } } - if (item.get("majorIds")) { - for (const major_id of item.get("majorIds")) { + if (item_stats.get("majorIds")) { + for (const major_id of item_stats.get("majorIds")) { major_ids.add(major_id); } } } statMap.set("activeMajorIDs", major_ids); for (const [setName, count] of this.activeSetCounts) { - const bonus = sets[setName].bonuses[count-1]; + const bonus = sets.get(setName).bonuses[count-1]; for (const id in bonus) { if (skp_order.includes(id)) { // pass. Don't include skillpoints in ids @@ -291,16 +295,8 @@ class Build{ statMap.set("poisonPct", 100); // The stuff relevant for damage calculation!!! @ferricles - statMap.set("atkSpd", this.weapon.get("atkSpd")); + statMap.set("atkSpd", this.weapon.statMap.get("atkSpd")); - for (const x of skp_elements) { - this.externalStats.set(x + "DamPct", 0); - } - this.externalStats.set("mdPct", 0); - this.externalStats.set("sdPct", 0); - this.externalStats.set("damageBonus", [0, 0, 0, 0, 0]); - this.externalStats.set("defBonus",[0, 0, 0, 0, 0]); - this.externalStats.set("poisonPct", 0); this.statMap = statMap; this.aggregateStats(); @@ -308,10 +304,11 @@ class Build{ aggregateStats() { let statMap = this.statMap; - statMap.set("damageRaw", [this.weapon.get("nDam"), this.weapon.get("eDam"), this.weapon.get("tDam"), this.weapon.get("wDam"), this.weapon.get("fDam"), this.weapon.get("aDam")]); + let weapon_stats = this.weapon.statMap; + statMap.set("damageRaw", [weapon_stats.get("nDam"), weapon_stats.get("eDam"), weapon_stats.get("tDam"), weapon_stats.get("wDam"), weapon_stats.get("fDam"), weapon_stats.get("aDam")]); statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]); statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]); statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]); - statMap.set("defMult", classDefenseMultipliers.get(this.weapon.get("type"))); + statMap.set("defMult", classDefenseMultipliers.get(weapon_stats.get("type"))); } } diff --git a/js/build_constants.js b/js/build_constants.js index fbcbb3f..2983328 100644 --- a/js/build_constants.js +++ b/js/build_constants.js @@ -88,7 +88,7 @@ let equipmentInputs = equipment_fields.map(x => x + "-choice"); let buildFields = equipment_fields.map(x => x+"-tooltip").concat(tome_fields.map(x => x + "-tooltip")); let tomeInputs = tome_fields.map(x => x + "-choice"); -let powderInputs = [ +let powder_inputs = [ "helmet-powder", "chestplate-powder", "leggings-powder", diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 9b168b1..0929e4e 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -1,3 +1,25 @@ +function parsePowdering(powder_info) { + // TODO: Make this run in linear instead of quadratic time... ew + let powdering = []; + for (let i = 0; i < 5; ++i) { + let powders = ""; + let n_blocks = Base64.toInt(powder_info.charAt(0)); + // console.log(n_blocks + " blocks"); + powder_info = powder_info.slice(1); + for (let j = 0; j < n_blocks; ++j) { + let block = powder_info.slice(0,5); + console.log(block); + let six_powders = Base64.toInt(block); + for (let k = 0; k < 6 && six_powders != 0; ++k) { + powders += powderNames.get((six_powders & 0x1f) - 1); + six_powders >>>= 5; + } + powder_info = powder_info.slice(5); + } + powdering[i] = powders; + } + return [powdering, powder_info]; +} /* * Populate fields based on url, and calculate build. @@ -107,8 +129,8 @@ function decodeBuild(url_tag) { info[1] = info[1].slice(7); } - for (let i in powderInputs) { - setValue(powderInputs[i], powdering[i]); + for (let i in powder_inputs) { + setValue(powder_inputs[i], powdering[i]); } } } diff --git a/js/builder.js b/js/builder.js index 249bf32..fcdf850 100644 --- a/js/builder.js +++ b/js/builder.js @@ -12,29 +12,6 @@ function getTomeNameFromID(id) { return tomeIDMap.get(id); } -function parsePowdering(powder_info) { - // TODO: Make this run in linear instead of quadratic time... ew - let powdering = []; - for (let i = 0; i < 5; ++i) { - let powders = ""; - let n_blocks = Base64.toInt(powder_info.charAt(0)); - // console.log(n_blocks + " blocks"); - powder_info = powder_info.slice(1); - for (let j = 0; j < n_blocks; ++j) { - let block = powder_info.slice(0,5); - console.log(block); - let six_powders = Base64.toInt(block); - for (let k = 0; k < 6 && six_powders != 0; ++k) { - powders += powderNames.get((six_powders & 0x1f) - 1); - six_powders >>>= 5; - } - powder_info = powder_info.slice(5); - } - powdering[i] = powders; - } - return [powdering, powder_info]; -} - function populateBuildList() { const buildList = document.getElementById("build-choice"); const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); @@ -139,6 +116,21 @@ function toggle_tab(tab) { } } + +let tabs = ['overall-stats', 'offensive-stats', 'defensive-stats']; +function show_tab(tab) { + //console.log(itemFilters) + + //hide all tabs, then show the tab of the div clicked and highlight the correct button + for (const i in tabs) { + document.querySelector("#" + tabs[i]).style.display = "none"; + document.getElementById("tab-" + tabs[i].split("-")[0] + "-btn").classList.remove("selected-btn"); + } + document.querySelector("#" + tab).style.display = ""; + document.getElementById("tab-" + tab.split("-")[0] + "-btn").classList.add("selected-btn"); +} + + // TODO: Learn and use await function init() { console.log("builder.js init"); diff --git a/js/builder_graph.js b/js/builder_graph.js index 111cd92..c1d80d4 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -42,14 +42,71 @@ class BuildAssembleNode extends ComputeNode { ]; let weapon = input_map.get('weapon-input'); let level = input_map.get('level-input'); - console.log('build node run'); + + let all_none = weapon.statMap.has('NONE'); + for (const item of equipments) { + all_none = all_none && item.statMap.has('NONE'); + } + if (all_none) { + return null; + } return new Build(level, equipments, [], weapon); } } +class PowderInputNode extends InputNode { + + constructor(name, input_field) { + super(name, input_field); + } + + compute_func(input_map) { + // TODO: haha improve efficiency to O(n) dumb + // also, error handling is missing + let input = this.input_field.value.trim(); + let powdering = []; + let errorederrors = []; + while (input) { + let first = input.slice(0, 2); + let powder = powderIDs.get(first); + if (powder === undefined) { + return null; + } else { + powdering.push(powder); + } + input = input.slice(2); + } + //console.log("POWDERING: " + powdering); + return powdering; + } +} + +class SpellDamageCalcNode extends ComputeNode { + constructor(spell_num) { + super("builder-spell"+spell_num+"-calc"); + this.spell_idx = spell_num; + } + + compute_func(input_map) { + // inputs: + let weapon = new Map(input_map.get('weapon-input').statMap); + let build = input_map.get('build'); + let weapon_powder = input_map.get('weapon-powder'); + weapon.set("powders", weapon_powder); + const i = this.spell_idx; + let spell = spell_table[weapon.get("type")][i]; + let parent_elem = document.getElementById("spell"+i+"-info"); + let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); + displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, i+1, weapon); + } +} + let item_nodes = []; +let powder_nodes = []; +let spell_nodes = []; document.addEventListener('DOMContentLoaded', function() { + // Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). for (const [eq, none_item] of zip(equipment_fields, none_items)) { let input_field = document.getElementById(eq+"-choice"); let item_image = document.getElementById(eq+"-img"); @@ -57,28 +114,70 @@ document.addEventListener('DOMContentLoaded', function() { let item_input = new ItemInputNode(eq+'-input', input_field, none_item); item_nodes.push(item_input); new ItemInputDisplayNode(eq+'-display', input_field, item_image).link_to(item_input); - new PrintNode(eq+'-debug').link_to(item_input); + //new PrintNode(eq+'-debug').link_to(item_input); //document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); - } + + // weapon image changer node. let weapon_image = document.getElementById("weapon-img"); new WeaponDisplayNode('weapon-type', weapon_image).link_to(item_nodes[8]); - let level_input = new InputNode('level-input', document.getElementById('level-choice')); - new PrintNode('lvl-debug').link_to(level_input); + // Level input node. + let level_input = new InputNode('level-input', document.getElementById('level-choice')); + + // "Build" now only refers to equipment and level (no powders). Powders are injected before damage calculation / stat display. let build_node = new BuildAssembleNode(); for (const input of item_nodes) { build_node.link_to(input); } build_node.link_to(level_input); + + + for (const input of powder_inputs) { + powder_nodes.push(new PowderInputNode(input, document.getElementById(input))); + } + + for (let i = 0; i < 4; ++i) { + let spell_node = new SpellDamageCalcNode(i); + spell_node.link_to(item_nodes[8], 'weapon-input'); + spell_node.link_to(build_node, 'build'); + spell_node.link_to(powder_nodes[4], 'weapon-powder'); + spell_nodes.push(spell_node); + } + console.log("Set up graph"); + let masonry = Macy({ + container: "#masonry-container", + columns: 1, + mobileFirst: true, + breakAt: { + 1200: 4, + }, + margin: { + x: 20, + y: 20, + } + + }); + + let search_masonry = Macy({ + container: "#search-results", + columns: 1, + mobileFirst: true, + breakAt: { + 1200: 4, + }, + margin: { + x: 20, + y: 20, + } + + }); }); // autocomplete initialize function init_autocomplete() { - console.log("autocomplete init"); - console.log(itemLists) let dropdowns = new Map(); for (const eq of equipment_keys) { if (tome_keys.includes(eq)) { diff --git a/js/computation_graph.js b/js/computation_graph.js index 155d95a..45ba267 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -7,13 +7,14 @@ class ComputeNode { */ constructor(name) { this.inputs = []; // parent nodes + this.input_translation = new Map(); this.children = []; - this.value = 0; + this.value = null; this.name = name; this.update_task = null; this.update_time = Date.now(); this.fail_cb = false; // Set to true to force updates even if parent failed. - this.dirty = false; + this.dirty = true; this.inputs_dirty = new Map(); this.inputs_dirty_count = 0; } @@ -32,7 +33,7 @@ class ComputeNode { } let calc_inputs = new Map(); for (const input of this.inputs) { - calc_inputs.set(input.name, input.value); + calc_inputs.set(this.input_translation.get(input.name), input.value); } this.value = this.compute_func(calc_inputs); this.dirty = false; @@ -41,18 +42,6 @@ class ComputeNode { } } - /** - * Set this node's value directly. Notifies children. - */ - set_value(value) { - let timestamp = Date.now(); - this.update_time = timestamp; - this.value = value; - for (const child of this.children) { - child.set_input(this.name, this.value, timestamp); - } - } - /** * Mark parent as not dirty. Propagates calculation if all inputs are present. */ @@ -99,8 +88,10 @@ class ComputeNode { throw "no compute func specified"; } - link_to(parent_node) { + link_to(parent_node, link_name) { this.inputs.push(parent_node) + link_name = (link_name !== undefined) ? link_name : parent_node.name; + this.input_translation.set(parent_node.name, link_name); this.inputs_dirty.set(parent_node.name, parent_node.dirty); if (parent_node.dirty) { this.inputs_dirty_count += 1; @@ -147,6 +138,7 @@ class InputNode extends ComputeNode { super(name); this.input_field = input_field; this.input_field.addEventListener("input", () => calcSchedule(this)); + calcSchedule(this); } compute_func(input_map) { @@ -168,6 +160,7 @@ class ItemInputNode extends InputNode { constructor(name, item_input_field, none_item) { super(name, item_input_field); this.none_item = new Item(none_item); + this.none_item.statMap.set('NONE', true); } compute_func(input_map) { @@ -231,9 +224,13 @@ class ItemInputDisplayNode extends ComputeNode { return null; } + if (item.statMap.has('NONE')) { + return null; + } const tier = item.statMap.get('tier'); this.input_field.classList.add(tier); this.image.classList.add(tier + "-shadow"); + return null; } } diff --git a/js/damage_calc.js b/js/damage_calc.js index 564ab2e..6aa7c64 100644 --- a/js/damage_calc.js +++ b/js/damage_calc.js @@ -1,29 +1,12 @@ const damageMultipliers = new Map([ ["allytotem", .15], ["yourtotem", .35], ["vanish", 0.80], ["warscream", 0.10], ["bash", 0.50] ]); // Calculate spell damage given a spell elemental conversion table, and a spell multiplier. // If spell mult is 0, its melee damage and we don't multiply by attack speed. -// externalStats should be a map -function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, spellMultiplier, weapon, total_skillpoints, damageMultiplier, externalStats) { +function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, spellMultiplier, weapon, total_skillpoints, damageMultiplier) { let buildStats = new Map(stats); let tooltipinfo = new Map(); //6x for damages, normal min normal max crit min crit max let damageformulas = [["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "]]; - if(externalStats) { //if nothing is passed in, then this hopefully won't trigger - for (const entry of externalStats) { - const key = entry[0]; - const value = entry[1]; - if (typeof value === "number") { - buildStats.set(key, buildStats.get(key) + value); - } else if (Array.isArray(value)) { - arr = []; - for (let j = 0; j < value.length; j++) { - arr[j] = buildStats.get(key)[j] + value[j]; - } - buildStats.set(key, arr); - } - } - } - let powders = weapon.get("powders").slice(); // Array of neutral + ewtfa damages. Each entry is a pair (min, max). diff --git a/js/load.js b/js/load.js index 4f6e4b2..521765e 100644 --- a/js/load.js +++ b/js/load.js @@ -6,7 +6,7 @@ let reload = false; let load_complete = false; let load_in_progress = false; let items; -let sets; +let sets = new Map(); let itemMap; let idMap; let redirectMap; @@ -20,7 +20,6 @@ async function load_local() { let sets_store = get_tx.objectStore('set_db'); let get_store = get_tx.objectStore('item_db'); let request = get_store.getAll(); - let request2 = sets_store.getAll(); request.onerror = function(event) { reject("Could not read local item db..."); } @@ -28,15 +27,27 @@ async function load_local() { console.log("Successfully read local item db."); } + // key-value iteration (hpp don't break this again) + // https://stackoverflow.com/questions/47931595/indexeddb-getting-all-data-with-keys + let request2 = sets_store.openCursor(); request2.onerror = function(event) { reject("Could not read local set db..."); } request2.onsuccess = function(event) { - console.log("Successfully read local set db."); - } + let cursor = event.target.result; + if (cursor) { + let key = cursor.primaryKey; + let value = cursor.value; + sets.set(key, value); + cursor.continue(); + } + else { + // no more results + console.log("Successfully read local set db."); + } + }; get_tx.oncomplete = function(event) { items = request.result; - sets = request2.result; init_maps(); load_complete = true; db.close(); diff --git a/js/skillpoints.js b/js/skillpoints.js index 12c8805..fcf24b6 100644 --- a/js/skillpoints.js +++ b/js/skillpoints.js @@ -31,14 +31,15 @@ function calculate_skillpoints(equipment, weapon) { let setCount = activeSetCounts.get(setName); let old_bonus = {}; if (setCount) { - old_bonus = sets[setName].bonuses[setCount-1]; + old_bonus = sets.get(setName).bonuses[setCount-1]; activeSetCounts.set(setName, setCount + 1); } else { setCount = 0; activeSetCounts.set(setName, 1); } - const new_bonus = sets[setName].bonuses[setCount]; + console.log(sets); + 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); @@ -74,8 +75,8 @@ function calculate_skillpoints(equipment, weapon) { if (setName) { // undefined/null means no set. const setCount = activeSetCounts.get(setName); if (setCount) { - const old_bonus = sets[setName].bonuses[setCount-1]; - const new_bonus = sets[setName].bonuses[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); diff --git a/js/sq2display.js b/js/sq2display.js index b6b588b..195aea5 100644 --- a/js/sq2display.js +++ b/js/sq2display.js @@ -41,7 +41,7 @@ function displaysq2BuildStats(parent_id,build,command_group){ // id instruction else { let id = command; - if (stats.get(id) || build.externalStats.get(id)) { + if (stats.get(id)) { let style = null; // TODO: add pos and neg style @@ -54,10 +54,6 @@ function displaysq2BuildStats(parent_id,build,command_group){ // ignore let id_val = stats.get(id); - if (build.externalStats.has(id)) { - id_val += build.externalStats.get(id); - } - if (reversedIDs.includes(id)) { style === "positive" ? style = "negative" : style = "positive"; } @@ -650,7 +646,7 @@ function displaysq2PoisonDamage(overallparent_elem, build) { let overallpoisonDamage = document.createElement("p"); let overallpoisonDamageFirst = document.createElement("span"); let overallpoisonDamageSecond = document.createElement("span"); - let poison_tick = Math.ceil(build.statMap.get("poison") * (1+skillPointsToPercentage(build.total_skillpoints[0])) * (build.statMap.get("poisonPct") + build.externalStats.get("poisonPct"))/100 /3); + let poison_tick = Math.ceil(build.statMap.get("poison") * (1+skillPointsToPercentage(build.total_skillpoints[0])) * (build.statMap.get("poisonPct"))/100 /3); overallpoisonDamageFirst.textContent = "Poison Tick: "; overallpoisonDamageSecond.textContent = Math.max(poison_tick,0); overallpoisonDamageSecond.classList.add("Damage"); @@ -1126,8 +1122,8 @@ function displaysq2PowderSpecials(parent_elem, powderSpecials, build, overall=fa let spell = (powderSpecialStats.indexOf(special[0]) == 3 ? spells[2] : spells[powderSpecialStats.indexOf(special[0])]); let part = spell["parts"][0]; let _results = calculateSpellDamage(stats, part.conversion, - stats.get("mdRaw"), stats.get("mdPct") + build.externalStats.get("mdPct"), - 0, build.weapon, build.total_skillpoints, build.damageMultiplier * ((part.multiplier[power-1] / 100)), build.externalStats);//part.multiplier[power] / 100 + stats.get("mdRaw"), stats.get("mdPct"), + 0, build.weapon, build.total_skillpoints, build.damageMultiplier * ((part.multiplier[power-1] / 100)));//part.multiplier[power] / 100 let critChance = skillPointsToPercentage(build.total_skillpoints[1]); let save_damages = []; @@ -1215,7 +1211,7 @@ function displaysq2PowderSpecials(parent_elem, powderSpecials, build, overall=fa } } -function displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, spellIdx) { +function displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, spellIdx, weapon) { parent_elem.textContent = ""; @@ -1264,7 +1260,7 @@ function displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, sp parent_elem.append(title_elem); overallparent_elem.append(title_elemavg); - overallparent_elem.append(displaysq2NextCosts(spell, build)); + overallparent_elem.append(displaysq2NextCosts(spell, build, weapon)); let critChance = skillPointsToPercentage(build.total_skillpoints[1]); @@ -1300,8 +1296,8 @@ function displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, sp if (part.type === "damage") { //console.log(build.expandedStats); let _results = calculateSpellDamage(stats, part.conversion, - stats.get("sdRaw") + stats.get("rainbowRaw"), stats.get("sdPct") + build.externalStats.get("sdPct"), - part.multiplier / 100, build.weapon, build.total_skillpoints, build.damageMultiplier, build.externalStats); + stats.get("sdRaw") + stats.get("rainbowRaw"), stats.get("sdPct"), + part.multiplier / 100, weapon, build.total_skillpoints, build.damageMultiplier); let totalDamNormal = _results[0]; let totalDamCrit = _results[1]; let results = _results[2]; @@ -1427,9 +1423,9 @@ function displaysq2EquipOrder(parent_elem, buildOrder){ } } -function displaysq2NextCosts(spell, build) { +function displaysq2NextCosts(spell, build, weapon) { let int = build.total_skillpoints[2]; - let spells = spell_table[build.weapon.get("type")]; + let spells = spell_table[weapon.get("type")]; let row = document.createElement("div"); row.classList.add("spellcost-tooltip"); From e5b653619f29f818ed2be314cbb11f7767c2227b Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 11:02:28 -0700 Subject: [PATCH 26/68] Working spell damage calculation pipeline broke like everything else --- builder/index.html | 1 - js/build_utils.js | 65 +++++++++++ js/builder_graph.js | 98 ++++++++++++++-- js/damage_calc.js | 29 +---- js/display.js | 278 +++++++++++--------------------------------- 5 files changed, 219 insertions(+), 252 deletions(-) diff --git a/builder/index.html b/builder/index.html index d9c3e8f..eaa3bc2 100644 --- a/builder/index.html +++ b/builder/index.html @@ -1390,7 +1390,6 @@ - diff --git a/js/build_utils.js b/js/build_utils.js index 9381035..4da3309 100644 --- a/js/build_utils.js +++ b/js/build_utils.js @@ -196,3 +196,68 @@ class Item { this.statMap = expandItem(item_obj); } } + +/* Takes in an ingredient object and returns an equivalent Map(). +*/ +function expandIngredient(ing) { + let expandedIng = new Map(); + let mapIds = ['consumableIDs', 'itemIDs', 'posMods']; + for (const id of mapIds) { + let idMap = new Map(); + for (const key of Object.keys(ing[id])) { + idMap.set(key, ing[id][key]); + } + expandedIng.set(id, idMap); + } + let normIds = ['lvl','name', 'displayName','tier','skills','id']; + for (const id of normIds) { + expandedIng.set(id, ing[id]); + } + if (ing['isPowder']) { + expandedIng.set("isPowder",ing['isPowder']); + expandedIng.set("pid",ing['pid']); + } + //now the actually hard one + let idMap = new Map(); + idMap.set("minRolls", new Map()); + idMap.set("maxRolls", new Map()); + for (const field of ingFields) { + let val = (ing['ids'][field] || 0); + idMap.get("minRolls").set(field, val['minimum']); + idMap.get("maxRolls").set(field, val['maximum']); + } + expandedIng.set("ids",idMap); + return expandedIng; +} + +/* Takes in a recipe object and returns an equivalent Map(). +*/ +function expandRecipe(recipe) { + let expandedRecipe = new Map(); + let normIDs = ["name", "skill", "type","id"]; + for (const id of normIDs) { + expandedRecipe.set(id,recipe[id]); + } + let rangeIDs = ["durability","lvl", "healthOrDamage", "duration", "basicDuration"]; + for (const id of rangeIDs) { + if(recipe[id]){ + expandedRecipe.set(id, [recipe[id]['minimum'], recipe[id]['maximum']]); + } else { + expandedRecipe.set(id, [0,0]); + } + } + expandedRecipe.set("materials", [ new Map([ ["item", recipe['materials'][0]['item']], ["amount", recipe['materials'][0]['amount']] ]) , new Map([ ["item", recipe['materials'][1]['item']], ["amount",recipe['materials'][1]['amount'] ] ]) ]); + return expandedRecipe; +} + +/*An independent helper function that rounds a rolled ID to the nearest integer OR brings the roll away from 0. +* @param id +*/ +function idRound(id){ + rounded = Math.round(id); + if(rounded == 0){ + return 1; //this is a hack, will need changing along w/ rest of ID system if anything changes + }else{ + return rounded; + } +} diff --git a/js/builder_graph.js b/js/builder_graph.js index c1d80d4..d4a8eb5 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -81,6 +81,36 @@ class PowderInputNode extends InputNode { } } +class SpellSelectNode extends ComputeNode { + constructor(spell_num) { + super("builder-spell"+spell_num+"-select"); + this.spell_idx = spell_num; + } + + compute_func(input_map) { + const build = input_map.get('build'); + + const i = this.spell_idx; + let spell = spell_table[build.weapon.statMap.get("type")][i]; + let stats = build.statMap; + + let spell_parts; + if (spell.parts) { + spell_parts = spell.parts; + } + else { + spell_parts = spell.variants.DEFAULT; + for (const majorID of stats.get("activeMajorIDs")) { + if (majorID in spell.variants) { + spell_parts = spell.variants[majorID]; + break; + } + } + } + return [spell, spell_parts]; + } +} + class SpellDamageCalcNode extends ComputeNode { constructor(spell_num) { super("builder-spell"+spell_num+"-calc"); @@ -88,22 +118,59 @@ class SpellDamageCalcNode extends ComputeNode { } compute_func(input_map) { - // inputs: - let weapon = new Map(input_map.get('weapon-input').statMap); - let build = input_map.get('build'); - let weapon_powder = input_map.get('weapon-powder'); + const weapon = new Map(input_map.get('weapon-input').statMap); + const build = input_map.get('build'); + const weapon_powder = input_map.get('weapon-powder'); + const damage_mult = 1; // TODO: hook up + const spell_info = input_map.get('spell-info'); + const spell_parts = spell_info[1]; + weapon.set("powders", weapon_powder); + let spell_results = [] + let stats = build.statMap; + + for (const part of spell_parts) { + if (part.type === "damage") { + let results = calculateSpellDamage(stats, part.conversion, + stats.get("sdRaw") + stats.get("rainbowRaw"), stats.get("sdPct"), + part.multiplier / 100, weapon, build.total_skillpoints, damage_mult); + spell_results.push(results); + } else if (part.type === "heal") { + // TODO: wynn2 formula + let heal_amount = (part.strength * build.getDefenseStats()[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); + spell_results.push(heal_amount); + } else if (part.type === "total") { + // TODO: remove "total" type + spell_results.push(null); + } + } + return spell_results; + } +} + +class SpellDisplayNode extends ComputeNode { + constructor(spell_num) { + super("builder-spell"+spell_num+"-display"); + this.spell_idx = spell_num; + } + + compute_func(input_map) { + const build = input_map.get('build'); + const spell_info = input_map.get('spell-info'); + const damages = input_map.get('spell-damage'); + const spell = spell_info[0]; + const spell_parts = spell_info[1]; + const i = this.spell_idx; - let spell = spell_table[weapon.get("type")][i]; let parent_elem = document.getElementById("spell"+i+"-info"); let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); - displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, i+1, weapon); + displaySpellDamage(parent_elem, overallparent_elem, build, spell, i+1, spell_parts, damages); } } let item_nodes = []; let powder_nodes = []; -let spell_nodes = []; +let spelldmg_nodes = []; document.addEventListener('DOMContentLoaded', function() { // Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). @@ -138,11 +205,20 @@ document.addEventListener('DOMContentLoaded', function() { } for (let i = 0; i < 4; ++i) { - let spell_node = new SpellDamageCalcNode(i); - spell_node.link_to(item_nodes[8], 'weapon-input'); + let spell_node = new SpellSelectNode(i); spell_node.link_to(build_node, 'build'); - spell_node.link_to(powder_nodes[4], 'weapon-powder'); - spell_nodes.push(spell_node); + + let calc_node = new SpellDamageCalcNode(i); + calc_node.link_to(item_nodes[8], 'weapon-input'); + calc_node.link_to(build_node, 'build'); + calc_node.link_to(powder_nodes[4], 'weapon-powder'); + calc_node.link_to(spell_node, 'spell-info'); + spelldmg_nodes.push(calc_node); + + let display_node = new SpellDisplayNode(i); + display_node.link_to(build_node, 'build'); + display_node.link_to(spell_node, 'spell-info'); + display_node.link_to(calc_node, 'spell-damage'); } console.log("Set up graph"); diff --git a/js/damage_calc.js b/js/damage_calc.js index 6aa7c64..9149a13 100644 --- a/js/damage_calc.js +++ b/js/damage_calc.js @@ -3,9 +3,7 @@ const damageMultipliers = new Map([ ["allytotem", .15], ["yourtotem", .35], ["va // If spell mult is 0, its melee damage and we don't multiply by attack speed. function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, spellMultiplier, weapon, total_skillpoints, damageMultiplier) { let buildStats = new Map(stats); - let tooltipinfo = new Map(); //6x for damages, normal min normal max crit min crit max - let damageformulas = [["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "]]; let powders = weapon.get("powders").slice(); @@ -75,28 +73,22 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, } - - //console.log(tooltipinfo); damages[0] = neutralRemainingRaw; - tooltipinfo.set("damageBases", damages); let damageMult = damageMultiplier; let melee = false; // If we are doing melee calculations: - tooltipinfo.set("dmgMult", damageMult); if (spellMultiplier == 0) { spellMultiplier = 1; melee = true; } else { - tooltipinfo.set("dmgMult", `(${tooltipinfo.get("dmgMult")} * ${spellMultiplier} * ${baseDamageMultiplier[attackSpeeds.indexOf(buildStats.get("atkSpd"))]})`) damageMult *= spellMultiplier * baseDamageMultiplier[attackSpeeds.indexOf(buildStats.get("atkSpd"))]; } //console.log(damages); //console.log(damageMult); - tooltipinfo.set("rawModifier", `(${rawModifier} * ${spellMultiplier} * ${damageMultiplier})`); rawModifier *= spellMultiplier * damageMultiplier; let totalDamNorm = [0, 0]; let totalDamCrit = [0, 0]; @@ -109,33 +101,21 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, let baseDamCrit = rawModifier * (1 + strBoost); totalDamNorm = [baseDam, baseDam]; totalDamCrit = [baseDamCrit, baseDamCrit]; - for (let arr of damageformulas) { - arr = arr.map(x => x + " + " +tooltipinfo.get("rawModifier")); - } } let staticBoost = (pctModifier / 100.); - tooltipinfo.set("staticBoost", `${(pctModifier/ 100.).toFixed(2)}`); - tooltipinfo.set("skillBoost",["","","","","",""]); let skillBoost = [0]; for (let i in total_skillpoints) { skillBoost.push(skillPointsToPercentage(total_skillpoints[i]) + buildStats.get("damageBonus")[i] / 100.); - tooltipinfo.get("skillBoost")[parseInt(i,10)+1] = `(${skillPointsToPercentage(total_skillpoints[i]).toFixed(2)} + ${(buildStats.get("damageBonus")[i]/100.).toFixed(2)})` } - tooltipinfo.get("skillBoost")[0] = undefined; for (let i in damages) { let damageBoost = 1 + skillBoost[i] + staticBoost; - tooltipinfo.set("damageBoost", `(1 + ${(tooltipinfo.get("skillBoost")[i] ? tooltipinfo.get("skillBoost")[i] + " + " : "")} ${tooltipinfo.get("staticBoost")})`) damages_results.push([ Math.max(damages[i][0] * strBoost * Math.max(damageBoost,0) * damageMult, 0), // Normal min Math.max(damages[i][1] * strBoost * Math.max(damageBoost,0) * damageMult, 0), // Normal max Math.max(damages[i][0] * (strBoost + 1) * Math.max(damageBoost,0) * damageMult, 0), // Crit min Math.max(damages[i][1] * (strBoost + 1) * Math.max(damageBoost,0) * damageMult, 0), // Crit max ]); - damageformulas[i][0] += `(max((${tooltipinfo.get("damageBases")[i][0]} * ${strBoost} * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))` - damageformulas[i][1] += `(max((${tooltipinfo.get("damageBases")[i][1]} * ${strBoost} * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))` - damageformulas[i][2] += `(max((${tooltipinfo.get("damageBases")[i][0]} * ${strBoost} * 2 * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))` - damageformulas[i][3] += `(max((${tooltipinfo.get("damageBases")[i][1]} * ${strBoost} * 2 * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))` totalDamNorm[0] += damages_results[i][0]; totalDamNorm[1] += damages_results[i][1]; totalDamCrit[0] += damages_results[i][2]; @@ -151,20 +131,13 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, damages_results[0][1] += strBoost*rawModifier; damages_results[0][2] += (strBoost + 1)*rawModifier; damages_results[0][3] += (strBoost + 1)*rawModifier; - for (let i = 0; i < 2; i++) { - damageformulas[0][i] += ` + (${strBoost} * ${tooltipinfo.get("rawModifier")})` - } - for (let i = 2; i < 4; i++) { - damageformulas[0][i] += ` + (2 * ${strBoost} * ${tooltipinfo.get("rawModifier")})` - } if (totalDamNorm[0] < 0) totalDamNorm[0] = 0; if (totalDamNorm[1] < 0) totalDamNorm[1] = 0; if (totalDamCrit[0] < 0) totalDamCrit[0] = 0; if (totalDamCrit[1] < 0) totalDamCrit[1] = 0; - tooltipinfo.set("damageformulas", damageformulas); - return [totalDamNorm, totalDamCrit, damages_results, tooltipinfo]; + return [totalDamNorm, totalDamCrit, damages_results]; } diff --git a/js/display.js b/js/display.js index 707efe3..c2ca48b 100644 --- a/js/display.js +++ b/js/display.js @@ -27,72 +27,6 @@ function applyArmorPowdersOnce(expandedItem, powders) { } } - -/* Takes in an ingredient object and returns an equivalent Map(). -*/ -function expandIngredient(ing) { - let expandedIng = new Map(); - let mapIds = ['consumableIDs', 'itemIDs', 'posMods']; - for (const id of mapIds) { - let idMap = new Map(); - for (const key of Object.keys(ing[id])) { - idMap.set(key, ing[id][key]); - } - expandedIng.set(id, idMap); - } - let normIds = ['lvl','name', 'displayName','tier','skills','id']; - for (const id of normIds) { - expandedIng.set(id, ing[id]); - } - if (ing['isPowder']) { - expandedIng.set("isPowder",ing['isPowder']); - expandedIng.set("pid",ing['pid']); - } - //now the actually hard one - let idMap = new Map(); - idMap.set("minRolls", new Map()); - idMap.set("maxRolls", new Map()); - for (const field of ingFields) { - let val = (ing['ids'][field] || 0); - idMap.get("minRolls").set(field, val['minimum']); - idMap.get("maxRolls").set(field, val['maximum']); - } - expandedIng.set("ids",idMap); - return expandedIng; -} - -/* Takes in a recipe object and returns an equivalent Map(). -*/ -function expandRecipe(recipe) { - let expandedRecipe = new Map(); - let normIDs = ["name", "skill", "type","id"]; - for (const id of normIDs) { - expandedRecipe.set(id,recipe[id]); - } - let rangeIDs = ["durability","lvl", "healthOrDamage", "duration", "basicDuration"]; - for (const id of rangeIDs) { - if(recipe[id]){ - expandedRecipe.set(id, [recipe[id]['minimum'], recipe[id]['maximum']]); - } else { - expandedRecipe.set(id, [0,0]); - } - } - expandedRecipe.set("materials", [ new Map([ ["item", recipe['materials'][0]['item']], ["amount", recipe['materials'][0]['amount']] ]) , new Map([ ["item", recipe['materials'][1]['item']], ["amount",recipe['materials'][1]['amount'] ] ]) ]); - return expandedRecipe; -} - -/*An independent helper function that rounds a rolled ID to the nearest integer OR brings the roll away from 0. -* @param id -*/ -function idRound(id){ - rounded = Math.round(id); - if(rounded == 0){ - return 1; //this is a hack, will need changing along w/ rest of ID system if anything changes - }else{ - return rounded; - } -} - function apply_elemental_format(p_elem, id, suffix) { suffix = (typeof suffix !== 'undefined') ? suffix : ""; // THIS IS SO JANK BUT IM TOO LAZY TO FIX IT TODO @@ -998,78 +932,54 @@ function displayExpandedIngredient(ingred, parent_id) { } } -function displayNextCosts(parent_id, build) { - let p_elem = document.getElementById(parent_id); +function displayNextCosts(spell, build, weapon) { let int = build.total_skillpoints[2]; - let spells = spell_table[build.weapon.get("type")]; + let spells = spell_table[weapon.get("type")]; - p_elem.textContent = ""; - - let title = document.createElement("p"); - title.classList.add("title"); - title.classList.add("Normal"); - title.textContent = "Next Spell Costs"; - - let int_title = document.createElement("p"); - int_title.classList.add("itemp"); - int_title.textContent = int + " Intelligence points."; - - p_elem.append(title); - p_elem.append(int_title); - - for (const spell of spells) { - let spellp = document.createElement("p"); - let spelltitle = document.createElement("p"); - spelltitle.classList.add("itemp"); - spelltitle.textContent = spell.title; - spellp.appendChild(spelltitle); - let row = document.createElement("p"); - row.classList.add("itemp"); - let init_cost = document.createElement("b"); - init_cost.textContent = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost); - init_cost.classList.add("Mana"); - let arrow = document.createElement("b"); - arrow.textContent = "\u279C"; - let next_cost = document.createElement("b"); - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1); - next_cost.classList.add("Mana"); - let int_needed = document.createElement("b"); - if (init_cost.textContent === "1") { - int_needed.textContent = ": n/a (+0)"; - }else { //do math - let target = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1; - let needed = int; - let noUpdate = false; - //forgive me... I couldn't inverse ceil, floor, and max. - while (build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) > target) { - if(needed > 150) { - noUpdate = true; - break; - } - needed++; - build.total_skillpoints[2] = needed; + let row = document.createElement("div"); + row.classList.add("spellcost-tooltip"); + let init_cost = document.createElement("b"); + init_cost.textContent = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost); + init_cost.classList.add("Mana"); + let arrow = document.createElement("b"); + arrow.textContent = "\u279C"; + let next_cost = document.createElement("b"); + next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1); + next_cost.classList.add("Mana"); + let int_needed = document.createElement("b"); + if (init_cost.textContent === "1") { + int_needed.textContent = ": n/a (+0)"; + }else { //do math + let target = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1; + let needed = int; + let noUpdate = false; + //forgive me... I couldn't inverse ceil, floor, and max. + while (build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) > target) { + if(needed > 150) { + noUpdate = true; + break; } - let missing = needed - int; - //in rare circumstances, the next spell cost can jump. - if (noUpdate) { - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)-1); - }else { - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)); - } - - - build.total_skillpoints[2] = int;//forgive me pt 2 - int_needed.textContent = ": " + (needed > 150 ? ">150" : needed) + " int (+" + (needed > 150 ? "n/a" : missing) + ")"; + needed++; + build.total_skillpoints[2] = needed; + } + let missing = needed - int; + //in rare circumstances, the next spell cost can jump. + if (noUpdate) { + next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)-1); + }else { + next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)); } - row.appendChild(init_cost); - row.appendChild(arrow); - row.appendChild(next_cost); - row.appendChild(int_needed); - spellp.appendChild(row); - - p_elem.append(spellp); + + build.total_skillpoints[2] = int;//forgive me pt 2 + int_needed.textContent = ": " + (needed > 150 ? ">150" : needed) + " int (+" + (needed > 150 ? "n/a" : missing) + ")"; } + + // row.appendChild(init_cost); + row.appendChild(arrow); + row.appendChild(next_cost); + row.appendChild(int_needed); + return row; } function displayRolledID(item, id, elemental_format) { @@ -1733,31 +1643,26 @@ function displayPowderSpecials(parent_elem, powderSpecials, build) { } } -function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spellIdx) { +function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spellIdx, spell_parts, damages) { + // TODO: remove spellIdx (just used to flag melee and cost) + // TODO: move cost calc out parent_elem.textContent = ""; - - let tooltip; let tooltiptext; const stats = build.statMap; let title_elem = document.createElement("p"); - title_elem.classList.add("smalltitle"); - title_elem.classList.add("Normal"); overallparent_elem.textContent = ""; - let title_elemavg = document.createElement("p"); - title_elemavg.classList.add('smalltitle'); - title_elemavg.classList.add('Normal'); + let title_elemavg = document.createElement("b"); if (spellIdx != 0) { - let first = document.createElement("b"); + let first = document.createElement("span"); first.textContent = spell.title + " ("; title_elem.appendChild(first.cloneNode(true)); //cloneNode is needed here. title_elemavg.appendChild(first); - let second = document.createElement("b"); + let second = document.createElement("span"); second.textContent = build.getSpellCost(spellIdx, spell.cost); second.classList.add("Mana"); - second.classList.add("tooltip"); let int_redux = skillPointsToPercentage(build.total_skillpoints[2]).toFixed(2); let spPct_redux = (build.statMap.get("spPct" + spellIdx)/100).toFixed(2); @@ -1765,17 +1670,14 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell spPct_redux >= 0 ? spPct_redux = "+ " + spPct_redux : spPct_redux = "- " + Math.abs(spPct_redux); spRaw_redux >= 0 ? spRaw_redux = "+ " + spRaw_redux : spRaw_redux = "- " + Math.abs(spRaw_redux); - tooltiptext = `= max(1, floor((ceil(${spell.cost} * (1 - ${int_redux})) ${spRaw_redux}) * (1 ${spPct_redux})))`; - tooltip = createTooltip(tooltip, "p", tooltiptext, second, ["spellcostcalc"]); - second.appendChild(tooltip); title_elem.appendChild(second.cloneNode(true)); title_elemavg.appendChild(second); - let third = document.createElement("b"); + let third = document.createElement("span"); third.textContent = ") [Base: " + build.getBaseSpellCost(spellIdx, spell.cost) + " ]"; title_elem.appendChild(third); - let third_summary = document.createElement("b"); + let third_summary = document.createElement("span"); third_summary.textContent = ")"; title_elemavg.appendChild(third_summary); } @@ -1787,48 +1689,33 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell parent_elem.append(title_elem); overallparent_elem.append(title_elemavg); + overallparent_elem.append(displayNextCosts(spell, build, build.weapon.statMap)); + + let critChance = skillPointsToPercentage(build.total_skillpoints[1]); let save_damages = []; let part_divavg = document.createElement("p"); - part_divavg.classList.add("lessbottom"); overallparent_elem.append(part_divavg); - let spell_parts; - if (spell.parts) { - spell_parts = spell.parts; - } - else { - spell_parts = spell.variants.DEFAULT; - for (const majorID of stats.get("activeMajorIDs")) { - if (majorID in spell.variants) { - spell_parts = spell.variants[majorID]; - break; - } - } - } - //console.log(spell_parts); + for (let i = 0; i < spell_parts.length; ++i) { + const part = spell_parts[i]; + const damage = damages[i]; - for (const part of spell_parts) { - parent_elem.append(document.createElement("br")); let part_div = document.createElement("p"); parent_elem.append(part_div); let subtitle_elem = document.createElement("p"); subtitle_elem.textContent = part.subtitle; - subtitle_elem.classList.add("nomargin"); part_div.append(subtitle_elem); if (part.type === "damage") { //console.log(build.expandedStats); - let _results = calculateSpellDamage(stats, part.conversion, - stats.get("sdRaw") + stats.get("rainbowRaw"), stats.get("sdPct") + build.externalStats.get("sdPct"), - part.multiplier / 100, build.weapon, build.total_skillpoints, build.damageMultiplier, build.externalStats); + let _results = damage; let totalDamNormal = _results[0]; let totalDamCrit = _results[1]; let results = _results[2]; - let tooltipinfo = _results[3]; for (let i = 0; i < 6; ++i) { for (let j in results[i]) { @@ -1841,30 +1728,25 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell let averageLabel = document.createElement("p"); averageLabel.textContent = "Average: "+averageDamage.toFixed(2); - tooltiptext = ` = ((1 - ${critChance}) * ${nonCritAverage.toFixed(2)}) + (${critChance} * ${critAverage.toFixed(2)})` - averageLabel.classList.add("damageSubtitle"); - tooltip = createTooltip(tooltip, "p", tooltiptext, averageLabel, ["spell-tooltip"]); + // averageLabel.classList.add("damageSubtitle"); part_div.append(averageLabel); if (part.summary == true) { let overallaverageLabel = document.createElement("p"); - let first = document.createElement("b"); - let second = document.createElement("b"); + let first = document.createElement("span"); + let second = document.createElement("span"); first.textContent = part.subtitle + " Average: "; second.textContent = averageDamage.toFixed(2); overallaverageLabel.appendChild(first); overallaverageLabel.appendChild(second); - tooltip = createTooltip(tooltip, "p", tooltiptext, overallaverageLabel, ["spell-tooltip", "summary-tooltip"]); second.classList.add("Damage"); - overallaverageLabel.classList.add("itemp"); part_divavg.append(overallaverageLabel); } function _damage_display(label_text, average, result_idx) { let label = document.createElement("p"); label.textContent = label_text+average.toFixed(2); - label.classList.add("damageSubtitle"); part_div.append(label); let arrmin = []; @@ -1872,84 +1754,56 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell for (let i = 0; i < 6; i++){ if (results[i][1] != 0){ let p = document.createElement("p"); - p.classList.add("damagep"); p.classList.add(damageClasses[i]); p.textContent = results[i][result_idx] + " \u2013 " + results[i][result_idx + 1]; - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(0,2).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, p, ["spell-tooltip"]); arrmin.push(results[i][result_idx]); arrmax.push(results[i][result_idx + 1]); part_div.append(p); } } - tooltiptext = ` = ((${arrmin.join(" + ")}) + (${arrmax.join(" + ")})) / 2`; - tooltip = createTooltip(tooltip, "p", tooltiptext, label, ["spell-tooltip"]); } _damage_display("Non-Crit Average: ", nonCritAverage, 0); _damage_display("Crit Average: ", critAverage, 2); save_damages.push(averageDamage); } else if (part.type === "heal") { - let heal_amount = (part.strength * build.getDefenseStats()[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); - tooltiptext = ` = ${part.strength} * ${build.getDefenseStats()[0]} * max(0.5, min(1.75, 1 + 0.5 * ${stats.get("wDamPct")/100}))`; + let heal_amount = damage; let healLabel = document.createElement("p"); healLabel.textContent = heal_amount; - healLabel.classList.add("damagep"); - tooltip = createTooltip(tooltip, "p", tooltiptext, healLabel, ["spell-tooltip"]); + // healLabel.classList.add("damagep"); part_div.append(healLabel); if (part.summary == true) { let overallhealLabel = document.createElement("p"); - let first = document.createElement("b"); - let second = document.createElement("b"); + let first = document.createElement("span"); + let second = document.createElement("span"); first.textContent = part.subtitle + ": "; second.textContent = heal_amount; overallhealLabel.appendChild(first); second.classList.add("Set"); overallhealLabel.appendChild(second); - overallhealLabel.classList.add("itemp"); - tooltip = createTooltip(tooltip, "p", tooltiptext, second, ["spell-tooltip"]); part_divavg.append(overallhealLabel); - - let effectiveHealLabel = document.createElement("p"); - first = document.createElement("b"); - second = document.createElement("b"); - let defStats = build.getDefenseStats(); - tooltiptext = ` = ${heal_amount} * ${defStats[1][0].toFixed(2)} / ${defStats[0]}`; - first.textContent = "Effective Heal: "; - second.textContent = (defStats[1][0]*heal_amount/defStats[0]).toFixed(2); - effectiveHealLabel.appendChild(first); - second.classList.add("Set"); - effectiveHealLabel.appendChild(second); - effectiveHealLabel.classList.add("itemp"); - tooltip = createTooltip(tooltip, "p", tooltiptext, second, ["spell-tooltip"]); - part_divavg.append(effectiveHealLabel); } } else if (part.type === "total") { let total_damage = 0; - tooltiptext = ""; for (let i in part.factors) { total_damage += save_damages[i] * part.factors[i]; } let dmgarr = part.factors.slice(); dmgarr = dmgarr.map(x => "(" + x + " * " + save_damages[dmgarr.indexOf(x)].toFixed(2) + ")"); - tooltiptext = " = " + dmgarr.join(" + "); let averageLabel = document.createElement("p"); averageLabel.textContent = "Average: "+total_damage.toFixed(2); averageLabel.classList.add("damageSubtitle"); - tooltip = createTooltip(tooltip, "p", tooltiptext, averageLabel, ["spell-tooltip"]); part_div.append(averageLabel); let overallaverageLabel = document.createElement("p"); - overallaverageLabel.classList.add("damageSubtitle"); - let overallaverageLabelFirst = document.createElement("b"); - let overallaverageLabelSecond = document.createElement("b"); + let overallaverageLabelFirst = document.createElement("span"); + let overallaverageLabelSecond = document.createElement("span"); overallaverageLabelFirst.textContent = "Average: "; overallaverageLabelSecond.textContent = total_damage.toFixed(2); overallaverageLabelSecond.classList.add("Damage"); - tooltip = createTooltip(tooltip, "p", tooltiptext, overallaverageLabel, ["spell-tooltip", "summary-tooltip"]); overallaverageLabel.appendChild(overallaverageLabelFirst); From fd97eb45e05d09b6d34d2a5e509f7a4c674d6e6e Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 11:30:10 -0700 Subject: [PATCH 27/68] Copying code from sq2display.js to display.js --- js/display.js | 591 +++++++++++++++++++++++++------------------------- 1 file changed, 297 insertions(+), 294 deletions(-) diff --git a/js/display.js b/js/display.js index c2ca48b..02104a5 100644 --- a/js/display.js +++ b/js/display.js @@ -1,23 +1,10 @@ /** - * Apply armor powdering. - * Applies twice for crafted items because wynn. - * Also for jeweling for crafted items. - */ -function applyArmorPowders(expandedItem, powders) { - applyArmorPowdersOnce(expandedItem, powders); - // NOTE: armor powder only applies once! - //if (expandedItem.get("crafted")) { - // applyArmorPowdersOnce(expandedItem, powders); - //} -} - -/** - * Apply armor powders once only. + * Apply armor powders. * Encoding shortcut assumes that all powders give +def to one element * and -def to the element "behind" it in cycle ETWFA, which is true * as of now and unlikely to change in the near future. */ -function applyArmorPowdersOnce(expandedItem, powders) { +function applyArmorPowders(expandedItem, powders) { for(const id of powders){ let powder = powderStats[id]; let name = powderNames.get(id).charAt(0); @@ -33,12 +20,12 @@ function apply_elemental_format(p_elem, id, suffix) { let parts = idPrefixes[id].split(/ (.*)/); let element_prefix = parts[0]; let desc = parts[1]; - let i_elem = document.createElement('b'); + let i_elem = document.createElement('span'); i_elem.classList.add(element_prefix); i_elem.textContent = element_prefix; p_elem.appendChild(i_elem); - let i_elem2 = document.createElement('b'); + let i_elem2 = document.createElement('span'); i_elem2.textContent = " " + desc + suffix; p_elem.appendChild(i_elem2); } @@ -90,48 +77,20 @@ function displaySetBonuses(parent_id,build) { } -function displayBuildStats(parent_id,build){ +function displayBuildStats(parent_id,build,command_group){ // Commands to "script" the creation of nice formatting. // #commands create a new element. // !elemental is some janky hack for elemental damage. // normals just display a thing. - let display_commands = build_overall_display_commands; - - // Clear the parent div. - setHTML(parent_id, ""); + let display_commands = command_group; let parent_div = document.getElementById(parent_id); - let title = document.createElement("p"); - title.classList.add("itemcenter"); - title.classList.add("itemp"); - title.classList.add("title"); - title.classList.add("Normal"); - title.textContent = "Overall Build Stats"; - parent_div.append(title); - parent_div.append(document.createElement("br")); - - if (build.activeSetCounts.size > 0) { - let set_summary_elem = document.createElement('p'); - set_summary_elem.classList.add('itemp'); - set_summary_elem.classList.add('left'); - set_summary_elem.textContent = "Set Summary:"; - parent_div.append(set_summary_elem); - for (const [setName, count] of build.activeSetCounts) { - const active_set = sets[setName]; - if (active_set["hidden"]) { continue; } - - let set_elem = document.createElement('p'); - set_elem.classList.add('itemp'); - set_elem.classList.add('left'); - set_elem.textContent = " "+setName+" Set: "+count+"/"+sets[setName].items.length; - set_summary_elem.append(set_elem); - } + // Clear the parent div. + if (parent_div != null) { + setHTML(parent_id, ""); } - displayDefenseStats(parent_div, build, true); - let stats = build.statMap; - //console.log(build.statMap); let active_elem; let elemental_format = false; @@ -140,37 +99,35 @@ function displayBuildStats(parent_id,build){ let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef"]; for (const command of display_commands) { + // style instructions + if (command.charAt(0) === "#") { - if (command === "#cdiv") { - active_elem = document.createElement('div'); - active_elem.classList.add('itemcenter'); + if (command === "#defense-stats") { + displayDefenseStats(parent_div, build, true); } - else if (command === "#ldiv") { - active_elem = document.createElement('div'); - active_elem.classList.add('itemleft'); - } - else if (command === "#table") { - active_elem = document.createElement('table'); - active_elem.classList.add('itemtable'); - } - parent_div.appendChild(active_elem); } - else if (command.charAt(0) === "!") { + if (command.charAt(0) === "!") { // TODO: This is sooo incredibly janky..... if (command === "!elemental") { elemental_format = !elemental_format; } } + + // id instruction else { let id = command; if (stats.get(id)) { let style = null; + + // TODO: add pos and neg style if (!staticIDs.includes(id)) { style = "positive"; if (stats.get(id) < 0) { style = "negative"; } } + + // ignore let id_val = stats.get(id); if (reversedIDs.includes(id)) { style === "positive" ? style = "negative" : style = "positive"; @@ -178,12 +135,14 @@ function displayBuildStats(parent_id,build){ if (id === "poison" && id_val > 0) { id_val = Math.ceil(id_val*build.statMap.get("poisonPct")/100); } - displayFixedID(active_elem, id, id_val, elemental_format, style); + displayFixedID(parent_div, id, id_val, elemental_format, style); if (id === "poison" && id_val > 0) { - let row = document.createElement('tr'); - let value_elem = document.createElement('td'); - value_elem.classList.add('right'); - value_elem.setAttribute("colspan", "2"); + let row = document.createElement('div'); + row.classList.add("row") + let value_elem = document.createElement('div'); + value_elem.classList.add('col'); + value_elem.classList.add('text-end'); + let prefix_elem = document.createElement('b'); prefix_elem.textContent = "\u279C With Strength: "; let number_elem = document.createElement('b'); @@ -192,23 +151,30 @@ function displayBuildStats(parent_id,build){ value_elem.append(prefix_elem); value_elem.append(number_elem); row.appendChild(value_elem); - - active_elem.appendChild(row); - } else if (id === "ls" && id_val != 0) { - let row = document.createElement("tr"); - let title = document.createElement("td"); - title.classList.add("left"); - title.textContent = "Effective Life Steal:" - let value = document.createElement("td"); - let defStats = build.getDefenseStats(); - value.textContent = Math.round(defStats[1][0]*id_val/defStats[0]) + "/3s"; - value.classList.add("right"); - value.classList.add(style); - row.appendChild(title); - row.appendChild(value); - active_elem.appendChild(row); + parent_div.appendChild(row); } - } else if (skp_order.includes(id)) { + else if (id === "ls" && id_val != 0) { + let row = document.createElement('div'); + row.classList.add("row") + let value_elem = document.createElement('div'); + value_elem.classList.add('col'); + value_elem.classList.add('text-end'); + + let prefix_elem = document.createElement('b'); + prefix_elem.textContent = "\u279C Effective LS: "; + + let defStats = build.getDefenseStats(); + let number_elem = document.createElement('b'); + number_elem.classList.add(style); + number_elem.textContent = Math.round(defStats[1][0]*id_val/defStats[0]) + "/3s"; + value_elem.append(prefix_elem); + value_elem.append(number_elem); + row.appendChild(value_elem); + parent_div.appendChild(row); + } + } + // sp thingy (WHY IS THIS HANDLED SEPARATELY TODO + else if (skp_order.includes(id)) { let total_assigned = build.total_skillpoints[skp_order.indexOf(id)]; let base_assigned = build.base_skillpoints[skp_order.indexOf(id)]; let diff = total_assigned - base_assigned; @@ -219,7 +185,7 @@ function displayBuildStats(parent_id,build){ style = "negative"; } if (diff != 0) { - displayFixedID(active_elem, id, diff, false, style); + displayFixedID(parent_div, id, diff, false, style); } } } @@ -276,38 +242,28 @@ function displayExpandedItem(item, parent_id){ } else if (item.get("category") === "armor") { } - let display_commands = item_display_commands; + let display_commands = sq2_item_display_commands; // Clear the parent div. setHTML(parent_id, ""); let parent_div = document.getElementById(parent_id); + parent_div.classList.add("border", "border-2", "border-dark"); - let active_elem; let fix_id = item.has("fixID") && item.get("fixID"); let elemental_format = false; for (let i = 0; i < display_commands.length; i++) { const command = display_commands[i]; - if (command.charAt(0) === "#") { - if (command === "#cdiv") { - active_elem = document.createElement('div'); - active_elem.classList.add('itemcenter'); - } - else if (command === "#ldiv") { - active_elem = document.createElement('div'); - active_elem.classList.add('itemleft'); - } - else if (command === "#table") { - active_elem = document.createElement('table'); - active_elem.classList.add('itemtable'); - } - active_elem.style.maxWidth = "100%"; - parent_div.appendChild(active_elem); - } - else if (command.charAt(0) === "!") { + if (command.charAt(0) === "!") { // TODO: This is sooo incredibly janky..... if (command === "!elemental") { elemental_format = !elemental_format; - } + } + else if (command === "!spacer") { + let spacer = document.createElement('div'); + spacer.classList.add("row", "my-2"); + parent_div.appendChild(spacer); + continue; + } } else { let id = command; @@ -319,12 +275,13 @@ function displayExpandedItem(item, parent_id){ } } if (id === "slots") { - let p_elem = document.createElement("p"); + let p_elem = document.createElement("div"); + p_elem.classList.add("col"); + // PROPER POWDER DISPLAYING let numerals = new Map([[1, "I"], [2, "II"], [3, "III"], [4, "IV"], [5, "V"], [6, "VI"]]); let powderPrefix = document.createElement("b"); - powderPrefix.classList.add("powderLeft"); powderPrefix.classList.add("left"); powderPrefix.textContent = "Powder Slots: " + item.get(id) + " ["; p_elem.appendChild(powderPrefix); @@ -337,21 +294,21 @@ function displayExpandedItem(item, parent_id){ } let powderSuffix = document.createElement("b"); - powderSuffix.classList.add("powderRight"); powderSuffix.classList.add("left"); powderSuffix.textContent = "]"; p_elem.appendChild(powderSuffix); - active_elem.appendChild(p_elem); + parent_div.appendChild(p_elem); } else if (id === "set") { if (item.get("hideSet")) { continue; } - let p_elem = document.createElement("p"); - p_elem.classList.add("itemp"); + let p_elem = document.createElement("div"); + p_elem.classList.add("col"); p_elem.textContent = "Set: " + item.get(id).toString(); - active_elem.appendChild(p_elem); + parent_div.appendChild(p_elem); } else if (id === "majorIds") { + //console.log(item.get(id)); for (let majorID of item.get(id)) { - let p_elem = document.createElement("p"); - p_elem.classList.add("itemp"); + let p_elem = document.createElement("div"); + p_elem.classList.add("col"); let title_elem = document.createElement("b"); let b_elem = document.createElement("b"); @@ -372,60 +329,94 @@ function displayExpandedItem(item, parent_id){ b_elem.textContent = name; p_elem.appendChild(b_elem); } - active_elem.appendChild(p_elem); + parent_div.appendChild(p_elem); } } else if (id === "lvl" && item.get("tier") === "Crafted") { - let p_elem = document.createElement("p"); - p_elem.classList.add("itemp"); + let p_elem = document.createElement("div"); + p_elem.classList.add("col"); p_elem.textContent = "Combat Level Min: " + item.get("lvlLow") + "-" + item.get(id); - active_elem.appendChild(p_elem); + parent_div.appendChild(p_elem); } else if (id === "displayName") { - let p_elem = document.createElement("a"); - p_elem.classList.add('itemp'); - p_elem.classList.add("smalltitle"); - p_elem.classList.add(item.has("tier") ? item.get("tier").replace(" ","") : "none"); + let row = document.createElement("div"); + + let a_elem = document.createElement("a"); + row.classList.add("row", "justify-content-center"); + a_elem.classList.add("col-auto", "text-center", "item-title", "p-0"); + a_elem.classList.add(item.has("tier") ? item.get("tier").replace(" ","") : "Normal"); + // a_elem.style.textGrow = 1; + row.appendChild(a_elem); + + /* + FUNCTIONALITY FOR THIS FEATURE HAS SINCE BEEN REMOVED (WITH SQ2). + IF WE WANT TO USE IT IN THE FUTURE, I'VE LEFT THE CODE TO ADD IT IN HERE + */ + + //allow the plus minus element to toggle upon click: âž•âž– + let plusminus = document.createElement("div"); + plusminus.id = parent_div.id.split("-")[0] + "-pm"; + plusminus.classList.add("col", "plus_minus", "text_end"); + plusminus.style.flexGrow = 0; + plusminus.textContent = "\u2795"; + row.appendChild(plusminus); + + if (item.get("custom")) { + a_elem.href = "../custom/#" + item.get("hash"); + a_elem.textContent = item.get("displayName"); + } else if (item.get("crafted")) { + a_elem.href = "../crafter/#" + item.get("hash"); + a_elem.textContent = item.get(id); + } else { + a_elem.href = "../item/#" + item.get("displayName"); + a_elem.textContent = item.get("displayName"); + } + parent_div.appendChild(row); + + let nolink_row = document.createElement("div"); + let p_elem = document.createElement("p"); + nolink_row.classList.add("row", "justify-content-center"); + nolink_row.style.display = "none"; + p_elem.classList.add("col-auto", "text-center", "item-title", "p-0"); + p_elem.classList.add(item.has("tier") ? item.get("tier").replace(" ","") : "Normal"); if (item.get("custom")) { - p_elem.href = url_base.replace(/\w+.html/, "") + "customizer.html#" + item.get("hash"); p_elem.textContent = item.get("displayName"); } else if (item.get("crafted")) { - p_elem.href = url_base.replace(/\w+.html/, "") + "crafter.html#" + item.get("hash"); p_elem.textContent = item.get(id); } else { - p_elem.href = url_base.replace(/\w+.html/, "") + "item.html#" + item.get("displayName"); p_elem.textContent = item.get("displayName"); } + + nolink_row.appendChild(p_elem); + parent_div.appendChild(nolink_row); - p_elem.target = "_blank"; - active_elem.appendChild(p_elem); let img = document.createElement("img"); if (item && item.has("type")) { img.src = "../media/items/" + (newIcons ? "new/":"old/") + "generic-" + item.get("type") + ".png"; img.alt = item.get("type"); - img.style = " z=index: 1;max-width: 64px; max-height: 64px; position: relative; top: 50%; transform: translateY(-50%);"; - let bckgrd = document.createElement("p"); - bckgrd.style = "width: 96px; height: 96px; border-radius: 50%;background-image: radial-gradient(closest-side, " + colorMap.get(item.get("tier")) + " 20%," + "#121516 80%); margin-left: auto; margin-right: auto;" - bckgrd.classList.add("center"); - bckgrd.classList.add("itemp"); - active_elem.appendChild(bckgrd); + img.style = " z=index: 1; position: relative;"; + let container = document.createElement("div"); + + let bckgrd = document.createElement("div"); + bckgrd.classList.add("col", "px-0", "d-flex", "align-items-center", "justify-content-center");// , "no-collapse"); + bckgrd.style = "border-radius: 50%;background-image: radial-gradient(closest-side, " + colorMap.get(item.get("tier")) + " 20%," + "hsl(0, 0%, 16%) 80%); margin-left: auto; margin-right: auto;" + bckgrd.classList.add("scaled-bckgrd"); + parent_div.appendChild(container); + container.appendChild(bckgrd); bckgrd.appendChild(img); } } else { let p_elem; - if ( !(item.get("tier") === "Crafted" && item.get("category") === "armor" && id === "hp") && (!skp_order.includes(id)) || (skp_order.includes(id) && item.get("tier") !== "Crafted" && active_elem.nodeName === "DIV") ) { //skp warp - p_elem = displayFixedID(active_elem, id, item.get(id), elemental_format); + if ( !(item.get("tier") === "Crafted" && item.get("category") === "armor" && id === "hp") && (!skp_order.includes(id)) || (skp_order.includes(id) && item.get("tier") !== "Crafted" && parent_div.nodeName === "table") ) { //skp warp + p_elem = displayFixedID(parent_div, id, item.get(id), elemental_format); } else if (item.get("tier") === "Crafted" && item.get("category") === "armor" && id === "hp") { - p_elem = displayFixedID(active_elem, id, item.get(id+"Low")+"-"+item.get(id), elemental_format); + p_elem = displayFixedID(parent_div, id, item.get(id+"Low")+"-"+item.get(id), elemental_format); } if (id === "lore") { p_elem.style = "font-style: italic"; - p_elem.classList.add("lore"); } else if (skp_order.includes(id)) { //id = str, dex, int, def, or agi - if ( item.get("tier") !== "Crafted" && active_elem.nodeName === "DIV") { - p_elem.textContent = ""; - p_elem.classList.add("itemp"); - row = document.createElement("p"); - row.classList.add("left"); + if ( item.get("tier") !== "Crafted") { + row = document.createElement("div"); + row.classList.add("col"); let title = document.createElement("b"); title.textContent = idPrefixes[id] + " "; @@ -438,10 +429,10 @@ function displayExpandedItem(item, parent_id){ boost.textContent = item.get(id); row.appendChild(title); row.appendChild(boost); - p_elem.appendChild(row); - } else if ( item.get("tier") === "Crafted" && active_elem.nodeName === "TABLE") { + parent_div.appendChild(row); + } else if ( item.get("tier") === "Crafted") { let row = displayRolledID(item, id, elemental_format); - active_elem.appendChild(row); + parent_div.appendChild(row); } } else if (id === "restrict") { p_elem.classList.add("restrict"); @@ -459,11 +450,17 @@ function displayExpandedItem(item, parent_id){ style === "positive" ? style = "negative" : style = "positive"; } if (fix_id) { - displayFixedID(active_elem, id, item.get("minRolls").get(id), elemental_format, style); + p_elem = document.createElement("div"); + p_elem.classList.add("col", "text-nowrap"); + if (id == "dex") { + console.log("dex activated at fix_id") + } + displayFixedID(p_elem, id, item.get("minRolls").get(id), elemental_format, style); + parent_div.appendChild(p_elem); } else { let row = displayRolledID(item, id, elemental_format); - active_elem.appendChild(row); + parent_div.appendChild(row); } }else{ // :/ @@ -473,8 +470,8 @@ function displayExpandedItem(item, parent_id){ //Show powder specials ;-; let nonConsumables = ["relik", "wand", "bow", "spear", "dagger", "chestplate", "helmet", "leggings", "boots", "ring", "bracelet", "necklace"]; if(nonConsumables.includes(item.get("type"))) { - let powder_special = document.createElement("p"); - powder_special.classList.add("left"); + let powder_special = document.createElement("div"); + powder_special.classList.add("col"); let powders = item.get("powders"); let element = ""; let power = 0; @@ -495,10 +492,9 @@ function displayExpandedItem(item, parent_id){ if (element !== "") {//powder special is "[e,t,w,f,a]+[0,1,2,3,4]" let powderSpecial = powderSpecialStats[ skp_elements.indexOf(element)]; let specialSuffixes = new Map([ ["Duration", " sec"], ["Radius", " blocks"], ["Chains", ""], ["Damage", "%"], ["Damage Boost", "%"], ["Knockback", " blocks"] ]); - let specialTitle = document.createElement("p"); - let specialEffects = document.createElement("p"); - addClasses(specialTitle, ["left", "itemp", damageClasses[skp_elements.indexOf(element) + 1]]); - addClasses(specialEffects, ["left", "itemp", "nocolor"]); + let specialTitle = document.createElement("span"); + let specialEffects = document.createElement("span"); + addClasses(specialTitle, [damageClasses[skp_elements.indexOf(element) + 1]]); let effects; if (item.get("category") === "weapon") {//weapon effects = powderSpecial["weaponSpecialEffects"]; @@ -510,7 +506,7 @@ function displayExpandedItem(item, parent_id){ for (const [key,value] of effects.entries()) { if (key !== "Description") { let effect = document.createElement("p"); - effect.classList.add("itemp"); + effect.classList.add("m-0"); effect.textContent = key + ": " + value[power] + specialSuffixes.get(key); if(key === "Damage"){ effect.textContent += elementIcons[skp_elements.indexOf(element)]; @@ -530,8 +526,8 @@ function displayExpandedItem(item, parent_id){ } if(item.get("tier") && item.get("tier") === "Crafted") { - let dura_elem = document.createElement("p"); - dura_elem.classList.add("itemp"); + let dura_elem = document.createElement("div"); + dura_elem.classList.add("col"); let dura = []; let suffix = ""; if(nonConsumables.includes(item.get("type"))) { @@ -543,8 +539,7 @@ function displayExpandedItem(item, parent_id){ suffix = " sec." let charges = document.createElement("b"); charges.textContent = "Charges: " + item.get("charges"); - charges.classList.add("spaceleft"); - active_elem.appendChild(charges); + parent_div.appendChild(charges); } if (typeof(dura) === "string") { @@ -552,16 +547,21 @@ function displayExpandedItem(item, parent_id){ } else { dura_elem.textContent += dura[0]+"-"+dura[1] + suffix; } - active_elem.append(dura_elem); + parent_div.append(dura_elem); } //Show item tier if (item.get("tier") && item.get("tier") !== " ") { - let item_desc_elem = document.createElement("p"); - item_desc_elem.classList.add('itemp'); + let item_desc_elem = document.createElement("div"); + item_desc_elem.classList.add("col"); item_desc_elem.classList.add(item.get("tier")); - item_desc_elem.textContent = item.get("tier")+" "+item.get("type"); - active_elem.append(item_desc_elem); + if (tome_types.includes(item.get("type"))) { + tome_type_map = new Map([["weaponTome", "Weapon Tome"],["armorTome", "Armor Tome"],["guildTome", "Guild Tome"]]); + item_desc_elem.textContent = item.get("tier")+" "+tome_type_map.get(item.get("type")); + } else { + item_desc_elem.textContent = item.get("tier")+" "+item.get("type"); + } + parent_div.append(item_desc_elem); } //Show item hash if applicable @@ -572,7 +572,7 @@ function displayExpandedItem(item, parent_id){ item_desc_elem.style.wordWrap = "break-word"; item_desc_elem.style.wordBreak = "break-word"; item_desc_elem.textContent = item.get("hash"); - active_elem.append(item_desc_elem); + parent_div.append(item_desc_elem); } if (item.get("category") === "weapon") { @@ -1021,9 +1021,12 @@ function displayRolledID(item, id, elemental_format) { function displayFixedID(active, id, value, elemental_format, style) { if (style) { - let row = document.createElement('tr'); - let desc_elem = document.createElement('td'); - desc_elem.classList.add('left'); + let row = document.createElement('div'); + row.classList.add("row"); + let desc_elem = document.createElement('div'); + desc_elem.classList.add('col'); + desc_elem.classList.add('text-start'); + if (elemental_format) { apply_elemental_format(desc_elem, id); } @@ -1032,8 +1035,9 @@ function displayFixedID(active, id, value, elemental_format, style) { } row.appendChild(desc_elem); - let value_elem = document.createElement('td'); - value_elem.classList.add('right'); + let value_elem = document.createElement('div'); + value_elem.classList.add('col'); + value_elem.classList.add('text-end'); value_elem.classList.add(style); value_elem.textContent = value + idSuffixes[id]; row.appendChild(value_elem); @@ -1045,8 +1049,8 @@ function displayFixedID(active, id, value, elemental_format, style) { if (value === "0-0" || value === "0-0\u279c0-0") { return; } - let p_elem = document.createElement('p'); - p_elem.classList.add('itemp'); + let p_elem = document.createElement('div'); + p_elem.classList.add('col'); if (elemental_format) { apply_elemental_format(p_elem, id, value); } @@ -1057,6 +1061,28 @@ function displayFixedID(active, id, value, elemental_format, style) { return p_elem; } } + +function displayPoisonDamage(overallparent_elem, build) { + overallparent_elem.textContent = ""; + + //Title + let title_elemavg = document.createElement("b"); + title_elemavg.textContent = "Poison Stats"; + overallparent_elem.append(title_elemavg); + + let overallpoisonDamage = document.createElement("p"); + let overallpoisonDamageFirst = document.createElement("span"); + let overallpoisonDamageSecond = document.createElement("span"); + let poison_tick = Math.ceil(build.statMap.get("poison") * (1+skillPointsToPercentage(build.total_skillpoints[0])) * (build.statMap.get("poisonPct"))/100 /3); + overallpoisonDamageFirst.textContent = "Poison Tick: "; + overallpoisonDamageSecond.textContent = Math.max(poison_tick,0); + overallpoisonDamageSecond.classList.add("Damage"); + + overallpoisonDamage.appendChild(overallpoisonDamageFirst); + overallpoisonDamage.appendChild(overallpoisonDamageSecond); + overallparent_elem.append(overallpoisonDamage); +} + function displayEquipOrder(parent_elem,buildOrder){ parent_elem.textContent = ""; const order = buildOrder.slice(); @@ -1076,29 +1102,6 @@ function displayEquipOrder(parent_elem,buildOrder){ } } -function displayPoisonDamage(overallparent_elem, build) { - overallparent_elem.textContent = ""; - - //Title - let title_elemavg = document.createElement("p"); - title_elemavg.classList.add("smalltitle"); - title_elemavg.classList.add("Normal"); - title_elemavg.textContent = "Poison Stats"; - overallparent_elem.append(title_elemavg); - - let overallpoisonDamage = document.createElement("p"); - overallpoisonDamage.classList.add("lessbottom"); - let overallpoisonDamageFirst = document.createElement("b"); - let overallpoisonDamageSecond = document.createElement("b"); - let poison_tick = Math.ceil(build.statMap.get("poison") * (1+skillPointsToPercentage(build.total_skillpoints[0])) * (build.statMap.get("poisonPct") + build.externalStats.get("poisonPct"))/100 /3); - overallpoisonDamageFirst.textContent = "Poison Tick: "; - overallpoisonDamageSecond.textContent = Math.max(poison_tick,0); - overallpoisonDamageSecond.classList.add("Damage"); - - overallpoisonDamage.appendChild(overallpoisonDamageFirst); - overallpoisonDamage.appendChild(overallpoisonDamageSecond); - overallparent_elem.append(overallpoisonDamage); -} function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ console.log("Melee Stats"); @@ -1314,40 +1317,8 @@ function displayDefenseStats(parent_elem, build, insertSummary){ } const stats = defenseStats.slice(); - if (!insertSummary) { - let title_elem = document.createElement("p"); - title_elem.textContent = "Defense Stats"; - title_elem.classList.add("title"); - title_elem.classList.add("Normal"); - title_elem.classList.add("itemp"); - parent_elem.append(title_elem); - - let base_stat_elem = document.createElement("p"); - base_stat_elem.id = "base-stat"; - parent_elem.append(base_stat_elem); - - let mock_item = new Map(); - - mock_item.set("fixID", true); - let mock_minRolls = new Map(); - mock_item.set("minRolls", mock_minRolls); - const stats = ["hp", "hpBonus", "hprRaw", "hprPct", "fDef", "wDef", "aDef", "tDef", "eDef", - "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct"]; - for (const stat of stats) { - if (rolledIDs.includes(stat)) { - mock_minRolls.set(stat, build.statMap.get(stat)); - } - else { - mock_item.set(stat, build.statMap.get(stat)); - } - } - mock_item.set("powders", []); - displayExpandedItem(mock_item, base_stat_elem.id); - } - - parent_elem.append(document.createElement("br")); - let statsTable = document.createElement("table"); - statsTable.classList.add("itemtable"); + // parent_elem.append(document.createElement("br")); + let statsTable = document.createElement("div"); //[total hp, ehp, total hpr, ehpr, [def%, agi%], [edef,tdef,wdef,fdef,adef]] for(const i in stats){ @@ -1361,18 +1332,26 @@ function displayDefenseStats(parent_elem, build, insertSummary){ } //total HP - let hpRow = document.createElement("tr"); - let hp = document.createElement("td"); + let hpRow = document.createElement("div"); + hpRow.classList.add('row'); + let hp = document.createElement("div"); + hp.classList.add('col'); hp.classList.add("Health"); - hp.classList.add("left"); + hp.classList.add("text-start"); hp.textContent = "Total HP:"; - let boost = document.createElement("td"); + let boost = document.createElement("div"); + boost.classList.add('col'); boost.textContent = stats[0]; - boost.classList.add("right"); + boost.classList.add("text-end"); hpRow.appendChild(hp); hpRow.append(boost); - statsTable.appendChild(hpRow); + + if (insertSummary) { + parent_elem.appendChild(hpRow); + } else { + statsTable.appendChild(hpRow); + } let tooltip; let tooltiptext; @@ -1380,100 +1359,113 @@ function displayDefenseStats(parent_elem, build, insertSummary){ if (!defMult) {defMult = 1} //EHP - let ehpRow = document.createElement("tr"); - let ehp = document.createElement("td"); - ehp.classList.add("left"); + let ehpRow = document.createElement("div"); + ehpRow.classList.add("row"); + let ehp = document.createElement("div"); + ehp.classList.add("col"); + ehp.classList.add("text-start"); ehp.textContent = "Effective HP:"; - boost = document.createElement("td"); + boost = document.createElement("div"); boost.textContent = stats[1][0]; - boost.classList.add("right"); + boost.classList.add("col"); + boost.classList.add("text-end"); tooltiptext = `= ${stats[0]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (1 - ${skillPointsToPercentage(build.total_skillpoints[4]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); + // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); ehpRow.appendChild(ehp); ehpRow.append(boost); - statsTable.append(ehpRow); - ehpRow = document.createElement("tr"); - ehp = document.createElement("td"); - ehp.classList.add("left"); + if (insertSummary) { + parent_elem.appendChild(ehpRow) + } else { + statsTable.append(ehpRow); + } + + ehpRow = document.createElement("div"); + ehpRow.classList.add("row"); + ehp = document.createElement("div"); + ehp.classList.add("col"); + ehp.classList.add("text-start"); ehp.textContent = "Effective HP (no agi):"; - boost = document.createElement("td"); + boost = document.createElement("div"); boost.textContent = stats[1][1]; - boost.classList.add("right"); + boost.classList.add("col"); + boost.classList.add("text-end"); tooltiptext = `= ${stats[0]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); + // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); ehpRow.appendChild(ehp); ehpRow.append(boost); statsTable.append(ehpRow); //total HPR - let hprRow = document.createElement("tr"); - let hpr = document.createElement("td"); + let hprRow = document.createElement("div"); + hprRow.classList.add("row") + let hpr = document.createElement("div"); hpr.classList.add("Health"); - hpr.classList.add("left"); + hpr.classList.add("col"); + hpr.classList.add("text-start"); hpr.textContent = "HP Regen (Total):"; - boost = document.createElement("td"); + boost = document.createElement("div"); boost.textContent = stats[2]; - boost.classList.add("right"); + boost.classList.add("col"); + boost.classList.add("text-end"); hprRow.appendChild(hpr); hprRow.appendChild(boost); - statsTable.appendChild(hprRow); + + if (insertSummary) { + parent_elem.appendChild(hprRow); + } else { + statsTable.appendChild(hprRow); + } + //EHPR - let ehprRow = document.createElement("tr"); - let ehpr = document.createElement("td"); - ehpr.classList.add("left"); + let ehprRow = document.createElement("div"); + ehprRow.classList.add("row") + let ehpr = document.createElement("div"); + ehpr.classList.add("col"); + ehpr.classList.add("text-start"); ehpr.textContent = "Effective HP Regen:"; - boost = document.createElement("td"); + boost = document.createElement("div"); boost.textContent = stats[3][0]; - boost.classList.add("right"); + boost.classList.add("col"); + boost.classList.add("text-end"); tooltiptext = `= ${stats[2]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (1 - ${skillPointsToPercentage(build.total_skillpoints[4]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); + // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); ehprRow.appendChild(ehpr); ehprRow.append(boost); statsTable.append(ehprRow); - /* - ehprRow = document.createElement("tr"); - ehpr = document.createElement("td"); - ehpr.classList.add("left"); - ehpr.textContent = "Effective HP Regen (no agi):"; - - boost = document.createElement("td"); - boost.textContent = stats[3][1]; - boost.classList.add("right"); - - ehprRow.appendChild(ehpr); - ehprRow.append(boost); - statsTable.append(ehprRow); */ //eledefs let eledefs = stats[5]; for (let i = 0; i < eledefs.length; i++){ - let eledefElemRow = document.createElement("tr"); + let eledefElemRow = document.createElement("div"); + eledefElemRow.classList.add("row") - let eledef = document.createElement("td"); - eledef.classList.add("left") - let eledefTitle = document.createElement("b"); + let eledef = document.createElement("div"); + eledef.classList.add("col"); + eledef.classList.add("text-start"); + let eledefTitle = document.createElement("span"); eledefTitle.textContent = damageClasses[i+1]; eledefTitle.classList.add(damageClasses[i+1]); - let defense = document.createElement("b"); + let defense = document.createElement("span"); defense.textContent = " Def (Total): "; eledef.appendChild(eledefTitle); eledef.appendChild(defense); eledefElemRow.appendChild(eledef); - let boost = document.createElement("td"); + let boost = document.createElement("div"); boost.textContent = eledefs[i]; boost.classList.add(eledefs[i] >= 0 ? "positive" : "negative"); - boost.classList.add("right"); + boost.classList.add("col"); + boost.classList.add("text-end"); let defRaw = build.statMap.get("defRaw")[i]; let defPct = build.statMap.get("defBonus")[i]/100; @@ -1484,39 +1476,51 @@ function displayDefenseStats(parent_elem, build, insertSummary){ defPct >= 0 ? defPct = "+ " + defPct: defPct = "- " + defPct; tooltiptext = `= ${defRaw} * (1 ${defPct})` } - tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); + // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); eledefElemRow.appendChild(boost); - - statsTable.appendChild(eledefElemRow); + + if (insertSummary) { + parent_elem.appendChild(eledefElemRow); + } else { + statsTable.appendChild(eledefElemRow); + } } if (!insertSummary) { //skp - let defRow = document.createElement("tr"); - let defElem = document.createElement("td"); - defElem.classList.add("left"); + let defRow = document.createElement("div"); + defRow.classList.add("row"); + let defElem = document.createElement("div"); + defElem.classList.add("col"); + defElem.classList.add("text-start"); defElem.textContent = "Damage Absorbed %:"; - boost = document.createElement("td"); - boost.classList.add("right"); + boost = document.createElement("div"); + boost.classList.add("col"); + boost.classList.add("text-end"); boost.textContent = stats[4][0] + "%"; defRow.appendChild(defElem); defRow.appendChild(boost); statsTable.append(defRow); - let agiRow = document.createElement("tr"); - let agiElem = document.createElement("td"); - agiElem.classList.add("left"); + let agiRow = document.createElement("div"); + agiRow.classList.add("row"); + let agiElem = document.createElement("div"); + agiElem.classList.add("col"); + agiElem.classList.add("text-start"); agiElem.textContent = "Dodge Chance %:"; - boost = document.createElement("td"); - boost.classList.add("right"); + boost = document.createElement("div"); + boost.classList.add("col"); + boost.classList.add("text-end"); boost.textContent = stats[4][1] + "%"; agiRow.appendChild(agiElem); agiRow.appendChild(boost); statsTable.append(agiRow); } - parent_elem.append(statsTable); + if (!insertSummary) { + parent_elem.append(statsTable); + } } function displayPowderSpecials(parent_elem, powderSpecials, build) { @@ -1691,7 +1695,6 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell overallparent_elem.append(displayNextCosts(spell, build, build.weapon.statMap)); - let critChance = skillPointsToPercentage(build.total_skillpoints[1]); let save_damages = []; From 81dfe767a3f11758002a607663a0b99c1cedfa32 Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 13:44:02 -0700 Subject: [PATCH 28/68] Item display, build display (partially) --- builder/index.html | 6 +- js/build_constants.js | 4 +- js/build_encode_decode.js | 46 ++-- js/builder.js | 252 ++++++++++++++++++- js/builder_graph.js | 505 +++++++++++++++++++------------------- js/computation_graph.js | 114 +-------- js/display.js | 347 +++++++++++++------------- js/utils.js | 3 +- 8 files changed, 704 insertions(+), 573 deletions(-) diff --git a/builder/index.html b/builder/index.html index d0ab71a..73e20b1 100644 --- a/builder/index.html +++ b/builder/index.html @@ -313,7 +313,7 @@
- +
@@ -1291,7 +1291,7 @@
-
+
diff --git a/js/build_constants.js b/js/build_constants.js index 2983328..820beb7 100644 --- a/js/build_constants.js +++ b/js/build_constants.js @@ -85,7 +85,7 @@ let tome_names = [ "Guild Tome", ] let equipmentInputs = equipment_fields.map(x => x + "-choice"); -let buildFields = equipment_fields.map(x => x+"-tooltip").concat(tome_fields.map(x => x + "-tooltip")); +let build_fields = equipment_fields.map(x => x+"-tooltip"); let tomeInputs = tome_fields.map(x => x + "-choice"); let powder_inputs = [ @@ -100,7 +100,7 @@ let weapon_keys = ['dagger', 'wand', 'bow', 'relik', 'spear']; let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots']; let accessory_keys= ['ring1', 'ring2', 'bracelet', 'necklace']; let powderable_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'weapon']; -let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon'].concat(tome_keys); +let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon']; let spell_disp = ['spell0-info', 'spell1-info', 'spell2-info', 'spell3-info']; let other_disp = ['build-order', 'set-info', 'int-info']; diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 0929e4e..12b45b1 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -137,7 +137,7 @@ function decodeBuild(url_tag) { /* Stores the entire build in a string using B64 encoding and adds it to the URL. */ -function encodeBuild(build) { +function encodeBuild(build, powders) { if (build) { let build_string; @@ -147,19 +147,15 @@ function encodeBuild(build) { build_string = ""; tome_string = ""; - let crafted_idx = 0; - let custom_idx = 0; for (const item of build.items) { - if (item.get("custom")) { - let custom = "CI-"+encodeCustom(build.customItems[custom_idx],true); + if (item.statMap.get("custom")) { + let custom = "CI-"+encodeCustom(item, true); build_string += Base64.fromIntN(custom.length, 3) + custom; - custom_idx += 1; build_version = Math.max(build_version, 5); - } else if (item.get("crafted")) { - build_string += "CR-"+encodeCraft(build.craftedItems[crafted_idx]); - crafted_idx += 1; - } else if (item.get("category") === "tome") { + } else if (item.statMap.get("crafted")) { + build_string += "CR-"+encodeCraft(item); + } else if (item.statMap.get("category") === "tome") { let tome_id = item.get("id"); if (tome_id <= 60) { // valid normal tome. ID 61-63 is for NONE tomes. @@ -167,7 +163,7 @@ function encodeBuild(build) { } tome_string += Base64.fromIntN(tome_id, 1); } else { - build_string += Base64.fromIntN(item.get("id"), 3); + build_string += Base64.fromIntN(item.statMap.get("id"), 3); } } @@ -175,7 +171,7 @@ function encodeBuild(build) { build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 } build_string += Base64.fromIntN(build.level, 2); - for (const _powderset of build.powders) { + for (const _powderset of powders) { let n_bits = Math.ceil(_powderset.length / 6); build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. // Slice copy. @@ -196,26 +192,24 @@ function encodeBuild(build) { } } -function copyBuild(build) { - if (build) { - copyTextToClipboard(url_base+location.hash); - document.getElementById("copy-button").textContent = "Copied!"; - } +function copyBuild() { + copyTextToClipboard(url_base+location.hash); + document.getElementById("copy-button").textContent = "Copied!"; } function shareBuild(build) { if (build) { let text = url_base+location.hash+"\n"+ "WynnBuilder build:\n"+ - "> "+build.helmet.get("displayName")+"\n"+ - "> "+build.chestplate.get("displayName")+"\n"+ - "> "+build.leggings.get("displayName")+"\n"+ - "> "+build.boots.get("displayName")+"\n"+ - "> "+build.ring1.get("displayName")+"\n"+ - "> "+build.ring2.get("displayName")+"\n"+ - "> "+build.bracelet.get("displayName")+"\n"+ - "> "+build.necklace.get("displayName")+"\n"+ - "> "+build.weapon.get("displayName")+" ["+build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; + "> "+build.helmet.statMap.get("displayName")+"\n"+ + "> "+build.chestplate.statMap.get("displayName")+"\n"+ + "> "+build.leggings.statMap.get("displayName")+"\n"+ + "> "+build.boots.statMap.get("displayName")+"\n"+ + "> "+build.ring1.statMap.get("displayName")+"\n"+ + "> "+build.ring2.statMap.get("displayName")+"\n"+ + "> "+build.bracelet.statMap.get("displayName")+"\n"+ + "> "+build.necklace.statMap.get("displayName")+"\n"+ + "> "+build.weapon.statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]"; copyTextToClipboard(text); document.getElementById("share-button").textContent = "Copied!"; } diff --git a/js/builder.js b/js/builder.js index 1ce6a3f..0251fef 100644 --- a/js/builder.js +++ b/js/builder.js @@ -1,3 +1,5 @@ +let build_powders; + function getItemNameFromID(id) { if (redirectMap.has(id)) { return getItemNameFromID(redirectMap.get(id)); @@ -142,15 +144,263 @@ function show_tab(tab) { document.getElementById("tab-" + tab.split("-")[0] + "-btn").classList.add("selected-btn"); } +// autocomplete initialize +function init_autocomplete() { + let dropdowns = new Map(); + for (const eq of equipment_keys) { + if (tome_keys.includes(eq)) { + continue; + } + // build dropdown + let item_arr = []; + if (eq == 'weapon') { + for (const weaponType of weapon_keys) { + for (const weapon of itemLists.get(weaponType)) { + let item_obj = itemMap.get(weapon); + if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { + continue; + } + if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { + continue; + } + item_arr.push(weapon); + } + } + } else { + for (const item of itemLists.get(eq.replace(/[0-9]/g, ''))) { + let item_obj = itemMap.get(item); + if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { + continue; + } + if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { + continue; + } + item_arr.push(item) + } + } + + // create dropdown + dropdowns.set(eq, new autoComplete({ + data: { + src: item_arr + }, + selector: "#"+ eq +"-choice", + wrapper: false, + resultsList: { + maxResults: 1000, + tabSelect: true, + noResults: true, + class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", + element: (list, data) => { + // dynamic result loc + let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); + list.style.top = position.bottom + window.scrollY +"px"; + list.style.left = position.x+"px"; + list.style.width = position.width+"px"; + list.style.maxHeight = position.height * 2 +"px"; + + if (!data.results.length) { + message = document.createElement('li'); + message.classList.add('scaled-font'); + message.textContent = "No results found!"; + list.prepend(message); + } + }, + }, + resultItem: { + class: "scaled-font search-item", + selected: "dark-5", + element: (item, data) => { + item.classList.add(itemMap.get(data.value).tier); + }, + }, + events: { + input: { + selection: (event) => { + if (event.detail.selection.value) { + event.target.value = event.detail.selection.value; + } + event.target.dispatchEvent(new Event('input')); + }, + }, + } + })); + } + + for (const eq of tome_keys) { + // build dropdown + let tome_arr = []; + for (const tome of tomeLists.get(eq.replace(/[0-9]/g, ''))) { + let tome_obj = tomeMap.get(tome); + if (tome_obj["restrict"] && tome_obj["restrict"] === "DEPRECATED") { + continue; + } + //this should suffice for tomes - jank + if (tome_obj["name"].includes('No ' + eq.charAt(0).toUpperCase())) { + continue; + } + let tome_name = tome; + tome_arr.push(tome_name); + } + + // create dropdown + dropdowns.set(eq, new autoComplete({ + data: { + src: tome_arr + }, + selector: "#"+ eq +"-choice", + wrapper: false, + resultsList: { + maxResults: 1000, + tabSelect: true, + noResults: true, + class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", + element: (list, data) => { + // dynamic result loc + let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); + list.style.top = position.bottom + window.scrollY +"px"; + list.style.left = position.x+"px"; + list.style.width = position.width+"px"; + list.style.maxHeight = position.height * 2 +"px"; + + if (!data.results.length) { + message = document.createElement('li'); + message.classList.add('scaled-font'); + message.textContent = "No results found!"; + list.prepend(message); + } + }, + }, + resultItem: { + class: "scaled-font search-item", + selected: "dark-5", + element: (tome, data) => { + tome.classList.add(tomeMap.get(data.value).tier); + }, + }, + events: { + input: { + selection: (event) => { + if (event.detail.selection.value) { + event.target.value = event.detail.selection.value; + } + }, + }, + } + })); + } + + let filter_loc = ["filter1", "filter2", "filter3", "filter4"]; + for (const i of filter_loc) { + dropdowns.set(i+"-choice", new autoComplete({ + data: { + src: sq2ItemFilters, + }, + selector: "#"+i+"-choice", + wrapper: false, + resultsList: { + tabSelect: true, + noResults: true, + class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", + element: (list, data) => { + // dynamic result loc + console.log(i); + list.style.zIndex = "100"; + let position = document.getElementById(i+"-dropdown").getBoundingClientRect(); + window_pos = document.getElementById("search-container").getBoundingClientRect(); + list.style.top = position.bottom - window_pos.top + 5 +"px"; + list.style.left = position.x - window_pos.x +"px"; + list.style.width = position.width+"px"; + + if (!data.results.length) { + message = document.createElement('li'); + message.classList.add('scaled-font'); + message.textContent = "No filters found!"; + list.prepend(message); + } + }, + }, + resultItem: { + class: "scaled-font search-item", + selected: "dark-5", + }, + events: { + input: { + selection: (event) => { + if (event.detail.selection.value) { + event.target.value = event.detail.selection.value; + } + }, + }, + } + })); + } +} + +function collapse_element(elmnt) { + elem_list = document.querySelector(elmnt).children; + if (elem_list) { + for (elem of elem_list) { + if (elem.classList.contains("no-collapse")) { continue; } + if (elem.style.display == "none") { + elem.style.display = ""; + } else { + elem.style.display = "none"; + } + } + } + // macy quirk + window.dispatchEvent(new Event('resize')); + // weird bug where display: none overrides?? + document.querySelector(elmnt).style.removeProperty('display'); +} // TODO: Learn and use await function init() { console.log("builder.js init"); init_autocomplete(); + + // Other "main" stuff + // Spell dropdowns + for (const i of spell_disp) { + document.querySelector("#"+i+"Avg").addEventListener("click", () => toggle_spell_tab(i)); + } + for (const eq of equipment_keys) { + document.querySelector("#"+eq+"-tooltip").addEventListener("click", () => collapse_element('#'+eq+'-tooltip')); + } + + // Masonry setup + let masonry = Macy({ + container: "#masonry-container", + columns: 1, + mobileFirst: true, + breakAt: { + 1200: 4, + }, + margin: { + x: 20, + y: 20, + } + + }); + + let search_masonry = Macy({ + container: "#search-results", + columns: 1, + mobileFirst: true, + breakAt: { + 1200: 4, + }, + margin: { + x: 20, + y: 20, + } + + }); decodeBuild(url_tag); + builder_graph_init(); } -//load_init(init3); (async function() { let load_promises = [ load_init(), load_ing_init(), load_tome_init() ]; await Promise.all(load_promises); diff --git a/js/builder_graph.js b/js/builder_graph.js index 4fcd47b..083eb32 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -1,22 +1,180 @@ - - -class BuildEncodeNode extends ComputeNode { - constructor() { - super("builder-encode"); +/** + * Node for getting an item's stats from an item input field. + * + * Signature: ItemInputNode() => Item | null + */ +class ItemInputNode extends InputNode { + /** + * Make an item stat pulling compute node. + * + * @param name: Name of this node. + * @param item_input_field: Input field (html element) to listen for item names from. + * @param none_item: Item object to use as the "none" for this field. + */ + constructor(name, item_input_field, none_item) { + super(name, item_input_field); + this.none_item = new Item(none_item); + this.none_item.statMap.set('NONE', true); } compute_func(input_map) { - if (input_map.size !== 1) { throw "BuildEncodeNode accepts exactly one input (build)"; } - const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - return encodeBuild(build); + // built on the assumption of no one will type in CI/CR letter by letter + + let item_text = this.input_field.value; + if (!item_text) { + return this.none_item; + } + + let item; + + if (item_text.slice(0, 3) == "CI-") { + item = getCustomFromHash(item_text); + } + else if (item_text.slice(0, 3) == "CR-") { + item = getCraftFromHash(item_text); + } + else if (itemMap.has(item_text)) { + item = new Item(itemMap.get(item_text)); + } + else if (tomeMap.has(item_text)) { + item = new Item(tomeMap.get(item_text)); + } + + if (item) { + let type_match; + if (this.none_item.statMap.get('category') === 'weapon') { + type_match = item.statMap.get('category') === 'weapon'; + } else { + type_match = item.statMap.get('type') === this.none_item.statMap.get('type'); + } + if (type_match) { return item; } + } + return null; } } -class URLUpdateNode extends ComputeNode { - constructor() { - super("builder-url-update"); +/** + * Node for updating item input fields from parsed items. + * + * Signature: ItemInputDisplayNode(item: Item) => null + */ +class ItemInputDisplayNode extends ComputeNode { + + constructor(name, eq, item_image) { + super(name); + this.input_field = document.getElementById(eq+"-choice"); + this.health_field = document.getElementById(eq+"-health"); + this.level_field = document.getElementById(eq+"-lv"); + this.image = item_image; + this.fail_cb = true; } + compute_func(input_map) { + if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + this.input_field.classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom'); + this.input_field.classList.add("text-light"); + this.image.classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow'); + + if (!item) { + this.input_field.classList.add("is-invalid"); + return null; + } + + if (item.statMap.has('NONE')) { + return null; + } + const tier = item.statMap.get('tier'); + this.input_field.classList.add(tier); + if (this.health_field) { + // Doesn't exist for weapons. + this.health_field.textContent = item.statMap.get('hp'); + } + this.level_field.textContent = item.statMap.get('lvl'); + this.image.classList.add(tier + "-shadow"); + return null; + } +} + +/** + * Node for rendering an item. + * + * Signature: ItemDisplayNode(item: Item) => null + */ +class ItemDisplayNode extends ComputeNode { + constructor(name, target_elem) { + super(name); + this.target_elem = target_elem; + } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + displayExpandedItem(item.statMap, this.target_elem); + collapse_element("#"+this.target_elem); + } +} + +/** + * Change the weapon to match correct type. + * + * Signature: WeaponInputDisplayNode(item: Item) => null + */ +class WeaponInputDisplayNode extends ComputeNode { + + constructor(name, image_field) { + super(name); + this.image = image_field; + } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "WeaponDisplayNode accepts exactly one input (item)"; } + const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + + const type = item.statMap.get('type'); + this.image.setAttribute('src', '../media/items/new/generic-'+type+'.png'); + } +} + +/** + * Encode the build into a url-able string. + * + * Signature: BuildEncodeNode(build: Build, + helmet-powder: List[powder], + chestplate-powder: List[powder], + leggings-powder: List[powder], + boots-powder: List[powder], + weapon-powder: List[powder]) => str + */ +class BuildEncodeNode extends ComputeNode { + constructor() { super("builder-encode"); } + + compute_func(input_map) { + const build = input_map.get('build'); + let powders = [ + input_map.get('helmet-powder'), + input_map.get('chestplate-powder'), + input_map.get('leggings-powder'), + input_map.get('boots-powder'), + input_map.get('weapon-powder') + ]; + // TODO: grr global state for copy button.. + player_build = build; + build_powders = powders; + return encodeBuild(build, powders); + } +} + +/** + * Update the window's URL. + * + * Signature: URLUpdateNode(build_str: str) => null + */ +class URLUpdateNode extends ComputeNode { + constructor() { super("builder-url-update"); } + compute_func(input_map) { if (input_map.size !== 1) { throw "URLUpdateNode accepts exactly one input (build_str)"; } const [build_str] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element @@ -24,10 +182,25 @@ class URLUpdateNode extends ComputeNode { } } +/** + * Create a "build" object from a set of equipments. + * Returns a new Build object, or null if all items are NONE items. + * + * TODO: add tomes + * + * Signature: BuildAssembleNode(helmet-input: Item, + * chestplate-input: Item, + * leggings-input: Item, + * boots-input: Item, + * ring1-input: Item, + * ring2-input: Item, + * bracelet-input: Item, + * necklace-input: Item, + * weapon-input: Item, + * level-input: int) => Build | null + */ class BuildAssembleNode extends ComputeNode { - constructor() { - super("builder-make-build"); - } + constructor() { super("builder-make-build"); } compute_func(input_map) { let equipments = [ @@ -54,11 +227,15 @@ class BuildAssembleNode extends ComputeNode { } } +/** + * Read an input field and parse into a list of powderings. + * Every two characters makes one powder. If parsing fails, NULL is returned. + * + * Signature: PowderInputNode() => List[powder] | null + */ class PowderInputNode extends InputNode { - constructor(name, input_field) { - super(name, input_field); - } + constructor(name, input_field) { super(name, input_field); } compute_func(input_map) { // TODO: haha improve efficiency to O(n) dumb @@ -81,6 +258,13 @@ class PowderInputNode extends InputNode { } } +/** + * Select a spell+spell "variation" based on a build / spell idx. + * Right now this isn't much logic and is only used to abstract away major id interactions + * but will become significantly more complex in wynn2. + * + * Signature: SpellSelectNode(build: Build) => [Spell, SpellParts] + */ class SpellSelectNode extends ComputeNode { constructor(spell_num) { super("builder-spell"+spell_num+"-select"); @@ -111,10 +295,18 @@ class SpellSelectNode extends ComputeNode { } } +/** + * Compute spell damage of spell parts. + * Currently kinda janky / TODO while we rework the internal rep. of spells. + * + * Signature: SpellDamageCalcNode(weapon-input: Item, + * build: Build, + * weapon-powder: List[powder], + * spell-info: [Spell, SpellParts]) => List[SpellDamage] + */ class SpellDamageCalcNode extends ComputeNode { constructor(spell_num) { super("builder-spell"+spell_num+"-calc"); - this.spell_idx = spell_num; } compute_func(input_map) { @@ -148,6 +340,15 @@ class SpellDamageCalcNode extends ComputeNode { } } + +/** + * Display spell damage from spell parts. + * Currently kinda janky / TODO while we rework the internal rep. of spells. + * + * Signature: SpellDisplayNode(build: Build, + * spell-info: [Spell, SpellParts], + * spell-damage: List[SpellDamage]) => null + */ class SpellDisplayNode extends ComputeNode { constructor(spell_num) { super("builder-spell"+spell_num+"-display"); @@ -168,26 +369,44 @@ class SpellDisplayNode extends ComputeNode { } } +/** + * Display build stats. + * + * Signature: BuildDisplayNode(build: Build) => null + */ +class BuildDisplayNode extends ComputeNode { + constructor(spell_num) { super("builder-stats-display"); } + + compute_func(input_map) { + if (input_map.size !== 1) { throw "BuildDisplayNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + displayBuildStats('overall-stats', build, build_all_display_commands); + displayBuildStats("offensive-stats", build, build_offensive_display_commands); + displaySetBonuses("set-info", build); + } +} + let item_nodes = []; let powder_nodes = []; let spelldmg_nodes = []; -document.addEventListener('DOMContentLoaded', function() { +function builder_graph_init() { // Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). - for (const [eq, none_item] of zip(equipment_fields, none_items)) { + for (const [eq, display_elem, none_item] of zip3(equipment_fields, build_fields, none_items)) { let input_field = document.getElementById(eq+"-choice"); let item_image = document.getElementById(eq+"-img"); let item_input = new ItemInputNode(eq+'-input', input_field, none_item); item_nodes.push(item_input); - new ItemInputDisplayNode(eq+'-display', input_field, item_image).link_to(item_input); + new ItemInputDisplayNode(eq+'-input-display', eq, item_image).link_to(item_input); + new ItemDisplayNode(eq+'-item-display', display_elem).link_to(item_input); //new PrintNode(eq+'-debug').link_to(item_input); //document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); } // weapon image changer node. let weapon_image = document.getElementById("weapon-img"); - new WeaponDisplayNode('weapon-type', weapon_image).link_to(item_nodes[8]); + new WeaponInputDisplayNode('weapon-type', weapon_image).link_to(item_nodes[8]); // Level input node. let level_input = new InputNode('level-input', document.getElementById('level-choice')); @@ -198,10 +417,19 @@ document.addEventListener('DOMContentLoaded', function() { build_node.link_to(input); } build_node.link_to(level_input); + new BuildDisplayNode().link_to(build_node, 'build'); + + let build_encode_node = new BuildEncodeNode(); + build_encode_node.link_to(build_node, 'build'); + + let url_update_node = new URLUpdateNode(); + url_update_node.link_to(build_encode_node, 'build-str'); for (const input of powder_inputs) { - powder_nodes.push(new PowderInputNode(input, document.getElementById(input))); + let powder_node = new PowderInputNode(input, document.getElementById(input)); + powder_nodes.push(powder_node); + build_encode_node.link_to(powder_node, input); } for (let i = 0; i < 4; ++i) { @@ -222,234 +450,5 @@ document.addEventListener('DOMContentLoaded', function() { } console.log("Set up graph"); - - // Other "main" stuff - // TODO: consolidate and comment - - // Spell dropdowns - for (const i of spell_disp) { - document.querySelector("#"+i+"Avg").addEventListener("click", () => toggle_spell_tab(i)); - } - - // Masonry setup - let masonry = Macy({ - container: "#masonry-container", - columns: 1, - mobileFirst: true, - breakAt: { - 1200: 4, - }, - margin: { - x: 20, - y: 20, - } - - }); - - let search_masonry = Macy({ - container: "#search-results", - columns: 1, - mobileFirst: true, - breakAt: { - 1200: 4, - }, - margin: { - x: 20, - y: 20, - } - - }); -}); - -// autocomplete initialize -function init_autocomplete() { - let dropdowns = new Map(); - for (const eq of equipment_keys) { - if (tome_keys.includes(eq)) { - continue; - } - // build dropdown - let item_arr = []; - if (eq == 'weapon') { - for (const weaponType of weapon_keys) { - for (const weapon of itemLists.get(weaponType)) { - let item_obj = itemMap.get(weapon); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { - continue; - } - item_arr.push(weapon); - } - } - } else { - for (const item of itemLists.get(eq.replace(/[0-9]/g, ''))) { - let item_obj = itemMap.get(item); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { - continue; - } - item_arr.push(item) - } - } - - // create dropdown - dropdowns.set(eq, new autoComplete({ - data: { - src: item_arr - }, - selector: "#"+ eq +"-choice", - wrapper: false, - resultsList: { - maxResults: 1000, - tabSelect: true, - noResults: true, - class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", - element: (list, data) => { - // dynamic result loc - let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); - list.style.top = position.bottom + window.scrollY +"px"; - list.style.left = position.x+"px"; - list.style.width = position.width+"px"; - list.style.maxHeight = position.height * 2 +"px"; - - if (!data.results.length) { - message = document.createElement('li'); - message.classList.add('scaled-font'); - message.textContent = "No results found!"; - list.prepend(message); - } - }, - }, - resultItem: { - class: "scaled-font search-item", - selected: "dark-5", - element: (item, data) => { - item.classList.add(itemMap.get(data.value).tier); - }, - }, - events: { - input: { - selection: (event) => { - if (event.detail.selection.value) { - event.target.value = event.detail.selection.value; - } - event.target.dispatchEvent(new Event('input')); - }, - }, - } - })); - } - - for (const eq of tome_keys) { - // build dropdown - let tome_arr = []; - for (const tome of tomeLists.get(eq.replace(/[0-9]/g, ''))) { - let tome_obj = tomeMap.get(tome); - if (tome_obj["restrict"] && tome_obj["restrict"] === "DEPRECATED") { - continue; - } - //this should suffice for tomes - jank - if (tome_obj["name"].includes('No ' + eq.charAt(0).toUpperCase())) { - continue; - } - let tome_name = tome; - tome_arr.push(tome_name); - } - - // create dropdown - dropdowns.set(eq, new autoComplete({ - data: { - src: tome_arr - }, - selector: "#"+ eq +"-choice", - wrapper: false, - resultsList: { - maxResults: 1000, - tabSelect: true, - noResults: true, - class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", - element: (list, data) => { - // dynamic result loc - let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); - list.style.top = position.bottom + window.scrollY +"px"; - list.style.left = position.x+"px"; - list.style.width = position.width+"px"; - list.style.maxHeight = position.height * 2 +"px"; - - if (!data.results.length) { - message = document.createElement('li'); - message.classList.add('scaled-font'); - message.textContent = "No results found!"; - list.prepend(message); - } - }, - }, - resultItem: { - class: "scaled-font search-item", - selected: "dark-5", - element: (tome, data) => { - tome.classList.add(tomeMap.get(data.value).tier); - }, - }, - events: { - input: { - selection: (event) => { - if (event.detail.selection.value) { - event.target.value = event.detail.selection.value; - } - }, - }, - } - })); - } - - let filter_loc = ["filter1", "filter2", "filter3", "filter4"]; - for (const i of filter_loc) { - dropdowns.set(i+"-choice", new autoComplete({ - data: { - src: sq2ItemFilters, - }, - selector: "#"+i+"-choice", - wrapper: false, - resultsList: { - tabSelect: true, - noResults: true, - class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", - element: (list, data) => { - // dynamic result loc - console.log(i); - list.style.zIndex = "100"; - let position = document.getElementById(i+"-dropdown").getBoundingClientRect(); - window_pos = document.getElementById("search-container").getBoundingClientRect(); - list.style.top = position.bottom - window_pos.top + 5 +"px"; - list.style.left = position.x - window_pos.x +"px"; - list.style.width = position.width+"px"; - - if (!data.results.length) { - message = document.createElement('li'); - message.classList.add('scaled-font'); - message.textContent = "No filters found!"; - list.prepend(message); - } - }, - }, - resultItem: { - class: "scaled-font search-item", - selected: "dark-5", - }, - events: { - input: { - selection: (event) => { - if (event.detail.selection.value) { - event.target.value = event.detail.selection.value; - } - }, - }, - } - })); - } } + diff --git a/js/computation_graph.js b/js/computation_graph.js index fc5ad46..11113bd 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -22,10 +22,10 @@ class ComputeNode { * Request update of this compute node. Pushes updates to children. */ update() { - if (!this.dirty) { + if (this.inputs_dirty_count != 0) { return; } - if (this.inputs_dirty_count != 0) { + if (!this.dirty) { return; } let calc_inputs = new Map(); @@ -128,6 +128,9 @@ class PrintNode extends ComputeNode { /** * Node for getting an input from an input field. + * Fires updates whenever the input field is updated. + * + * Signature: InputNode() => str */ class InputNode extends ComputeNode { constructor(name, input_field) { @@ -141,110 +144,3 @@ class InputNode extends ComputeNode { return this.input_field.value; } } - -/** - * Node for getting an item's stats from an item input field. - */ -class ItemInputNode extends InputNode { - /** - * Make an item stat pulling compute node. - * - * @param name: Name of this node. - * @param item_input_field: Input field (html element) to listen for item names from. - * @param none_item: Item object to use as the "none" for this field. - */ - constructor(name, item_input_field, none_item) { - super(name, item_input_field); - this.none_item = new Item(none_item); - this.none_item.statMap.set('NONE', true); - } - - compute_func(input_map) { - // built on the assumption of no one will type in CI/CR letter by letter - - let item_text = this.input_field.value; - if (!item_text) { - return this.none_item; - } - - let item; - - if (item_text.slice(0, 3) == "CI-") { - item = getCustomFromHash(item_text); - } - else if (item_text.slice(0, 3) == "CR-") { - item = getCraftFromHash(item_text); - } - else if (itemMap.has(item_text)) { - item = new Item(itemMap.get(item_text)); - } - else if (tomeMap.has(item_text)) { - item = new Item(tomeMap.get(item_text)); - } - - if (item) { - let type_match; - if (this.none_item.statMap.get('category') === 'weapon') { - type_match = item.statMap.get('category') === 'weapon'; - } else { - type_match = item.statMap.get('type') === this.none_item.statMap.get('type'); - } - if (type_match) { return item; } - } - return null; - } -} - -/** - * Node for updating item input fields from parsed items. - */ -class ItemInputDisplayNode extends ComputeNode { - - constructor(name, item_input_field, item_image) { - super(name); - this.input_field = item_input_field; - this.image = item_image; - this.fail_cb = true; - } - - compute_func(input_map) { - if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; } - const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - - this.input_field.classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom'); - this.input_field.classList.add("text-light"); - this.image.classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow'); - - if (!item) { - this.input_field.classList.add("is-invalid"); - return null; - } - - if (item.statMap.has('NONE')) { - return null; - } - const tier = item.statMap.get('tier'); - this.input_field.classList.add(tier); - this.image.classList.add(tier + "-shadow"); - return null; - } -} - -/** - * Change the weapon to match correct type. - */ -class WeaponDisplayNode extends ComputeNode { - - constructor(name, image_field) { - super(name); - this.image = image_field; - } - - compute_func(input_map) { - if (input_map.size !== 1) { throw "WeaponDisplayNode accepts exactly one input (item)"; } - const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - - const type = item.statMap.get('type'); - this.image.setAttribute('src', '../media/items/new/generic-'+type+'.png'); - } -} diff --git a/js/display.js b/js/display.js index 0ef1c02..76f85cf 100644 --- a/js/display.js +++ b/js/display.js @@ -46,7 +46,7 @@ function displaySetBonuses(parent_id,build) { } for (const [setName, count] of build.activeSetCounts) { - const active_set = sets[setName]; + const active_set = sets.get(setName); if (active_set["hidden"]) { continue; } let set_elem = document.createElement('p'); @@ -353,12 +353,12 @@ function displayExpandedItem(item, parent_id){ */ //allow the plus minus element to toggle upon click: âž•âž– - let plusminus = document.createElement("div"); - plusminus.id = parent_div.id.split("-")[0] + "-pm"; - plusminus.classList.add("col", "plus_minus", "text_end"); - plusminus.style.flexGrow = 0; - plusminus.textContent = "\u2795"; - row.appendChild(plusminus); + //let plusminus = document.createElement("div"); + //plusminus.id = parent_div.id.split("-")[0] + "-pm"; + //plusminus.classList.add("col", "plus_minus", "text_end"); + //plusminus.style.flexGrow = 0; + //plusminus.textContent = "\u2795"; + //row.appendChild(plusminus); if (item.get("custom")) { a_elem.href = "../custom/#" + item.get("hash"); @@ -595,12 +595,17 @@ function displayExpandedItem(item, parent_id){ } } -/* Displays stats about a recipe that are NOT displayed in the craft stats. -* Includes: mat name and amounts -* ingred names in an "array" with ingred effectiveness +/* +* Displays stats about a recipe that are NOT displayed in the craft stats. +* Includes: mat name and amounts, ingred names in an "array" with ingred effectiveness */ function displayRecipeStats(craft, parent_id) { let elem = document.getElementById(parent_id); + if (!elem.classList.contains("col")) { + elem.classList.add("col"); + } + + //local vars elem.textContent = ""; recipe = craft["recipe"]; mat_tiers = craft["mat_tiers"]; @@ -610,30 +615,33 @@ function displayRecipeStats(craft, parent_id) { } let effectiveness = craft["statMap"].get("ingredEffectiveness"); - let ldiv = document.createElement("div"); - ldiv.classList.add("itemleft"); - let title = document.createElement("p"); - title.classList.add("smalltitle"); + let title = document.createElement("div"); + title.classList.add("row", "box-title", "fw-bold", "justify-content-center"); title.textContent = "Recipe Stats"; - ldiv.appendChild(title); - let mats = document.createElement("p"); - mats.classList.add("itemp"); + elem.appendChild(title); + + let mats = document.createElement("div"); + mats.classList.add("row"); mats.textContent = "Crafting Materials: "; + elem.appendChild(mats); + for (let i = 0; i < 2; i++) { let tier = mat_tiers[i]; - let row = document.createElement("p"); - row.classList.add("left"); - let b = document.createElement("b"); + let row = document.createElement("div"); + row.classList.add("row", "px-0", "mx-0"); + let b = document.createElement("div"); let mat = recipe.get("materials")[i]; b.textContent = "- " + mat.get("amount") + "x " + mat.get("item").split(" ").slice(1).join(" "); - b.classList.add("space"); - let starsB = document.createElement("b"); - starsB.classList.add("T1-bracket"); - starsB.textContent = "["; + b.classList.add("col"); row.appendChild(b); + + let starsB = document.createElement("div"); + starsB.classList.add("T1-bracket", "col-auto", "px-0"); + starsB.textContent = "["; row.appendChild(starsB); for(let j = 0; j < 3; j ++) { - let star = document.createElement("b"); + let star = document.createElement("div"); + star.classList.add("col-auto", "px-0"); star.textContent = "\u272B"; if(j < tier) { star.classList.add("T1"); @@ -642,51 +650,57 @@ function displayRecipeStats(craft, parent_id) { } row.append(star); } - let starsE = document.createElement("b"); - starsE.classList.add("T1-bracket"); + let starsE = document.createElement("div"); + starsE.classList.add("T1-bracket", "col-auto", "px-0"); starsE.textContent = "]"; row.appendChild(starsE); - mats.appendChild(row); - } - ldiv.appendChild(mats); - let ingredTable = document.createElement("table"); - ingredTable.classList.add("itemtable"); - ingredTable.classList.add("ingredTable"); + elem.appendChild(row); + } + + let ingredTable = document.createElement("div"); + ingredTable.classList.add("row"); + for (let i = 0; i < 3; i++) { - let row = document.createElement("tr"); + let row = document.createElement("div"); + row.classList.add("row", "g-1", "justify-content-center"); + + for (let j = 0; j < 2; j++) { + if (j == 1) { + let spacer = document.createElement("div"); + spacer.classList.add("col-1"); + row.appendChild(spacer); + } let ingredName = ingreds[2 * i + j]; - let cell = document.createElement("td"); - cell.style.minWidth = "50%"; - cell.classList.add("center"); - cell.classList.add("box"); - cell.classList.add("tooltip"); - let b = document.createElement("b"); - b.textContent = ingredName; - b.classList.add("space"); - let eff = document.createElement("b"); + let col = document.createElement("div"); + col.classList.add("col-5", "rounded", "dark-6", "border", "border-3", "dark-shadow"); + + let temp_row = document.createElement("div"); + temp_row.classList.add("row"); + col.appendChild(temp_row); + + let ingred_div = document.createElement("div"); + ingred_div.classList.add("col"); + ingred_div.textContent = ingredName; + temp_row.appendChild(ingred_div); + + let eff_div = document.createElement("div"); + eff_div.classList.add("col-auto"); let e = effectiveness[2 * i + j]; if (e > 0) { - eff.classList.add("positive"); + eff_div.classList.add("positive"); } else if (e < 0) { - eff.classList.add("negative"); + eff_div.classList.add("negative"); } - eff.textContent = "[" + e + "%]"; - cell.appendChild(b); - cell.appendChild(eff); - row.appendChild(cell); + eff_div.textContent = "[" + e + "%]"; - let tooltip = document.createElement("div"); - tooltip.classList.add("tooltiptext"); - tooltip.classList.add("ing-tooltip"); - tooltip.classList.add("center"); - tooltip.id = "tooltip-" + (2*i + j); - cell.appendChild(tooltip); + temp_row.appendChild(eff_div); + + row.appendChild(col); } ingredTable.appendChild(row); } - elem.appendChild(ldiv); elem.appendChild(ingredTable); } @@ -696,23 +710,14 @@ function displayCraftStats(craft, parent_id) { displayExpandedItem(mock_item,parent_id); } -//Displays an ingredient in item format. However, an ingredient is too far from a normal item to display as one. +/* +* Displays an ingredient in item format. +* However, an ingredient is too far from a normal item to display as one. +*/ function displayExpandedIngredient(ingred, parent_id) { let parent_elem = document.getElementById(parent_id); parent_elem.textContent = ""; - let display_order = [ - "#cdiv", - "displayName", //tier will be displayed w/ name - "#table", - "ids", - "#ldiv", - "posMods", - "itemIDs", - "consumableIDs", - "#ldiv", - "lvl", - "skills", - ] + let item_order = [ "dura", "strReq", @@ -787,83 +792,85 @@ function displayExpandedIngredient(ingred, parent_id) { let active_elem; let elemental_format = false; let style; - for (const command of display_order) { - if (command.charAt(0) === "#") { - if (command === "#cdiv") { - active_elem = document.createElement('div'); - active_elem.classList.add('itemcenter'); + for (const command of sq2_ing_display_order) { + if (command.charAt(0) === "!") { + // TODO: This is sooo incredibly janky..... + if (command === "!elemental") { + elemental_format = !elemental_format; } - else if (command === "#ldiv") { - active_elem = document.createElement('div'); - active_elem.classList.add('itemleft'); + else if (command === "!spacer") { + let spacer = document.createElement('div'); + spacer.classList.add("row", "my-2"); + parent_elem.appendChild(spacer); + continue; } - else if (command === "#table") { - active_elem = document.createElement('table'); - active_elem.classList.add('itemtable'); - } - parent_elem.appendChild(active_elem); - }else { - let p_elem = document.createElement("p"); - p_elem.classList.add("left"); + } else { + let div = document.createElement("div"); + div.classList.add("row"); if (command === "displayName") { - p_elem.classList.add("title"); - p_elem.classList.remove("left"); - let title_elem = document.createElement("b"); + div.classList.add("box-title"); + let title_elem = document.createElement("div"); + title_elem.classList.add("col-auto", "justify-content-center", "pr-1"); title_elem.textContent = ingred.get("displayName"); - p_elem.appendChild(title_elem); - - let space = document.createElement("b"); - space.classList.add("space"); - p_elem.appendChild(space); + div.appendChild(title_elem); let tier = ingred.get("tier"); //tier in [0,3] let begin = document.createElement("b"); - begin.classList.add("T"+tier+"-bracket"); + begin.classList.add("T"+tier+"-bracket", "col-auto", "px-0"); begin.textContent = "["; - p_elem.appendChild(begin); + div.appendChild(begin); for (let i = 0; i < 3; i++) { let tier_elem = document.createElement("b"); - if(i < tier) {tier_elem.classList.add("T"+tier)} - else {tier_elem.classList.add("T0")} + if (i < tier) { + tier_elem.classList.add("T"+tier); + } else { + tier_elem.classList.add("T0"); + } + tier_elem.classList.add("px-0", "col-auto"); tier_elem.textContent = "\u272B"; - p_elem.appendChild(tier_elem); + div.appendChild(tier_elem); } let end = document.createElement("b"); - end.classList.add("T"+tier+"-bracket"); + end.classList.add("T"+tier+"-bracket", "px-0", "col-auto"); end.textContent = "]"; - p_elem.appendChild(end); + div.appendChild(end); }else if (command === "lvl") { - p_elem.textContent = "Crafting Lvl Min: " + ingred.get("lvl"); + div.textContent = "Crafting Lvl Min: " + ingred.get("lvl"); }else if (command === "posMods") { for (const [key,value] of ingred.get("posMods")) { - let p = document.createElement("p"); - p.classList.add("nomarginp"); + let posModRow = document.createElement("div"); + posModRow.classList.add("row"); if (value != 0) { - let title = document.createElement("b"); - title.textContent = posModPrefixes[key]; - let val = document.createElement("b"); + let posMod = document.createElement("div"); + posMod.classList.add("col-auto"); + posMod.textContent = posModPrefixes[key]; + posModRow.appendChild(posMod); + + let val = document.createElement("div"); + val.classList.add("col-auto", "px-0"); val.textContent = value + posModSuffixes[key]; if(value > 0) { val.classList.add("positive"); } else { val.classList.add("negative"); } - p.appendChild(title); - p.appendChild(val); - p_elem.appendChild(p); + posModRow.appendChild(val); + div.appendChild(posModRow); } } } else if (command === "itemIDs") { //dura, reqs for (const [key,value] of ingred.get("itemIDs")) { - let p = document.createElement("p"); - p.classList.add("nomarginp"); + let idRow = document.createElement("div"); + idRow.classList.add("row"); if (value != 0) { - let title = document.createElement("b"); + let title = document.createElement("div"); + title.classList.add("col-auto"); title.textContent = itemIDPrefixes[key]; - p.appendChild(title); + idRow.appendChild(title); } - let desc = document.createElement("b"); + let desc = document.createElement("div"); + desc.classList.add("col-auto"); if(value > 0) { if(key !== "dura") { desc.classList.add("negative"); @@ -880,20 +887,22 @@ function displayExpandedIngredient(ingred, parent_id) { desc.textContent = value; } if(value != 0){ - p.appendChild(desc); + idRow.appendChild(desc); } - p_elem.append(p); + div.appendChild(idRow); } } else if (command === "consumableIDs") { //dura, charges for (const [key,value] of ingred.get("consumableIDs")) { - let p = document.createElement("p"); - p.classList.add("nomarginp"); + let idRow = document.createElement("div"); + idRow.classList.add("row"); if (value != 0) { - let title = document.createElement("b"); + let title = document.createElement("div"); + title.classList.add("col-auto"); title.textContent = consumableIDPrefixes[key]; - p.appendChild(title); + idRow.appendChild(title); } - let desc = document.createElement("b"); + let desc = document.createElement("div"); + desc.classList.add("col-auto"); if(value > 0) { desc.classList.add("positive"); desc.textContent = "+"+value; @@ -902,32 +911,41 @@ function displayExpandedIngredient(ingred, parent_id) { desc.textContent = value; } if(value != 0){ - p.appendChild(desc); - let suffix = document.createElement("b"); + idRow.appendChild(desc); + let suffix = document.createElement("div"); + suffix.classList.add("col-auto"); suffix.textContent = consumableIDSuffixes[key]; - p.appendChild(suffix); + idRow.appendChild(suffix); } - p_elem.append(p); + div.appendChild(idRow); } }else if (command === "skills") { - p_elem.textContent = "Used in:"; + let row = document.createElement("div"); + row.classList.add("row"); + let title = document.createElement("div"); + title.classList.add("row"); + title.textContent = "Used in:"; + row.appendChild(title); for(const skill of ingred.get("skills")) { - let p = document.createElement("p"); - p.textContent = skill.charAt(0) + skill.substring(1).toLowerCase(); - p.classList.add("left"); - p_elem.append(p); + let skill_div = document.createElement("div"); + skill_div.classList.add("row"); + skill_div.textContent = skill.charAt(0) + skill.substring(1).toLowerCase(); + row.appendChild(skill_div); } + div.appendChild(row); } else if (command === "ids") { //warp for (let [key,value] of ingred.get("ids").get("maxRolls")) { if (value !== undefined && value != 0) { - let row = displayRolledID(ingred.get("ids"), key, false, "auto"); - active_elem.appendChild(row); + let row = displayRolledID(ingred.get("ids"), key, elemental_format); + row.classList.remove("col"); + row.classList.remove("col-12"); + div.appendChild(row); } } } else {//this shouldn't be happening } - active_elem.appendChild(p_elem); + parent_elem.appendChild(div); } } } @@ -1083,29 +1101,21 @@ function displayPoisonDamage(overallparent_elem, build) { overallparent_elem.append(overallpoisonDamage); } -function displayEquipOrder(parent_elem,buildOrder){ +function displayEquipOrder(parent_elem, buildOrder){ parent_elem.textContent = ""; const order = buildOrder.slice(); - let title_elem = document.createElement("p"); + let title_elem = document.createElement("b"); title_elem.textContent = "Equip order "; - title_elem.classList.add("title"); - title_elem.classList.add("Normal"); - title_elem.classList.add("itemp"); + title_elem.classList.add("Normal", "text-center"); parent_elem.append(title_elem); - parent_elem.append(document.createElement("br")); for (const item of order) { - let p_elem = document.createElement("p"); - p_elem.classList.add("itemp"); - p_elem.classList.add("left"); + let p_elem = document.createElement("b"); p_elem.textContent = item.get("displayName"); parent_elem.append(p_elem); } } - -function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ - console.log("Melee Stats"); - console.log(meleeStats); +function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { let tooltipinfo = meleeStats[13]; let attackSpeeds = ["Super Slow", "Very Slow", "Slow", "Normal", "Fast", "Very Fast", "Super Fast"]; //let damagePrefixes = ["Neutral Damage: ","Earth Damage: ","Thunder Damage: ","Water Damage: ","Fire Damage: ","Air Damage: "]; @@ -1123,7 +1133,7 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ stats[i][j] = stats[i][j].toFixed(2); } } - for (let i = 8; i < 11; ++i){ + for (let i = 8; i < 11; ++i) { stats[i] = stats[i].toFixed(2); } //tooltipelem, tooltiptext @@ -1132,24 +1142,18 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ //title let title_elem = document.createElement("p"); title_elem.classList.add("title"); - title_elem.classList.add("Normal"); - title_elem.classList.add("itemp"); title_elem.textContent = "Melee Stats"; parent_elem.append(title_elem); parent_elem.append(document.createElement("br")); //overall title - let title_elemavg = document.createElement("p"); - title_elemavg.classList.add("smalltitle"); - title_elemavg.classList.add("Normal"); + let title_elemavg = document.createElement("b"); title_elemavg.textContent = "Melee Stats"; overallparent_elem.append(title_elemavg); //average DPS let averageDamage = document.createElement("p"); averageDamage.classList.add("left"); - averageDamage.classList.add("itemp"); - averageDamage.classList.add("tooltip"); averageDamage.textContent = "Average DPS: " + stats[10]; tooltiptext = `= ((${stats[8]} * ${(stats[6][2]).toFixed(2)}) + (${stats[9]} * ${(stats[7][2]).toFixed(2)}))` tooltip = createTooltip(tooltip, "p", tooltiptext, averageDamage, ["melee-tooltip"]); @@ -1158,14 +1162,12 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ //overall average DPS let overallaverageDamage = document.createElement("p"); - overallaverageDamage.classList.add("itemp"); - let overallaverageDamageFirst = document.createElement("b"); + let overallaverageDamageFirst = document.createElement("span"); overallaverageDamageFirst.textContent = "Average DPS: " - let overallaverageDamageSecond = document.createElement("b"); + let overallaverageDamageSecond = document.createElement("span"); overallaverageDamageSecond.classList.add("Damage"); overallaverageDamageSecond.textContent = stats[10]; - tooltip = createTooltip(tooltip, "p", tooltiptext, overallaverageDamage, ["melee-tooltip", "summary-tooltip"]); overallaverageDamage.appendChild(overallaverageDamageFirst); overallaverageDamage.appendChild(overallaverageDamageSecond); @@ -1175,18 +1177,15 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ //attack speed let atkSpd = document.createElement("p"); atkSpd.classList.add("left"); - atkSpd.classList.add("itemp"); atkSpd.textContent = "Attack Speed: " + attackSpeeds[stats[11]]; parent_elem.append(atkSpd); parent_elem.append(document.createElement("br")); //overall attack speed let overallatkSpd = document.createElement("p"); - overallatkSpd.classList.add("center"); - overallatkSpd.classList.add("itemp"); - let overallatkSpdFirst = document.createElement("b"); + let overallatkSpdFirst = document.createElement("span"); overallatkSpdFirst.textContent = "Attack Speed: "; - let overallatkSpdSecond = document.createElement("b"); + let overallatkSpdSecond = document.createElement("span"); overallatkSpdSecond.classList.add("Damage"); overallatkSpdSecond.textContent = attackSpeeds[stats[11]]; overallatkSpd.appendChild(overallatkSpdFirst); @@ -1196,11 +1195,10 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ //Non-Crit: n->elem, total dmg, DPS let nonCritStats = document.createElement("p"); nonCritStats.classList.add("left"); - nonCritStats.classList.add("itemp"); nonCritStats.textContent = "Non-Crit Stats: "; nonCritStats.append(document.createElement("br")); - for (let i = 0; i < 6; i++){ - if(stats[i][1] != 0){ + for (let i = 0; i < 6; i++) { + if (stats[i][1] != 0) { let dmg = document.createElement("p"); dmg.textContent = stats[i][0] + " \u2013 " + stats[i][1]; dmg.classList.add(damageClasses[i]); @@ -1213,7 +1211,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ let normalDamage = document.createElement("p"); normalDamage.textContent = "Total: " + stats[6][0] + " \u2013 " + stats[6][1]; - normalDamage.classList.add("itemp"); let tooltiparr = ["Min: = ", "Max: = "] let arr = []; let arr2 = []; for (let i = 0; i < 6; i++) { @@ -1228,7 +1225,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ let normalDPS = document.createElement("p"); normalDPS.textContent = "Normal DPS: " + stats[8]; - normalDPS.classList.add("itemp"); normalDPS.classList.add("tooltip"); tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; tooltip = createTooltip(tooltip, "p", tooltiptext, normalDPS, ["melee-tooltip"]); @@ -1236,14 +1232,13 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ //overall average DPS let singleHitDamage = document.createElement("p"); - singleHitDamage.classList.add("itemp"); - let singleHitDamageFirst = document.createElement("b"); + let singleHitDamageFirst = document.createElement("span"); singleHitDamageFirst.textContent = "Single Hit Average: "; - let singleHitDamageSecond = document.createElement("b"); + let singleHitDamageSecond = document.createElement("span"); singleHitDamageSecond.classList.add("Damage"); singleHitDamageSecond.textContent = stats[12].toFixed(2); tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${stats[6][2].toFixed(2)} + ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${stats[7][2].toFixed(2)}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, singleHitDamage, ["melee-tooltip", "summary-tooltip"]); + // tooltip = createTooltip(tooltip, "p", tooltiptext, singleHitDamage, ["melee-tooltip", "summary-tooltip"]); singleHitDamage.appendChild(singleHitDamageFirst); singleHitDamage.appendChild(singleHitDamageSecond); @@ -1251,7 +1246,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ let normalChance = document.createElement("p"); normalChance.textContent = "Non-Crit Chance: " + (stats[6][2]*100).toFixed(2) + "%"; - normalChance.classList.add("itemp"); normalChance.append(document.createElement("br")); normalChance.append(document.createElement("br")); nonCritStats.append(normalChance); @@ -1262,7 +1256,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ //Crit: n->elem, total dmg, DPS let critStats = document.createElement("p"); critStats.classList.add("left"); - critStats.classList.add("itemp"); critStats.textContent = "Crit Stats: "; critStats.append(document.createElement("br")); for (let i = 0; i < 6; i++){ @@ -1278,7 +1271,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ } let critDamage = document.createElement("p"); critDamage.textContent = "Total: " + stats[7][0] + " \u2013 " + stats[7][1]; - critDamage.classList.add("itemp"); tooltiparr = ["Min: = ", "Max: = "] arr = []; arr2 = []; for (let i = 0; i < 6; i++) { @@ -1294,14 +1286,12 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats){ let critDPS = document.createElement("p"); critDPS.textContent = "Crit DPS: " + stats[9]; - critDPS.classList.add("itemp"); tooltiptext = ` = ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; tooltip = createTooltip(tooltip, "p", tooltiptext, critDPS, ["melee-tooltip"]); critStats.append(critDPS); let critChance = document.createElement("p"); critChance.textContent = "Crit Chance: " + (stats[7][2]*100).toFixed(2) + "%"; - critChance.classList.add("itemp"); critChance.append(document.createElement("br")); critChance.append(document.createElement("br")); critStats.append(critChance); @@ -2249,6 +2239,7 @@ function stringPDF(id,val,base,amp) { document.getElementById(id + "-pdf").appendChild(b2); document.getElementById(id + "-pdf").appendChild(b3); } + function stringCDF(id,val,base,amp) { let p; let min; let max; let minr; let maxr; let minround; let maxround; if (base > 0) { diff --git a/js/utils.js b/js/utils.js index 164543d..5691ac9 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,7 +1,8 @@ let getUrl = window.location; const url_base = getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.split('/')[1]; -const zip = (a, b) => a.map((k, i) => [k, b[i]]); +const zip2 = (a, b) => a.map((k, i) => [k, b[i]]); +const zip3 = (a, b, c) => a.map((k, i) => [k, b[i], c[i]]); function clamp(num, low, high){ return Math.min(Math.max(num, low), high); From 7980980f9a806fbc89f3cabfb3663af77a792e6d Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 13:59:16 -0700 Subject: [PATCH 29/68] Build display working, still have to do edit ids / skillpoints --- builder/index.html | 1 - js/build.js | 10 +++--- js/build_constants.js | 1 + js/build_utils.js | 6 ++-- js/builder_graph.js | 6 ++++ js/display.js | 67 +++++++---------------------------------- js/display_constants.js | 63 ++++++++++++++++++++++++++++++-------- js/load_tome.js | 2 +- 8 files changed, 78 insertions(+), 78 deletions(-) diff --git a/builder/index.html b/builder/index.html index 73e20b1..283d301 100644 --- a/builder/index.html +++ b/builder/index.html @@ -1391,7 +1391,6 @@ - diff --git a/js/build.js b/js/build.js index 00300c1..f451ff8 100644 --- a/js/build.js +++ b/js/build.js @@ -221,19 +221,19 @@ class Build{ //EHP let ehp = [totalHp, totalHp]; let defMult = classDefenseMultipliers.get(this.weapon.statMap.get("type")); - ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); - ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); + ehp[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); + ehp[1] /= (1-def_pct)*(2-defMult); defenseStats.push(ehp); //HPR let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); defenseStats.push(totalHpr); //EHPR let ehpr = [totalHpr, totalHpr]; - ehpr[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); - ehpr[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); + ehpr[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); + ehpr[1] /= (1-def_pct)*(2-defMult); defenseStats.push(ehpr); //skp stats - defenseStats.push([ (1 - ((1-def_pct) * (2 - this.defenseMultiplier)))*100, agi_pct*100]); + defenseStats.push([ def_pct*100, agi_pct*100]); //eledefs - TODO POWDERS let eledefs = [0, 0, 0, 0, 0]; for(const i in skp_elements){ //kinda jank but ok diff --git a/js/build_constants.js b/js/build_constants.js index 820beb7..eeaae5d 100644 --- a/js/build_constants.js +++ b/js/build_constants.js @@ -101,6 +101,7 @@ let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots']; let accessory_keys= ['ring1', 'ring2', 'bracelet', 'necklace']; let powderable_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'weapon']; let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon']; +let tome_keys = ['weaponTome1', 'weaponTome2', 'armorTome1', 'armorTome2', 'armorTome3', 'armorTome4', 'guildTome1']; let spell_disp = ['spell0-info', 'spell1-info', 'spell2-info', 'spell3-info']; let other_disp = ['build-order', 'set-info', 'int-info']; diff --git a/js/build_utils.js b/js/build_utils.js index 4da3309..bbb2d89 100644 --- a/js/build_utils.js +++ b/js/build_utils.js @@ -51,16 +51,16 @@ const armorTypes = [ "helmet", "chestplate", "leggings", "boots" ]; const accessoryTypes = [ "ring", "bracelet", "necklace" ]; const weaponTypes = [ "wand", "spear", "bow", "dagger", "relik" ]; const consumableTypes = [ "potion", "scroll", "food"]; -const tomeTypes = ["armorTome", "weaponTome", "guildTome"]; //"dungeonTome", "gatheringTome", "slayingTome" +const tome_types = ['weaponTome', 'armorTome', 'guildTome']; const attackSpeeds = ["SUPER_SLOW", "VERY_SLOW", "SLOW", "NORMAL", "FAST", "VERY_FAST", "SUPER_FAST"]; const baseDamageMultiplier = [ 0.51, 0.83, 1.5, 2.05, 2.5, 3.1, 4.3 ]; //0.51, 0.82, 1.50, 2.05, 2.50, 3.11, 4.27 const classes = ["Warrior", "Assassin", "Mage", "Archer", "Shaman"]; const tiers = ["Normal", "Unique", "Rare", "Legendary", "Fabled", "Mythic", "Set", "Crafted"] //I'm not sure why you would make a custom crafted but if you do you should be able to use it w/ the correct powder formula -const types = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(consumableTypes).concat(tomeTypes).map(x => x.substring(0,1).toUpperCase() + x.substring(1)); +const types = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(consumableTypes).concat(tome_types).map(x => x.substring(0,1).toUpperCase() + x.substring(1)); //weaponTypes.push("sword"); //console.log(types) -let itemTypes = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(tomeTypes); +let itemTypes = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(tome_types); let elementIcons = ["\u2724","\u2726", "\u2749", "\u2739", "\u274b" ]; let skpReqs = skp_order.map(x => x + "Req"); diff --git a/js/builder_graph.js b/js/builder_graph.js index 083eb32..24c1d2d 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -383,6 +383,12 @@ class BuildDisplayNode extends ComputeNode { displayBuildStats('overall-stats', build, build_all_display_commands); displayBuildStats("offensive-stats", build, build_offensive_display_commands); displaySetBonuses("set-info", build); + let meleeStats = build.getMeleeStats(); + displayMeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); + + displayDefenseStats(document.getElementById("defensive-stats"), build); + + displayPoisonDamage(document.getElementById("build-poison-stats"), build); } } diff --git a/js/display.js b/js/display.js index 76f85cf..3f02bd5 100644 --- a/js/display.js +++ b/js/display.js @@ -1116,7 +1116,6 @@ function displayEquipOrder(parent_elem, buildOrder){ } function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { - let tooltipinfo = meleeStats[13]; let attackSpeeds = ["Super Slow", "Very Slow", "Slow", "Normal", "Fast", "Very Fast", "Super Fast"]; //let damagePrefixes = ["Neutral Damage: ","Earth Damage: ","Thunder Damage: ","Water Damage: ","Fire Damage: ","Air Damage: "]; parent_elem.textContent = ""; @@ -1136,8 +1135,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { for (let i = 8; i < 11; ++i) { stats[i] = stats[i].toFixed(2); } - //tooltipelem, tooltiptext - let tooltip; let tooltiptext; //title let title_elem = document.createElement("p"); @@ -1155,9 +1152,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { let averageDamage = document.createElement("p"); averageDamage.classList.add("left"); averageDamage.textContent = "Average DPS: " + stats[10]; - tooltiptext = `= ((${stats[8]} * ${(stats[6][2]).toFixed(2)}) + (${stats[9]} * ${(stats[7][2]).toFixed(2)}))` - tooltip = createTooltip(tooltip, "p", tooltiptext, averageDamage, ["melee-tooltip"]); - averageDamage.appendChild(tooltip); parent_elem.append(averageDamage); //overall average DPS @@ -1203,31 +1197,16 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { dmg.textContent = stats[i][0] + " \u2013 " + stats[i][1]; dmg.classList.add(damageClasses[i]); dmg.classList.add("itemp"); - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(0,2).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, dmg, ["melee-tooltip"]); nonCritStats.append(dmg); } } let normalDamage = document.createElement("p"); normalDamage.textContent = "Total: " + stats[6][0] + " \u2013 " + stats[6][1]; - let tooltiparr = ["Min: = ", "Max: = "] - let arr = []; let arr2 = []; - for (let i = 0; i < 6; i++) { - if (stats[i][0] != 0) { - arr.push(stats[i][0]); - arr2.push(stats[i][1]); - } - } - tooltiptext = tooltiparr[0] + arr.join(" + ") + "\n" + tooltiparr[1] + arr2.join(" + "); - tooltip = createTooltip(tooltip, "p", tooltiptext, normalDamage, ["melee-tooltip"]); nonCritStats.append(normalDamage); let normalDPS = document.createElement("p"); normalDPS.textContent = "Normal DPS: " + stats[8]; - normalDPS.classList.add("tooltip"); - tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, normalDPS, ["melee-tooltip"]); nonCritStats.append(normalDPS); //overall average DPS @@ -1237,9 +1216,6 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { let singleHitDamageSecond = document.createElement("span"); singleHitDamageSecond.classList.add("Damage"); singleHitDamageSecond.textContent = stats[12].toFixed(2); - tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${stats[6][2].toFixed(2)} + ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${stats[7][2].toFixed(2)}`; - // tooltip = createTooltip(tooltip, "p", tooltiptext, singleHitDamage, ["melee-tooltip", "summary-tooltip"]); - singleHitDamage.appendChild(singleHitDamageFirst); singleHitDamage.appendChild(singleHitDamageSecond); overallparent_elem.append(singleHitDamage); @@ -1264,30 +1240,15 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { dmg.textContent = stats[i][2] + " \u2013 " + stats[i][3]; dmg.classList.add(damageClasses[i]); dmg.classList.add("itemp"); - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(2,4).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, dmg, ["melee-tooltip"]); critStats.append(dmg); } } let critDamage = document.createElement("p"); critDamage.textContent = "Total: " + stats[7][0] + " \u2013 " + stats[7][1]; - tooltiparr = ["Min: = ", "Max: = "] - arr = []; arr2 = []; - for (let i = 0; i < 6; i++) { - if (stats[i][0] != 0) { - arr.push(stats[i][2]); - arr2.push(stats[i][3]); - } - } - tooltiptext = tooltiparr[0] + arr.join(" + ") + "\n" + tooltiparr[1] + arr2.join(" + "); - tooltip = createTooltip(tooltip, "p", tooltiptext, critDamage, ["melee-tooltip"]); - critStats.append(critDamage); let critDPS = document.createElement("p"); critDPS.textContent = "Crit DPS: " + stats[9]; - tooltiptext = ` = ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, critDPS, ["melee-tooltip"]); critStats.append(critDPS); let critChance = document.createElement("p"); @@ -1343,8 +1304,6 @@ function displayDefenseStats(parent_elem, build, insertSummary){ statsTable.appendChild(hpRow); } - let tooltip; let tooltiptext; - let defMult = build.statMap.get("defMult"); if (!defMult) {defMult = 1} @@ -1360,9 +1319,6 @@ function displayDefenseStats(parent_elem, build, insertSummary){ boost.textContent = stats[1][0]; boost.classList.add("col"); boost.classList.add("text-end"); - tooltiptext = `= ${stats[0]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (1 - ${skillPointsToPercentage(build.total_skillpoints[4]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - ehpRow.appendChild(ehp); ehpRow.append(boost); @@ -1383,12 +1339,13 @@ function displayDefenseStats(parent_elem, build, insertSummary){ boost.textContent = stats[1][1]; boost.classList.add("col"); boost.classList.add("text-end"); - tooltiptext = `= ${stats[0]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - ehpRow.appendChild(ehp); ehpRow.append(boost); - statsTable.append(ehpRow); + if (insertSummary) { + parent_elem.appendChild(ehpRow) + } else { + statsTable.append(ehpRow); + } //total HPR let hprRow = document.createElement("div"); @@ -1424,12 +1381,14 @@ function displayDefenseStats(parent_elem, build, insertSummary){ boost.textContent = stats[3][0]; boost.classList.add("col"); boost.classList.add("text-end"); - tooltiptext = `= ${stats[2]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (1 - ${skillPointsToPercentage(build.total_skillpoints[4]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - ehprRow.appendChild(ehpr); ehprRow.append(boost); - statsTable.append(ehprRow); + + if (insertSummary) { + parent_elem.appendChild(ehprRow); + } else { + statsTable.appendChild(ehprRow); + } //eledefs let eledefs = stats[5]; @@ -1461,13 +1420,9 @@ function displayDefenseStats(parent_elem, build, insertSummary){ let defPct = build.statMap.get("defBonus")[i]/100; if (defRaw < 0) { defPct >= 0 ? defPct = "- " + defPct: defPct = "+ " + defPct; - tooltiptext = `= min(0, ${defRaw} * (1 ${defPct}))` } else { defPct >= 0 ? defPct = "+ " + defPct: defPct = "- " + defPct; - tooltiptext = `= ${defRaw} * (1 ${defPct})` } - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - eledefElemRow.appendChild(boost); if (insertSummary) { diff --git a/js/display_constants.js b/js/display_constants.js index 93f77e6..4901e88 100644 --- a/js/display_constants.js +++ b/js/display_constants.js @@ -209,8 +209,8 @@ let posModSuffixes = { /* * Display commands */ -let build_overall_display_commands = [ - "#table", +let build_all_display_commands = [ + "#defense-stats", "str", "dex", "int", "def", "agi", "mr", "ms", "hprRaw", "hprPct", @@ -235,26 +235,51 @@ let build_overall_display_commands = [ "gXp", "gSpd", ]; -let item_display_commands = [ - "#cdiv", +let build_offensive_display_commands = [ + "str", "dex", "int", "def", "agi", + "mr", "ms", + "sdRaw", "sdPct", + "mdRaw", "mdPct", + "ref", "thorns", + "ls", + "poison", + "expd", + "spd", + "atkTier", + "rainbowRaw", + "!elemental", + "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", + "!elemental", + "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", +]; + +let build_basic_display_commands = [ + '#defense-stats', + // defense stats [hp, ehp, hpr, ] + // "sPot", // base * atkspd + spell raws + // melee potential + // "mPot", // melee% * (base * atkspd) + melee raws + "mr", "ms", + "ls", + "poison", + "spd", + "atkTier", +] + +let sq2_item_display_commands = [ "displayName", - //"type", //REPLACE THIS WITH SKIN - "#ldiv", "atkSpd", - "#ldiv", "!elemental", "hp", "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", + "!spacer", "fDef", "wDef", "aDef", "tDef", "eDef", "!elemental", - "#ldiv", "classReq", "lvl", "strReq", "dexReq", "intReq", "defReq","agiReq", - "#ldiv", + "!spacer", "str", "dex", "int", "def", "agi", - "#table", - "str", "dex", "int", "def", "agi", //jank lmao "hpBonus", "hprRaw", "hprPct", "sdRaw", "sdPct", @@ -278,11 +303,25 @@ let item_display_commands = [ "spRegen", "eSteal", "gXp", "gSpd", - "#ldiv", "majorIds", + "!spacer", "slots", + "!spacer", "set", "lore", "quest", "restrict" ]; + +let sq2_ing_display_order = [ + "displayName", //tier will be displayed w/ name + "!spacer", + "ids", + "!spacer", + "posMods", + "itemIDs", + "consumableIDs", + "!spacer", + "lvl", + "skills", +] diff --git a/js/load_tome.js b/js/load_tome.js index 2186a60..62bb135 100644 --- a/js/load_tome.js +++ b/js/load_tome.js @@ -128,7 +128,7 @@ function init_tome_maps() { tomeIDMap = new Map(); tomeRedirectMap = new Map(); - for (const it of tomeTypes) { + for (const it of tome_types) { tomeLists.set(it, []); } From 4a54635e2d9b319dec82e05cad11d8c832469b4f Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 19:07:59 -0700 Subject: [PATCH 30/68] More cleanup --- js/builder_graph.js | 13 +++++++++ js/computation_graph.js | 2 +- js/display.js | 63 ++--------------------------------------- 3 files changed, 16 insertions(+), 62 deletions(-) diff --git a/js/builder_graph.js b/js/builder_graph.js index 24c1d2d..663e14a 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -389,9 +389,17 @@ class BuildDisplayNode extends ComputeNode { displayDefenseStats(document.getElementById("defensive-stats"), build); displayPoisonDamage(document.getElementById("build-poison-stats"), build); + displayEquipOrder(document.getElementById("build-order"), build.equip_order); } } +/** + * Set the editble id fields. + */ +class EditableIDSetterNode extends ComputeNode { + +} + let item_nodes = []; let powder_nodes = []; let spelldmg_nodes = []; @@ -455,6 +463,11 @@ function builder_graph_init() { display_node.link_to(calc_node, 'spell-damage'); } + for (const input_node of item_nodes.concat(powder_nodes)) { + input_node.update(); + } + level_input.update(); + console.log("Set up graph"); } diff --git a/js/computation_graph.js b/js/computation_graph.js index 11113bd..68a1717 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -137,7 +137,7 @@ class InputNode extends ComputeNode { super(name); this.input_field = input_field; this.input_field.addEventListener("input", () => calcSchedule(this)); - calcSchedule(this); + //calcSchedule(this); Manually fire first update for better control } compute_func(input_map) { diff --git a/js/display.js b/js/display.js index 3f02bd5..c9ea548 100644 --- a/js/display.js +++ b/js/display.js @@ -35,15 +35,9 @@ function displaySetBonuses(parent_id,build) { let parent_div = document.getElementById(parent_id); let set_summary_elem = document.createElement('p'); - set_summary_elem.classList.add('itemcenter'); - set_summary_elem.textContent = "Set Bonuses:"; + set_summary_elem.classList.add('text-center'); + set_summary_elem.textContent = "Set Bonuses"; parent_div.append(set_summary_elem); - - if (build.activeSetCounts.size) { - parent_div.parentElement.style.display = "block"; - } else { - parent_div.parentElement.style.display = "none"; - } for (const [setName, count] of build.activeSetCounts) { const active_set = sets.get(setName); @@ -76,7 +70,6 @@ function displaySetBonuses(parent_id,build) { } } - function displayBuildStats(parent_id,build,command_group){ // Commands to "script" the creation of nice formatting. // #commands create a new element. @@ -1916,58 +1909,6 @@ function displayAdditionalInfo(elemID, item) { return; } -/** Displays all set bonuses (0/n, 1/n, ... n/n) for a given set - * - * @param {String} parent_id - id of the parent element - * @param {String} setName - the name of the set - */ - function displayAllSetBonuses(parent_id,setName) { - let parent_elem = document.getElementById(parent_id); - parent_elem.style.display = ""; - let set = sets[setName]; - let title_elem = document.createElement("p"); - title_elem.textContent = setName + " Set Bonuses"; - title_elem.classList.add("Set"); - title_elem.classList.add("title"); - parent_elem.appendChild(title_elem); - let grid_elem = document.createElement("div"); - grid_elem.style.display = "flex"; - grid_elem.style.flexDirection = "rows"; - grid_elem.style.flexWrap = "wrap"; - grid_elem.style.gap = "5px"; - parent_elem.appendChild(grid_elem); - - for (let i = 0; i < set.items.length; i++) { - - let set_elem = document.createElement('p'); - set_elem.classList.add("container"); - set_elem.style = "grid-item-"+(i+1); - set_elem.style.maxWidth = "max(180px, 15%)"; - set_elem.id = "set-"+setName+"-"+i; - grid_elem.appendChild(set_elem); - const bonus = set.bonuses[i]; - let mock_item = new Map(); - mock_item.set("fixID", true); - mock_item.set("displayName", setName+" Set: " + (i+1) + "/"+sets[setName].items.length); - set_elem.textContent = mock_item.get("displayName"); - let mock_minRolls = new Map(); - let mock_maxRolls = new Map(); - mock_item.set("minRolls", mock_minRolls); - mock_item.set("maxRolls", mock_maxRolls); - for (const id in bonus) { - if (rolledIDs.includes(id)) { - mock_minRolls.set(id, bonus[id]); - mock_maxRolls.set(id, bonus[id]); - } - else { - mock_item.set(id, bonus[id]); - } - } - mock_item.set("powders", []); - displayExpandedItem(mock_item, set_elem.id); - } - -} /** Displays the individual probabilities of each possible value of each rollable ID for this item. * From 4d7197fc53554482c58ba1fbc1c9af79ce212c34 Mon Sep 17 00:00:00 2001 From: hppeng Date: Sun, 19 Jun 2022 20:20:22 -0700 Subject: [PATCH 31/68] Add instructions --- js/builder_graph.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/js/builder_graph.js b/js/builder_graph.js index 663e14a..fb2fc45 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -405,6 +405,8 @@ let powder_nodes = []; let spelldmg_nodes = []; function builder_graph_init() { + // Phase 1/2: Set up item input, propagate updates, etc. + // Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). for (const [eq, display_elem, none_item] of zip3(equipment_fields, build_fields, none_items)) { let input_field = document.getElementById(eq+"-choice"); @@ -446,9 +448,28 @@ function builder_graph_init() { build_encode_node.link_to(powder_node, input); } + for (const input_node of item_nodes.concat(powder_nodes)) { + input_node.update(); + } + level_input.update(); + + // Phase 2/2: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage + + // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) + // let stat_agg_node = + for (const field of editable_elems) { + // Create nodes that listens to each editable id input, the node name should match the "id" + + // stat_agg_node.link_to( ... ) + } + + // Also do something similar for skill points + for (let i = 0; i < 4; ++i) { let spell_node = new SpellSelectNode(i); spell_node.link_to(build_node, 'build'); + // link and rewrite spell_node to the stat agg node + // spell_node.link_to(stat_agg_node, 'stats') let calc_node = new SpellDamageCalcNode(i); calc_node.link_to(item_nodes[8], 'weapon-input'); @@ -462,11 +483,17 @@ function builder_graph_init() { display_node.link_to(spell_node, 'spell-info'); display_node.link_to(calc_node, 'spell-damage'); } + + // Create a node that binds the build to the edit ids text boxes + // (make it export to the textboxes) + // This node is a bit tricky since it has to make a "weak link" (directly mark dirty and call update() for each of the stat nodes). + // IMPORTANT: mark all children dirty, then update each child, in that order (2 loops). else performance issues will be bad + // let id_exporter_node = ... + // id_exporter_node.link_to(build_node, 'build') - for (const input_node of item_nodes.concat(powder_nodes)) { - input_node.update(); - } - level_input.update(); + // call node.update() for each skillpoint node and stat edit listener node manually + // NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them + // this will propagate the update to the `stat_agg_node`, and then to damage calc console.log("Set up graph"); } From 18d21e21ae5ddab8a2b0d89a84995d5d099f481c Mon Sep 17 00:00:00 2001 From: hppeng Date: Mon, 20 Jun 2022 06:12:22 -0700 Subject: [PATCH 32/68] Ready for dev branch merge --- js/build.js | 41 +---- js/build_encode_decode.js | 11 +- js/builder_graph.js | 347 ++++++++++++++++++++++++++++++++++---- js/computation_graph.js | 1 + js/custom.js | 1 + js/display.js | 24 ++- js/skillpoints.js | 1 - 7 files changed, 332 insertions(+), 94 deletions(-) diff --git a/js/build.js b/js/build.js index f451ff8..a655290 100644 --- a/js/build.js +++ b/js/build.js @@ -156,7 +156,8 @@ class Build{ } getBaseSpellCost(spellIdx, cost) { - // old intelligence: cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); + // old intelligence: + cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); cost += this.statMap.get("spRaw"+spellIdx); return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100)); } @@ -206,44 +207,6 @@ class Build{ return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]); } - /* - Get all defensive stats for this build. - */ - getDefenseStats(){ - const stats = this.statMap; - let defenseStats = []; - let def_pct = skillPointsToPercentage(this.total_skillpoints[3]); - let agi_pct = skillPointsToPercentage(this.total_skillpoints[4]); - //total hp - let totalHp = stats.get("hp") + stats.get("hpBonus"); - if (totalHp < 5) totalHp = 5; - defenseStats.push(totalHp); - //EHP - let ehp = [totalHp, totalHp]; - let defMult = classDefenseMultipliers.get(this.weapon.statMap.get("type")); - ehp[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); - ehp[1] /= (1-def_pct)*(2-defMult); - defenseStats.push(ehp); - //HPR - let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); - defenseStats.push(totalHpr); - //EHPR - let ehpr = [totalHpr, totalHpr]; - ehpr[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); - ehpr[1] /= (1-def_pct)*(2-defMult); - defenseStats.push(ehpr); - //skp stats - defenseStats.push([ def_pct*100, agi_pct*100]); - //eledefs - TODO POWDERS - let eledefs = [0, 0, 0, 0, 0]; - for(const i in skp_elements){ //kinda jank but ok - eledefs[i] = rawToPct(stats.get(skp_elements[i] + "Def"), stats.get(skp_elements[i] + "DefPct")/100.); - } - defenseStats.push(eledefs); - - //[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]] - return defenseStats; - } /* Get all stats for this build. Stores in this.statMap. @pre The build itself should be valid. No checking of validity of pieces is done here. diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 12b45b1..834afdd 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -132,12 +132,16 @@ function decodeBuild(url_tag) { for (let i in powder_inputs) { setValue(powder_inputs[i], powdering[i]); } + for (let i in skillpoints) { + console.log(skillpoints[i]); + setValue(skp_order[i] + "-skp", skillpoints[i]); + } } } /* Stores the entire build in a string using B64 encoding and adds it to the URL. */ -function encodeBuild(build, powders) { +function encodeBuild(build, powders, skillpoints) { if (build) { let build_string; @@ -148,7 +152,6 @@ function encodeBuild(build, powders) { tome_string = ""; for (const item of build.items) { - if (item.statMap.get("custom")) { let custom = "CI-"+encodeCustom(item, true); build_string += Base64.fromIntN(custom.length, 3) + custom; @@ -167,8 +170,8 @@ function encodeBuild(build, powders) { } } - for (const skp of skp_order) { - build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 + for (const skp of skillpoints) { + build_string += Base64.fromIntN(skp, 2); // Maximum skillpoints: 2048 } build_string += Base64.fromIntN(build.level, 2); for (const _powderset of powders) { diff --git a/js/builder_graph.js b/js/builder_graph.js index fb2fc45..d1b7292 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -160,10 +160,17 @@ class BuildEncodeNode extends ComputeNode { input_map.get('boots-powder'), input_map.get('weapon-powder') ]; + const skillpoints = [ + input_map.get('str'), + input_map.get('dex'), + input_map.get('int'), + input_map.get('def'), + input_map.get('agi') + ]; // TODO: grr global state for copy button.. player_build = build; build_powders = powders; - return encodeBuild(build, powders); + return encodeBuild(build, powders, skillpoints); } } @@ -295,12 +302,50 @@ class SpellSelectNode extends ComputeNode { } } +/* + * Get all defensive stats for this build. + */ +function getDefenseStats(stats) { + let defenseStats = []; + let def_pct = skillPointsToPercentage(stats.get('def')); + let agi_pct = skillPointsToPercentage(stats.get('agi')); + //total hp + let totalHp = stats.get("hp") + stats.get("hpBonus"); + if (totalHp < 5) totalHp = 5; + defenseStats.push(totalHp); + //EHP + let ehp = [totalHp, totalHp]; + let defMult = stats.get("classDef"); + ehp[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); + ehp[1] /= (1-def_pct)*(2-defMult); + defenseStats.push(ehp); + //HPR + let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); + defenseStats.push(totalHpr); + //EHPR + let ehpr = [totalHpr, totalHpr]; + ehpr[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); + ehpr[1] /= (1-def_pct)*(2-defMult); + defenseStats.push(ehpr); + //skp stats + defenseStats.push([ def_pct*100, agi_pct*100]); + //eledefs - TODO POWDERS + let eledefs = [0, 0, 0, 0, 0]; + for(const i in skp_elements){ //kinda jank but ok + eledefs[i] = rawToPct(stats.get(skp_elements[i] + "Def"), stats.get(skp_elements[i] + "DefPct")/100.); + } + defenseStats.push(eledefs); + + //[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]] + return defenseStats; +} + /** * Compute spell damage of spell parts. * Currently kinda janky / TODO while we rework the internal rep. of spells. * * Signature: SpellDamageCalcNode(weapon-input: Item, - * build: Build, + * stats: StatMap, * weapon-powder: List[powder], * spell-info: [Spell, SpellParts]) => List[SpellDamage] */ @@ -311,25 +356,31 @@ class SpellDamageCalcNode extends ComputeNode { compute_func(input_map) { const weapon = new Map(input_map.get('weapon-input').statMap); - const build = input_map.get('build'); const weapon_powder = input_map.get('weapon-powder'); const damage_mult = 1; // TODO: hook up const spell_info = input_map.get('spell-info'); const spell_parts = spell_info[1]; + const stats = input_map.get('stats'); + const skillpoints = [ + stats.get('str'), + stats.get('dex'), + stats.get('int'), + stats.get('def'), + stats.get('agi') + ]; weapon.set("powders", weapon_powder); let spell_results = [] - let stats = build.statMap; for (const part of spell_parts) { if (part.type === "damage") { let results = calculateSpellDamage(stats, part.conversion, stats.get("sdRaw") + stats.get("rainbowRaw"), stats.get("sdPct"), - part.multiplier / 100, weapon, build.total_skillpoints, damage_mult); + part.multiplier / 100, weapon, skillpoints, damage_mult); spell_results.push(results); } else if (part.type === "heal") { // TODO: wynn2 formula - let heal_amount = (part.strength * build.getDefenseStats()[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); + let heal_amount = (part.strength * getDefenseStats(stats)[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); spell_results.push(heal_amount); } else if (part.type === "total") { // TODO: remove "total" type @@ -345,7 +396,7 @@ class SpellDamageCalcNode extends ComputeNode { * Display spell damage from spell parts. * Currently kinda janky / TODO while we rework the internal rep. of spells. * - * Signature: SpellDisplayNode(build: Build, + * Signature: SpellDisplayNode(stats: StatMap, * spell-info: [Spell, SpellParts], * spell-damage: List[SpellDamage]) => null */ @@ -375,29 +426,231 @@ class SpellDisplayNode extends ComputeNode { * Signature: BuildDisplayNode(build: Build) => null */ class BuildDisplayNode extends ComputeNode { - constructor(spell_num) { super("builder-stats-display"); } + constructor() { super("builder-stats-display"); } compute_func(input_map) { - if (input_map.size !== 1) { throw "BuildDisplayNode accepts exactly one input (build)"; } - const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element - displayBuildStats('overall-stats', build, build_all_display_commands); - displayBuildStats("offensive-stats", build, build_offensive_display_commands); + const build = input_map.get('build'); + const stats = input_map.get('stats'); + displayBuildStats('overall-stats', build, build_all_display_commands, stats); + displayBuildStats("offensive-stats", build, build_offensive_display_commands, stats); displaySetBonuses("set-info", build); let meleeStats = build.getMeleeStats(); displayMeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); - displayDefenseStats(document.getElementById("defensive-stats"), build); + displayDefenseStats(document.getElementById("defensive-stats"), stats); displayPoisonDamage(document.getElementById("build-poison-stats"), build); displayEquipOrder(document.getElementById("build-order"), build.equip_order); } } +/** + * Show warnings for skillpoints, level, set bonus for a build + * Also shosw skill point remaining and other misc. info + * + * Signature: DisplayBuildWarningNode(build: Build, str: int, dex: int, int: int, def: int, agi: int) => null + */ +class DisplayBuildWarningsNode extends ComputeNode { + constructor() { super("builder-show-warnings"); } + + compute_func(input_map) { + const build = input_map.get('build'); + const min_assigned = build.base_skillpoints; + const base_totals = build.total_skillpoints; + const skillpoints = [ + input_map.get('str'), + input_map.get('dex'), + input_map.get('int'), + input_map.get('def'), + input_map.get('agi') + ]; + let skp_effects = ["% more damage dealt.","% chance to crit.","% spell cost reduction.","% less damage taken.","% chance to dodge."]; + let total_assigned = 0; + for (let i in skp_order){ //big bren + const assigned = skillpoints[i] - base_totals[i] + min_assigned[i] + setText(skp_order[i] + "-skp-assign", "Assign: " + assigned); + setValue(skp_order[i] + "-skp", skillpoints[i]); + let linebreak = document.createElement("br"); + linebreak.classList.add("itemp"); + setText(skp_order[i] + "-skp-pct", (skillPointsToPercentage(skillpoints[i])*100).toFixed(1).concat(skp_effects[i])); + document.getElementById(skp_order[i]+"-warnings").textContent = '' + if (assigned > 100) { + let skp_warning = document.createElement("p"); + skp_warning.classList.add("warning"); skp_warning.classList.add("small-text"); + skp_warning.textContent += "Cannot assign " + assigned + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually."; + document.getElementById(skp_order[i]+"-warnings").appendChild(skp_warning); + } + total_assigned += assigned; + } + + let summarybox = document.getElementById("summary-box"); + summarybox.textContent = ""; + let skpRow = document.createElement("p"); + + let remainingSkp = document.createElement("p"); + remainingSkp.classList.add("scaled-font"); + let remainingSkpTitle = document.createElement("b"); + remainingSkpTitle.textContent = "Assigned " + total_assigned + " skillpoints. Remaining skillpoints: "; + let remainingSkpContent = document.createElement("b"); + remainingSkpContent.textContent = "" + (levelToSkillPoints(build.level) - total_assigned); + remainingSkpContent.classList.add(levelToSkillPoints(build.level) - total_assigned < 0 ? "negative" : "positive"); + + remainingSkp.appendChild(remainingSkpTitle); + remainingSkp.appendChild(remainingSkpContent); + + summarybox.append(skpRow); + summarybox.append(remainingSkp); + if(total_assigned > levelToSkillPoints(build.level)){ + let skpWarning = document.createElement("span"); + //skpWarning.classList.add("itemp"); + skpWarning.classList.add("warning"); + skpWarning.textContent = "WARNING: Too many skillpoints need to be assigned!"; + let skpCount = document.createElement("p"); + skpCount.classList.add("warning"); + skpCount.textContent = "For level " + (build.level>101 ? "101+" : build.level) + ", there are only " + levelToSkillPoints(build.level) + " skill points available."; + summarybox.append(skpWarning); + summarybox.append(skpCount); + } + let lvlWarning; + for (const item of build.items) { + let item_lvl; + if (item.statMap.get("crafted")) { + //item_lvl = item.get("lvlLow") + "-" + item.get("lvl"); + item_lvl = item.statMap.get("lvlLow"); + } + else { + item_lvl = item.statMap.get("lvl"); + } + + if (build.level < item_lvl) { + if (!lvlWarning) { + lvlWarning = document.createElement("p"); + lvlWarning.classList.add("itemp"); + lvlWarning.classList.add("warning"); + lvlWarning.textContent = "WARNING: A level " + build.level + " player cannot use some piece(s) of this build." + } + let baditem = document.createElement("p"); + baditem.classList.add("nocolor"); + baditem.classList.add("itemp"); + baditem.textContent = item.get("displayName") + " requires level " + item_lvl + " to use."; + lvlWarning.appendChild(baditem); + } + } + if(lvlWarning){ + summarybox.append(lvlWarning); + } + for (const [setName, count] of build.activeSetCounts) { + const bonus = sets.get(setName).bonuses[count-1]; + // console.log(setName); + if (bonus["illegal"]) { + let setWarning = document.createElement("p"); + setWarning.classList.add("itemp"); + setWarning.classList.add("warning"); + setWarning.textContent = "WARNING: illegal item combination: " + setName + summarybox.append(setWarning); + } + } + } +} + +/** + * Aggregate stats from the build and from inputs. + * + * Signature: AggregateStatsNode(build: Build, *args) => StatMap + */ +class AggregateStatsNode extends ComputeNode { + constructor() { super("builder-aggregate-stats"); } + + compute_func(input_map) { + const build = input_map.get('build'); + const weapon = input_map.get('weapon'); + const output_stats = new Map(build.statMap); + for (const [k, v] of input_map.entries()) { + if (k === 'build') { + continue; + } + output_stats.set(k, v); + } + output_stats.set('classDef', classDefenseMultipliers.get(weapon.statMap.get("type"))); + return output_stats; + } +} + /** * Set the editble id fields. + * + * Signature: EditableIDSetterNode(build: Build) => null */ class EditableIDSetterNode extends ComputeNode { + constructor() { super("builder-id-setter"); } + compute_func(input_map) { + if (input_map.size !== 1) { throw "EditableIDSetterNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + for (const id of editable_item_fields) { + document.getElementById(id).value = build.statMap.get(id); + } + } +} + +/** + * Set skillpoint fields from build. + * This is separate because..... because of the way we work with edit ids vs skill points during the load sequence.... + * + * Signature: SkillPointSetterNode(build: Build) => null + */ +class SkillPointSetterNode extends ComputeNode { + constructor(notify_nodes) { + super("builder-skillpoint-setter"); + this.notify_nodes = notify_nodes; + } + + compute_func(input_map) { + console.log("mmm"); + if (input_map.size !== 1) { throw "SkillPointSetterNode accepts exactly one input (build)"; } + const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element + for (const [idx, elem] of skp_order.entries()) { + setText(elem + "-skp-base", "Original: " + build.base_skillpoints[idx]); + document.getElementById(elem+'-skp').value = build.total_skillpoints[idx]; + } + // NOTE: DO NOT merge these loops for performance reasons!!! + for (const node of this.notify_nodes) { + node.mark_dirty(); + } + for (const node of this.notify_nodes) { + node.update(); + } + } +} + +/** + * Get number (possibly summed) from a text input. + * + * Signature: SumNumberInputNode() => int + */ +class SumNumberInputNode extends InputNode { + compute_func(input_map) { + const value = this.input_field.value; + if (value === "") { value = 0; } + + let input_num = 0; + if (value.includes("+")) { + let skp = value.split("+"); + for (const s of skp) { + const val = parseInt(s,10); + if (isNaN(val)) { + return null; + } + input_num += val; + } + } else { + input_num = parseInt(value,10); + if (isNaN(input_num)) { + return null; + } + } + return input_num; + } } let item_nodes = []; @@ -433,7 +686,6 @@ function builder_graph_init() { build_node.link_to(input); } build_node.link_to(level_input); - new BuildDisplayNode().link_to(build_node, 'build'); let build_encode_node = new BuildEncodeNode(); build_encode_node.link_to(build_node, 'build'); @@ -448,48 +700,69 @@ function builder_graph_init() { build_encode_node.link_to(powder_node, input); } + // Edit IDs setter declared up here to set ids so they will be populated by default. + let edit_id_output = new EditableIDSetterNode(); + edit_id_output.link_to(build_node); + + // Phase 2/2: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage + + let build_disp_node = new BuildDisplayNode() + build_disp_node.link_to(build_node, 'build'); + let build_warnings_node = new DisplayBuildWarningsNode(); + build_warnings_node.link_to(build_node, 'build'); + + // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) + let stat_agg_node = new AggregateStatsNode(); + stat_agg_node.link_to(build_node, 'build').link_to(item_nodes[8], 'weapon'); + let edit_input_nodes = []; + for (const field of editable_item_fields) { + // Create nodes that listens to each editable id input, the node name should match the "id" + const elem = document.getElementById(field); + const node = new SumNumberInputNode('builder-'+field+'-input', elem); + + stat_agg_node.link_to(node, field); + edit_input_nodes.push(node); + } + for (const skp of skp_order) { + const elem = document.getElementById(skp+'-skp'); + const node = new SumNumberInputNode('builder-'+skp+'-input', elem); + + stat_agg_node.link_to(node, skp); + build_encode_node.link_to(node, skp); + build_warnings_node.link_to(node, skp); + edit_input_nodes.push(node); + } + build_disp_node.link_to(stat_agg_node, 'stats'); + for (const input_node of item_nodes.concat(powder_nodes)) { input_node.update(); } level_input.update(); - // Phase 2/2: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage - - // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) - // let stat_agg_node = - for (const field of editable_elems) { - // Create nodes that listens to each editable id input, the node name should match the "id" - - // stat_agg_node.link_to( ... ) - } - // Also do something similar for skill points for (let i = 0; i < 4; ++i) { let spell_node = new SpellSelectNode(i); spell_node.link_to(build_node, 'build'); - // link and rewrite spell_node to the stat agg node - // spell_node.link_to(stat_agg_node, 'stats') + // TODO: link and rewrite spell_node to the stat agg node + spell_node.link_to(stat_agg_node, 'stats') let calc_node = new SpellDamageCalcNode(i); - calc_node.link_to(item_nodes[8], 'weapon-input'); - calc_node.link_to(build_node, 'build'); - calc_node.link_to(powder_nodes[4], 'weapon-powder'); - calc_node.link_to(spell_node, 'spell-info'); + calc_node.link_to(item_nodes[8], 'weapon-input').link_to(stat_agg_node, 'stats') + .link_to(powder_nodes[4], 'weapon-powder').link_to(spell_node, 'spell-info'); spelldmg_nodes.push(calc_node); let display_node = new SpellDisplayNode(i); - display_node.link_to(build_node, 'build'); + display_node.link_to(build_node, 'build'); // TODO: same here.. display_node.link_to(spell_node, 'spell-info'); display_node.link_to(calc_node, 'spell-damage'); } + for (const node of edit_input_nodes) { + node.update(); + } - // Create a node that binds the build to the edit ids text boxes - // (make it export to the textboxes) - // This node is a bit tricky since it has to make a "weak link" (directly mark dirty and call update() for each of the stat nodes). - // IMPORTANT: mark all children dirty, then update each child, in that order (2 loops). else performance issues will be bad - // let id_exporter_node = ... - // id_exporter_node.link_to(build_node, 'build') + let skp_output = new SkillPointSetterNode(edit_input_nodes); + skp_output.link_to(build_node); // call node.update() for each skillpoint node and stat edit listener node manually // NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them diff --git a/js/computation_graph.js b/js/computation_graph.js index 68a1717..f99a966 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -94,6 +94,7 @@ class ComputeNode { this.inputs_dirty_count += 1; } parent_node.children.push(this); + return this; } } diff --git a/js/custom.js b/js/custom.js index 50ead5b..8c712af 100644 --- a/js/custom.js +++ b/js/custom.js @@ -175,6 +175,7 @@ function getCustomFromHash(hash) { } } statMap.set("hash", "CI-" + name); + statMap.set("custom", true); return new Custom(statMap); } } catch (error) { diff --git a/js/display.js b/js/display.js index c9ea548..30175c8 100644 --- a/js/display.js +++ b/js/display.js @@ -50,7 +50,7 @@ function displaySetBonuses(parent_id,build) { const bonus = active_set.bonuses[count-1]; let mock_item = new Map(); mock_item.set("fixID", true); - mock_item.set("displayName", setName+" Set: "+count+"/"+sets[setName].items.length); + mock_item.set("displayName", setName+" Set: "+count+"/"+sets.get(setName).items.length); let mock_minRolls = new Map(); let mock_maxRolls = new Map(); mock_item.set("minRolls", mock_minRolls); @@ -70,7 +70,7 @@ function displaySetBonuses(parent_id,build) { } } -function displayBuildStats(parent_id,build,command_group){ +function displayBuildStats(parent_id,build,command_group,stats){ // Commands to "script" the creation of nice formatting. // #commands create a new element. // !elemental is some janky hack for elemental damage. @@ -82,8 +82,6 @@ function displayBuildStats(parent_id,build,command_group){ if (parent_div != null) { setHTML(parent_id, ""); } - - let stats = build.statMap; let active_elem; let elemental_format = false; @@ -96,7 +94,7 @@ function displayBuildStats(parent_id,build,command_group){ if (command.charAt(0) === "#") { if (command === "#defense-stats") { - displayDefenseStats(parent_div, build, true); + displayDefenseStats(parent_div, stats, true); } } if (command.charAt(0) === "!") { @@ -126,7 +124,7 @@ function displayBuildStats(parent_id,build,command_group){ style === "positive" ? style = "negative" : style = "positive"; } if (id === "poison" && id_val > 0) { - id_val = Math.ceil(id_val*build.statMap.get("poisonPct")/100); + id_val = Math.ceil(id_val*stats.get("poisonPct")/100); } displayFixedID(parent_div, id, id_val, elemental_format, style); if (id === "poison" && id_val > 0) { @@ -140,7 +138,7 @@ function displayBuildStats(parent_id,build,command_group){ prefix_elem.textContent = "\u279C With Strength: "; let number_elem = document.createElement('b'); number_elem.classList.add(style); - number_elem.textContent = (id_val * (1+skillPointsToPercentage(build.total_skillpoints[0])) ).toFixed(0) + idSuffixes[id]; + number_elem.textContent = (id_val * (1+skillPointsToPercentage(stats.get('str'))) ).toFixed(0) + idSuffixes[id]; value_elem.append(prefix_elem); value_elem.append(number_elem); row.appendChild(value_elem); @@ -156,7 +154,7 @@ function displayBuildStats(parent_id,build,command_group){ let prefix_elem = document.createElement('b'); prefix_elem.textContent = "\u279C Effective LS: "; - let defStats = build.getDefenseStats(); + let defStats = getDefenseStats(stats); let number_elem = document.createElement('b'); number_elem.classList.add(style); number_elem.textContent = Math.round(defStats[1][0]*id_val/defStats[0]) + "/3s"; @@ -1253,8 +1251,8 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) { parent_elem.append(critStats); } -function displayDefenseStats(parent_elem, build, insertSummary){ - let defenseStats = build.getDefenseStats(); +function displayDefenseStats(parent_elem, statMap, insertSummary){ + let defenseStats = getDefenseStats(statMap); insertSummary = (typeof insertSummary !== 'undefined') ? insertSummary : false; if (!insertSummary) { parent_elem.textContent = ""; @@ -1297,7 +1295,7 @@ function displayDefenseStats(parent_elem, build, insertSummary){ statsTable.appendChild(hpRow); } - let defMult = build.statMap.get("defMult"); + let defMult = statMap.get("defMult"); if (!defMult) {defMult = 1} //EHP @@ -1409,8 +1407,8 @@ function displayDefenseStats(parent_elem, build, insertSummary){ boost.classList.add("col"); boost.classList.add("text-end"); - let defRaw = build.statMap.get("defRaw")[i]; - let defPct = build.statMap.get("defBonus")[i]/100; + let defRaw = statMap.get("defRaw")[i]; + let defPct = statMap.get("defBonus")[i]/100; if (defRaw < 0) { defPct >= 0 ? defPct = "- " + defPct: defPct = "+ " + defPct; } else { diff --git a/js/skillpoints.js b/js/skillpoints.js index fcf24b6..5748a00 100644 --- a/js/skillpoints.js +++ b/js/skillpoints.js @@ -38,7 +38,6 @@ function calculate_skillpoints(equipment, weapon) { setCount = 0; activeSetCounts.set(setName, 1); } - console.log(sets); const new_bonus = sets.get(setName).bonuses[setCount]; //let skp_order = ["str","dex","int","def","agi"]; for (const i in skp_order) { From f7a1f4fc7e3be9276428e55ea07f4a7dcb369236 Mon Sep 17 00:00:00 2001 From: hppeng Date: Mon, 20 Jun 2022 07:37:14 -0700 Subject: [PATCH 33/68] Fix melee damage, fix edit ids spell calc --- builder/index.html | 54 ++++++++++++++++++------------------- js/build.js | 58 ---------------------------------------- js/builder_graph.js | 65 +++++++++++++++++++++++++++++++++++++++------ js/damage_calc.js | 2 +- js/display.js | 55 ++++++++++++++++++++------------------ js/optimize.js | 2 +- 6 files changed, 115 insertions(+), 121 deletions(-) diff --git a/builder/index.html b/builder/index.html index 283d301..2084f43 100644 --- a/builder/index.html +++ b/builder/index.html @@ -619,7 +619,7 @@ Spell Damage %:
- +
Original Value: 0 @@ -630,7 +630,7 @@ Spell Damage Raw:
- +
Original Value: 0 @@ -641,7 +641,7 @@ Melee Damage %:
- +
Original Value: 0 @@ -652,7 +652,7 @@ Melee Damage Raw:
- +
Original Value: 0 @@ -665,7 +665,7 @@ Poison:
- +
Original Value: 0 @@ -676,7 +676,7 @@ Damage %:
- +
Original Value: 0 @@ -687,7 +687,7 @@ Damage %:
- +
Original Value: 0 @@ -698,7 +698,7 @@ Damage %:
- +
Original Value: 0 @@ -711,7 +711,7 @@ Damage %:
- +
Original Value: 0 @@ -722,7 +722,7 @@ Damage %:
- +
Original Value: 0 @@ -733,7 +733,7 @@ + Tier:
- +
Original Value: 0 @@ -752,7 +752,7 @@ Defense %:
- +
Original Value: 0 @@ -763,7 +763,7 @@ Defense %:
- +
Original Value: 0 @@ -774,7 +774,7 @@ Defense %:
- +
Original Value: 0 @@ -785,7 +785,7 @@ Defense %:
- +
Original Value: 0 @@ -798,7 +798,7 @@ Defense %:
- +
Original Value: 0 @@ -809,7 +809,7 @@ Health Regen Raw:
- +
Original Value: 0 @@ -820,7 +820,7 @@ Health Regen %:
- +
Original Value: 0 @@ -831,7 +831,7 @@ Health Bonus:
- +
Original Value: 0 @@ -847,7 +847,7 @@ 1st Spell Cost %:
- +
Original Value: 0 @@ -858,7 +858,7 @@ 2nd Spell Cost %:
- +
Original Value: 0 @@ -869,7 +869,7 @@ 3rd Spell Cost %:
- +
Original Value: 0 @@ -880,7 +880,7 @@ 4th Spell Cost %:
- +
Original Value: 0 @@ -893,7 +893,7 @@ 1st Spell Cost Raw:
- +
Original Value: 0 @@ -904,7 +904,7 @@ 2nd Spell Cost Raw:
- +
Original Value: 0 @@ -915,7 +915,7 @@ 3rd Spell Cost Raw:
- +
Original Value: 0 @@ -926,7 +926,7 @@ 4th Spell Cost Raw:
- +
Original Value: 0 diff --git a/js/build.js b/js/build.js index a655290..78cf2ac 100644 --- a/js/build.js +++ b/js/build.js @@ -149,64 +149,6 @@ class Build{ return [this.equipment,this.weapon,this.tomes].flat(); } - /* Getters */ - - getSpellCost(spellIdx, cost) { - return Math.max(1, this.getBaseSpellCost(spellIdx, cost)); - } - - getBaseSpellCost(spellIdx, cost) { - // old intelligence: - cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); - cost += this.statMap.get("spRaw"+spellIdx); - return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100)); - } - - - /* Get melee stats for build. - Returns an array in the order: - */ - getMeleeStats(){ - const stats = this.statMap; - const weapon_stats = this.weapon.statMap; - if (weapon_stats.get("tier") === "Crafted") { - stats.set("damageBases", [weapon_stats.get("nDamBaseHigh"),weapon_stats.get("eDamBaseHigh"),weapon_stats.get("tDamBaseHigh"),weapon_stats.get("wDamBaseHigh"),weapon_stats.get("fDamBaseHigh"),weapon_stats.get("aDamBaseHigh")]); - } - let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier"); - if(adjAtkSpd > 6){ - adjAtkSpd = 6; - }else if(adjAtkSpd < 0){ - adjAtkSpd = 0; - } - - let damage_mult = 1; - if (weapon_stats.get("type") === "relik") { - damage_mult = 0.99; // CURSE YOU WYNNCRAFT - //One day we will create WynnWynn and no longer have shaman 99% melee injustice. - //In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams. - } - // 0spellmult for melee damage. - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct"), 0, this.weapon.statMap, this.total_skillpoints, damage_mult * this.damageMultiplier); - - let dex = this.total_skillpoints[1]; - - let totalDamNorm = results[0]; - let totalDamCrit = results[1]; - totalDamNorm.push(1-skillPointsToPercentage(dex)); - totalDamCrit.push(skillPointsToPercentage(dex)); - let damages_results = results[2]; - - let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2]) - +(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2; - - //Now do math - let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd]; - let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd]; - let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex))); - //[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit] - return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]); - } - /* Get all stats for this build. Stores in this.statMap. @pre The build itself should be valid. No checking of validity of pieces is done here. diff --git a/js/builder_graph.js b/js/builder_graph.js index d1b7292..29ec4d6 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -260,7 +260,6 @@ class PowderInputNode extends InputNode { } input = input.slice(2); } - //console.log("POWDERING: " + powdering); return powdering; } } @@ -407,7 +406,7 @@ class SpellDisplayNode extends ComputeNode { } compute_func(input_map) { - const build = input_map.get('build'); + const stats = input_map.get('stats'); const spell_info = input_map.get('spell-info'); const damages = input_map.get('spell-damage'); const spell = spell_info[0]; @@ -416,10 +415,60 @@ class SpellDisplayNode extends ComputeNode { const i = this.spell_idx; let parent_elem = document.getElementById("spell"+i+"-info"); let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); - displaySpellDamage(parent_elem, overallparent_elem, build, spell, i+1, spell_parts, damages); + displaySpellDamage(parent_elem, overallparent_elem, stats, spell, i+1, spell_parts, damages); } } +/* Get melee stats for build. + Returns an array in the order: +*/ +function getMeleeStats(stats, weapon) { + const weapon_stats = weapon.statMap; + const skillpoints = [ + stats.get('str'), + stats.get('dex'), + stats.get('int'), + stats.get('def'), + stats.get('agi') + ]; + if (weapon_stats.get("tier") === "Crafted") { + stats.set("damageBases", [weapon_stats.get("nDamBaseHigh"),weapon_stats.get("eDamBaseHigh"),weapon_stats.get("tDamBaseHigh"),weapon_stats.get("wDamBaseHigh"),weapon_stats.get("fDamBaseHigh"),weapon_stats.get("aDamBaseHigh")]); + } + let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier"); + if(adjAtkSpd > 6){ + adjAtkSpd = 6; + }else if(adjAtkSpd < 0){ + adjAtkSpd = 0; + } + + let damage_mult = 1; + if (weapon_stats.get("type") === "relik") { + damage_mult = 0.99; // CURSE YOU WYNNCRAFT + //One day we will create WynnWynn and no longer have shaman 99% melee injustice. + //In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams. + } + // 0spellmult for melee damage. + let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct"), 0, weapon_stats, skillpoints, damage_mult); + + let dex = skillpoints[1]; + + let totalDamNorm = results[0]; + let totalDamCrit = results[1]; + totalDamNorm.push(1-skillPointsToPercentage(dex)); + totalDamCrit.push(skillPointsToPercentage(dex)); + let damages_results = results[2]; + + let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2]) + +(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2; + + //Now do math + let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd]; + let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd]; + let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex))); + //[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit] + return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]); +} + /** * Display build stats. * @@ -434,7 +483,9 @@ class BuildDisplayNode extends ComputeNode { displayBuildStats('overall-stats', build, build_all_display_commands, stats); displayBuildStats("offensive-stats", build, build_offensive_display_commands, stats); displaySetBonuses("set-info", build); - let meleeStats = build.getMeleeStats(); + let meleeStats = getMeleeStats(stats, build.weapon); + // TODO: move weapon out? + console.log(meleeStats); displayMeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); displayDefenseStats(document.getElementById("defensive-stats"), stats); @@ -541,7 +592,6 @@ class DisplayBuildWarningsNode extends ComputeNode { } for (const [setName, count] of build.activeSetCounts) { const bonus = sets.get(setName).bonuses[count-1]; - // console.log(setName); if (bonus["illegal"]) { let setWarning = document.createElement("p"); setWarning.classList.add("itemp"); @@ -606,7 +656,6 @@ class SkillPointSetterNode extends ComputeNode { } compute_func(input_map) { - console.log("mmm"); if (input_map.size !== 1) { throw "SkillPointSetterNode accepts exactly one input (build)"; } const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element for (const [idx, elem] of skp_order.entries()) { @@ -656,6 +705,7 @@ class SumNumberInputNode extends InputNode { let item_nodes = []; let powder_nodes = []; let spelldmg_nodes = []; +let edit_input_nodes = []; function builder_graph_init() { // Phase 1/2: Set up item input, propagate updates, etc. @@ -714,7 +764,6 @@ function builder_graph_init() { // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) let stat_agg_node = new AggregateStatsNode(); stat_agg_node.link_to(build_node, 'build').link_to(item_nodes[8], 'weapon'); - let edit_input_nodes = []; for (const field of editable_item_fields) { // Create nodes that listens to each editable id input, the node name should match the "id" const elem = document.getElementById(field); @@ -753,7 +802,7 @@ function builder_graph_init() { spelldmg_nodes.push(calc_node); let display_node = new SpellDisplayNode(i); - display_node.link_to(build_node, 'build'); // TODO: same here.. + display_node.link_to(stat_agg_node, 'stats'); // TODO: same here.. display_node.link_to(spell_node, 'spell-info'); display_node.link_to(calc_node, 'spell-damage'); } diff --git a/js/damage_calc.js b/js/damage_calc.js index 9149a13..eef3825 100644 --- a/js/damage_calc.js +++ b/js/damage_calc.js @@ -105,7 +105,7 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, let staticBoost = (pctModifier / 100.); let skillBoost = [0]; for (let i in total_skillpoints) { - skillBoost.push(skillPointsToPercentage(total_skillpoints[i]) + buildStats.get("damageBonus")[i] / 100.); + skillBoost.push(skillPointsToPercentage(total_skillpoints[i]) + buildStats.get(skp_elements[i]+"DamPct") / 100.); } for (let i in damages) { diff --git a/js/display.js b/js/display.js index 30175c8..c87f689 100644 --- a/js/display.js +++ b/js/display.js @@ -941,46 +941,45 @@ function displayExpandedIngredient(ingred, parent_id) { } } -function displayNextCosts(spell, build, weapon) { - let int = build.total_skillpoints[2]; - let spells = spell_table[weapon.get("type")]; +function displayNextCosts(_stats, spell, spellIdx) { + let stats = new Map(_stats); + let intel = stats.get('int'); let row = document.createElement("div"); row.classList.add("spellcost-tooltip"); let init_cost = document.createElement("b"); - init_cost.textContent = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost); + init_cost.textContent = getSpellCost(stats, spellIdx, spell.cost); init_cost.classList.add("Mana"); let arrow = document.createElement("b"); arrow.textContent = "\u279C"; let next_cost = document.createElement("b"); - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1); + next_cost.textContent = (init_cost.textContent === "1" ? 1 : getSpellCost(stats, spellIdx, spell.cost) - 1); next_cost.classList.add("Mana"); let int_needed = document.createElement("b"); if (init_cost.textContent === "1") { int_needed.textContent = ": n/a (+0)"; }else { //do math - let target = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1; - let needed = int; + let target = getSpellCost(stats, spellIdx, spell.cost) - 1; + let needed = intel; let noUpdate = false; //forgive me... I couldn't inverse ceil, floor, and max. - while (build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) > target) { + while (getSpellCost(stats, spellIdx, spell.cost) > target) { if(needed > 150) { noUpdate = true; break; } needed++; - build.total_skillpoints[2] = needed; + stats.set('int', stats.get('int') + 1); } - let missing = needed - int; + let missing = needed - intel; //in rare circumstances, the next spell cost can jump. if (noUpdate) { - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)-1); + next_cost.textContent = (init_cost.textContent === "1" ? 1 : getSpellCost(stats, spellIdx, spell.cost)-1); }else { - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)); + next_cost.textContent = (init_cost.textContent === "1" ? 1 : getSpellCost(stats, spellIdx, spell.cost)); } - build.total_skillpoints[2] = int;//forgive me pt 2 int_needed.textContent = ": " + (needed > 150 ? ">150" : needed) + " int (+" + (needed > 150 ? "n/a" : missing) + ")"; } @@ -1583,12 +1582,23 @@ function displayPowderSpecials(parent_elem, powderSpecials, build) { } } -function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spellIdx, spell_parts, damages) { +function getSpellCost(stats, spellIdx, cost) { + return Math.max(1, getBaseSpellCost(stats, spellIdx, cost)); +} + +function getBaseSpellCost(stats, spellIdx, cost) { + // old intelligence: + cost = Math.ceil(cost * (1 - skillPointsToPercentage(stats.get('int')))); + cost += stats.get("spRaw"+spellIdx); + return Math.floor(cost * (1 + stats.get("spPct"+spellIdx) / 100)); +} + + +function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spellIdx, spell_parts, damages) { // TODO: remove spellIdx (just used to flag melee and cost) // TODO: move cost calc out parent_elem.textContent = ""; - const stats = build.statMap; let title_elem = document.createElement("p"); overallparent_elem.textContent = ""; @@ -1601,21 +1611,15 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell title_elemavg.appendChild(first); let second = document.createElement("span"); - second.textContent = build.getSpellCost(spellIdx, spell.cost); + second.textContent = getSpellCost(stats, spellIdx, spell.cost); second.classList.add("Mana"); - let int_redux = skillPointsToPercentage(build.total_skillpoints[2]).toFixed(2); - let spPct_redux = (build.statMap.get("spPct" + spellIdx)/100).toFixed(2); - let spRaw_redux = (build.statMap.get("spRaw" + spellIdx)).toFixed(2); - spPct_redux >= 0 ? spPct_redux = "+ " + spPct_redux : spPct_redux = "- " + Math.abs(spPct_redux); - spRaw_redux >= 0 ? spRaw_redux = "+ " + spRaw_redux : spRaw_redux = "- " + Math.abs(spRaw_redux); - title_elem.appendChild(second.cloneNode(true)); title_elemavg.appendChild(second); let third = document.createElement("span"); - third.textContent = ") [Base: " + build.getBaseSpellCost(spellIdx, spell.cost) + " ]"; + third.textContent = ") [Base: " + getBaseSpellCost(stats, spellIdx, spell.cost) + " ]"; title_elem.appendChild(third); let third_summary = document.createElement("span"); third_summary.textContent = ")"; @@ -1629,9 +1633,9 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell parent_elem.append(title_elem); overallparent_elem.append(title_elemavg); - overallparent_elem.append(displayNextCosts(spell, build, build.weapon.statMap)); + overallparent_elem.append(displayNextCosts(stats, spell, spellIdx)); - let critChance = skillPointsToPercentage(build.total_skillpoints[1]); + let critChance = skillPointsToPercentage(stats.get('dex')); let save_damages = []; @@ -1650,7 +1654,6 @@ function displaySpellDamage(parent_elem, overallparent_elem, build, spell, spell part_div.append(subtitle_elem); if (part.type === "damage") { - //console.log(build.expandedStats); let _results = damage; let totalDamNormal = _results[0]; let totalDamCrit = _results[1]; diff --git a/js/optimize.js b/js/optimize.js index 6185edd..fdce392 100644 --- a/js/optimize.js +++ b/js/optimize.js @@ -40,7 +40,7 @@ function optimizeStrDex() { for (const part of spell_parts) { if (part.type === "damage") { let _results = calculateSpellDamage(stats, part.conversion, - stats.get("sdRaw"), stats.get("sdPct") + player_build.externalStats.get("sdPct"), + stats.get("sdRaw"), stats.get("sdPct"), part.multiplier / 100, player_build.weapon, total_skillpoints, player_build.damageMultiplier, player_build.externalStats); let totalDamNormal = _results[0]; From ebcdf9ae271e91924983dfce558dd12c7efd9d50 Mon Sep 17 00:00:00 2001 From: hppeng Date: Mon, 20 Jun 2022 10:51:17 -0700 Subject: [PATCH 34/68] Powder specials and potion boosts --- builder/index.html | 72 +++++------ js/build_constants.js | 2 +- js/build_encode_decode.js | 6 +- js/builder.js | 47 +++++++- js/builder_graph.js | 244 +++++++++++++++++++++++++++++++------- js/damage_calc.js | 3 - js/display.js | 210 +++++++++++++++++--------------- js/load_tome.js | 1 - 8 files changed, 391 insertions(+), 194 deletions(-) diff --git a/builder/index.html b/builder/index.html index 2084f43..bc5c7d3 100644 --- a/builder/index.html +++ b/builder/index.html @@ -307,7 +307,7 @@
- +
@@ -944,27 +944,27 @@
-
-
-
-
-
@@ -1001,27 +1001,27 @@
-
-
-
-
-
@@ -1031,7 +1031,7 @@ Rage (Passive)
- +
@@ -1045,27 +1045,27 @@
-
-
-
-
-
@@ -1075,7 +1075,7 @@ Kill Streak (Passive)
- +
@@ -1089,27 +1089,27 @@
-
-
-
-
-
@@ -1119,7 +1119,7 @@ Concentration (Passive)
- +
@@ -1133,27 +1133,27 @@
-
-
-
-
-
@@ -1163,7 +1163,7 @@ Endurance (Passive)
- +
@@ -1177,27 +1177,27 @@
-
-
-
-
-
@@ -1207,7 +1207,7 @@ Dodge (Passive)
- +
diff --git a/js/build_constants.js b/js/build_constants.js index eeaae5d..edd88ac 100644 --- a/js/build_constants.js +++ b/js/build_constants.js @@ -84,7 +84,7 @@ let tome_names = [ "Armor Tome", "Guild Tome", ] -let equipmentInputs = equipment_fields.map(x => x + "-choice"); +let equipment_inputs = equipment_fields.map(x => x + "-choice"); let build_fields = equipment_fields.map(x => x+"-tooltip"); let tomeInputs = tome_fields.map(x => x + "-choice"); diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 834afdd..2601830 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -8,7 +8,6 @@ function parsePowdering(powder_info) { powder_info = powder_info.slice(1); for (let j = 0; j < n_blocks; ++j) { let block = powder_info.slice(0,5); - console.log(block); let six_powders = Base64.toInt(block); for (let k = 0; k < 6 && six_powders != 0; ++k) { powders += powderNames.get((six_powders & 0x1f) - 1); @@ -84,7 +83,7 @@ function decodeBuild(url_tag) { } //constant in all versions for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); + setValue(equipment_inputs[i], equipment[i]); } //level, skill point assignments, and powdering @@ -133,7 +132,6 @@ function decodeBuild(url_tag) { setValue(powder_inputs[i], powdering[i]); } for (let i in skillpoints) { - console.log(skillpoints[i]); setValue(skp_order[i] + "-skp", skillpoints[i]); } } @@ -147,7 +145,7 @@ function encodeBuild(build, powders, skillpoints) { let build_string; //V6 encoding - Tomes - build_version = 4; + build_version = 5; build_string = ""; tome_string = ""; diff --git a/js/builder.js b/js/builder.js index 0251fef..ec4f93f 100644 --- a/js/builder.js +++ b/js/builder.js @@ -69,20 +69,48 @@ function loadBuild() { } function resetFields(){ - for (let i in powderInputs) { - setValue(powderInputs[i], ""); + for (const i of powder_inputs) { + setValue(i, ""); } - for (let i in equipmentInputs) { - setValue(equipmentInputs[i], ""); + for (const i of equipment_inputs) { + setValue(i, ""); } setValue("str-skp", "0"); setValue("dex-skp", "0"); setValue("int-skp", "0"); setValue("def-skp", "0"); setValue("agi-skp", "0"); + for (const special_name of specialNames) { + for (let i = 1; i < 6; i++) { //toggle all pressed buttons of the same powder special off + //name is same, power is i + let elem = document.getElementById(special_name.replace(" ", "_")+'-'+i); + if (elem.classList.contains("toggleOn")) { + elem.classList.remove("toggleOn"); + } + } + } + for (const [key, value] of damageMultipliers) { + let elem = document.getElementById(key + "-boost") + if (elem.classList.contains("toggleOn")) { + elem.classList.remove("toggleOn"); + } + } + + const nodes_to_reset = item_nodes.concat(powder_nodes.concat(edit_input_nodes)); + for (const node of nodes_to_reset) { + node.mark_dirty(); + } + powder_special_input.mark_dirty(); + boosts_node.mark_dirty(); + + for (const node of nodes_to_reset) { + node.update(); + } + powder_special_input.update(); + boosts_node.update(); + setValue("level-choice", "106"); location.hash = ""; - calculateBuild(); } function toggleID() { @@ -130,6 +158,15 @@ function toggle_spell_tab(tab) { } } +function toggle_boost_tab(tab) { + for (const i of skp_order) { + document.querySelector("#"+i+"-boost").style.display = "none"; + document.getElementById(i + "-boost-tab").classList.remove("selected-btn"); + } + document.querySelector("#"+tab+"-boost").style.display = ""; + document.getElementById(tab + "-boost-tab").classList.add("selected-btn"); + +} let tabs = ['overall-stats', 'offensive-stats', 'defensive-stats']; function show_tab(tab) { diff --git a/js/builder_graph.js b/js/builder_graph.js index 29ec4d6..ae36651 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -1,7 +1,136 @@ +let boosts_node; +/* Updates all spell boosts +*/ +function updateBoosts(buttonId) { + let elem = document.getElementById(buttonId); + if (elem.classList.contains("toggleOn")) { + elem.classList.remove("toggleOn"); + } else { + elem.classList.add("toggleOn"); + } + boosts_node.mark_dirty(); + boosts_node.update(); +} + +class BoostsInputNode extends ComputeNode { + constructor() { super('builder-boost-input'); } + + compute_func(input_map) { + let damage_boost = 0; + let def_boost = 0; + for (const [key, value] of damageMultipliers) { + let elem = document.getElementById(key + "-boost") + if (elem.classList.contains("toggleOn")) { + damage_boost += value; + if (key === "warscream") { def_boost += .20 } + if (key === "vanish") { def_boost += .15 } + } + } + return [damage_boost, def_boost]; + } +} + +let powder_special_input; +let specialNames = ["Quake", "Chain Lightning", "Curse", "Courage", "Wind Prison"]; + +function updatePowderSpecials(buttonId) { + //console.log(player_build.statMap); + + let name = (buttonId).split("-")[0]; + let power = (buttonId).split("-")[1]; // [1, 5] + + let elem = document.getElementById(buttonId); + if (elem.classList.contains("toggleOn")) { //toggle the pressed button off + elem.classList.remove("toggleOn"); + } else { + for (let i = 1;i < 6; i++) { //toggle all pressed buttons of the same powder special off + //name is same, power is i + if(document.getElementById(name.replace(" ", "_") + "-" + i).classList.contains("toggleOn")) { + document.getElementById(name.replace(" ", "_") + "-" + i).classList.remove("toggleOn"); + } + } + //toggle the pressed button on + elem.classList.add("toggleOn"); + } + + powder_special_input.mark_dirty(); + powder_special_input.update(); +} + +class PowderSpecialInputNode extends ComputeNode { + constructor() { super('builder-powder-special-input'); } + + compute_func(input_map) { + let powder_specials = []; // [ [special, power], [special, power]] + for (const sName of specialNames) { + for (let i = 1;i < 6; i++) { + if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) { + let powder_special = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))]; + powder_specials.push([powder_special, i]); + break; + } + } + } + return powder_specials; + } +} + +class PowderSpecialCalcNode extends ComputeNode { + constructor() { super('builder-powder-special-apply'); } + + compute_func(input_map) { + const powder_specials = input_map.get('powder-specials'); + let stats = new Map(); + for (const [special, power] of powder_specials) { + if (special["weaponSpecialEffects"].has("Damage Boost")) { + let name = special["weaponSpecialName"]; + if (name === "Courage" || name === "Curse") { //courage and curse are is universal damage boost + stats.set("sdPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); + stats.set("mdPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); + stats.set("poisonPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); + } else if (name === "Wind Prison") { + stats.set("aDamPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]); + } + } + } + return stats; + } +} + +class PowderSpecialDisplayNode extends ComputeNode { + constructor() { + super('builder-powder-special-display'); + this.fail_cb = true; + } + + compute_func(input_map) { + const powder_specials = input_map.get('powder-specials'); + const stats = input_map.get('stats'); + const weapon = input_map.get('weapon'); + displayPowderSpecials(document.getElementById("powder-special-stats"), powder_specials, stats, weapon.statMap, true); + } +} + +/** + * Apply armor powders. + * Encoding shortcut assumes that all powders give +def to one element + * and -def to the element "behind" it in cycle ETWFA, which is true + * as of now and unlikely to change in the near future. + */ +function applyArmorPowders(expandedItem, powders) { + for(const id of powders){ + let powder = powderStats[id]; + let name = powderNames.get(id).charAt(0); + let prevName = skp_elements[(skp_elements.indexOf(name) + 4 )% 5]; + expandedItem.set(name+"Def", (expandedItem.get(name+"Def") || 0) + powder["defPlus"]); + expandedItem.set(prevName+"Def", (expandedItem.get(prevName+"Def") || 0) - powder["defMinus"]); + } +} + /** * Node for getting an item's stats from an item input field. * - * Signature: ItemInputNode() => Item | null + * Signature: ItemInputNode(powdering: Optional[list[powder]]) => Item | null */ class ItemInputNode extends InputNode { /** @@ -18,36 +147,36 @@ class ItemInputNode extends InputNode { } compute_func(input_map) { - // built on the assumption of no one will type in CI/CR letter by letter + const powdering = input_map.get('powdering'); + // built on the assumption of no one will type in CI/CR letter by letter let item_text = this.input_field.value; if (!item_text) { return this.none_item; } let item; - - if (item_text.slice(0, 3) == "CI-") { - item = getCustomFromHash(item_text); - } - else if (item_text.slice(0, 3) == "CR-") { - item = getCraftFromHash(item_text); - } - else if (itemMap.has(item_text)) { - item = new Item(itemMap.get(item_text)); - } - else if (tomeMap.has(item_text)) { - item = new Item(tomeMap.get(item_text)); - } + if (item_text.slice(0, 3) == "CI-") { item = getCustomFromHash(item_text); } + else if (item_text.slice(0, 3) == "CR-") { item = getCraftFromHash(item_text); } + else if (itemMap.has(item_text)) { item = new Item(itemMap.get(item_text)); } + else if (tomeMap.has(item_text)) { item = new Item(tomeMap.get(item_text)); } if (item) { + if (powdering !== undefined) { + item.statMap.set('powders', powdering); + } let type_match; if (this.none_item.statMap.get('category') === 'weapon') { type_match = item.statMap.get('category') === 'weapon'; } else { type_match = item.statMap.get('type') === this.none_item.statMap.get('type'); } - if (type_match) { return item; } + if (type_match) { + if (item.statMap.get('category') === 'armor' && powdering !== undefined) { + applyArmorPowders(item.statMap, powdering); + } + return item; + } } return null; } @@ -142,11 +271,11 @@ class WeaponInputDisplayNode extends ComputeNode { * Encode the build into a url-able string. * * Signature: BuildEncodeNode(build: Build, - helmet-powder: List[powder], - chestplate-powder: List[powder], - leggings-powder: List[powder], - boots-powder: List[powder], - weapon-powder: List[powder]) => str + * helmet-powder: List[powder], + * chestplate-powder: List[powder], + * leggings-powder: List[powder], + * boots-powder: List[powder], + * weapon-powder: List[powder]) => str */ class BuildEncodeNode extends ComputeNode { constructor() { super("builder-encode"); } @@ -227,7 +356,7 @@ class BuildAssembleNode extends ComputeNode { for (const item of equipments) { all_none = all_none && item.statMap.has('NONE'); } - if (all_none) { + if (all_none && !location.hash) { return null; } return new Build(level, equipments, [], weapon); @@ -314,17 +443,17 @@ function getDefenseStats(stats) { defenseStats.push(totalHp); //EHP let ehp = [totalHp, totalHp]; - let defMult = stats.get("classDef"); - ehp[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); - ehp[1] /= (1-def_pct)*(2-defMult); + let defMult = (2 - stats.get("classDef")) * (1 - stats.get("defBonus")); + ehp[0] /= (1-def_pct)*(1-agi_pct)*defMult; + ehp[1] /= (1-def_pct)*defMult; defenseStats.push(ehp); //HPR let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); defenseStats.push(totalHpr); //EHPR let ehpr = [totalHpr, totalHpr]; - ehpr[0] /= (1-def_pct)*(1-agi_pct)*(2-defMult); - ehpr[1] /= (1-def_pct)*(2-defMult); + ehpr[0] /= (1-def_pct)*(1-agi_pct)*defMult; + ehpr[1] /= (1-def_pct)*defMult; defenseStats.push(ehpr); //skp stats defenseStats.push([ def_pct*100, agi_pct*100]); @@ -345,7 +474,6 @@ function getDefenseStats(stats) { * * Signature: SpellDamageCalcNode(weapon-input: Item, * stats: StatMap, - * weapon-powder: List[powder], * spell-info: [Spell, SpellParts]) => List[SpellDamage] */ class SpellDamageCalcNode extends ComputeNode { @@ -355,11 +483,10 @@ class SpellDamageCalcNode extends ComputeNode { compute_func(input_map) { const weapon = new Map(input_map.get('weapon-input').statMap); - const weapon_powder = input_map.get('weapon-powder'); - const damage_mult = 1; // TODO: hook up const spell_info = input_map.get('spell-info'); const spell_parts = spell_info[1]; const stats = input_map.get('stats'); + const damage_mult = stats.get('damageMultiplier'); const skillpoints = [ stats.get('str'), stats.get('dex'), @@ -367,8 +494,6 @@ class SpellDamageCalcNode extends ComputeNode { stats.get('def'), stats.get('agi') ]; - - weapon.set("powders", weapon_powder); let spell_results = [] for (const part of spell_parts) { @@ -441,7 +566,7 @@ function getMeleeStats(stats, weapon) { adjAtkSpd = 0; } - let damage_mult = 1; + let damage_mult = stats.get("damageMultiplier"); if (weapon_stats.get("type") === "relik") { damage_mult = 0.99; // CURSE YOU WYNNCRAFT //One day we will create WynnWynn and no longer have shaman 99% melee injustice. @@ -485,7 +610,6 @@ class BuildDisplayNode extends ComputeNode { displaySetBonuses("set-info", build); let meleeStats = getMeleeStats(stats, build.weapon); // TODO: move weapon out? - console.log(meleeStats); displayMeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); displayDefenseStats(document.getElementById("defensive-stats"), stats); @@ -576,13 +700,11 @@ class DisplayBuildWarningsNode extends ComputeNode { if (build.level < item_lvl) { if (!lvlWarning) { lvlWarning = document.createElement("p"); - lvlWarning.classList.add("itemp"); - lvlWarning.classList.add("warning"); + lvlWarning.classList.add("itemp"); lvlWarning.classList.add("warning"); lvlWarning.textContent = "WARNING: A level " + build.level + " player cannot use some piece(s) of this build." } let baditem = document.createElement("p"); - baditem.classList.add("nocolor"); - baditem.classList.add("itemp"); + baditem.classList.add("nocolor"); baditem.classList.add("itemp"); baditem.textContent = item.get("displayName") + " requires level " + item_lvl + " to use."; lvlWarning.appendChild(baditem); } @@ -594,8 +716,7 @@ class DisplayBuildWarningsNode extends ComputeNode { const bonus = sets.get(setName).bonuses[count-1]; if (bonus["illegal"]) { let setWarning = document.createElement("p"); - setWarning.classList.add("itemp"); - setWarning.classList.add("warning"); + setWarning.classList.add("itemp"); setWarning.classList.add("warning"); setWarning.textContent = "WARNING: illegal item combination: " + setName summarybox.append(setWarning); } @@ -612,15 +733,26 @@ class AggregateStatsNode extends ComputeNode { constructor() { super("builder-aggregate-stats"); } compute_func(input_map) { - const build = input_map.get('build'); - const weapon = input_map.get('weapon'); + const build = input_map.get('build'); input_map.delete('build'); + const powder_boost = input_map.get('powder-boost'); input_map.delete('powder-boost'); + const potion_boost = input_map.get('potion-boost'); input_map.delete('potion-boost'); + const weapon = input_map.get('weapon'); input_map.delete('weapon'); + const output_stats = new Map(build.statMap); + output_stats.set("damageMultiplier", 1 + potion_boost[0]); + output_stats.set("defBonus", potion_boost[1]); for (const [k, v] of input_map.entries()) { - if (k === 'build') { - continue; - } output_stats.set(k, v); } + for (const [k, v] of powder_boost.entries()) { + if (output_stats.has(k)) { + output_stats.set(k, v + output_stats.get(k)); + } + else { + output_stats.set(k, v); + } + } + output_stats.set('classDef', classDefenseMultipliers.get(weapon.statMap.get("type"))); return output_stats; } @@ -656,6 +788,7 @@ class SkillPointSetterNode extends ComputeNode { } compute_func(input_map) { + console.log("a"); if (input_map.size !== 1) { throw "SkillPointSetterNode accepts exactly one input (build)"; } const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element for (const [idx, elem] of skp_order.entries()) { @@ -750,6 +883,12 @@ function builder_graph_init() { build_encode_node.link_to(powder_node, input); } + item_nodes[0].link_to(powder_nodes[0], 'powdering'); + item_nodes[1].link_to(powder_nodes[1], 'powdering'); + item_nodes[2].link_to(powder_nodes[2], 'powdering'); + item_nodes[3].link_to(powder_nodes[3], 'powdering'); + item_nodes[8].link_to(powder_nodes[4], 'powdering'); + // Edit IDs setter declared up here to set ids so they will be populated by default. let edit_id_output = new EditableIDSetterNode(); edit_id_output.link_to(build_node); @@ -788,6 +927,19 @@ function builder_graph_init() { } level_input.update(); + // Powder specials. + powder_special_input = new PowderSpecialInputNode(); + let powder_special_calc = new PowderSpecialCalcNode().link_to(powder_special_input, 'powder-specials'); + new PowderSpecialDisplayNode().link_to(powder_special_input, 'powder-specials') + .link_to(stat_agg_node, 'stats').link_to(item_nodes[8], 'weapon'); + stat_agg_node.link_to(powder_special_calc, 'powder-boost'); + powder_special_input.update(); + + // Potion boost. + boosts_node = new BoostsInputNode(); + stat_agg_node.link_to(boosts_node, 'potion-boost'); + boosts_node.update(); + // Also do something similar for skill points for (let i = 0; i < 4; ++i) { @@ -798,7 +950,7 @@ function builder_graph_init() { let calc_node = new SpellDamageCalcNode(i); calc_node.link_to(item_nodes[8], 'weapon-input').link_to(stat_agg_node, 'stats') - .link_to(powder_nodes[4], 'weapon-powder').link_to(spell_node, 'spell-info'); + .link_to(spell_node, 'spell-info'); spelldmg_nodes.push(calc_node); let display_node = new SpellDisplayNode(i); diff --git a/js/damage_calc.js b/js/damage_calc.js index eef3825..490713f 100644 --- a/js/damage_calc.js +++ b/js/damage_calc.js @@ -72,9 +72,6 @@ function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, damages[element+1][1] += powder.max; } - - //console.log(tooltipinfo); - damages[0] = neutralRemainingRaw; let damageMult = damageMultiplier; diff --git a/js/display.js b/js/display.js index c87f689..61c57f0 100644 --- a/js/display.js +++ b/js/display.js @@ -1,18 +1,3 @@ -/** - * Apply armor powders. - * Encoding shortcut assumes that all powders give +def to one element - * and -def to the element "behind" it in cycle ETWFA, which is true - * as of now and unlikely to change in the near future. - */ -function applyArmorPowders(expandedItem, powders) { - for(const id of powders){ - let powder = powderStats[id]; - let name = powderNames.get(id).charAt(0); - let prevName = skp_elements[(skp_elements.indexOf(name) + 4 )% 5]; - expandedItem.set(name+"Def", (expandedItem.get(name+"Def") || 0) + powder["defPlus"]); - expandedItem.set(prevName+"Def", (expandedItem.get(prevName+"Def") || 0) - powder["defMinus"]); - } -} function apply_elemental_format(p_elem, id, suffix) { suffix = (typeof suffix !== 'undefined') ? suffix : ""; @@ -192,6 +177,11 @@ function displayExpandedItem(item, parent_id){ if (item.get("category") === "weapon") { let stats = new Map(); stats.set("atkSpd", item.get("atkSpd")); + stats.set("eDamPct", 0); + stats.set("tDamPct", 0); + stats.set("wDamPct", 0); + stats.set("fDamPct", 0); + stats.set("aDamPct", 0); stats.set("damageBonus", [0, 0, 0, 0, 0]); //SUPER JANK @HPP PLS FIX @@ -991,9 +981,15 @@ function displayNextCosts(_stats, spell, spellIdx) { } function displayRolledID(item, id, elemental_format) { - let row = document.createElement('tr'); - let min_elem = document.createElement('td'); - min_elem.classList.add('left'); + let row = document.createElement('div'); + row.classList.add('col'); + + let item_div = document.createElement('div'); + item_div.classList.add('row'); + + let min_elem = document.createElement('div'); + min_elem.classList.add('col', 'text-start'); + min_elem.style.cssText += "flex-grow: 0"; let id_min = item.get("minRolls").get(id) let style = id_min < 0 ? "negative" : "positive"; if(reversedIDs.includes(id)){ @@ -1001,10 +997,11 @@ function displayRolledID(item, id, elemental_format) { } min_elem.classList.add(style); min_elem.textContent = id_min + idSuffixes[id]; - row.appendChild(min_elem); + item_div.appendChild(min_elem); - let desc_elem = document.createElement('td'); - desc_elem.classList.add('center'); + let desc_elem = document.createElement('div'); + desc_elem.classList.add('col', 'text-center');//, 'text-nowrap'); + desc_elem.style.cssText += "flex-grow: 1"; //TODO elemental format jank if (elemental_format) { apply_elemental_format(desc_elem, id); @@ -1012,18 +1009,20 @@ function displayRolledID(item, id, elemental_format) { else { desc_elem.textContent = idPrefixes[id]; } - row.appendChild(desc_elem); + item_div.appendChild(desc_elem); - let max_elem = document.createElement('td'); + let max_elem = document.createElement('div'); let id_max = item.get("maxRolls").get(id) - max_elem.classList.add('right'); + max_elem.classList.add('col', 'text-end'); + max_elem.style.cssText += "flex-grow: 0"; style = id_max < 0 ? "negative" : "positive"; - if(reversedIDs.includes(id)){ + if (reversedIDs.includes(id)) { style === "positive" ? style = "negative" : style = "positive"; } max_elem.classList.add(style); max_elem.textContent = id_max + idSuffixes[id]; - row.appendChild(max_elem); + item_div.appendChild(max_elem); + row.appendChild(item_div); return row; } @@ -1458,55 +1457,60 @@ function displayDefenseStats(parent_elem, statMap, insertSummary){ } } -function displayPowderSpecials(parent_elem, powderSpecials, build) { - parent_elem.textContent = "Powder Specials"; +function displayPowderSpecials(parent_elem, powderSpecials, stats, weapon, overall=false) { + const skillpoints = [ + stats.get('str'), + stats.get('dex'), + stats.get('int'), + stats.get('def'), + stats.get('agi') + ]; + parent_elem.textContent = "" + let title = document.createElement("b"); + title.textContent = "Powder Specials"; + parent_elem.appendChild(title); let specials = powderSpecials.slice(); - let stats = build.statMap; let expandedStats = new Map(); //each entry of powderSpecials is [ps, power] for (special of specials) { //iterate through the special and display its effects. let powder_special = document.createElement("p"); - powder_special.classList.add("left"); let specialSuffixes = new Map([ ["Duration", " sec"], ["Radius", " blocks"], ["Chains", ""], ["Damage", "%"], ["Damage Boost", "%"], ["Knockback", " blocks"] ]); let specialTitle = document.createElement("p"); let specialEffects = document.createElement("p"); - specialTitle.classList.add("left"); - specialTitle.classList.add("itemp"); specialTitle.classList.add(damageClasses[powderSpecialStats.indexOf(special[0]) + 1]); - specialEffects.classList.add("left"); - specialEffects.classList.add("itemp"); - specialEffects.classList.add("nocolor"); let effects = special[0]["weaponSpecialEffects"]; let power = special[1]; specialTitle.textContent = special[0]["weaponSpecialName"] + " " + Math.floor((power-1)*0.5 + 4) + (power % 2 == 0 ? ".5" : ""); - for (const [key,value] of effects) { - let effect = document.createElement("p"); - effect.classList.add("itemp"); - effect.textContent += key + ": " + value[power-1] + specialSuffixes.get(key); - if(key === "Damage"){ - effect.textContent += elementIcons[powderSpecialStats.indexOf(special[0])]; - } - if(special[0]["weaponSpecialName"] === "Wind Prison" && key === "Damage Boost") { - effect.textContent += " (only 1st hit)"; - } - specialEffects.appendChild(effect); - } + if (!overall || powderSpecialStats.indexOf(special[0]) == 2 || powderSpecialStats.indexOf(special[0]) == 3 || powderSpecialStats.indexOf(special[0]) == 4) { + for (const [key,value] of effects) { + let effect = document.createElement("p"); + effect.textContent += key + ": " + value[power-1] + specialSuffixes.get(key); + if(key === "Damage"){ + effect.textContent += elementIcons[powderSpecialStats.indexOf(special[0])]; + } + if(special[0]["weaponSpecialName"] === "Wind Prison" && key === "Damage Boost") { + effect.textContent += " (only 1st hit)"; + } + specialEffects.appendChild(effect); + } + } powder_special.appendChild(specialTitle); powder_special.appendChild(specialEffects); //if this special is an instant-damage special (Quake, Chain Lightning, Courage Burst), display the damage. let specialDamage = document.createElement("p"); + // specialDamage.classList.add("item-margin"); let spells = spell_table["powder"]; if (powderSpecialStats.indexOf(special[0]) == 0 || powderSpecialStats.indexOf(special[0]) == 1 || powderSpecialStats.indexOf(special[0]) == 3) { //Quake, Chain Lightning, or Courage let spell = (powderSpecialStats.indexOf(special[0]) == 3 ? spells[2] : spells[powderSpecialStats.indexOf(special[0])]); let part = spell["parts"][0]; let _results = calculateSpellDamage(stats, part.conversion, - stats.get("mdRaw"), stats.get("mdPct") + build.externalStats.get("mdPct"), - 0, build.weapon, build.total_skillpoints, build.damageMultiplier * ((part.multiplier[power-1] / 100)), build.externalStats);//part.multiplier[power] / 100 + stats.get("mdRaw"), stats.get("mdPct"), + 0, weapon, skillpoints, stats.get('damageMultiplier') * ((part.multiplier[power-1] / 100)));//part.multiplier[power] / 100 - let critChance = skillPointsToPercentage(build.total_skillpoints[1]); + let critChance = skillPointsToPercentage(skillpoints[1]); let save_damages = []; let totalDamNormal = _results[0]; @@ -1521,59 +1525,69 @@ function displayPowderSpecials(parent_elem, powderSpecials, build) { let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; - let averageLabel = document.createElement("p"); - averageLabel.textContent = "Average: "+averageDamage.toFixed(2); - averageLabel.classList.add("damageSubtitle"); - specialDamage.append(averageLabel); - - - let nonCritLabel = document.createElement("p"); - nonCritLabel.textContent = "Non-Crit Average: "+nonCritAverage.toFixed(2); - nonCritLabel.classList.add("damageSubtitle"); - specialDamage.append(nonCritLabel); + let averageWrap = document.createElement("p"); + let averageLabel = document.createElement("span"); + averageLabel.textContent = "Average: "; - for (let i = 0; i < 6; i++){ - if (results[i][1] > 0){ - let p = document.createElement("p"); - p.classList.add("damagep"); - p.classList.add(damageClasses[i]); - p.textContent = results[i][0]+"-"+results[i][1]; - specialDamage.append(p); - } - } - let normalDamage = document.createElement("p"); - normalDamage.textContent = "Total: " + totalDamNormal[0].toFixed(2) + "-" + totalDamNormal[1].toFixed(2); - normalDamage.classList.add("itemp"); - specialDamage.append(normalDamage); + let averageLabelDmg = document.createElement("span"); + averageLabelDmg.classList.add("Damage"); + averageLabelDmg.textContent = averageDamage.toFixed(2); - let nonCritChanceLabel = document.createElement("p"); - nonCritChanceLabel.textContent = "Non-Crit Chance: " + ((1-critChance)*100).toFixed(2) + "%"; - specialDamage.append(nonCritChanceLabel); - - let critLabel = document.createElement("p"); - critLabel.textContent = "Crit Average: "+critAverage.toFixed(2); - critLabel.classList.add("damageSubtitle"); + averageWrap.appendChild(averageLabel); + averageWrap.appendChild(averageLabelDmg); + specialDamage.appendChild(averageWrap); - specialDamage.append(critLabel); - for (let i = 0; i < 6; i++){ - if (results[i][1] > 0){ - let p = document.createElement("p"); - p.classList.add("damagep"); - p.classList.add(damageClasses[i]); - p.textContent = results[i][2]+"-"+results[i][3]; - specialDamage.append(p); + if (!overall) { + let nonCritLabel = document.createElement("p"); + nonCritLabel.textContent = "Non-Crit Average: "+nonCritAverage.toFixed(2); + nonCritLabel.classList.add("damageSubtitle"); + nonCritLabel.classList.add("item-margin"); + specialDamage.append(nonCritLabel); + + for (let i = 0; i < 6; i++){ + if (results[i][1] > 0){ + let p = document.createElement("p"); + p.classList.add("damagep"); + p.classList.add(damageClasses[i]); + p.textContent = results[i][0]+"-"+results[i][1]; + specialDamage.append(p); + } } + let normalDamage = document.createElement("p"); + normalDamage.textContent = "Total: " + totalDamNormal[0].toFixed(2) + "-" + totalDamNormal[1].toFixed(2); + normalDamage.classList.add("itemp"); + specialDamage.append(normalDamage); + + let nonCritChanceLabel = document.createElement("p"); + nonCritChanceLabel.textContent = "Non-Crit Chance: " + ((1-critChance)*100).toFixed(2) + "%"; + specialDamage.append(nonCritChanceLabel); + + let critLabel = document.createElement("p"); + critLabel.textContent = "Crit Average: "+critAverage.toFixed(2); + critLabel.classList.add("damageSubtitle"); + critLabel.classList.add("item-margin"); + + specialDamage.append(critLabel); + for (let i = 0; i < 6; i++){ + if (results[i][1] > 0){ + let p = document.createElement("p"); + p.classList.add("damagep"); + p.classList.add(damageClasses[i]); + p.textContent = results[i][2]+"-"+results[i][3]; + specialDamage.append(p); + } + } + let critDamage = document.createElement("p"); + critDamage.textContent = "Total: " + totalDamCrit[0].toFixed(2) + "-" + totalDamCrit[1].toFixed(2); + critDamage.classList.add("itemp"); + specialDamage.append(critDamage); + + let critChanceLabel = document.createElement("p"); + critChanceLabel.textContent = "Crit Chance: " + (critChance*100).toFixed(2) + "%"; + specialDamage.append(critChanceLabel); + + save_damages.push(averageDamage); } - let critDamage = document.createElement("p"); - critDamage.textContent = "Total: " + totalDamCrit[0].toFixed(2) + "-" + totalDamCrit[1].toFixed(2); - critDamage.classList.add("itemp"); - specialDamage.append(critDamage); - - let critChanceLabel = document.createElement("p"); - critChanceLabel.textContent = "Crit Chance: " + (critChance*100).toFixed(2) + "%"; - specialDamage.append(critChanceLabel); - - save_damages.push(averageDamage); powder_special.append(specialDamage); } diff --git a/js/load_tome.js b/js/load_tome.js index 62bb135..e6fded5 100644 --- a/js/load_tome.js +++ b/js/load_tome.js @@ -25,7 +25,6 @@ async function load_tome_local() { console.log("Successfully read local tome db."); } get_tx.oncomplete = function(event) { - console.log('b'); console.log(request.readyState); tomes = request.result; init_tome_maps(); From d1a468a227a60909fc6da3699850bcc05ac27f2b Mon Sep 17 00:00:00 2001 From: hppeng Date: Mon, 20 Jun 2022 10:52:46 -0700 Subject: [PATCH 35/68] Remove unused files (should be merged into builder.js... etc now.) --- js/sq2bs.js | 522 -------- js/sq2build.js | 692 ----------- js/sq2builder.js | 1127 ----------------- js/sq2display.js | 2407 ------------------------------------ js/sq2display_constants.js | 191 --- 5 files changed, 4939 deletions(-) delete mode 100644 js/sq2bs.js delete mode 100644 js/sq2build.js delete mode 100644 js/sq2builder.js delete mode 100644 js/sq2display.js delete mode 100644 js/sq2display_constants.js diff --git a/js/sq2bs.js b/js/sq2bs.js deleted file mode 100644 index dfdc701..0000000 --- a/js/sq2bs.js +++ /dev/null @@ -1,522 +0,0 @@ - -document.addEventListener('DOMContentLoaded', function() { - - for (const eq of equipment_keys) { - document.querySelector("#"+eq+"-choice").setAttribute("oninput", "update_field('"+ eq +"'); calcBuildSchedule();"); - document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); - } - - for (const eq of powderable_keys) { - document.querySelector("#"+eq+"-powder").setAttribute("oninput", "calcBuildSchedule(); update_field('"+ eq +"');"); - } - - for (const eq of tome_keys) { - document.querySelector("#" + eq + "-choice").setAttribute("oninput", "update_field('" + eq + "'); calcBuildSchedule();"); - document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); - } - - for (const i of spell_disp) { - document.querySelector("#"+i+"Avg").setAttribute("onclick", "toggle_spell_tab('"+i+"')"); - } - - document.querySelector("#level-choice").setAttribute("oninput", "calcBuildSchedule()") - document.querySelector("#weapon-choice").setAttribute("oninput", document.querySelector("#weapon-choice").getAttribute("oninput") + "resetArmorPowderSpecials();"); - // document.querySelector("#edit-IDs-button").setAttribute("onclick", "toggle_edit_id_tab()"); - - let skp_fields = document.getElementsByClassName("skp-update"); - - for (i = 0; i < skp_fields.length; i++) { - skp_fields[i].setAttribute("oninput", "updateStatSchedule()"); - } - - let masonry = Macy({ - container: "#masonry-container", - columns: 1, - mobileFirst: true, - breakAt: { - 1200: 4, - }, - margin: { - x: 20, - y: 20, - } - - }); - - let search_masonry = Macy({ - container: "#search-results", - columns: 1, - mobileFirst: true, - breakAt: { - 1200: 4, - }, - margin: { - x: 20, - y: 20, - } - - }); - - document.querySelector("#search-container").addEventListener("keyup", function(event) { - if (event.key === "Escape") { - document.querySelector("#search-container").style.display = "none"; - }; - }); - -}); - -// phanta scheduler -let calcBuildTask = null; -let updateStatTask = null; -let doSearchTask = null; - -function calcBuildSchedule(){ - if (calcBuildTask !== null) { - clearTimeout(calcBuildTask); - } - calcBuildTask = setTimeout(function(){ - calcBuildTask = null; - resetEditableIDs(); - calculateBuild(); - }, 500); -} - -function updateStatSchedule(){ - if (updateStatTask !== null) { - clearTimeout(updateStatTask); - } - updateStatTask = setTimeout(function(){ - updateStatTask = null; - updateStats(); - }, 500); -} - -function doSearchSchedule(){ - console.log("Search Schedule called"); - if (doSearchTask !== null) { - clearTimeout(doSearchTask); - } - doSearchTask = setTimeout(function(){ - doSearchTask = null; - doItemSearch(); - window.dispatchEvent(new Event('resize')); - }, 500); -} - -function sq2ResetFields(){ - for (let i in powderInputs) { - setValue(powderInputs[i], ""); - } - for (let i in equipmentInputs) { - setValue(equipmentInputs[i], ""); - } - - for (let i in tomeInputs) { - setValue(tomeInputs[i], ""); - } - setValue("str-skp", "0"); - setValue("dex-skp", "0"); - setValue("int-skp", "0"); - setValue("def-skp", "0"); - setValue("agi-skp", "0"); - setValue("level-choice", "106"); - location.hash = ""; - calculateBuild(); -} - -// equipment field dynamic styling -function update_field(field) { - // built on the assumption of no one will type in CI/CR letter by letter - // resets - document.querySelector("#"+field+"-choice").classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom'); - document.querySelector("#"+field+"-choice").classList.add("text-light"); - document.querySelector("#" + field + "-img").classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow'); - - item = document.querySelector("#"+field+"-choice").value - let powder_slots; - let tier; - let category; - let type; - - // get item info - if (item.slice(0, 3) == "CI-") { - item = getCustomFromHash(item); - powder_slots = item.statMap.get("slots"); - tier = item.statMap.get("tier"); - category = item.statMap.get("category"); - type = item.statMap.get("type"); - } - else if (item.slice(0, 3) == "CR-") { - item = getCraftFromHash(item); - powder_slots = item.statMap.get("slots"); - tier = item.statMap.get("tier"); - category = item.statMap.get("category"); - type = item.statMap.get("type"); - } - else if (itemMap.get(item)) { - item = itemMap.get(item); - if (!item) {return false;} - powder_slots = item.slots; - tier = item.tier; - category = item.category; - type = item.type; - } - else if (tomeMap.get(item)) { - tome = tomeMap.get(item); - if (!tome) {return false;} - powder_slots = 0; - tier = tome.tier; - category = tome.category; - type = tome.type; - } - else { - // item not found - document.querySelector("#"+field+"-choice").classList.add("text-light"); - if (item) { document.querySelector("#"+field+"-choice").classList.add("is-invalid"); } - - /*if (!accessory_keys.contains(type.toLowerCase())) { - document.querySelector("#"+type+"-powder").disabled = true; - }*/ - return false; - } - - - if ((type != field.replace(/[0-9]/g, '')) && (category != field.replace(/[0-9]/g, ''))) { - document.querySelector("#"+field+"-choice").classList.add("text-light"); - if (item) { document.querySelector("#"+field+"-choice").classList.add("is-invalid"); } - - //document.querySelector("#"+equipment_keys[i]+"-powder").disabled = true; - return false; - } - - // set item color - document.querySelector("#"+field+"-choice").classList.add(tier); - document.querySelector("#"+field+"-img").classList.add(tier + "-shadow"); - - - - if (powderable_keys.includes(field)) { - // set powder slots - document.querySelector("#"+field+"-powder").setAttribute("placeholder", powder_slots+" slots"); - - if (powder_slots == 0) { - document.querySelector("#"+field+"-powder").disabled = true; - } else { - document.querySelector("#"+field+"-powder").disabled = false; - } - - // powder error handling - document.querySelector("#" + field + "-powder").classList.remove("is-invalid"); - let powder_string = document.querySelector("#"+field+"-powder").value; - - if (powder_string.length % 2 != 0 || powder_string.length / 2 > powder_slots) { - document.querySelector("#"+field+"-powder").classList.add("is-invalid"); - } else { - for (i = 0; i < powder_string.length / 2; i++) { - if (skp_elements.includes(powder_string.substring(i*2, i*2+2).split("")[0]) == false || isNaN(powder_string.substring(i*2, i*2+2).split("")[1]) || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) < 1 || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) > 6) { - document.querySelector("#"+field+"-powder").classList.add("is-invalid"); - } - } - }; - } - - // set weapon img - if (category == 'weapon') { - document.querySelector("#weapon-img").setAttribute('src', '../media/items/new/generic-'+type+'.png'); - } -} -/* tabulars | man i hate this code but too lazy to fix /shrug */ - -let tabs = ['overall-stats', 'offensive-stats', 'defensive-stats']; - -function show_tab(tab) { - //console.log(itemFilters) - - //hide all tabs, then show the tab of the div clicked and highlight the correct button - for (const i in tabs) { - document.querySelector("#" + tabs[i]).style.display = "none"; - document.getElementById("tab-" + tabs[i].split("-")[0] + "-btn").classList.remove("selected-btn"); - } - document.querySelector("#" + tab).style.display = ""; - document.getElementById("tab-" + tab.split("-")[0] + "-btn").classList.add("selected-btn"); -} - -function toggle_spell_tab(tab) { - let arrow_img = document.querySelector("#" + "arrow_" + tab + "Avg"); - if (document.querySelector("#"+tab).style.display == "none") { - document.querySelector("#"+tab).style.display = ""; - arrow_img.src = arrow_img.src.replace("down", "up"); - } else { - document.querySelector("#"+tab).style.display = "none"; - arrow_img.src = arrow_img.src.replace("up", "down"); - } -} - -function toggle_boost_tab(tab) { - for (const i of skp_order) { - document.querySelector("#"+i+"-boost").style.display = "none"; - document.getElementById(i + "-boost-tab").classList.remove("selected-btn"); - } - document.querySelector("#"+tab+"-boost").style.display = ""; - document.getElementById(tab + "-boost-tab").classList.add("selected-btn"); - -} - -// toggle tab -function toggle_tab(tab) { - if (document.querySelector("#"+tab).style.display == "none") { - document.querySelector("#"+tab).style.display = ""; - } else { - document.querySelector("#"+tab).style.display = "none"; - } -} - -function collapse_element(elmnt) { - elem_list = document.querySelector(elmnt).children; - if (elem_list) { - for (elem of elem_list) { - if (elem.classList.contains("no-collapse")) { continue; } - if (elem.style.display == "none") { - elem.style.display = ""; - } else { - elem.style.display = "none"; - } - } - } - // macy quirk - window.dispatchEvent(new Event('resize')); - // weird bug where display: none overrides?? - document.querySelector(elmnt).style.removeProperty('display'); -} - -// search misc -function set_item(item) { - document.querySelector("#search-container").style.display = "none"; - let type; - // if (!player_build) {return false;} - if (item.get("category") === "weapon") { - type = "weapon"; - } else if (item.get("type") === "ring") { - if (!document.querySelector("#ring1-choice").value) { - type = "ring1"; - } else { - type = "ring2"; - } - } else { - type = item.get("type"); - } - document.querySelector("#"+type+"-choice").value = item.get("displayName"); - calcBuildSchedule(); - update_field(type); -} - -// disable boosts - -function reset_powder_specials() { - let specials = ["Quake", "Chain_Lightning", "Curse", "Courage", "Wind_Prison"] - for (const special of specials) { - for (i = 1; i < 6; i++) { - if (document.querySelector("#"+special+"-"+i).classList.contains("toggleOn")) { - document.querySelector("#"+special+"-"+i).classList.remove("toggleOn"); - } - } - } -} - -// autocomplete initialize -function init_autocomplete() { - let dropdowns = new Map() - for (const eq of equipment_keys) { - if (tome_keys.includes(eq)) { - continue; - } - // build dropdown - let item_arr = []; - if (eq == 'weapon') { - for (const weaponType of weapon_keys) { - for (const weapon of itemLists.get(weaponType)) { - let item_obj = itemMap.get(weapon); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { - continue; - } - item_arr.push(weapon); - } - } - } else { - for (const item of itemLists.get(eq.replace(/[0-9]/g, ''))) { - let item_obj = itemMap.get(item); - if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") { - continue; - } - if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) { - continue; - } - item_arr.push(item) - } - } - - // create dropdown - dropdowns.set(eq, new autoComplete({ - data: { - src: item_arr - }, - selector: "#"+ eq +"-choice", - wrapper: false, - resultsList: { - maxResults: 1000, - tabSelect: true, - noResults: true, - class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", - element: (list, data) => { - // dynamic result loc - let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); - list.style.top = position.bottom + window.scrollY +"px"; - list.style.left = position.x+"px"; - list.style.width = position.width+"px"; - list.style.maxHeight = position.height * 2 +"px"; - - if (!data.results.length) { - message = document.createElement('li'); - message.classList.add('scaled-font'); - message.textContent = "No results found!"; - list.prepend(message); - } - }, - }, - resultItem: { - class: "scaled-font search-item", - selected: "dark-5", - element: (item, data) => { - item.classList.add(itemMap.get(data.value).tier); - }, - }, - events: { - input: { - selection: (event) => { - if (event.detail.selection.value) { - event.target.value = event.detail.selection.value; - } - update_field(eq); - calcBuildSchedule(); - }, - }, - } - })); - } - - for (const eq of tome_keys) { - // build dropdown - let tome_arr = []; - for (const tome of tomeLists.get(eq.replace(/[0-9]/g, ''))) { - let tome_obj = tomeMap.get(tome); - if (tome_obj["restrict"] && tome_obj["restrict"] === "DEPRECATED") { - continue; - } - //this should suffice for tomes - jank - if (tome_obj["name"].includes('No ' + eq.charAt(0).toUpperCase())) { - continue; - } - let tome_name = tome; - tome_arr.push(tome_name); - } - - // create dropdown - dropdowns.set(eq, new autoComplete({ - data: { - src: tome_arr - }, - selector: "#"+ eq +"-choice", - wrapper: false, - resultsList: { - maxResults: 1000, - tabSelect: true, - noResults: true, - class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", - element: (list, data) => { - // dynamic result loc - let position = document.getElementById(eq+'-dropdown').getBoundingClientRect(); - list.style.top = position.bottom + window.scrollY +"px"; - list.style.left = position.x+"px"; - list.style.width = position.width+"px"; - list.style.maxHeight = position.height * 2 +"px"; - - if (!data.results.length) { - message = document.createElement('li'); - message.classList.add('scaled-font'); - message.textContent = "No results found!"; - list.prepend(message); - } - }, - }, - resultItem: { - class: "scaled-font search-item", - selected: "dark-5", - element: (tome, data) => { - tome.classList.add(tomeMap.get(data.value).tier); - }, - }, - events: { - input: { - selection: (event) => { - if (event.detail.selection.value) { - event.target.value = event.detail.selection.value; - } - update_field(eq); - calcBuildSchedule(); - }, - }, - } - })); - } - - let filter_loc = ["filter1", "filter2", "filter3", "filter4"]; - for (const i of filter_loc) { - dropdowns.set(i+"-choice", new autoComplete({ - data: { - src: sq2ItemFilters, - }, - selector: "#"+i+"-choice", - wrapper: false, - resultsList: { - tabSelect: true, - noResults: true, - class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", - element: (list, data) => { - // dynamic result loc - console.log(i); - list.style.zIndex = "100"; - let position = document.getElementById(i+"-dropdown").getBoundingClientRect(); - window_pos = document.getElementById("search-container").getBoundingClientRect(); - list.style.top = position.bottom - window_pos.top + 5 +"px"; - list.style.left = position.x - window_pos.x +"px"; - list.style.width = position.width+"px"; - - if (!data.results.length) { - message = document.createElement('li'); - message.classList.add('scaled-font'); - message.textContent = "No filters found!"; - list.prepend(message); - } - }, - }, - resultItem: { - class: "scaled-font search-item", - selected: "dark-5", - }, - events: { - input: { - selection: (event) => { - if (event.detail.selection.value) { - event.target.value = event.detail.selection.value; - } - doSearchSchedule(); - }, - }, - } - })); - } -} - diff --git a/js/sq2build.js b/js/sq2build.js deleted file mode 100644 index 7551d34..0000000 --- a/js/sq2build.js +++ /dev/null @@ -1,692 +0,0 @@ - - -const classDefenseMultipliers = new Map([ ["relik",0.50], ["bow",0.60], ["wand", 0.80], ["dagger", 1.0], ["spear",1.20], ["sword", 1.10]]); - -/** - * @description Error to catch items that don't exist. - * @module ItemNotFound - */ -class ItemNotFound { - /** - * @class - * @param {String} item the item name entered - * @param {String} type the type of item - * @param {Boolean} genElement whether to generate an element from inputs - * @param {String} override override for item type - */ - constructor(item, type, genElement, override) { - /** - * @public - * @type {String} - */ - this.message = `Cannot find ${override||type} named ${item}`; - if (genElement) - /** - * @public - * @type {Element} - */ - this.element = document.getElementById(`${type}-choice`).parentElement.querySelectorAll("p.error")[0]; - else - this.element = document.createElement("div"); - } -} - -/** - * @description Error to catch incorrect input. - * @module IncorrectInput - */ -class IncorrectInput { - /** - * @class - * @param {String} input the inputted text - * @param {String} format the correct format - * @param {String} sibling the id of the error node's sibling - */ - constructor(input, format, sibling) { - /** - * @public - * @type {String} - */ - this.message = `${input} is incorrect. Example: ${format}`; - /** - * @public - * @type {String} - */ - this.id = sibling; - } -} - -/** - * @description Error that inputs an array of items to generate errors of. - * @module ListError - * @extends Error - */ -class ListError extends Error { - /** - * @class - * @param {Array} errors array of errors - */ - constructor(errors) { - let ret = []; - if (typeof errors[0] == "string") { - super(errors[0]); - } else { - super(errors[0].message); - } - for (let i of errors) { - if (typeof i == "string") { - ret.push(new Error(i)); - } else { - ret.push(i); - } - } - /** - * @public - * @type {Object[]} - */ - this.errors = ret; - } -} - -/*Class that represents a wynn player's build. -*/ -class Build{ - - /** - * @description Construct a build. - * @param {Number} level : Level of the player. - * @param {String[]} equipment : List of equipment names that make up the build. - * In order: boots, Chestplate, Leggings, Boots, Ring1, Ring2, Brace, Neck, Weapon. - * @param {Number[]} powders : Powder application. List of lists of integers (powder IDs). - * In order: boots, Chestplate, Leggings, Boots, Weapon. - * @param {Object[]} inputerrors : List of instances of error-like classes. - * - * @param {Object[]} tomes: List of tomes. - * In order: 2x Weapon Mastery Tome, 4x Armor Mastery Tome, 1x Guild Tome. - * 2x Slaying Mastery Tome, 2x Dungeoneering Mastery Tome, 2x Gathering Mastery Tome are in game, but do not have "useful" stats (those that affect damage calculations or building) - */ - constructor(level,equipment, powders, externalStats, inputerrors=[], tomes){ - - let errors = inputerrors; - //this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem). - this.craftedItems = []; - this.customItems = []; - // NOTE: powders is just an array of arrays of powder IDs. Not powder objects. - this.powders = powders; - if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") { - const helmet = itemMap.get(equipment[0]); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - } else { - try { - let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined); - if (helmet.statMap.get("type") !== "helmet") { - throw new Error("Not a helmet"); - } - this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots")); - helmet.statMap.set("powders",this.powders[0].slice()); - this.helmet = helmet.statMap; - applyArmorPowders(this.helmet, this.powders[0]); - if (this.helmet.get("custom")) { - this.customItems.push(helmet); - } else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(helmet); - } - - } catch (Error) { - const helmet = itemMap.get("No Helmet"); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - errors.push(new ItemNotFound(equipment[0], "helmet", true)); - } - } - if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") { - const chestplate = itemMap.get(equipment[1]); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - } else { - try { - let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined); - if (chestplate.statMap.get("type") !== "chestplate") { - throw new Error("Not a chestplate"); - } - this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots")); - chestplate.statMap.set("powders",this.powders[1].slice()); - this.chestplate = chestplate.statMap; - applyArmorPowders(this.chesplate, this.powders[1]); - if (this.chestplate.get("custom")) { - this.customItems.push(chestplate); - } else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(chestplate); - } - } catch (Error) { - console.log(Error); - const chestplate = itemMap.get("No Chestplate"); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - errors.push(new ItemNotFound(equipment[1], "chestplate", true)); - } - } - if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") { - const leggings = itemMap.get(equipment[2]); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - } else { - try { - let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined); - if (leggings.statMap.get("type") !== "leggings") { - throw new Error("Not a leggings"); - } - this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots")); - leggings.statMap.set("powders",this.powders[2].slice()); - this.leggings = leggings.statMap; - applyArmorPowders(this.leggings, this.powders[2]); - if (this.leggings.get("custom")) { - this.customItems.push(leggings); - } else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(leggings); - } - } catch (Error) { - const leggings = itemMap.get("No Leggings"); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - errors.push(new ItemNotFound(equipment[2], "leggings", true)); - } - } - if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") { - const boots = itemMap.get(equipment[3]); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - } else { - try { - let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined); - if (boots.statMap.get("type") !== "boots") { - throw new Error("Not a boots"); - } - this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots")); - boots.statMap.set("powders",this.powders[3].slice()); - this.boots = boots.statMap; - applyArmorPowders(this.boots, this.powders[3]); - if (this.boots.get("custom")) { - this.customItems.push(boots); - } else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(boots); - } - } catch (Error) { - const boots = itemMap.get("No Boots"); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - errors.push(new ItemNotFound(equipment[3], "boots", true)); - } - } - if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") { - const ring = itemMap.get(equipment[4]); - this.ring1 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring1 = ring.statMap; - if (this.ring1.get("custom")) { - this.customItems.push(ring); - } else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 1"); - this.ring1 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring")); - } - } - if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") { - const ring = itemMap.get(equipment[5]); - this.ring2 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring2 = ring.statMap; - if (this.ring2.get("custom")) { - this.customItems.push(ring); - } else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 2"); - this.ring2 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring")); - } - } - if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") { - const bracelet = itemMap.get(equipment[6]); - this.bracelet = expandItem(bracelet, []); - }else{ - try { - let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined); - if (bracelet.statMap.get("type") !== "bracelet") { - throw new Error("Not a bracelet"); - } - this.bracelet = bracelet.statMap; - if (this.bracelet.get("custom")) { - this.customItems.push(bracelet); - } else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(bracelet); - } - } catch (Error) { - const bracelet = itemMap.get("No Bracelet"); - this.bracelet = expandItem(bracelet, []); - errors.push(new ItemNotFound(equipment[6], "bracelet", true)); - } - } - if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") { - const necklace = itemMap.get(equipment[7]); - this.necklace = expandItem(necklace, []); - }else{ - try { - let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined); - if (necklace.statMap.get("type") !== "necklace") { - throw new Error("Not a necklace"); - } - this.necklace = necklace.statMap; - if (this.necklace.get("custom")) { - this.customItems.push(necklace); - } else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(necklace); - } - } catch (Error) { - const necklace = itemMap.get("No Necklace"); - this.necklace = expandItem(necklace, []); - errors.push(new ItemNotFound(equipment[7], "necklace", true)); - } - } - if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") { - const weapon = itemMap.get(equipment[8]); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - }else{ - try { - let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined); - if (weapon.statMap.get("category") !== "weapon") { - throw new Error("Not a weapon"); - } - this.weapon = weapon.statMap; - if (this.weapon.get("custom")) { - this.customItems.push(weapon); - } else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(weapon); - } - this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots")); - this.weapon.set("powders",this.powders[4].slice()); - // document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } catch (Error) { - const weapon = itemMap.get("No Weapon"); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - // document.getElementsByClassName("powder-specials")[0].style.display = "none"; - errors.push(new ItemNotFound(equipment[8], "weapon", true)); - } - } - - //cannot craft tomes - - if(tomeMap.get(tomes[0]) && tomeMap.get(tomes[0]).type === "weaponTome") { - const weaponTome1 = tomeMap.get(tomes[0]); - this.weaponTome1 = expandItem(weaponTome1, []); - } else { - try { - let weaponTome1 = getCustomFromHash(tomes[0]) ? getCustomFromHash(tomes[0]) : undefined; - if (weaponTome1.statMap.get("type") !== "weaponTome") { - throw new Error("Not a Weapon Tome"); - } - if (this.weaponTome1.get("custom")) { - this.customItems.push(weaponTome1); - } //can't craft tomes - - } catch (Error) { - const weaponTome1 = tomeMap.get("No Weapon Tome"); - this.weaponTome1 = expandItem(weaponTome1, []); - errors.push(new ItemNotFound(tomes[0], "weaponTome1", true)); - } - } - if(tomeMap.get(tomes[1]) && tomeMap.get(tomes[1]).type === "weaponTome") { - const weaponTome2 = tomeMap.get(tomes[1]); - this.weaponTome2 = expandItem(weaponTome2, []); - } else { - try { - let weaponTome2 = getCustomFromHash(tomes[1]) ? getCustomFromHash(tomes[1]) : undefined; - if (weaponTome2.statMap.get("type") !== "weaponTome") { - throw new Error("Not a Weapon Tome"); - } - if (this.weaponTome2.get("custom")) { - this.customItems.push(weaponTome2); - } //can't craft tomes - - } catch (Error) { - const weaponTome2 = tomeMap.get("No Weapon Tome"); - this.weaponTome2 = expandItem(weaponTome2, []); - errors.push(new ItemNotFound(tomes[1], "weaponTome2", true)); - } - } - if(tomeMap.get(tomes[2]) && tomeMap.get(tomes[2]).type === "armorTome") { - const armorTome1 = tomeMap.get(tomes[2]); - this.armorTome1 = expandItem(armorTome1, []); - } else { - try { - let armorTome1 = getCustomFromHash(tomes[2]) ? getCustomFromHash(tomes[2]) : undefined; - if (armorTome1.statMap.get("type") !== "armorTome") { - throw new Error("Not an Armor Tome"); - } - if (this.armorTome1.get("custom")) { - this.customItems.push(armorTome1); - } //can't craft tomes - - } catch (Error) { - const armorTome1 = tomeMap.get("No Armor Tome"); - this.armorTome1 = expandItem(armorTome1, []); - errors.push(new ItemNotFound(tomes[2], "armorTome1", true)); - } - } - if(tomeMap.get(tomes[3]) && tomeMap.get(tomes[3]).type === "armorTome") { - const armorTome2 = tomeMap.get(tomes[3]); - this.armorTome2 = expandItem(armorTome2, []); - } else { - try { - let armorTome2 = getCustomFromHash(tomes[3]) ? getCustomFromHash(tomes[3]) : undefined; - if (armorTome2.statMap.get("type") !== "armorTome") { - throw new Error("Not an Armor Tome"); - } - if (this.armorTome2.get("custom")) { - this.customItems.push(armorTome2); - } //can't craft tomes - - } catch (Error) { - const armorTome2 = tomeMap.get("No Armor Tome"); - this.armorTome2 = expandItem(armorTome2, []); - errors.push(new ItemNotFound(tomes[3], "armorTome2", true)); - } - } - if(tomeMap.get(tomes[4]) && tomeMap.get(tomes[4]).type === "armorTome") { - const armorTome3 = tomeMap.get(tomes[4]); - this.armorTome3 = expandItem(armorTome3, []); - } else { - try { - let armorTome3 = getCustomFromHash(tomes[4]) ? getCustomFromHash(tomes[4]) : undefined; - if (armorTome3.statMap.get("type") !== "armorTome") { - throw new Error("Not an Armor Tome"); - } - if (this.armorTome3.get("custom")) { - this.customItems.push(armorTome3); - } //can't craft tomes - - } catch (Error) { - const armorTome3 = tomeMap.get("No Armor Tome"); - this.armorTome3 = expandItem(armorTome3, []); - errors.push(new ItemNotFound(tomes[4], "armorTome3", true)); - } - } - if(tomeMap.get(tomes[5]) && tomeMap.get(tomes[5]).type === "armorTome") { - const armorTome4 = tomeMap.get(tomes[5]); - this.armorTome4 = expandItem(armorTome4, []); - } else { - try { - let armorTome4 = getCustomFromHash(tomes[5]) ? getCustomFromHash(tomes[5]) : undefined; - if (armorTome4.statMap.get("type") !== "armorTome") { - throw new Error("Not an Armor Tome"); - } - if (this.armorTome4.get("custom")) { - this.customItems.push(armorTome4); - } //can't craft tomes - - } catch (Error) { - const armorTome4 = tomeMap.get("No Armor Tome"); - this.armorTome4 = expandItem(armorTome4, []); - errors.push(new ItemNotFound(tomes[5], "armorTome4", true)); - } - } - if(tomeMap.get(tomes[6]) && tomeMap.get(tomes[6]).type === "guildTome") { - const guildTome1 = tomeMap.get(tomes[6]); - this.guildTome1 = expandItem(guildTome1, []); - } else { - try { - let guildTome1 = getCustomFromHash(tomes[6]) ? getCustomFromHash(tomes[6]) : undefined; - if (guildTome1.statMap.get("type") !== "guildTome1") { - throw new Error("Not an Guild Tome"); - } - if (this.guildTome1.get("custom")) { - this.customItems.push(guildTome1); - } //can't craft tomes - - } catch (Error) { - const guildTome1 = tomeMap.get("No Guild Tome"); - this.guildTome1 = expandItem(guildTome1, []); - errors.push(new ItemNotFound(tomes[6], "guildTome1", true)); - } - } - - //console.log(this.craftedItems) - - if (level < 1) { //Should these be constants? - this.level = 1; - } else if (level > 106) { - this.level = 106; - } else if (level <= 106 && level >= 1) { - 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."); - } - document.getElementById("level-choice").value = this.level; - - this.availableSkillpoints = levelToSkillPoints(this.level); - this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ]; - this.tomes = [this.weaponTome1, this.weaponTome2, this.armorTome1, this.armorTome2, this.armorTome3, this.armorTome4, this.guildTome1]; - this.items = this.equipment.concat([this.weapon]).concat(this.tomes); - // return [equip_order, best_skillpoints, final_skillpoints, best_total]; - let result = calculate_skillpoints(this.equipment.concat(this.tomes), this.weapon); - console.log(result); - this.equip_order = result[0]; - // How many skillpoints the player had to assign (5 number) - this.base_skillpoints = result[1]; - // How many skillpoints the build ended up with (5 number) - this.total_skillpoints = result[2]; - // How many skillpoints assigned (1 number, sum of base_skillpoints) - this.assigned_skillpoints = result[3]; - this.activeSetCounts = result[4]; - - // For strength boosts like warscream, vanish, etc. - this.damageMultiplier = 1.0; - this.defenseMultiplier = 1.0; - - // For other external boosts ;-; - this.externalStats = externalStats; - - this.initBuildStats(); - - // Remove every error before adding specific ones - for (let i of document.getElementsByClassName("error")) { - i.textContent = ""; - } - this.errors = errors; - if (errors.length > 0) this.errored = true; - } - - /*Returns build in string format - */ - toString(){ - return [this.equipment,this.weapon,this.tomes].flat(); - } - - /* Getters */ - - getSpellCost(spellIdx, cost) { - return Math.max(1, this.getBaseSpellCost(spellIdx, cost)); - } - - getBaseSpellCost(spellIdx, cost) { - cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); - cost += this.statMap.get("spRaw"+spellIdx); - return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100)); - } - - - /* Get melee stats for build. - Returns an array in the order: - */ - getMeleeStats(){ - const stats = this.statMap; - if (this.weapon.get("tier") === "Crafted") { - stats.set("damageBases", [this.weapon.get("nDamBaseHigh"),this.weapon.get("eDamBaseHigh"),this.weapon.get("tDamBaseHigh"),this.weapon.get("wDamBaseHigh"),this.weapon.get("fDamBaseHigh"),this.weapon.get("aDamBaseHigh")]); - } - let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier"); - if(adjAtkSpd > 6){ - adjAtkSpd = 6; - }else if(adjAtkSpd < 0){ - adjAtkSpd = 0; - } - - let damage_mult = 1; - if (this.weapon.get("type") === "relik") { - damage_mult = 0.99; // CURSE YOU WYNNCRAFT - //One day we will create WynnWynn and no longer have shaman 99% melee injustice. - //In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams. - } - // 0spellmult for melee damage. - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct") + this.externalStats.get("mdPct"), 0, this.weapon, this.total_skillpoints, damage_mult * this.damageMultiplier, this.externalStats); - - let dex = this.total_skillpoints[1]; - - let totalDamNorm = results[0]; - let totalDamCrit = results[1]; - totalDamNorm.push(1-skillPointsToPercentage(dex)); - totalDamCrit.push(skillPointsToPercentage(dex)); - let damages_results = results[2]; - - let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2]) - +(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2; - - //Now do math - let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd]; - let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd]; - let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex))); - //[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit] - return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]); - } - - /* - Get all defensive stats for this build. - */ - getDefenseStats(){ - const stats = this.statMap; - let defenseStats = []; - let def_pct = skillPointsToPercentage(this.total_skillpoints[3]); - let agi_pct = skillPointsToPercentage(this.total_skillpoints[4]); - //total hp - let totalHp = stats.get("hp") + stats.get("hpBonus"); - if (totalHp < 5) totalHp = 5; - defenseStats.push(totalHp); - //EHP - let ehp = [totalHp, totalHp]; - let defMult = classDefenseMultipliers.get(this.weapon.get("type")); - ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); - ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); - defenseStats.push(ehp); - //HPR - let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); - defenseStats.push(totalHpr); - //EHPR - let ehpr = [totalHpr, totalHpr]; - ehpr[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); - ehpr[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); - defenseStats.push(ehpr); - //skp stats - defenseStats.push([ (1 - ((1-def_pct) * (2 - this.defenseMultiplier)))*100, agi_pct*100]); - //eledefs - TODO POWDERS - let eledefs = [0, 0, 0, 0, 0]; - for(const i in skp_elements){ //kinda jank but ok - eledefs[i] = rawToPct(stats.get(skp_elements[i] + "Def"), stats.get(skp_elements[i] + "DefPct")/100.); - } - defenseStats.push(eledefs); - - //[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]] - return defenseStats; - } - - /* Get all stats for this build. Stores in this.statMap. - @pre The build itself should be valid. No checking of validity of pieces is done here. - */ - initBuildStats(){ - - let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "def", "agi"]; - - //Create a map of this build's stats - let statMap = new Map(); - - for (const staticID of staticIDs) { - statMap.set(staticID, 0); - } - statMap.set("hp", levelToHPBase(this.level)); - - let major_ids = new Set(); - for (const item of this.items){ - for (let [id, value] of item.get("maxRolls")) { - if (staticIDs.includes(id)) { - continue; - } - statMap.set(id,(statMap.get(id) || 0)+value); - } - for (const staticID of staticIDs) { - if (item.get(staticID)) { - statMap.set(staticID, statMap.get(staticID) + item.get(staticID)); - } - } - if (item.get("majorIds")) { - for (const major_id of item.get("majorIds")) { - major_ids.add(major_id); - } - } - } - statMap.set("activeMajorIDs", major_ids); - for (const [setName, count] of this.activeSetCounts) { - const bonus = sets[setName].bonuses[count-1]; - for (const id in bonus) { - if (skp_order.includes(id)) { - // pass. Don't include skillpoints in ids - } - else { - statMap.set(id,(statMap.get(id) || 0)+bonus[id]); - } - } - } - statMap.set("poisonPct", 100); - - // The stuff relevant for damage calculation!!! @ferricles - statMap.set("atkSpd", this.weapon.get("atkSpd")); - - for (const x of skp_elements) { - this.externalStats.set(x + "DamPct", 0); - } - this.externalStats.set("mdPct", 0); - this.externalStats.set("sdPct", 0); - this.externalStats.set("damageBonus", [0, 0, 0, 0, 0]); - this.externalStats.set("defBonus",[0, 0, 0, 0, 0]); - this.externalStats.set("poisonPct", 0); - this.statMap = statMap; - - this.aggregateStats(); - } - - aggregateStats() { - let statMap = this.statMap; - statMap.set("damageRaw", [this.weapon.get("nDam"), this.weapon.get("eDam"), this.weapon.get("tDam"), this.weapon.get("wDam"), this.weapon.get("fDam"), this.weapon.get("aDam")]); - statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]); - statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]); - statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]); - statMap.set("defMult", classDefenseMultipliers.get(this.weapon.get("type"))); - } -} diff --git a/js/sq2builder.js b/js/sq2builder.js deleted file mode 100644 index 31a0657..0000000 --- a/js/sq2builder.js +++ /dev/null @@ -1,1127 +0,0 @@ -const url_tag = location.hash.slice(1); -// console.log(url_base); -// console.log(url_tag); - - -const BUILD_VERSION = "7.0.19"; - -let player_build; - - -// THIS IS SUPER DANGEROUS, WE SHOULD NOT BE KEEPING THIS IN SO MANY PLACES -let editable_item_fields = [ "sdPct", "sdRaw", "mdPct", "mdRaw", "poison", - "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", - "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", - "hprRaw", "hprPct", "hpBonus", "atkTier", - "spPct1", "spRaw1", "spPct2", "spRaw2", - "spPct3", "spRaw3", "spPct4", "spRaw4" ]; - -let editable_elems = []; - -for (let i of editable_item_fields) { - let elem = document.getElementById(i); - elem.addEventListener("change", (event) => { - elem.classList.add("highlight"); - }); - editable_elems.push(elem); -} - -for (let i of skp_order) { - let elem = document.getElementById(i+"-skp"); - elem.addEventListener("change", (event) => { - elem.classList.add("highlight"); - }); - editable_elems.push(elem); -} - -function clear_highlights() { - for (let i of editable_elems) { - i.classList.remove("highlight"); - } -} - - -let equipment_fields = [ - "helmet", - "chestplate", - "leggings", - "boots", - "ring1", - "ring2", - "bracelet", - "necklace", - "weapon" -]; -let tome_fields = [ - "weaponTome1", - "weaponTome2", - "armorTome1", - "armorTome2", - "armorTome3", - "armorTome4", - "guildTome1", -] -let equipment_names = [ - "Helmet", - "Chestplate", - "Leggings", - "Boots", - "Ring 1", - "Ring 2", - "Bracelet", - "Necklace", - "Weapon" -]; - -let tome_names = [ - "Weapon Tome", - "Weapon Tome", - "Armor Tome", - "Armor Tome", - "Armor Tome", - "Armor Tome", - "Guild Tome", -] -let equipmentInputs = equipment_fields.map(x => x + "-choice"); -let buildFields = equipment_fields.map(x => x+"-tooltip").concat(tome_fields.map(x => x + "-tooltip")); -let tomeInputs = tome_fields.map(x => x + "-choice"); - -let powderInputs = [ - "helmet-powder", - "chestplate-powder", - "leggings-powder", - "boots-powder", - "weapon-powder", -]; - -function getItemNameFromID(id) { - if (redirectMap.has(id)) { - return getItemNameFromID(redirectMap.get(id)); - } - return idMap.get(id); -} - -function getTomeNameFromID(id) { - if (tomeRedirectMap.has(id)) { - return getTomeNameFromID(tomeRedirectMap.get(id)); - } - return tomeIDMap.get(id); -} - -function parsePowdering(powder_info) { - // TODO: Make this run in linear instead of quadratic time... ew - let powdering = []; - for (let i = 0; i < 5; ++i) { - let powders = ""; - let n_blocks = Base64.toInt(powder_info.charAt(0)); - powder_info = powder_info.slice(1); - for (let j = 0; j < n_blocks; ++j) { - let block = powder_info.slice(0,5); - let six_powders = Base64.toInt(block); - for (let k = 0; k < 6 && six_powders != 0; ++k) { - powders += powderNames.get((six_powders & 0x1f) - 1); - six_powders >>>= 5; - } - powder_info = powder_info.slice(5); - } - powdering[i] = powders; - } - return [powdering, powder_info]; -} - -/* - * Populate fields based on url, and calculate build. - */ -function decodeBuild(url_tag) { - if (url_tag) { - //default values - let equipment = [null, null, null, null, null, null, null, null, null]; - let tomes = [null, null, null, null, null, null, null]; - let powdering = ["", "", "", "", ""]; - let info = url_tag.split("_"); - let version = info[0]; - let save_skp = false; - let skillpoints = [0, 0, 0, 0, 0]; - let level = 106; - - version_number = parseInt(version) - //equipment (items) - // TODO: use filters - if (version_number < 4) { - let equipments = info[1]; - for (let i = 0; i < 9; ++i ) { - let equipment_str = equipments.slice(i*3,i*3+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - } - info[1] = equipments.slice(27); - } - else if (version_number == 4) { - let info_str = info[1]; - let start_idx = 0; - for (let i = 0; i < 9; ++i ) { - if (info_str.slice(start_idx,start_idx+3) === "CR-") { - equipment[i] = info_str.slice(start_idx, start_idx+20); - start_idx += 20; - } else { - let equipment_str = info_str.slice(start_idx, start_idx+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - start_idx += 3; - } - } - info[1] = info_str.slice(start_idx); - } - else if (version_number <= 6) { - let info_str = info[1]; - let start_idx = 0; - for (let i = 0; i < 9; ++i ) { - if (info_str.slice(start_idx,start_idx+3) === "CR-") { - equipment[i] = info_str.slice(start_idx, start_idx+20); - start_idx += 20; - } else if (info_str.slice(start_idx+3,start_idx+6) === "CI-") { - let len = Base64.toInt(info_str.slice(start_idx,start_idx+3)); - equipment[i] = info_str.slice(start_idx+3,start_idx+3+len); - start_idx += (3+len); - } else { - let equipment_str = info_str.slice(start_idx, start_idx+3); - equipment[i] = getItemNameFromID(Base64.toInt(equipment_str)); - start_idx += 3; - } - } - info[1] = info_str.slice(start_idx); - } - //constant in all versions - for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); - } - - //level, skill point assignments, and powdering - if (version_number == 1) { - let powder_info = info[1]; - let res = parsePowdering(powder_info); - powdering = res[0]; - } else if (version_number == 2) { - save_skp = true; - let skillpoint_info = info[1].slice(0, 10); - for (let i = 0; i < 5; ++i ) { - skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); - } - - let powder_info = info[1].slice(10); - let res = parsePowdering(powder_info); - powdering = res[0]; - } else if (version_number <= 6){ - level = Base64.toInt(info[1].slice(10,12)); - setValue("level-choice",level); - save_skp = true; - let skillpoint_info = info[1].slice(0, 10); - for (let i = 0; i < 5; ++i ) { - skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2)); - } - - let powder_info = info[1].slice(12); - - let res = parsePowdering(powder_info); - powdering = res[0]; - info[1] = res[1]; - } - // Tomes. - if (version_number == 6) { - //tome values do not appear in anything before v6. - for (let i = 0; i < 7; ++i) { - let tome_str = info[1].charAt(i); - setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); - } - info[1] = info[1].slice(7); - } - - for (let i in powderInputs) { - setValue(powderInputs[i], powdering[i]); - } - - calculateBuild(save_skp, skillpoints); - } -} - -/* Stores the entire build in a string using B64 encoding and adds it to the URL. -*/ -function encodeBuild() { - - if (player_build) { - let build_string; - - //V6 encoding - Tomes - build_version = 4; - build_string = ""; - tome_string = ""; - - let crafted_idx = 0; - let custom_idx = 0; - for (const item of player_build.items) { - - if (item.get("custom")) { - let custom = "CI-"+encodeCustom(player_build.customItems[custom_idx],true); - build_string += Base64.fromIntN(custom.length, 3) + custom; - custom_idx += 1; - build_version = Math.max(build_version, 5); - } else if (item.get("crafted")) { - build_string += "CR-"+encodeCraft(player_build.craftedItems[crafted_idx]); - crafted_idx += 1; - } else if (item.get("category") === "tome") { - let tome_id = item.get("id"); - if (tome_id <= 60) { - // valid normal tome. ID 61-63 is for NONE tomes. - build_version = Math.max(build_version, 6); - } - tome_string += Base64.fromIntN(tome_id, 1); - } else { - build_string += Base64.fromIntN(item.get("id"), 3); - } - } - - for (const skp of skp_order) { - build_string += Base64.fromIntN(getValue(skp + "-skp"), 2); // Maximum skillpoints: 2048 - } - build_string += Base64.fromIntN(player_build.level, 2); - for (const _powderset of player_build.powders) { - let n_bits = Math.ceil(_powderset.length / 6); - build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders. - // Slice copy. - let powderset = _powderset.slice(); - while (powderset.length != 0) { - let firstSix = powderset.slice(0,6).reverse(); - let powder_hash = 0; - for (const powder of firstSix) { - powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first. - } - build_string += Base64.fromIntN(powder_hash, 5); - powderset = powderset.slice(6); - } - } - build_string += tome_string; - - return build_version.toString() + "_" + build_string; - } -} - -function calculateBuild(save_skp, skp){ - try { - resetEditableIDs(); - if (player_build) { - reset_powder_specials(); - updateBoosts("skip", false); - updatePowderSpecials("skip", false); - } - let weaponName = getValue(equipmentInputs[8]); - //bruh @hpp - if (weaponName.startsWith("Morph-")) { - let equipment = [ "Morph-Stardust", "Morph-Steel", "Morph-Iron", "Morph-Gold", "Morph-Topaz", "Morph-Emerald", "Morph-Amethyst", "Morph-Ruby", weaponName.substring(6) ]; - for (let i in equipment) { - setValue(equipmentInputs[i], equipment[i]); - } - } - - //updatePowderSpecials("skip"); //jank pt 1 - save_skp = (typeof save_skp !== 'undefined') ? save_skp : false; - /* TODO: implement level changing - Make this entire function prettier - */ - let equipment = [ null, null, null, null, null, null, null, null, null ]; - for (let i in equipment) { - let equip = getValue(equipmentInputs[i]).trim(); - if (equip === "") { - equip = "No " + equipment_names[i] - } - else { - setValue(equipmentInputs[i], equip); - } - equipment[i] = equip; - } - let powderings = []; - let errors = []; - for (const i in powderInputs) { - // read in two characters at a time. - // TODO: make this more robust. - let input = getValue(powderInputs[i]).trim(); - let powdering = []; - let errorederrors = []; - while (input) { - let first = input.slice(0, 2); - let powder = powderIDs.get(first); - if (powder === undefined) { - errorederrors.push(first); - } else { - powdering.push(powder); - } - input = input.slice(2); - } - if (errorederrors.length > 0) { - if (errorederrors.length > 1) - errors.push(new IncorrectInput(errorederrors.join(""), "t6w6", powderInputs[i])); - else - errors.push(new IncorrectInput(errorederrors[0], "t6 or e3", powderInputs[i])); - } - //console.log("POWDERING: " + powdering); - powderings.push(powdering); - } - let tomes = [ null, null, null, null, null, null, null]; - for (let i in tomes) { - let equip = getValue(tomeInputs[i]).trim(); - if (equip === "") { - equip = "No " + tome_names[i] - } - else { - setValue(tomeInputs[i], equip); - } - tomes[i] = equip; - } - - - let level = document.getElementById("level-choice").value; - player_build = new Build(level, equipment, powderings, new Map(), errors, tomes); - console.log(player_build); - - //isn't this deprecated? - for (let i of document.getElementsByClassName("hide-container-block")) { - i.style.display = "block"; - } - for (let i of document.getElementsByClassName("hide-container-grid")) { - i.style.display = "grid"; - } - - console.log(player_build.toString()); - displaysq2EquipOrder(document.getElementById("build-order"),player_build.equip_order); - - const assigned = player_build.base_skillpoints; - const skillpoints = player_build.total_skillpoints; - for (let i in skp_order){ //big bren - setText(skp_order[i] + "-skp-base", "Original: " + skillpoints[i]); - } - - if (save_skp) { - // TODO: reduce duplicated code, @updateStats - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; - for (let i in skp_order) { - let manual_assigned = skp[i]; - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; - } - player_build.assigned_skillpoints += delta_total; - } - - updateEditableIDs(); - calculateBuildStats(); - if (player_build.errored) - throw new ListError(player_build.errors); - } - catch (error) { - console.log(error); - } -} - -function handleBuilderError(error) { - if (error instanceof ListError) { - for (let i of error.errors) { - if (i instanceof ItemNotFound) { - i.element.textContent = i.message; - } else if (i instanceof IncorrectInput) { - if (document.getElementById(i.id) !== null) { - document.getElementById(i.id).parentElement.querySelectorAll("p.error")[0].textContent = i.message; - } - } else { - let msg = i.stack; - let lines = msg.split("\n"); - let header = document.getElementById("header"); - header.textContent = ""; - for (const line of lines) { - let p = document.createElement("p"); - p.classList.add("itemp"); - p.textContent = line; - header.appendChild(p); - } - let p2 = document.createElement("p"); - p2.textContent = "If you believe this is an error, contact hppeng on forums or discord."; - header.appendChild(p2); - } - } - } else { - let msg = error.stack; - let lines = msg.split("\n"); - let header = document.getElementById("header"); - header.textContent = ""; - for (const line of lines) { - let p = document.createElement("p"); - p.classList.add("itemp"); - p.textContent = line; - header.appendChild(p); - } - let p2 = document.createElement("p"); - p2.textContent = "If you believe this is an error, contact hppeng on forums or discord."; - header.appendChild(p2); - } -} - -/* Updates all build statistics based on (for now) the skillpoint input fields and then calculates build stats. -*/ -function updateStats() { - - let specialNames = ["Quake", "Chain_Lightning", "Curse", "Courage", "Wind_Prison"]; - for (const sName of specialNames) { - for (let i = 1; i < 6; i++) { - let elem = document.getElementById(sName + "-" + i); - let name = sName.replace("_", " "); - if (elem.classList.contains("toggleOn")) { //toggle the pressed button off - elem.classList.remove("toggleOn"); - let special = powderSpecialStats[specialNames.indexOf(sName)]; - console.log(special); - if (special["weaponSpecialEffects"].has("Damage Boost")) { - if (name === "Courage" || name === "Curse") { //courage is universal damage boost - //player_build.damageMultiplier -= special.weaponSpecialEffects.get("Damage Boost")[i-1]/100; - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.get("damageBonus")[4] -= special.weaponSpecialEffects.get("Damage Boost")[i-1]; - } - } - } - } - } - - - - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; - for (let i in skp_order) { - let value = document.getElementById(skp_order[i] + "-skp").value; - if (value === ""){value = 0; setValue(skp_order[i] + "-skp", value)} - let manual_assigned = 0; - if (value.includes("+")) { - let skp = value.split("+"); - for (const s of skp) { - manual_assigned += parseInt(s,10); - } - } else { - manual_assigned = parseInt(value,10); - } - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; - } - player_build.assigned_skillpoints += delta_total; - if(player_build){ - updatePowderSpecials("skip", false); - updateArmorPowderSpecials("skip", false); - updateBoosts("skip", false); - for (let id of editable_item_fields) { - player_build.statMap.set(id, parseInt(getValue(id))); - } - } - player_build.aggregateStats(); - console.log(player_build.statMap); - calculateBuildStats(); -} - - -/* Updates all IDs in the edit IDs section. Resets each input and original value text to the correct text according to the current build. -*/ -function updateEditableIDs() { - if (player_build) { - for (const id of editable_item_fields) { - let edit_input = document.getElementById(id); - let val = player_build.statMap.get(id); - edit_input.value = val; - edit_input.placeholder = val; - - let value_label = document.getElementById(id + "-base"); - value_label.textContent = "Original Value: " + val; - //a hack to make resetting easier - value_label.value = val; - } - } -} - -/* Resets all IDs in the edit IDs section to their "original" values. -*/ -function resetEditableIDs() { - if (player_build) { - for (const id of editable_item_fields) { - let edit_input = document.getElementById(id); - let value_label = document.getElementById(id + "-base"); - - edit_input.value = value_label.value; - edit_input.placeholder = value_label.value; - } - } else { - //no player build, reset to 0 - for (const id of editable_item_fields) { - let edit_input = document.getElementById(id); - - edit_input.value = 0; - edit_input.placeholder = 0; - } - } -} - -/* Updates all spell boosts -*/ -function updateBoosts(buttonId, recalcStats) { - let elem = document.getElementById(buttonId); - let name = buttonId.split("-")[0]; - if(buttonId !== "skip") { - if (elem.classList.contains("toggleOn")) { - player_build.damageMultiplier -= damageMultipliers.get(name); - if (name === "warscream") { - player_build.defenseMultiplier -= .20; - } - if (name === "vanish") { - player_build.defenseMultiplier -= .15; - } - elem.classList.remove("toggleOn"); - }else{ - player_build.damageMultiplier += damageMultipliers.get(name); - if (name === "warscream") { - player_build.defenseMultiplier += .20; - } - if (name === "vanish") { - player_build.defenseMultiplier += .15; - } - elem.classList.add("toggleOn"); - } - updatePowderSpecials("skip", false); //jank pt 1 - } else { - for (const [key, value] of damageMultipliers) { - let elem = document.getElementById(key + "-boost") - if (elem.classList.contains("toggleOn")) { - elem.classList.remove("toggleOn"); - player_build.damageMultiplier -= value; - if (key === "warscream") { player_build.defenseMultiplier -= .20 } - if (key === "vanish") { player_build.defenseMultiplier -= .15 } - } - } - } - if (recalcStats) { - calculateBuildStats(); - } -} - -/* Updates ACTIVE powder special boosts (weapons) -*/ -function updatePowderSpecials(buttonId, recalcStats) { - //console.log(player_build.statMap); - - let name = (buttonId).split("-")[0]; - let power = (buttonId).split("-")[1]; // [1, 5] - let specialNames = ["Quake", "Chain Lightning", "Curse", "Courage", "Wind Prison"]; - let powderSpecials = []; // [ [special, power], [special, power]] - - - if(name !== "skip"){ - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { //toggle the pressed button off - elem.classList.remove("toggleOn"); - let special = powderSpecialStats[specialNames.indexOf(name.replace("_", " "))]; - if (special.weaponSpecialEffects.has("Damage Boost")) { - name = name.replace("_", " "); - if (name === "Courage" || name === "Curse") { //courage and curse are universal damage boost - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - //poison? - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") - special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.get("damageBonus")[4] -= special.weaponSpecialEffects.get("Damage Boost")[power-1]; - } - } - } else { - for (let i = 1;i < 6; i++) { //toggle all pressed buttons of the same powder special off - //name is same, power is i - if(document.getElementById(name.replace(" ", "_") + "-" + i).classList.contains("toggleOn")) { - document.getElementById(name.replace(" ", "_") + "-" + i).classList.remove("toggleOn"); - let special = powderSpecialStats[specialNames.indexOf(name.replace("_", " "))]; - if (special.weaponSpecialEffects.has("Damage Boost")) { - name = name.replace("_", " "); //might be redundant - if (name === "Courage" || name === "Curse") { //courage is universal damage boost - //player_build.damageMultiplier -= special.weaponSpecialEffects.get("Damage Boost")[i-1]/100; - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") - special.weaponSpecialEffects.get("Damage Boost")[i-1]); - player_build.externalStats.get("damageBonus")[4] -= special.weaponSpecialEffects.get("Damage Boost")[i-1]; - } - } - } - } - //toggle the pressed button on - elem.classList.add("toggleOn"); - } - } - - for (const sName of specialNames) { - for (let i = 1;i < 6; i++) { - if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) { - let powderSpecial = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))]; - powderSpecials.push([powderSpecial, i]); - break; - } - } - } - - - if (name !== "skip") { - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { - let special = powderSpecialStats[specialNames.indexOf(name.replace("_", " "))]; - if (special["weaponSpecialEffects"].has("Damage Boost")) { - let name = special["weaponSpecialName"]; - if (name === "Courage" || name === "Curse") { //courage and curse are is universal damage boost - player_build.externalStats.set("sdPct", player_build.externalStats.get("sdPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("mdPct", player_build.externalStats.get("mdPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.set("poisonPct", player_build.externalStats.get("poisonPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - } else if (name === "Wind Prison") { - player_build.externalStats.set("aDamPct", player_build.externalStats.get("aDamPct") + special.weaponSpecialEffects.get("Damage Boost")[power-1]); - player_build.externalStats.get("damageBonus")[4] += special.weaponSpecialEffects.get("Damage Boost")[power-1]; - } - } - } - } - - if (recalcStats) { - calculateBuildStats(); - } - displaysq2PowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build, true); -} - - -/* Updates PASSIVE powder special boosts (armors) -*/ -function updateArmorPowderSpecials(elem_id, recalc_stats) { - //we only update the powder special + external stats if the player has a build - if (elem_id !== "skip") { - if (player_build !== undefined && player_build.weapon !== undefined && player_build.weapon.get("name") !== "No Weapon") { - let wynn_elem = elem_id.split("_")[0]; //str, dex, int, def, agi - - //update the label associated w/ the slider - let elem = document.getElementById(elem_id); - let label = document.getElementById(elem_id + "_label"); - let prev_label = document.getElementById(elem_id + "_prev"); - - let value = elem.value; - - //for use in editing build stats - let prev_value = prev_label.value; - let value_diff = value - prev_value; - - //update the "previous" label - prev_label.value = value; - - label.textContent = label.textContent.split(":")[0] + ": " + value; - - let dmg_id = elem_chars[skp_names.indexOf(wynn_elem)] + "DamPct"; - let new_dmgboost = player_build.externalStats.get(dmg_id) + value_diff; - - //update build external stats - the second one is the relevant one for damage calc purposes - player_build.externalStats.set(dmg_id, new_dmgboost); - player_build.externalStats.get("damageBonus")[skp_names.indexOf(wynn_elem)] = new_dmgboost; - - //update the slider's graphics - let bg_color = elem_colors[skp_names.indexOf(wynn_elem)]; - let pct = Math.round(100 * value / powderSpecialStats[skp_names.indexOf(wynn_elem)].cap); - elem.style.background = `linear-gradient(to right, ${bg_color}, ${bg_color} ${pct}%, #AAAAAA ${pct}%, #AAAAAA 100%)`; - - } - } else { - if (player_build !== undefined) { - for (let i = 0; i < skp_names.length; ++i) { - skp_name = skp_names[i]; - skp_char = elem_chars[i]; - player_build.externalStats.set(skp_char + "DamPct", player_build.externalStats.get(skp_char + "DamPct") - document.getElementById(skp_name+"_boost_armor").value); - player_build.externalStats.get("damageBonus")[i] -= document.getElementById(skp_name+"_boost_armor").value; - } - } - } - - - if (recalc_stats && player_build) { - //calc build stats and display powder special - calculateBuildStats(); - // displaysq2PowderSpecials(document.getElementById("powder-special-stats"), powderSpecials, player_build, true); - } - -} - -function resetArmorPowderSpecials() { - for (const skp of skp_names) { - document.getElementById(skp + "_boost_armor").value = 0; - document.getElementById(skp + "_boost_armor_prev").value = 0; - document.getElementById(skp + "_boost_armor").style.background = `linear-gradient(to right, #AAAAAA, #AAAAAA 0%, #AAAAAA 100%)`; - document.getElementById(skp + "_boost_armor_label").textContent = `% ${capitalizeFirst(elem_names[skp_names.indexOf(skp)])} Damage Boost: 0` - } -} - -/* Calculates all build statistics and updates the entire display. -*/ -function calculateBuildStats() { - const assigned = player_build.base_skillpoints; - const skillpoints = player_build.total_skillpoints; - let skp_effects = ["% more damage dealt.","% chance to crit.","% spell cost reduction.","% less damage taken.","% chance to dodge."]; - for (let i in skp_order){ //big bren - setText(skp_order[i] + "-skp-assign", "Assign: " + assigned[i]); - setValue(skp_order[i] + "-skp", skillpoints[i]); - let linebreak = document.createElement("br"); - linebreak.classList.add("itemp"); - document.getElementById(skp_order[i] + "-skp-label"); - setText(skp_order[i] + "-skp-pct", (skillPointsToPercentage(skillpoints[i])*100).toFixed(1).concat(skp_effects[i])); - document.getElementById(skp_order[i]+"-warnings").textContent = '' - if (assigned[i] > 100) { - let skp_warning = document.createElement("p"); - skp_warning.classList.add("warning"); - skp_warning.classList.add("small-text") - skp_warning.textContent += "Cannot assign " + assigned[i] + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually."; - document.getElementById(skp_order[i]+"-warnings").textContent = '' - document.getElementById(skp_order[i]+"-warnings").appendChild(skp_warning); - } - } - - let summarybox = document.getElementById("summary-box"); - summarybox.textContent = ""; - let skpRow = document.createElement("p"); - - let remainingSkp = document.createElement("p"); - remainingSkp.classList.add("scaled-font"); - let remainingSkpTitle = document.createElement("b"); - remainingSkpTitle.textContent = "Assigned " + player_build.assigned_skillpoints + " skillpoints. Remaining skillpoints: "; - let remainingSkpContent = document.createElement("b"); - remainingSkpContent.textContent = "" + (levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints); - remainingSkpContent.classList.add(levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints < 0 ? "negative" : "positive"); - - remainingSkp.appendChild(remainingSkpTitle); - remainingSkp.appendChild(remainingSkpContent); - - - summarybox.append(skpRow); - summarybox.append(remainingSkp); - if(player_build.assigned_skillpoints > levelToSkillPoints(player_build.level)){ - let skpWarning = document.createElement("span"); - //skpWarning.classList.add("itemp"); - skpWarning.classList.add("warning"); - skpWarning.textContent = "WARNING: Too many skillpoints need to be assigned!"; - let skpCount = document.createElement("p"); - skpCount.classList.add("warning"); - skpCount.textContent = "For level " + (player_build.level>101 ? "101+" : player_build.level) + ", there are only " + levelToSkillPoints(player_build.level) + " skill points available."; - summarybox.append(skpWarning); - summarybox.append(skpCount); - } - let lvlWarning; - for (const item of player_build.items) { - let item_lvl; - if (item.get("crafted")) { - //item_lvl = item.get("lvlLow") + "-" + item.get("lvl"); - item_lvl = item.get("lvlLow"); - } - else { - item_lvl = item.get("lvl"); - } - - if (player_build.level < item_lvl) { - if (!lvlWarning) { - lvlWarning = document.createElement("p"); - lvlWarning.classList.add("itemp"); - lvlWarning.classList.add("warning"); - lvlWarning.textContent = "WARNING: A level " + player_build.level + " player cannot use some piece(s) of this build." - } - let baditem = document.createElement("p"); - baditem.classList.add("nocolor"); - baditem.classList.add("itemp"); - baditem.textContent = item.get("displayName") + " requires level " + item.get("lvl") + " to use."; - lvlWarning.appendChild(baditem); - } - } - if(lvlWarning){ - summarybox.append(lvlWarning); - } - for (const [setName, count] of player_build.activeSetCounts) { - const bonus = sets[setName].bonuses[count-1]; - // console.log(setName); - if (bonus["illegal"]) { - let setWarning = document.createElement("p"); - setWarning.classList.add("itemp"); - setWarning.classList.add("warning"); - setWarning.textContent = "WARNING: illegal item combination: " + setName - summarybox.append(setWarning); - } - } - - for (let i in player_build.items) { - displaysq2ExpandedItem(player_build.items[i], buildFields[i]); - collapse_element("#"+equipment_keys[i]+"-tooltip"); - } - - displaysq2ArmorStats(player_build); - displaysq2BuildStats('overall-stats', player_build, build_all_display_commands); - displaysq2BuildStats("offensive-stats",player_build, build_offensive_display_commands); - displaysq2SetBonuses("set-info",player_build); - displaysq2WeaponStats(player_build); - - let meleeStats = player_build.getMeleeStats(); - displaysq2MeleeDamage(document.getElementById("build-melee-stats"), document.getElementById("build-melee-statsAvg"), meleeStats); - - displaysq2DefenseStats(document.getElementById("defensive-stats"),player_build); - - displaysq2PoisonDamage(document.getElementById("build-poison-stats"),player_build); - - let spells = spell_table[player_build.weapon.get("type")]; - for (let i = 0; i < 4; ++i) { - let parent_elem = document.getElementById("spell"+i+"-info"); - let overallparent_elem = document.getElementById("spell"+i+"-infoAvg"); - displaysq2SpellDamage(parent_elem, overallparent_elem, player_build, spells[i], i+1); - } - - location.hash = encodeBuild(); - clear_highlights(); -} - -function copyBuild() { - if (player_build) { - copyTextToClipboard(url_base+location.hash); - document.getElementById("copy-button").textContent = "Copied!"; - } -} - -function shareBuild() { - if (player_build) { - let text = url_base+location.hash+"\n"+ - "WynnBuilder build:\n"+ - "> "+player_build.helmet.get("displayName")+"\n"+ - "> "+player_build.chestplate.get("displayName")+"\n"+ - "> "+player_build.leggings.get("displayName")+"\n"+ - "> "+player_build.boots.get("displayName")+"\n"+ - "> "+player_build.ring1.get("displayName")+"\n"+ - "> "+player_build.ring2.get("displayName")+"\n"+ - "> "+player_build.bracelet.get("displayName")+"\n"+ - "> "+player_build.necklace.get("displayName")+"\n"+ - "> "+player_build.weapon.get("displayName")+" ["+player_build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]"; - copyTextToClipboard(text); - document.getElementById("share-button").textContent = "Copied!"; - } -} - -function populateBuildList() { - const buildList = document.getElementById("build-choice"); - const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); - - for (const buildName of Object.keys(savedBuilds).sort()) { - const buildOption = document.createElement("option"); - buildOption.setAttribute("value", buildName); - buildList.appendChild(buildOption); - } -} - -function saveBuild() { - if (player_build) { - const savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); - const saveName = document.getElementById("build-name").value; - const encodedBuild = encodeBuild(); - if ((!Object.keys(savedBuilds).includes(saveName) - || document.getElementById("saved-error").textContent !== "") && encodedBuild !== "") { - savedBuilds[saveName] = encodedBuild.replace("#", ""); - window.localStorage.setItem("builds", JSON.stringify(savedBuilds)); - - document.getElementById("saved-error").textContent = ""; - document.getElementById("saved-build").textContent = "Build saved locally"; - - const buildList = document.getElementById("build-choice"); - const buildOption = document.createElement("option"); - buildOption.setAttribute("value", saveName); - buildList.appendChild(buildOption); - } else { - document.getElementById("saved-build").textContent = ""; - if (encodedBuild === "") { - document.getElementById("saved-error").textContent = "Empty build"; - } - else { - document.getElementById("saved-error").textContent = "Exists. Overwrite?"; - } - } - } -} - -function loadBuild() { - let savedBuilds = window.localStorage.getItem("builds") === null ? {} : JSON.parse(window.localStorage.getItem("builds")); - let saveName = document.getElementById("build-name").value; - - if (Object.keys(savedBuilds).includes(saveName)) { - decodeBuild(savedBuilds[saveName]) - document.getElementById("loaded-error").textContent = ""; - document.getElementById("loaded-build").textContent = "Build loaded"; - } else { - document.getElementById("loaded-build").textContent = ""; - document.getElementById("loaded-error").textContent = "Build doesn't exist"; - } -} - -function resetFields(){ - for (let i in powderInputs) { - setValue(powderInputs[i], ""); - } - for (let i in equipmentInputs) { - setValue(equipmentInputs[i], ""); - } - setValue("str-skp", "0"); - setValue("dex-skp", "0"); - setValue("int-skp", "0"); - setValue("def-skp", "0"); - setValue("agi-skp", "0"); - setValue("level-choice", "106"); - location.hash = ""; - calculateBuild(); -} - -function toggleID() { - let button = document.getElementById("show-id-button"); - let targetDiv = document.getElementById("id-edit"); - if (button.classList.contains("toggleOn")) { //toggle the pressed button off - targetDiv.style.display = "none"; - button.classList.remove("toggleOn"); - } - else { - targetDiv.style.display = "block"; - button.classList.add("toggleOn"); - } -} - -function toggleButton(button_id) { - let button = document.getElementById(button_id); - if (button) { - if (button.classList.contains("toggleOn")) { - button.classList.remove("toggleOn"); - } else { - button.classList.add("toggleOn"); - } - } -} - -function optimizeStrDex() { - if (!player_build) { - return; - } - const remaining = levelToSkillPoints(player_build.level) - player_build.assigned_skillpoints; - const base_skillpoints = player_build.base_skillpoints; - const max_str_boost = 100 - base_skillpoints[0]; - const max_dex_boost = 100 - base_skillpoints[1]; - if (Math.min(remaining, max_str_boost, max_dex_boost) < 0) return; // Unwearable - - const base_total_skillpoints = player_build.total_skillpoints; - let str_bonus = remaining; - let dex_bonus = 0; - let best_skillpoints = player_build.total_skillpoints; - let best_damage = 0; - for (let i = 0; i <= remaining; ++i) { - let total_skillpoints = base_total_skillpoints.slice(); - total_skillpoints[0] += Math.min(max_str_boost, str_bonus); - total_skillpoints[1] += Math.min(max_dex_boost, dex_bonus); - - // Calculate total 3rd spell damage - let spell = spell_table[player_build.weapon.get("type")][2]; - const stats = player_build.statMap; - let critChance = skillPointsToPercentage(total_skillpoints[1]); - let save_damages = []; - let spell_parts; - if (spell.parts) { - spell_parts = spell.parts; - } - else { - spell_parts = spell.variants.DEFAULT; - for (const majorID of stats.get("activeMajorIDs")) { - if (majorID in spell.variants) { - spell_parts = spell.variants[majorID]; - break; - } - } - } - let total_damage = 0; - for (const part of spell_parts) { - if (part.type === "damage") { - let _results = calculateSpellDamage(stats, part.conversion, - stats.get("sdRaw"), stats.get("sdPct") + player_build.externalStats.get("sdPct"), - part.multiplier / 100, player_build.weapon, total_skillpoints, - player_build.damageMultiplier, player_build.externalStats); - let totalDamNormal = _results[0]; - let totalDamCrit = _results[1]; - let results = _results[2]; - let tooltipinfo = _results[3]; - - for (let i = 0; i < 6; ++i) { - for (let j in results[i]) { - results[i][j] = results[i][j].toFixed(2); - } - } - let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0; - let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; - let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; - - save_damages.push(averageDamage); - if (part.summary == true) { - total_damage = averageDamage; - } - } else if (part.type === "total") { - total_damage = 0; - for (let i in part.factors) { - total_damage += save_damages[i] * part.factors[i]; - } - } - } // END Calculate total 3rd spell damage (total_damage) - if (total_damage > best_damage) { - best_damage = total_damage; - best_skillpoints = total_skillpoints.slice(); - } - - str_bonus -= 1; - dex_bonus += 1; - - } - // TODO: reduce duplicated code, @calculateBuild - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; - for (let i in skp_order) { - let manual_assigned = best_skillpoints[i]; - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; - } - player_build.assigned_skillpoints += delta_total; - - try { - calculateBuildStats(); - if (player_build.errored) - throw new ListError(player_build.errors); - } - catch (error) { - handleBuilderError(error); - } -} - -// TODO: Learn and use await -function init() { - console.log("builder.js init"); - init_autocomplete(); - decodeBuild(url_tag); - for (const i of equipment_keys) { - update_field(i); - } -} -function init2() { - load_ing_init(init); -} -function init3() { - load_tome_init(init2) -} - - -load_init(init3); diff --git a/js/sq2display.js b/js/sq2display.js deleted file mode 100644 index 10b1701..0000000 --- a/js/sq2display.js +++ /dev/null @@ -1,2407 +0,0 @@ -function displaysq2BuildStats(parent_id,build,command_group){ - // Commands to "script" the creation of nice formatting. - // #commands create a new element. - // !elemental is some janky hack for elemental damage. - // normals just display a thing. - - let display_commands = command_group; - //console.log(display_commands); - - - let parent_div = document.getElementById(parent_id); - // Clear the parent div. - if (parent_div != null) { - setHTML(parent_id, ""); - } - - let stats = build.statMap; - //console.log(build.statMap); - - let active_elem; - let elemental_format = false; - - //TODO this is put here for readability, consolidate with definition in build.js - let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef"]; - - for (const command of display_commands) { - // style instructions - - if (command.charAt(0) === "#") { - if (command === "#defense-stats") { - displaysq2DefenseStats(parent_div, build, true); - } - } - if (command.charAt(0) === "!") { - // TODO: This is sooo incredibly janky..... - if (command === "!elemental") { - elemental_format = !elemental_format; - } - } - - // id instruction - else { - let id = command; - if (stats.get(id)) { - let style = null; - - // TODO: add pos and neg style - if (!staticIDs.includes(id)) { - style = "positive"; - if (stats.get(id) < 0) { - style = "negative"; - } - } - - // ignore - let id_val = stats.get(id); - if (reversedIDs.includes(id)) { - style === "positive" ? style = "negative" : style = "positive"; - } - if (id === "poison" && id_val > 0) { - id_val = Math.ceil(id_val*build.statMap.get("poisonPct")/100); - } - displaysq2FixedID(parent_div, id, id_val, elemental_format, style); - if (id === "poison" && id_val > 0) { - let row = document.createElement('div'); - row.classList.add("row") - let value_elem = document.createElement('div'); - value_elem.classList.add('col'); - value_elem.classList.add('text-end'); - - let prefix_elem = document.createElement('b'); - prefix_elem.textContent = "\u279C With Strength: "; - let number_elem = document.createElement('b'); - number_elem.classList.add(style); - number_elem.textContent = (id_val * (1+skillPointsToPercentage(build.total_skillpoints[0])) ).toFixed(0) + idSuffixes[id]; - value_elem.append(prefix_elem); - value_elem.append(number_elem); - row.appendChild(value_elem); - - parent_div.appendChild(row); - } - - // sp thingy - } else if (skp_order.includes(id)) { - let total_assigned = build.total_skillpoints[skp_order.indexOf(id)]; - let base_assigned = build.base_skillpoints[skp_order.indexOf(id)]; - let diff = total_assigned - base_assigned; - let style; - if (diff > 0) { - style = "positive"; - } else if (diff < 0) { - style = "negative"; - } - if (diff != 0) { - displaysq2FixedID(parent_div, id, diff, false, style); - } - } - } - } -} - -function displaysq2ExpandedItem(item, parent_id){ - // Commands to "script" the creation of nice formatting. - // #commands create a new element. - // !elemental is some janky hack for elemental damage. - // normals just display a thing. - if (item.get("category") === "weapon") { - let stats = new Map(); - stats.set("atkSpd", item.get("atkSpd")); - stats.set("damageBonus", [0, 0, 0, 0, 0]); - - //SUPER JANK @HPP PLS FIX - let damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ]; - if (item.get("tier") !== "Crafted") { - stats.set("damageRaw", [item.get("nDam"), item.get("eDam"), item.get("tDam"), item.get("wDam"), item.get("fDam"), item.get("aDam")]); - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], 0, 0, 0, item, [0, 0, 0, 0, 0], 1, undefined); - let damages = results[2]; - let total_damage = 0; - for (const i in damage_keys) { - total_damage += damages[i][0] + damages[i][1]; - item.set(damage_keys[i], damages[i][0]+"-"+damages[i][1]); - } - total_damage = total_damage / 2; - item.set("basedps", total_damage); - - } else { - stats.set("damageRaw", [item.get("nDamLow"), item.get("eDamLow"), item.get("tDamLow"), item.get("wDamLow"), item.get("fDamLow"), item.get("aDamLow")]); - stats.set("damageBases", [item.get("nDamBaseLow"),item.get("eDamBaseLow"),item.get("tDamBaseLow"),item.get("wDamBaseLow"),item.get("fDamBaseLow"),item.get("aDamBaseLow")]); - let resultsLow = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], 0, 0, 0, item, [0, 0, 0, 0, 0], 1, undefined); - let damagesLow = resultsLow[2]; - stats.set("damageRaw", [item.get("nDam"), item.get("eDam"), item.get("tDam"), item.get("wDam"), item.get("fDam"), item.get("aDam")]); - stats.set("damageBases", [item.get("nDamBaseHigh"),item.get("eDamBaseHigh"),item.get("tDamBaseHigh"),item.get("wDamBaseHigh"),item.get("fDamBaseHigh"),item.get("aDamBaseHigh")]); - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], 0, 0, 0, item, [0, 0, 0, 0, 0], 1, undefined); - let damages = results[2]; - console.log(damages); - - let total_damage_min = 0; - let total_damage_max = 0; - for (const i in damage_keys) { - total_damage_min += damagesLow[i][0] + damagesLow[i][1]; - total_damage_max += damages[i][0] + damages[i][1]; - item.set(damage_keys[i], damagesLow[i][0]+"-"+damagesLow[i][1]+"\u279c"+damages[i][0]+"-"+damages[i][1]); - } - total_damage_min = total_damage_min / 2; - total_damage_max = total_damage_max / 2; - item.set("basedps", [total_damage_min, total_damage_max]); - } - } else if (item.get("category") === "armor") { - } - - let display_commands = sq2_item_display_commands; - - // Clear the parent div. - setHTML(parent_id, ""); - let parent_div = document.getElementById(parent_id); - parent_div.classList.add("border", "border-2", "border-dark"); - - let fix_id = item.has("fixID") && item.get("fixID"); - let elemental_format = false; - for (let i = 0; i < display_commands.length; i++) { - const command = display_commands[i]; - if (command.charAt(0) === "!") { - // TODO: This is sooo incredibly janky..... - if (command === "!elemental") { - elemental_format = !elemental_format; - } - else if (command === "!spacer") { - let spacer = document.createElement('div'); - spacer.classList.add("row", "my-2"); - parent_div.appendChild(spacer); - continue; - } - } - else { - let id = command; - if(nonRolledIDs.includes(id)){//nonRolledID & non-0/non-null/non-und ID - if (!item.get(id)) { - if (! (item.get("crafted") && skp_order.includes(id) && - (item.get("maxRolls").get(id) || item.get("minRolls").get(id)))) { - continue; - } - } - if (id === "slots") { - let p_elem = document.createElement("div"); - p_elem.classList.add("col"); - - // PROPER POWDER DISPLAYING - let numerals = new Map([[1, "I"], [2, "II"], [3, "III"], [4, "IV"], [5, "V"], [6, "VI"]]); - - let powderPrefix = document.createElement("b"); - powderPrefix.textContent = "Powder Slots: " + item.get(id) + " ["; - p_elem.appendChild(powderPrefix); - - let powders = item.get("powders"); - for (let i = 0; i < powders.length; i++) { - let powder = document.createElement("b"); - powder.textContent = numerals.get((powders[i]%6)+1)+" "; - powder.classList.add(damageClasses[Math.floor(powders[i]/6)+1]+"_powder"); - p_elem.appendChild(powder); - } - - let powderSuffix = document.createElement("b"); - powderSuffix.textContent = "]"; - p_elem.appendChild(powderSuffix); - parent_div.appendChild(p_elem); - } else if (id === "set") { - if (item.get("hideSet")) { continue; } - - let p_elem = document.createElement("div"); - p_elem.classList.add("col"); - p_elem.textContent = "Set: " + item.get(id).toString(); - parent_div.appendChild(p_elem); - } else if (id === "majorIds") { - //console.log(item.get(id)); - for (let majorID of item.get(id)) { - let p_elem = document.createElement("div"); - p_elem.classList.add("col"); - - let title_elem = document.createElement("b"); - let b_elem = document.createElement("b"); - if (majorID.includes(":")) { - let name = majorID.substring(0, majorID.indexOf(":")+1); - let mid = majorID.substring(majorID.indexOf(":")+1); - if (name.charAt(0) !== "+") {name = "+" + name} - title_elem.classList.add("Legendary"); - title_elem.textContent = name; - b_elem.classList.add("Crafted"); - b_elem.textContent = mid; - p_elem.appendChild(title_elem); - p_elem.appendChild(b_elem); - } else { - let name = item.get(id).toString() - if (name.charAt(0) !== "+") {name = "+" + name} - b_elem.classList.add("Legendary"); - b_elem.textContent = name; - p_elem.appendChild(b_elem); - } - parent_div.appendChild(p_elem); - } - } else if (id === "lvl" && item.get("tier") === "Crafted") { - let p_elem = document.createElement("div"); - p_elem.classList.add("col"); - p_elem.textContent = "Combat Level Min: " + item.get("lvlLow") + "-" + item.get(id); - parent_div.appendChild(p_elem); - } else if (id === "displayName") { - let row = document.createElement("div"); - - let a_elem = document.createElement("a"); - row.classList.add("row", "justify-content-center"); - a_elem.classList.add("col-auto", "text-center", "item-title", "p-0"); - a_elem.classList.add(item.has("tier") ? item.get("tier").replace(" ","") : "Normal"); - // a_elem.style.textGrow = 1; - - row.appendChild(a_elem); - - /* - FUNCTIONALITY FOR THIS FEATURE HAS SINCE BEEN REMOVED (WITH SQ2). - IF WE WANT TO USE IT IN THE FUTURE, I'VE LEFT THE CODE TO ADD IT IN HERE - */ - - //allow the plus minus element to toggle upon click: âž•âž– - // let plusminus = document.createElement("div"); - // plusminus.id = parent_div.id.split("-")[0] + "-pm"; - // plusminus.classList.add("col", "plus_minus", "text_end"); - // plusminus.style.flexGrow = 0; - // plusminus.textContent = "\u2795"; - // row.appendChild(plusminus); - - if (item.get("custom")) { - a_elem.href = "../custom/#" + item.get("hash"); - a_elem.textContent = item.get("displayName"); - } else if (item.get("crafted")) { - a_elem.href = "../crafter/#" + item.get("hash"); - a_elem.textContent = item.get(id); - } else { - a_elem.href = "../item/#" + item.get("displayName"); - a_elem.textContent = item.get("displayName"); - } - parent_div.appendChild(row); - - let nolink_row = document.createElement("div"); - let p_elem = document.createElement("p"); - nolink_row.classList.add("row", "justify-content-center"); - nolink_row.style.display = "none"; - p_elem.classList.add("col-auto", "text-center", "item-title", "p-0"); - p_elem.classList.add(item.has("tier") ? item.get("tier").replace(" ","") : "Normal"); - if (item.get("custom")) { - p_elem.textContent = item.get("displayName"); - } else if (item.get("crafted")) { - p_elem.textContent = item.get(id); - } else { - p_elem.textContent = item.get("displayName"); - } - - nolink_row.appendChild(p_elem); - parent_div.appendChild(nolink_row); - - let img = document.createElement("img"); - if (item && item.has("type")) { - img.src = "../media/items/" + (newIcons ? "new/":"old/") + "generic-" + item.get("type") + ".png"; - img.alt = item.get("type"); - img.style = " z=index: 1; position: relative;"; - let container = document.createElement("div"); - - let bckgrd = document.createElement("div"); - bckgrd.classList.add("col", "px-0", "d-flex", "align-items-center", "justify-content-center");// , "no-collapse"); - bckgrd.style = "border-radius: 50%;background-image: radial-gradient(closest-side, " + colorMap.get(item.get("tier")) + " 20%," + "hsl(0, 0%, 16%) 80%); margin-left: auto; margin-right: auto;" - bckgrd.classList.add("scaled-bckgrd"); - parent_div.appendChild(container); - container.appendChild(bckgrd); - bckgrd.appendChild(img); - } - } else { - let p_elem; - if ( !(item.get("tier") === "Crafted" && item.get("category") === "armor" && id === "hp") && (!skp_order.includes(id)) || (skp_order.includes(id) && item.get("tier") !== "Crafted" && parent_div.nodeName === "table") ) { //skp warp - p_elem = displaysq2FixedID(parent_div, id, item.get(id), elemental_format); - } else if (item.get("tier") === "Crafted" && item.get("category") === "armor" && id === "hp") { - p_elem = displaysq2FixedID(parent_div, id, item.get(id+"Low")+"-"+item.get(id), elemental_format); - } - if (id === "lore") { - p_elem.style = "font-style: italic"; - } else if (skp_order.includes(id)) { //id = str, dex, int, def, or agi - if ( item.get("tier") !== "Crafted") { - row = document.createElement("div"); - row.classList.add("col"); - - let title = document.createElement("b"); - title.textContent = idPrefixes[id] + " "; - let boost = document.createElement("b"); - if (item.get(id) < 0) { - boost.classList.add("negative"); - } else { //boost = 0 SHOULD not come up - boost.classList.add("positive"); - } - boost.textContent = item.get(id); - row.appendChild(title); - row.appendChild(boost); - parent_div.appendChild(row); - } else if ( item.get("tier") === "Crafted") { - let row = displaysq2RolledID(item, id, elemental_format); - parent_div.appendChild(row); - } - } else if (id === "restrict") { - p_elem.classList.add("restrict"); - } - } - } - else if ( rolledIDs.includes(id) && - ((item.get("maxRolls") && item.get("maxRolls").get(id)) - || (item.get("minRolls") && item.get("minRolls").get(id)))) { - let style = "positive"; - if (item.get("minRolls").get(id) < 0) { - style = "negative"; - } - if(reversedIDs.includes(id)){ - style === "positive" ? style = "negative" : style = "positive"; - } - if (fix_id) { - p_elem = document.createElement("div"); - p_elem.classList.add("col", "text-nowrap"); - if (id == "dex") { - console.log("dex activated at fix_id") - } - displaysq2FixedID(p_elem, id, item.get("minRolls").get(id), elemental_format, style); - parent_div.appendChild(p_elem); - } - else { - let row = displaysq2RolledID(item, id, elemental_format); - parent_div.appendChild(row); - } - }else{ - // :/ - } - } - } - //Show powder specials ;-; - let nonConsumables = ["relik", "wand", "bow", "spear", "dagger", "chestplate", "helmet", "leggings", "boots", "ring", "bracelet", "necklace"]; - if(nonConsumables.includes(item.get("type"))) { - let powder_special = document.createElement("div"); - powder_special.classList.add("col"); - let powders = item.get("powders"); - let element = ""; - let power = 0; - for (let i = 0; i < powders.length; i++) { - let firstPowderType = skp_elements[Math.floor(powders[i]/6)]; - if (element !== "") break; - else if (powders[i]%6 > 2) { //t4+ - for (let j = i+1; j < powders.length; j++) { - let currentPowderType = skp_elements[Math.floor(powders[j]/6)] - if (powders[j] % 6 > 2 && firstPowderType === currentPowderType) { - element = currentPowderType; - power = Math.round(((powders[i] % 6 + powders[j] % 6 + 2) / 2 - 4) * 2); - break; - } - } - } - } - if (element !== "") {//powder special is "[e,t,w,f,a]+[0,1,2,3,4]" - let powderSpecial = powderSpecialStats[ skp_elements.indexOf(element)]; - let specialSuffixes = new Map([ ["Duration", " sec"], ["Radius", " blocks"], ["Chains", ""], ["Damage", "%"], ["Damage Boost", "%"], ["Knockback", " blocks"] ]); - let specialTitle = document.createElement("span"); - let specialEffects = document.createElement("span"); - addClasses(specialTitle, [damageClasses[skp_elements.indexOf(element) + 1]]); - let effects; - if (item.get("category") === "weapon") {//weapon - effects = powderSpecial["weaponSpecialEffects"]; - specialTitle.textContent = powderSpecial["weaponSpecialName"]; - }else if (item.get("category") === "armor") {//armor - effects = powderSpecial["armorSpecialEffects"]; - specialTitle.textContent += powderSpecial["armorSpecialName"] + ": "; - } - for (const [key,value] of effects.entries()) { - if (key !== "Description") { - let effect = document.createElement("p"); - effect.classList.add("m-0"); - effect.textContent = key + ": " + value[power] + specialSuffixes.get(key); - if(key === "Damage"){ - effect.textContent += elementIcons[skp_elements.indexOf(element)]; - } - if (element === "w" && item.get("category") === "armor") { - effect.textContent += " / Mana Used"; - } - specialEffects.appendChild(effect); - }else{ - specialTitle.textContent += "[ " + effects.get("Description") + " ]"; - } - } - powder_special.appendChild(specialTitle); - powder_special.appendChild(specialEffects); - parent_div.appendChild(powder_special); - } - } - - if(item.get("tier") && item.get("tier") === "Crafted") { - let dura_elem = document.createElement("div"); - dura_elem.classList.add("col"); - let dura = []; - let suffix = ""; - if(nonConsumables.includes(item.get("type"))) { - dura = item.get("durability"); - dura_elem.textContent = "Durability: " - } else { - dura = item.get("duration"); - dura_elem.textContent = "Duration: " - suffix = " sec." - let charges = document.createElement("b"); - charges.textContent = "Charges: " + item.get("charges"); - parent_div.appendChild(charges); - } - - if (typeof(dura) === "string") { - dura_elem.textContent += dura + suffix; - } else { - dura_elem.textContent += dura[0]+"-"+dura[1] + suffix; - } - parent_div.append(dura_elem); - - } - //Show item tier - if (item.get("tier") && item.get("tier") !== " ") { - let item_desc_elem = document.createElement("div"); - item_desc_elem.classList.add("col"); - item_desc_elem.classList.add(item.get("tier")); - if (tome_types.includes(item.get("type"))) { - tome_type_map = new Map([["weaponTome", "Weapon Tome"],["armorTome", "Armor Tome"],["guildTome", "Guild Tome"]]); - item_desc_elem.textContent = item.get("tier")+" "+tome_type_map.get(item.get("type")); - } else { - item_desc_elem.textContent = item.get("tier")+" "+item.get("type"); - } - parent_div.append(item_desc_elem); - } - - //Show item hash if applicable - if (item.get("crafted") || item.get("custom")) { - let item_desc_elem = document.createElement("p"); - item_desc_elem.classList.add('itemp'); - item_desc_elem.style.maxWidth = "100%"; - item_desc_elem.style.wordWrap = "break-word"; - item_desc_elem.style.wordBreak = "break-word"; - item_desc_elem.textContent = item.get("hash"); - parent_div.append(item_desc_elem); - } - - if (item.get("category") === "weapon") { - let damage_mult = baseDamageMultiplier[attackSpeeds.indexOf(item.get("atkSpd"))]; - let total_damages = item.get("basedps"); - let base_dps_elem = document.createElement("p"); - base_dps_elem.classList.add("left"); - base_dps_elem.classList.add("itemp"); - if (item.get("tier") === "Crafted") { - let base_dps_min = total_damages[0] * damage_mult; - let base_dps_max = total_damages[1] * damage_mult; - - // base_dps_elem.textContent = "Base DPS: "+base_dps_min.toFixed(3)+"\u279c"+base_dps_max.toFixed(3); - base_dps_elem.textContent = base_dps_min.toFixed(3)+"\u279c"+base_dps_max.toFixed(3); - } - else { - let bdps = total_damages * damage_mult; - base_dps_elem.textContent = (bdps ? bdps : 0); - } - parent_div.appendChild(document.createElement("p")); - parent_div.appendChild(base_dps_elem); - } -} - -function displaysq2RolledID(item, id, elemental_format) { - let row = document.createElement('div'); - row.classList.add('col'); - - let item_div = document.createElement('div'); - item_div.classList.add('row'); - - let min_elem = document.createElement('div'); - min_elem.classList.add('col', 'text-start'); - min_elem.style.cssText += "flex-grow: 0"; - let id_min = item.get("minRolls").get(id) - let style = id_min < 0 ? "negative" : "positive"; - if(reversedIDs.includes(id)){ - style === "positive" ? style = "negative" : style = "positive"; - } - min_elem.classList.add(style); - min_elem.textContent = id_min + idSuffixes[id]; - item_div.appendChild(min_elem); - - let desc_elem = document.createElement('div'); - desc_elem.classList.add('col', 'text-center');//, 'text-nowrap'); - desc_elem.style.cssText += "flex-grow: 1"; - //TODO elemental format jank - if (elemental_format) { - apply_sq2_elemental_format(desc_elem, id); - } - else { - desc_elem.textContent = idPrefixes[id]; - } - item_div.appendChild(desc_elem); - - let max_elem = document.createElement('div'); - let id_max = item.get("maxRolls").get(id) - max_elem.classList.add('col', 'text-end'); - max_elem.style.cssText += "flex-grow: 0"; - style = id_max < 0 ? "negative" : "positive"; - if (reversedIDs.includes(id)) { - style === "positive" ? style = "negative" : style = "positive"; - } - max_elem.classList.add(style); - max_elem.textContent = id_max + idSuffixes[id]; - item_div.appendChild(max_elem); - row.appendChild(item_div); - return row; -} - -function displaysq2WeaponStats(build) { - // let base_damage = build.get('damageRaw'); - let damage_keys = [ "nDam", "eDam", "tDam", "wDam", "fDam", "aDam" ]; - - // pP base calc (why do i still use pP) - let item = build.weapon; - let stats = new Map(); - stats.set("atkSpd", item.get("atkSpd")); - stats.set("damageBonus", [0, 0, 0, 0, 0]); - stats.set("damageRaw", [item.get("nDam"), item.get("eDam"), item.get("tDam"), item.get("wDam"), item.get("fDam"), item.get("aDam")]); - - //needed for damage calc CR powders - if (build.weapon.get("tier") === "Crafted") { - stats.set("damageBases", [item.get("nDamBaseHigh"), item.get("eDamBaseHigh"), item.get("tDamBaseHigh"), item.get("wDamBaseHigh"), item.get("fDamBaseHigh"), item.get("aDamBaseHigh")]); - } - - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], 0, 0, 0, build.weapon, [0, 0, 0, 0, 0], 1, undefined); - let powdered_base = results[2]; - - const powdered_map = new Map(); - powdered_map.set('nDam', powdered_base[0][0]+'-'+powdered_base[0][1]); - powdered_map.set('eDam', powdered_base[1][0]+'-'+powdered_base[1][1]); - powdered_map.set('tDam', powdered_base[2][0]+'-'+powdered_base[2][1]); - powdered_map.set('wDam', powdered_base[3][0]+'-'+powdered_base[3][1]); - powdered_map.set('fDam', powdered_base[4][0]+'-'+powdered_base[4][1]); - powdered_map.set('aDam', powdered_base[5][0]+'-'+powdered_base[5][1]); - - // display - - //I think this is res's code? in any case it doesn't work - ferri - /* for (const i in damage_keys) { - document.getElementById(damage_keys[i]+"-base").textContent = powdered_map.get(damage_keys[i]); - } */ - - let tot = 0; - //sum up elemental damages to get sum of base dps's - for (let i = 0; i < 6; i++) { - tot += powdered_base[i][0] + powdered_base[i][1]; - } - tot /= 2; - let dps = Math.max(0, Math.round(tot * baseDamageMultiplier[attackSpeeds.indexOf(item.get("atkSpd"))] )); //atkspeeds - - // document.getElementById("weapon-dps").textContent = "base dps: " + (isNaN(dps) ? 0 : dps); - document.getElementById("weapon-dps").textContent = (isNaN(dps) ? 0 : dps); - document.getElementById("weapon-lv").textContent = item.get("lvl"); - - if (item.get("type")) { - document.getElementById("weapon-img").src = "../media/items/" + (newIcons ? "new/":"old/") + "generic-" + item.get("type") + ".png"; - } -} - -function displaysq2FixedID(active, id, value, elemental_format, style) { - if (style) { - let row = document.createElement('div'); - row.classList.add("row"); - let desc_elem = document.createElement('div'); - desc_elem.classList.add('col'); - desc_elem.classList.add('text-start'); - - if (elemental_format) { - apply_sq2_elemental_format(desc_elem, id); - } - else { - desc_elem.textContent = idPrefixes[id]; - } - row.appendChild(desc_elem); - - let value_elem = document.createElement('div'); - value_elem.classList.add('col'); - value_elem.classList.add('text-end'); - value_elem.classList.add(style); - value_elem.textContent = value + idSuffixes[id]; - row.appendChild(value_elem); - active.appendChild(row); - return row; - } - else { - // HACK TO AVOID DISPLAYING ZERO DAMAGE! TODO - if (value === "0-0" || value === "0-0\u279c0-0") { - return; - } - let p_elem = document.createElement('div'); - p_elem.classList.add('col'); - if (elemental_format) { - apply_sq2_elemental_format(p_elem, id, value); - } - else { - p_elem.textContent = idPrefixes[id].concat(value, idSuffixes[id]); - } - active.appendChild(p_elem); - return p_elem; - } -} - -function displaysq2PoisonDamage(overallparent_elem, build) { - overallparent_elem.textContent = ""; - - //Title - let title_elemavg = document.createElement("b"); - title_elemavg.textContent = "Poison Stats"; - overallparent_elem.append(title_elemavg); - - let overallpoisonDamage = document.createElement("p"); - let overallpoisonDamageFirst = document.createElement("span"); - let overallpoisonDamageSecond = document.createElement("span"); - let poison_tick = Math.ceil(build.statMap.get("poison") * (1+skillPointsToPercentage(build.total_skillpoints[0])) * (build.statMap.get("poisonPct"))/100 /3); - overallpoisonDamageFirst.textContent = "Poison Tick: "; - overallpoisonDamageSecond.textContent = Math.max(poison_tick,0); - overallpoisonDamageSecond.classList.add("Damage"); - - overallpoisonDamage.appendChild(overallpoisonDamageFirst); - overallpoisonDamage.appendChild(overallpoisonDamageSecond); - overallparent_elem.append(overallpoisonDamage); -} - -function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ - // console.log("Melee Stats"); - // console.log(meleeStats); - let tooltipinfo = meleeStats[13]; - let attackSpeeds = ["Super Slow", "Very Slow", "Slow", "Normal", "Fast", "Very Fast", "Super Fast"]; - //let damagePrefixes = ["Neutral Damage: ","Earth Damage: ","Thunder Damage: ","Water Damage: ","Fire Damage: ","Air Damage: "]; - parent_elem.textContent = ""; - overallparent_elem.textContent = ""; - const stats = meleeStats.slice(); - - for (let i = 0; i < 6; ++i) { - for (let j in stats[i]) { - stats[i][j] = stats[i][j].toFixed(2); - } - } - for (let i = 6; i < 8; ++i) { - for (let j = 0; j < 2; j++) { - stats[i][j] = stats[i][j].toFixed(2); - } - } - for (let i = 8; i < 11; ++i){ - stats[i] = stats[i].toFixed(2); - } - //tooltipelem, tooltiptext - let tooltip; let tooltiptext; - - //title - let title_elem = document.createElement("p"); - title_elem.classList.add("title"); - title_elem.textContent = "Melee Stats"; - parent_elem.append(title_elem); - parent_elem.append(document.createElement("br")); - - //overall title - let title_elemavg = document.createElement("b"); - title_elemavg.textContent = "Melee Stats"; - overallparent_elem.append(title_elemavg); - - //average DPS - let averageDamage = document.createElement("p"); - averageDamage.classList.add("left"); - averageDamage.textContent = "Average DPS: " + stats[10]; - tooltiptext = `= ((${stats[8]} * ${(stats[6][2]).toFixed(2)}) + (${stats[9]} * ${(stats[7][2]).toFixed(2)}))` - tooltip = createTooltip(tooltip, "p", tooltiptext, averageDamage, ["melee-tooltip"]); - averageDamage.appendChild(tooltip); - parent_elem.append(averageDamage); - - //overall average DPS - let overallaverageDamage = document.createElement("p"); - let overallaverageDamageFirst = document.createElement("span"); - overallaverageDamageFirst.textContent = "Average DPS: " - - let overallaverageDamageSecond = document.createElement("span"); - overallaverageDamageSecond.classList.add("Damage"); - overallaverageDamageSecond.textContent = stats[10]; - overallaverageDamage.appendChild(overallaverageDamageFirst); - overallaverageDamage.appendChild(overallaverageDamageSecond); - - overallparent_elem.append(overallaverageDamage); - //overallparent_elem.append(document.createElement("br")); - - //attack speed - let atkSpd = document.createElement("p"); - atkSpd.classList.add("left"); - atkSpd.textContent = "Attack Speed: " + attackSpeeds[stats[11]]; - parent_elem.append(atkSpd); - parent_elem.append(document.createElement("br")); - - //overall attack speed - let overallatkSpd = document.createElement("p"); - let overallatkSpdFirst = document.createElement("span"); - overallatkSpdFirst.textContent = "Attack Speed: "; - let overallatkSpdSecond = document.createElement("span"); - overallatkSpdSecond.classList.add("Damage"); - overallatkSpdSecond.textContent = attackSpeeds[stats[11]]; - overallatkSpd.appendChild(overallatkSpdFirst); - overallatkSpd.appendChild(overallatkSpdSecond); - overallparent_elem.append(overallatkSpd); - - //Non-Crit: n->elem, total dmg, DPS - let nonCritStats = document.createElement("p"); - nonCritStats.classList.add("left"); - nonCritStats.textContent = "Non-Crit Stats: "; - nonCritStats.append(document.createElement("br")); - for (let i = 0; i < 6; i++){ - if(stats[i][1] != 0){ - let dmg = document.createElement("p"); - dmg.textContent = stats[i][0] + " \u2013 " + stats[i][1]; - dmg.classList.add(damageClasses[i]); - dmg.classList.add("itemp"); - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(0,2).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, dmg, ["melee-tooltip"]); - nonCritStats.append(dmg); - } - } - - let normalDamage = document.createElement("p"); - normalDamage.textContent = "Total: " + stats[6][0] + " \u2013 " + stats[6][1]; - let tooltiparr = ["Min: = ", "Max: = "] - let arr = []; let arr2 = []; - for (let i = 0; i < 6; i++) { - if (stats[i][0] != 0) { - arr.push(stats[i][0]); - arr2.push(stats[i][1]); - } - } - tooltiptext = tooltiparr[0] + arr.join(" + ") + "\n" + tooltiparr[1] + arr2.join(" + "); - tooltip = createTooltip(tooltip, "p", tooltiptext, normalDamage, ["melee-tooltip"]); - nonCritStats.append(normalDamage); - - let normalDPS = document.createElement("p"); - normalDPS.textContent = "Normal DPS: " + stats[8]; - normalDPS.classList.add("tooltip"); - tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, normalDPS, ["melee-tooltip"]); - nonCritStats.append(normalDPS); - - //overall average DPS - let singleHitDamage = document.createElement("p"); - let singleHitDamageFirst = document.createElement("span"); - singleHitDamageFirst.textContent = "Single Hit Average: "; - let singleHitDamageSecond = document.createElement("span"); - singleHitDamageSecond.classList.add("Damage"); - singleHitDamageSecond.textContent = stats[12].toFixed(2); - tooltiptext = ` = ((${stats[6][0]} + ${stats[6][1]}) / 2) * ${stats[6][2].toFixed(2)} + ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${stats[7][2].toFixed(2)}`; - // tooltip = createTooltip(tooltip, "p", tooltiptext, singleHitDamage, ["melee-tooltip", "summary-tooltip"]); - - singleHitDamage.appendChild(singleHitDamageFirst); - singleHitDamage.appendChild(singleHitDamageSecond); - overallparent_elem.append(singleHitDamage); - - let normalChance = document.createElement("p"); - normalChance.textContent = "Non-Crit Chance: " + (stats[6][2]*100).toFixed(2) + "%"; - normalChance.append(document.createElement("br")); - normalChance.append(document.createElement("br")); - nonCritStats.append(normalChance); - - parent_elem.append(nonCritStats); - parent_elem.append(document.createElement("br")); - - //Crit: n->elem, total dmg, DPS - let critStats = document.createElement("p"); - critStats.classList.add("left"); - critStats.textContent = "Crit Stats: "; - critStats.append(document.createElement("br")); - for (let i = 0; i < 6; i++){ - if(stats[i][3] != 0) { - dmg = document.createElement("p"); - dmg.textContent = stats[i][2] + " \u2013 " + stats[i][3]; - dmg.classList.add(damageClasses[i]); - dmg.classList.add("itemp"); - tooltiptext = tooltipinfo.get("damageformulas")[i].slice(2,4).join("\n"); - tooltip = createTooltip(tooltip, "p", tooltiptext, dmg, ["melee-tooltip"]); - critStats.append(dmg); - } - } - let critDamage = document.createElement("p"); - critDamage.textContent = "Total: " + stats[7][0] + " \u2013 " + stats[7][1]; - tooltiparr = ["Min: = ", "Max: = "] - arr = []; arr2 = []; - for (let i = 0; i < 6; i++) { - if (stats[i][0] != 0) { - arr.push(stats[i][2]); - arr2.push(stats[i][3]); - } - } - tooltiptext = tooltiparr[0] + arr.join(" + ") + "\n" + tooltiparr[1] + arr2.join(" + "); - tooltip = createTooltip(tooltip, "p", tooltiptext, critDamage, ["melee-tooltip"]); - - critStats.append(critDamage); - - let critDPS = document.createElement("p"); - critDPS.textContent = "Crit DPS: " + stats[9]; - tooltiptext = ` = ((${stats[7][0]} + ${stats[7][1]}) / 2) * ${baseDamageMultiplier[stats[11]]}`; - tooltip = createTooltip(tooltip, "p", tooltiptext, critDPS, ["melee-tooltip"]); - critStats.append(critDPS); - - let critChance = document.createElement("p"); - critChance.textContent = "Crit Chance: " + (stats[7][2]*100).toFixed(2) + "%"; - critChance.append(document.createElement("br")); - critChance.append(document.createElement("br")); - critStats.append(critChance); - - parent_elem.append(critStats); -} - -function displaysq2ArmorStats(build) { - let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace']; - - for (const i in armor_keys) { - document.getElementById(armor_keys[i]+'-health').textContent = build[armor_keys[i]].get('hp'); - document.getElementById(armor_keys[i]+'-lv').textContent = build[armor_keys[i]].get('lvl'); - } -} - -function displaysq2DefenseStats(parent_elem, build, insertSummary){ - let defenseStats = build.getDefenseStats(); - insertSummary = (typeof insertSummary !== 'undefined') ? insertSummary : false; - if (!insertSummary) { - parent_elem.textContent = ""; - } - const stats = defenseStats.slice(); - - // parent_elem.append(document.createElement("br")); - let statsTable = document.createElement("div"); - - //[total hp, ehp, total hpr, ehpr, [def%, agi%], [edef,tdef,wdef,fdef,adef]] - for(const i in stats){ - if(typeof stats[i] === "number"){ - stats[i] = stats[i].toFixed(2); - }else{ - for(const j in stats[i]){ - stats[i][j] = stats[i][j].toFixed(2); - } - } - } - - //total HP - let hpRow = document.createElement("div"); - hpRow.classList.add('row'); - let hp = document.createElement("div"); - hp.classList.add('col'); - hp.classList.add("Health"); - hp.classList.add("text-start"); - hp.textContent = "Total HP:"; - let boost = document.createElement("div"); - boost.classList.add('col'); - boost.textContent = stats[0]; - boost.classList.add("text-end"); - - hpRow.appendChild(hp); - hpRow.append(boost); - - if (insertSummary) { - parent_elem.appendChild(hpRow); - } else { - statsTable.appendChild(hpRow); - } - - let tooltip; let tooltiptext; - - let defMult = build.statMap.get("defMult"); - if (!defMult) {defMult = 1} - - //EHP - let ehpRow = document.createElement("div"); - ehpRow.classList.add("row"); - let ehp = document.createElement("div"); - ehp.classList.add("col"); - ehp.classList.add("text-start"); - ehp.textContent = "Effective HP:"; - - boost = document.createElement("div"); - boost.textContent = stats[1][0]; - boost.classList.add("col"); - boost.classList.add("text-end"); - tooltiptext = `= ${stats[0]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (1 - ${skillPointsToPercentage(build.total_skillpoints[4]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - - ehpRow.appendChild(ehp); - ehpRow.append(boost); - - if (insertSummary) { - parent_elem.appendChild(ehpRow) - } else { - statsTable.append(ehpRow); - } - - ehpRow = document.createElement("div"); - ehpRow.classList.add("row"); - ehp = document.createElement("div"); - ehp.classList.add("col"); - ehp.classList.add("text-start"); - ehp.textContent = "Effective HP (no agi):"; - - boost = document.createElement("div"); - boost.textContent = stats[1][1]; - boost.classList.add("col"); - boost.classList.add("text-end"); - tooltiptext = `= ${stats[0]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - - ehpRow.appendChild(ehp); - ehpRow.append(boost); - statsTable.append(ehpRow); - - //total HPR - let hprRow = document.createElement("div"); - hprRow.classList.add("row") - let hpr = document.createElement("div"); - hpr.classList.add("Health"); - hpr.classList.add("col"); - hpr.classList.add("text-start"); - hpr.textContent = "HP Regen (Total):"; - boost = document.createElement("div"); - boost.textContent = stats[2]; - boost.classList.add("col"); - boost.classList.add("text-end"); - - hprRow.appendChild(hpr); - hprRow.appendChild(boost); - - if (insertSummary) { - parent_elem.appendChild(hprRow); - } else { - statsTable.appendChild(hprRow); - } - - //EHPR - let ehprRow = document.createElement("div"); - ehprRow.classList.add("row") - let ehpr = document.createElement("div"); - ehpr.classList.add("col"); - ehpr.classList.add("text-start"); - ehpr.textContent = "Effective HP Regen:"; - - boost = document.createElement("div"); - boost.textContent = stats[3][0]; - boost.classList.add("col"); - boost.classList.add("text-end"); - tooltiptext = `= ${stats[2]} / ((1 - ${skillPointsToPercentage(build.total_skillpoints[3]).toFixed(3)}) * (1 - ${skillPointsToPercentage(build.total_skillpoints[4]).toFixed(3)}) * (2 - ${defMult}) * (2 - ${build.defenseMultiplier}))` - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - - ehprRow.appendChild(ehpr); - ehprRow.append(boost); - statsTable.append(ehprRow); - /* - ehprRow = document.createElement("tr"); - ehpr = document.createElement("td"); - ehpr.classList.add("left"); - ehpr.textContent = "Effective HP Regen (no agi):"; - - boost = document.createElement("td"); - boost.textContent = stats[3][1]; - boost.classList.add("right"); - - ehprRow.appendChild(ehpr); - ehprRow.append(boost); - statsTable.append(ehprRow); */ - - //eledefs - let eledefs = stats[5]; - for (let i = 0; i < eledefs.length; i++){ - let eledefElemRow = document.createElement("div"); - eledefElemRow.classList.add("row") - - let eledef = document.createElement("div"); - eledef.classList.add("col"); - eledef.classList.add("text-start"); - let eledefTitle = document.createElement("span"); - eledefTitle.textContent = damageClasses[i+1]; - eledefTitle.classList.add(damageClasses[i+1]); - - let defense = document.createElement("span"); - defense.textContent = " Def (Total): "; - - eledef.appendChild(eledefTitle); - eledef.appendChild(defense); - eledefElemRow.appendChild(eledef); - - let boost = document.createElement("div"); - boost.textContent = eledefs[i]; - boost.classList.add(eledefs[i] >= 0 ? "positive" : "negative"); - boost.classList.add("col"); - boost.classList.add("text-end"); - - let defRaw = build.statMap.get("defRaw")[i]; - let defPct = build.statMap.get("defBonus")[i]/100; - if (defRaw < 0) { - defPct >= 0 ? defPct = "- " + defPct: defPct = "+ " + defPct; - tooltiptext = `= min(0, ${defRaw} * (1 ${defPct}))` - } else { - defPct >= 0 ? defPct = "+ " + defPct: defPct = "- " + defPct; - tooltiptext = `= ${defRaw} * (1 ${defPct})` - } - // tooltip = createTooltip(tooltip, "p", tooltiptext, boost, ["def-tooltip"]); - - eledefElemRow.appendChild(boost); - - if (insertSummary) { - parent_elem.appendChild(eledefElemRow); - } else { - statsTable.appendChild(eledefElemRow); - } - } - - if (!insertSummary) { - //skp - let defRow = document.createElement("div"); - defRow.classList.add("row"); - let defElem = document.createElement("div"); - defElem.classList.add("col"); - defElem.classList.add("text-start"); - defElem.textContent = "Damage Absorbed %:"; - boost = document.createElement("div"); - boost.classList.add("col"); - boost.classList.add("text-end"); - boost.textContent = stats[4][0] + "%"; - defRow.appendChild(defElem); - defRow.appendChild(boost); - statsTable.append(defRow); - - let agiRow = document.createElement("div"); - agiRow.classList.add("row"); - let agiElem = document.createElement("div"); - agiElem.classList.add("col"); - agiElem.classList.add("text-start"); - agiElem.textContent = "Dodge Chance %:"; - boost = document.createElement("div"); - boost.classList.add("col"); - boost.classList.add("text-end"); - boost.textContent = stats[4][1] + "%"; - agiRow.appendChild(agiElem); - agiRow.appendChild(boost); - statsTable.append(agiRow); - } - - if (!insertSummary) { - parent_elem.append(statsTable); - } -} - -function displaysq2PowderSpecials(parent_elem, powderSpecials, build, overall=false) { - parent_elem.textContent = "" - let title = document.createElement("b"); - title.textContent = "Powder Specials"; - parent_elem.appendChild(title); - let specials = powderSpecials.slice(); - let stats = build.statMap; - let expandedStats = new Map(); - //each entry of powderSpecials is [ps, power] - for (special of specials) { - //iterate through the special and display its effects. - let powder_special = document.createElement("p"); - let specialSuffixes = new Map([ ["Duration", " sec"], ["Radius", " blocks"], ["Chains", ""], ["Damage", "%"], ["Damage Boost", "%"], ["Knockback", " blocks"] ]); - let specialTitle = document.createElement("p"); - let specialEffects = document.createElement("p"); - specialTitle.classList.add(damageClasses[powderSpecialStats.indexOf(special[0]) + 1]); - let effects = special[0]["weaponSpecialEffects"]; - let power = special[1]; - specialTitle.textContent = special[0]["weaponSpecialName"] + " " + Math.floor((power-1)*0.5 + 4) + (power % 2 == 0 ? ".5" : ""); - - if (!overall || powderSpecialStats.indexOf(special[0]) == 2 || powderSpecialStats.indexOf(special[0]) == 3 || powderSpecialStats.indexOf(special[0]) == 4) { - for (const [key,value] of effects) { - let effect = document.createElement("p"); - effect.textContent += key + ": " + value[power-1] + specialSuffixes.get(key); - if(key === "Damage"){ - effect.textContent += elementIcons[powderSpecialStats.indexOf(special[0])]; - } - if(special[0]["weaponSpecialName"] === "Wind Prison" && key === "Damage Boost") { - effect.textContent += " (only 1st hit)"; - } - specialEffects.appendChild(effect); - } - } - powder_special.appendChild(specialTitle); - powder_special.appendChild(specialEffects); - - //if this special is an instant-damage special (Quake, Chain Lightning, Courage Burst), display the damage. - let specialDamage = document.createElement("p"); - // specialDamage.classList.add("item-margin"); - let spells = spell_table["powder"]; - if (powderSpecialStats.indexOf(special[0]) == 0 || powderSpecialStats.indexOf(special[0]) == 1 || powderSpecialStats.indexOf(special[0]) == 3) { //Quake, Chain Lightning, or Courage - let spell = (powderSpecialStats.indexOf(special[0]) == 3 ? spells[2] : spells[powderSpecialStats.indexOf(special[0])]); - let part = spell["parts"][0]; - let _results = calculateSpellDamage(stats, part.conversion, - stats.get("mdRaw"), stats.get("mdPct"), - 0, build.weapon, build.total_skillpoints, build.damageMultiplier * ((part.multiplier[power-1] / 100)));//part.multiplier[power] / 100 - - let critChance = skillPointsToPercentage(build.total_skillpoints[1]); - let save_damages = []; - - let totalDamNormal = _results[0]; - let totalDamCrit = _results[1]; - let results = _results[2]; - for (let i = 0; i < 6; ++i) { - for (let j in results[i]) { - results[i][j] = results[i][j].toFixed(2); - } - } - let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0; - let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; - let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; - - let averageWrap = document.createElement("p"); - let averageLabel = document.createElement("span"); - averageLabel.textContent = "Average: "; - - let averageLabelDmg = document.createElement("span"); - averageLabelDmg.classList.add("Damage"); - averageLabelDmg.textContent = averageDamage.toFixed(2); - - averageWrap.appendChild(averageLabel); - averageWrap.appendChild(averageLabelDmg); - specialDamage.appendChild(averageWrap); - - if (!overall) { - let nonCritLabel = document.createElement("p"); - nonCritLabel.textContent = "Non-Crit Average: "+nonCritAverage.toFixed(2); - nonCritLabel.classList.add("damageSubtitle"); - nonCritLabel.classList.add("item-margin"); - specialDamage.append(nonCritLabel); - - for (let i = 0; i < 6; i++){ - if (results[i][1] > 0){ - let p = document.createElement("p"); - p.classList.add("damagep"); - p.classList.add(damageClasses[i]); - p.textContent = results[i][0]+"-"+results[i][1]; - specialDamage.append(p); - } - } - let normalDamage = document.createElement("p"); - normalDamage.textContent = "Total: " + totalDamNormal[0].toFixed(2) + "-" + totalDamNormal[1].toFixed(2); - normalDamage.classList.add("itemp"); - specialDamage.append(normalDamage); - - let nonCritChanceLabel = document.createElement("p"); - nonCritChanceLabel.textContent = "Non-Crit Chance: " + ((1-critChance)*100).toFixed(2) + "%"; - specialDamage.append(nonCritChanceLabel); - - let critLabel = document.createElement("p"); - critLabel.textContent = "Crit Average: "+critAverage.toFixed(2); - critLabel.classList.add("damageSubtitle"); - critLabel.classList.add("item-margin"); - - specialDamage.append(critLabel); - for (let i = 0; i < 6; i++){ - if (results[i][1] > 0){ - let p = document.createElement("p"); - p.classList.add("damagep"); - p.classList.add(damageClasses[i]); - p.textContent = results[i][2]+"-"+results[i][3]; - specialDamage.append(p); - } - } - let critDamage = document.createElement("p"); - critDamage.textContent = "Total: " + totalDamCrit[0].toFixed(2) + "-" + totalDamCrit[1].toFixed(2); - critDamage.classList.add("itemp"); - specialDamage.append(critDamage); - - let critChanceLabel = document.createElement("p"); - critChanceLabel.textContent = "Crit Chance: " + (critChance*100).toFixed(2) + "%"; - specialDamage.append(critChanceLabel); - - save_damages.push(averageDamage); - } - - powder_special.append(specialDamage); - } - - parent_elem.appendChild(powder_special); - } -} - -function displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, spellIdx, weapon) { - parent_elem.textContent = ""; - - - let tooltip; let tooltiptext; - const stats = build.statMap; - let title_elem = document.createElement("p"); - - overallparent_elem.textContent = ""; - let title_elemavg = document.createElement("b"); - - if (spellIdx != 0) { - let first = document.createElement("span"); - first.textContent = spell.title + " ("; - title_elem.appendChild(first.cloneNode(true)); //cloneNode is needed here. - title_elemavg.appendChild(first); - - let second = document.createElement("span"); - second.textContent = build.getSpellCost(spellIdx, spell.cost); - second.classList.add("Mana"); - - let int_redux = skillPointsToPercentage(build.total_skillpoints[2]).toFixed(2); - let spPct_redux = (build.statMap.get("spPct" + spellIdx)/100).toFixed(2); - let spRaw_redux = (build.statMap.get("spRaw" + spellIdx)).toFixed(2); - spPct_redux >= 0 ? spPct_redux = "+ " + spPct_redux : spPct_redux = "- " + Math.abs(spPct_redux); - spRaw_redux >= 0 ? spRaw_redux = "+ " + spRaw_redux : spRaw_redux = "- " + Math.abs(spRaw_redux); - - // tooltiptext = `= max(1, floor((ceil(${spell.cost} * (1 - ${int_redux})) ${spRaw_redux}) * (1 ${spPct_redux})))`; - // tooltip = createTooltip(tooltip, "p", tooltiptext, second, ["spellcostcalc"]); - // second.appendChild(tooltip); - title_elem.appendChild(second.cloneNode(true)); - title_elemavg.appendChild(second); - - - let third = document.createElement("span"); - third.textContent = ") [Base: " + build.getBaseSpellCost(spellIdx, spell.cost) + " ]"; - title_elem.appendChild(third); - let third_summary = document.createElement("span"); - third_summary.textContent = ")"; - title_elemavg.appendChild(third_summary); - } - else { - title_elem.textContent = spell.title; - title_elemavg.textContent = spell.title; - } - - parent_elem.append(title_elem); - overallparent_elem.append(title_elemavg); - - overallparent_elem.append(displaysq2NextCosts(spell, build, weapon)); - - - let critChance = skillPointsToPercentage(build.total_skillpoints[1]); - - let save_damages = []; - - let part_divavg = document.createElement("p"); - overallparent_elem.append(part_divavg); - - let spell_parts; - if (spell.parts) { - spell_parts = spell.parts; - } - else { - spell_parts = spell.variants.DEFAULT; - for (const majorID of stats.get("activeMajorIDs")) { - if (majorID in spell.variants) { - spell_parts = spell.variants[majorID]; - break; - } - } - } - //console.log(spell_parts); - - for (const part of spell_parts) { - let part_div = document.createElement("p"); - parent_elem.append(part_div); - - let subtitle_elem = document.createElement("p"); - subtitle_elem.textContent = part.subtitle; - part_div.append(subtitle_elem); - - if (part.type === "damage") { - //console.log(build.expandedStats); - let _results = calculateSpellDamage(stats, part.conversion, - stats.get("sdRaw") + stats.get("rainbowRaw"), stats.get("sdPct"), - part.multiplier / 100, weapon, build.total_skillpoints, build.damageMultiplier); - let totalDamNormal = _results[0]; - let totalDamCrit = _results[1]; - let results = _results[2]; - let tooltipinfo = _results[3]; - - for (let i = 0; i < 6; ++i) { - for (let j in results[i]) { - results[i][j] = results[i][j].toFixed(2); - } - } - let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0; - let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0; - let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0; - - let averageLabel = document.createElement("p"); - averageLabel.textContent = "Average: "+averageDamage.toFixed(2); - tooltiptext = ` = ((1 - ${critChance}) * ${nonCritAverage.toFixed(2)}) + (${critChance} * ${critAverage.toFixed(2)})` - // averageLabel.classList.add("damageSubtitle"); - // tooltip = createTooltip(tooltip, "p", tooltiptext, averageLabel, ["spell-tooltip"]); - part_div.append(averageLabel); - - - if (part.summary == true) { - let overallaverageLabel = document.createElement("p"); - let first = document.createElement("span"); - let second = document.createElement("span"); - first.textContent = part.subtitle + " Average: "; - second.textContent = averageDamage.toFixed(2); - overallaverageLabel.appendChild(first); - overallaverageLabel.appendChild(second); - // tooltip = createTooltip(tooltip, "p", tooltiptext, overallaverageLabel, ["spell-tooltip", "summary-tooltip"]); - second.classList.add("Damage"); - part_divavg.append(overallaverageLabel); - } - - function _damage_display(label_text, average, result_idx) { - let label = document.createElement("p"); - label.textContent = label_text+average.toFixed(2); - part_div.append(label); - - let arrmin = []; - let arrmax = []; - for (let i = 0; i < 6; i++){ - if (results[i][1] != 0){ - let p = document.createElement("p"); - p.classList.add(damageClasses[i]); - p.textContent = results[i][result_idx] + " \u2013 " + results[i][result_idx + 1]; - arrmin.push(results[i][result_idx]); - arrmax.push(results[i][result_idx + 1]); - part_div.append(p); - } - } - tooltiptext = ` = ((${arrmin.join(" + ")}) + (${arrmax.join(" + ")})) / 2`; - // tooltip = createTooltip(tooltip, "p", tooltiptext, label, ["spell-tooltip"]); - } - _damage_display("Non-Crit Average: ", nonCritAverage, 0); - _damage_display("Crit Average: ", critAverage, 2); - - save_damages.push(averageDamage); - } else if (part.type === "heal") { - let heal_amount = (part.strength * build.getDefenseStats()[0] * Math.max(0.5,Math.min(1.75, 1 + 0.5 * stats.get("wDamPct")/100))).toFixed(2); - tooltiptext = ` = ${part.strength} * ${build.getDefenseStats()[0]} * max(0.5, min(1.75, 1 + 0.5 * ${stats.get("wDamPct")/100}))`; - let healLabel = document.createElement("p"); - healLabel.textContent = heal_amount; - // healLabel.classList.add("damagep"); - // tooltip = createTooltip(tooltip, "p", tooltiptext, healLabel, ["spell-tooltip"]); - part_div.append(healLabel); - if (part.summary == true) { - let overallhealLabel = document.createElement("p"); - let first = document.createElement("span"); - let second = document.createElement("span"); - first.textContent = part.subtitle + ": "; - second.textContent = heal_amount; - overallhealLabel.appendChild(first); - second.classList.add("Set"); - overallhealLabel.appendChild(second); - part_divavg.append(overallhealLabel); - } - } else if (part.type === "total") { - let total_damage = 0; - tooltiptext = ""; - for (let i in part.factors) { - total_damage += save_damages[i] * part.factors[i]; - } - - let dmgarr = part.factors.slice(); - dmgarr = dmgarr.map(x => "(" + x + " * " + save_damages[dmgarr.indexOf(x)].toFixed(2) + ")"); - tooltiptext = " = " + dmgarr.join(" + "); - - - let averageLabel = document.createElement("p"); - averageLabel.textContent = "Average: "+total_damage.toFixed(2); - averageLabel.classList.add("damageSubtitle"); - tooltip = createTooltip(tooltip, "p", tooltiptext, averageLabel, ["spell-tooltip"]); - part_div.append(averageLabel); - - let overallaverageLabel = document.createElement("p"); - let overallaverageLabelFirst = document.createElement("span"); - let overallaverageLabelSecond = document.createElement("span"); - overallaverageLabelFirst.textContent = "Average: "; - overallaverageLabelSecond.textContent = total_damage.toFixed(2); - overallaverageLabelSecond.classList.add("Damage"); - - - overallaverageLabel.appendChild(overallaverageLabelFirst); - overallaverageLabel.appendChild(overallaverageLabelSecond); - part_divavg.append(overallaverageLabel); - } - } - - //up and down arrow - done ugly - let arrow = document.createElement("img"); - arrow.id = "arrow_" + overallparent_elem.id; - arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem"; - arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png"; - overallparent_elem.appendChild(arrow); -} - -function displaysq2EquipOrder(parent_elem, buildOrder){ - parent_elem.textContent = ""; - const order = buildOrder.slice(); - let title_elem = document.createElement("b"); - title_elem.textContent = "Equip order "; - title_elem.classList.add("Normal", "text-center"); - parent_elem.append(title_elem); - for (const item of order) { - let p_elem = document.createElement("b"); - p_elem.textContent = item.get("displayName"); - parent_elem.append(p_elem); - } -} - -function displaysq2NextCosts(spell, build, weapon) { - let int = build.total_skillpoints[2]; - let spells = spell_table[weapon.get("type")]; - - let row = document.createElement("div"); - row.classList.add("spellcost-tooltip"); - let init_cost = document.createElement("b"); - init_cost.textContent = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost); - init_cost.classList.add("Mana"); - let arrow = document.createElement("b"); - arrow.textContent = "\u279C"; - let next_cost = document.createElement("b"); - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1); - next_cost.classList.add("Mana"); - let int_needed = document.createElement("b"); - if (init_cost.textContent === "1") { - int_needed.textContent = ": n/a (+0)"; - }else { //do math - let target = build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) - 1; - let needed = int; - let noUpdate = false; - //forgive me... I couldn't inverse ceil, floor, and max. - while (build.getSpellCost(spells.indexOf(spell) + 1, spell.cost) > target) { - if(needed > 150) { - noUpdate = true; - break; - } - needed++; - build.total_skillpoints[2] = needed; - } - let missing = needed - int; - //in rare circumstances, the next spell cost can jump. - if (noUpdate) { - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)-1); - }else { - next_cost.textContent = (init_cost.textContent === "1" ? 1 : build.getSpellCost(spells.indexOf(spell) + 1, spell.cost)); - } - - - build.total_skillpoints[2] = int;//forgive me pt 2 - int_needed.textContent = ": " + (needed > 150 ? ">150" : needed) + " int (+" + (needed > 150 ? "n/a" : missing) + ")"; - } - - // row.appendChild(init_cost); - row.appendChild(arrow); - row.appendChild(next_cost); - row.appendChild(int_needed); - return row; -} - -function apply_sq2_elemental_format(p_elem, id, suffix) { - suffix = (typeof suffix !== 'undefined') ? suffix : ""; - // THIS IS SO JANK BUT IM TOO LAZY TO FIX IT TODO - let parts = idPrefixes[id].split(/ (.*)/); - let element_prefix = parts[0]; - let desc = parts[1]; - let i_elem = document.createElement('span'); - i_elem.classList.add(element_prefix); - i_elem.textContent = element_prefix; - p_elem.appendChild(i_elem); - - let i_elem2 = document.createElement('span'); - i_elem2.textContent = " " + desc + suffix; - p_elem.appendChild(i_elem2); -} - -function displaysq2SetBonuses(parent_id,build) { - setHTML(parent_id, ""); - let parent_div = document.getElementById(parent_id); - - let set_summary_elem = document.createElement('p'); - set_summary_elem.classList.add('text-center'); - set_summary_elem.textContent = "Set Bonuses"; - parent_div.append(set_summary_elem); - - for (const [setName, count] of build.activeSetCounts) { - const active_set = sets[setName]; - if (active_set["hidden"]) { continue; } - - let set_elem = document.createElement('p'); - set_elem.id = "set-"+setName; - set_summary_elem.append(set_elem); - - const bonus = active_set.bonuses[count-1]; - let mock_item = new Map(); - mock_item.set("fixID", true); - mock_item.set("displayName", setName+" Set: "+count+"/"+sets[setName].items.length); - let mock_minRolls = new Map(); - let mock_maxRolls = new Map(); - mock_item.set("minRolls", mock_minRolls); - mock_item.set("maxRolls", mock_maxRolls); - for (const id in bonus) { - if (rolledIDs.includes(id)) { - mock_minRolls.set(id, bonus[id]); - mock_maxRolls.set(id, bonus[id]); - } - else { - mock_item.set(id, bonus[id]); - } - } - mock_item.set("powders", []); - displaysq2ExpandedItem(mock_item, set_elem.id); - console.log(mock_item); - } -} - -function toggle_plus_minus(elem_id) { - let elem = document.getElementById(elem_id); - if (elem.classList.contains("plus_minus")) { - if (elem.textContent == "\u2795") { - elem.textContent = "\u2796"; - } else if (elem.textContent == "\u2796"){ - elem.textContent = "\u2795"; - } else { - // ???? - } - } -} - -// updates the powders within this element. -function updatePowders(elem_id) { - // let elem = document.getElementById(elem_id); - // for (let i = 0; i < elem.value.length - 1; i++) { - // if ('etwfa'.includes(elem.value.charAt(i)) && elem.value.charAt(i + 1) >= '1' && elem.value.charAt(i + 1) <= '6') { - // elem.value = elem.value.substring(0, i) + powder_chars['etwfa'.indexOf(elem.value.charAt(i))] + elem.value.substring(i + 2); - // } - // } -} - -/* -* Displays stats about a recipe that are NOT displayed in the craft stats. -* Includes: mat name and amounts, ingred names in an "array" with ingred effectiveness -*/ -function displaysq2RecipeStats(craft, parent_id) { - let elem = document.getElementById(parent_id); - if (!elem.classList.contains("col")) { - elem.classList.add("col"); - } - - //local vars - elem.textContent = ""; - recipe = craft["recipe"]; - mat_tiers = craft["mat_tiers"]; - ingreds = []; - for (const n of craft["ingreds"]) { - ingreds.push(n.get("name")); - } - let effectiveness = craft["statMap"].get("ingredEffectiveness"); - - let title = document.createElement("div"); - title.classList.add("row", "box-title", "fw-bold", "justify-content-center"); - title.textContent = "Recipe Stats"; - elem.appendChild(title); - - let mats = document.createElement("div"); - mats.classList.add("row"); - mats.textContent = "Crafting Materials: "; - elem.appendChild(mats); - - for (let i = 0; i < 2; i++) { - let tier = mat_tiers[i]; - let row = document.createElement("div"); - row.classList.add("row", "px-0", "mx-0"); - let b = document.createElement("div"); - let mat = recipe.get("materials")[i]; - b.textContent = "- " + mat.get("amount") + "x " + mat.get("item").split(" ").slice(1).join(" "); - b.classList.add("col"); - row.appendChild(b); - - let starsB = document.createElement("div"); - starsB.classList.add("T1-bracket", "col-auto", "px-0"); - starsB.textContent = "["; - row.appendChild(starsB); - for(let j = 0; j < 3; j ++) { - let star = document.createElement("div"); - star.classList.add("col-auto", "px-0"); - star.textContent = "\u272B"; - if(j < tier) { - star.classList.add("T1"); - } else { - star.classList.add("T0"); - } - row.append(star); - } - let starsE = document.createElement("div"); - starsE.classList.add("T1-bracket", "col-auto", "px-0"); - starsE.textContent = "]"; - row.appendChild(starsE); - - elem.appendChild(row); - } - - let ingredTable = document.createElement("div"); - ingredTable.classList.add("row"); - - for (let i = 0; i < 3; i++) { - let row = document.createElement("div"); - row.classList.add("row", "g-1", "justify-content-center"); - - - for (let j = 0; j < 2; j++) { - if (j == 1) { - let spacer = document.createElement("div"); - spacer.classList.add("col-1"); - row.appendChild(spacer); - } - let ingredName = ingreds[2 * i + j]; - let col = document.createElement("div"); - col.classList.add("col-5", "rounded", "dark-6", "border", "border-3", "dark-shadow"); - - let temp_row = document.createElement("div"); - temp_row.classList.add("row"); - col.appendChild(temp_row); - - let ingred_div = document.createElement("div"); - ingred_div.classList.add("col"); - ingred_div.textContent = ingredName; - temp_row.appendChild(ingred_div); - - let eff_div = document.createElement("div"); - eff_div.classList.add("col-auto"); - let e = effectiveness[2 * i + j]; - if (e > 0) { - eff_div.classList.add("positive"); - } else if (e < 0) { - eff_div.classList.add("negative"); - } - eff_div.textContent = "[" + e + "%]"; - - temp_row.appendChild(eff_div); - - row.appendChild(col); - } - ingredTable.appendChild(row); - } - elem.appendChild(ingredTable); -} - -/* -* Displays an ingredient in item format. -* However, an ingredient is too far from a normal item to display as one. -*/ -function displaysq2ExpandedIngredient(ingred, parent_id) { - let parent_elem = document.getElementById(parent_id); - parent_elem.textContent = ""; - - let item_order = [ - "dura", - "strReq", - "dexReq", - "intReq", - "defReq", - "agiReq" - ] - let consumable_order = [ - "dura", - "charges" - ] - let posMods_order = [ - "above", - "under", - "left", - "right", - "touching", - "notTouching" - ]; - let id_display_order = [ - "eDefPct", - "tDefPct", - "wDefPct", - "fDefPct", - "aDefPct", - "eDamPct", - "tDamPct", - "wDamPct", - "fDamPct", - "aDamPct", - "str", - "dex", - "int", - "agi", - "def", - "hpBonus", - "mr", - "ms", - "ls", - "hprRaw", - "hprPct", - "sdRaw", - "sdPct", - "mdRaw", - "mdPct", - "xpb", - "lb", - "lq", - "ref", - "thorns", - "expd", - "spd", - "atkTier", - "poison", - "spRegen", - "eSteal", - "spRaw1", - "spRaw2", - "spRaw3", - "spRaw4", - "spPct1", - "spPct2", - "spPct3", - "spPct4", - "jh", - "sprint", - "sprintReg", - "gXp", - "gSpd", - ]; - let active_elem; - let elemental_format = false; - let style; - for (const command of sq2_ing_display_order) { - if (command.charAt(0) === "!") { - // TODO: This is sooo incredibly janky..... - if (command === "!elemental") { - elemental_format = !elemental_format; - } - else if (command === "!spacer") { - let spacer = document.createElement('div'); - spacer.classList.add("row", "my-2"); - parent_elem.appendChild(spacer); - continue; - } - } else { - let div = document.createElement("div"); - div.classList.add("row"); - if (command === "displayName") { - div.classList.add("box-title"); - let title_elem = document.createElement("div"); - title_elem.classList.add("col-auto", "justify-content-center", "pr-1"); - title_elem.textContent = ingred.get("displayName"); - div.appendChild(title_elem); - - let tier = ingred.get("tier"); //tier in [0,3] - let begin = document.createElement("b"); - begin.classList.add("T"+tier+"-bracket", "col-auto", "px-0"); - begin.textContent = "["; - div.appendChild(begin); - - for (let i = 0; i < 3; i++) { - let tier_elem = document.createElement("b"); - if (i < tier) { - tier_elem.classList.add("T"+tier); - } else { - tier_elem.classList.add("T0"); - } - tier_elem.classList.add("px-0", "col-auto"); - tier_elem.textContent = "\u272B"; - div.appendChild(tier_elem); - } - let end = document.createElement("b"); - end.classList.add("T"+tier+"-bracket", "px-0", "col-auto"); - end.textContent = "]"; - div.appendChild(end); - }else if (command === "lvl") { - div.textContent = "Crafting Lvl Min: " + ingred.get("lvl"); - }else if (command === "posMods") { - for (const [key,value] of ingred.get("posMods")) { - let posModRow = document.createElement("div"); - posModRow.classList.add("row"); - if (value != 0) { - let posMod = document.createElement("div"); - posMod.classList.add("col-auto"); - posMod.textContent = posModPrefixes[key]; - posModRow.appendChild(posMod); - - let val = document.createElement("div"); - val.classList.add("col-auto", "px-0"); - val.textContent = value + posModSuffixes[key]; - if(value > 0) { - val.classList.add("positive"); - } else { - val.classList.add("negative"); - } - posModRow.appendChild(val); - div.appendChild(posModRow); - } - } - } else if (command === "itemIDs") { //dura, reqs - for (const [key,value] of ingred.get("itemIDs")) { - let idRow = document.createElement("div"); - idRow.classList.add("row"); - if (value != 0) { - let title = document.createElement("div"); - title.classList.add("col-auto"); - title.textContent = itemIDPrefixes[key]; - idRow.appendChild(title); - } - let desc = document.createElement("div"); - desc.classList.add("col-auto"); - if(value > 0) { - if(key !== "dura") { - desc.classList.add("negative"); - } else{ - desc.classList.add("positive"); - } - desc.textContent = "+"+value; - } else if (value < 0){ - if(key !== "dura") { - desc.classList.add("positive"); - } else{ - desc.classList.add("negative"); - } - desc.textContent = value; - } - if(value != 0){ - idRow.appendChild(desc); - } - div.appendChild(idRow); - } - } else if (command === "consumableIDs") { //dura, charges - for (const [key,value] of ingred.get("consumableIDs")) { - let idRow = document.createElement("div"); - idRow.classList.add("row"); - if (value != 0) { - let title = document.createElement("div"); - title.classList.add("col-auto"); - title.textContent = consumableIDPrefixes[key]; - idRow.appendChild(title); - } - let desc = document.createElement("div"); - desc.classList.add("col-auto"); - if(value > 0) { - desc.classList.add("positive"); - desc.textContent = "+"+value; - } else if (value < 0){ - desc.classList.add("negative"); - desc.textContent = value; - } - if(value != 0){ - idRow.appendChild(desc); - let suffix = document.createElement("div"); - suffix.classList.add("col-auto"); - suffix.textContent = consumableIDSuffixes[key]; - idRow.appendChild(suffix); - } - div.appendChild(idRow); - } - }else if (command === "skills") { - let row = document.createElement("div"); - row.classList.add("row"); - let title = document.createElement("div"); - title.classList.add("row"); - title.textContent = "Used in:"; - row.appendChild(title); - for(const skill of ingred.get("skills")) { - let skill_div = document.createElement("div"); - skill_div.classList.add("row"); - skill_div.textContent = skill.charAt(0) + skill.substring(1).toLowerCase(); - row.appendChild(skill_div); - } - div.appendChild(row); - } else if (command === "ids") { //warp - for (let [key,value] of ingred.get("ids").get("maxRolls")) { - if (value !== undefined && value != 0) { - let row = displaysq2RolledID(ingred.get("ids"), key, elemental_format); - row.classList.remove("col"); - row.classList.remove("col-12"); - div.appendChild(row); - } - } - } else {//this shouldn't be happening - } - - parent_elem.appendChild(div); - } - } -} - -//TODO: translate the below to BS - -/** Displays Additional Info for - * - * @param {String} elemID - the parent element's id - * @param {Map} item - the statMap of the item - * @returns - */ -function displaysq2AdditionalInfo(elemID, item) { - let parent_elem = document.getElementById(elemID); - - let title = document.createElement("div"); - title.classList.add("big-title", "justify-content-center"); - title.textContent = "Additional Info"; - parent_elem.appendChild(title); - - let droptype_elem = document.createElement("div"); - droptype_elem.classList.add("row"); - droptype_elem.textContent = "Drop type: " + (item.has("drop") ? item.get("drop"): "NEVER"); - parent_elem.appendChild(droptype_elem); - - let warning_elem = document.createElement("div"); - warning_elem.classList.add("row"); - warning_elem.textContent = "This page is incomplete. Will work on it later."; - parent_elem.appendChild(warning_elem); - - return; -} - -/** Displays the ID costs of an item - * - * @param {String} elemID - the id of the parent element. - * @param {Map} item - the statMap of an item. - */ - function displaysq2IDCosts(elemID, item) { - let parent_elem = document.getElementById(elemID); - let tier = item.get("tier"); - if ( (item.has("fixID") && item.get("fixID")) || ["Normal","Crafted","Custom","none", " ",].includes(item.get("tier"))) { - return; - } else { - /** Returns the number of inventory slots minimum an amount of emeralds would take up + the configuration of doing so. - * Returns an array of [invSpace, E, EB, LE, Stx LE] - * - * @param {number} ems - the total numerical value of emeralds to compact. - */ - function emsToInvSpace(ems) { - let stx = Math.floor(ems/262144); - ems -= stx*4096*64; - let LE = Math.floor(ems/4096); - ems -= LE*4096; - let EB = Math.floor(ems/64); - ems -= EB*64; - let e = ems; - return [ stx + Math.ceil(LE/64) + Math.ceil(EB/64) + Math.ceil(e/64) , e, EB, LE, stx]; - } - /** - * - * @param {String} tier - item tier - * @param {Number} lvl - item level - */ - function getIDCost(tier, lvl) { - switch (tier) { - case "Unique": - return Math.round(0.5*lvl + 3); - case "Rare": - return Math.round(1.2*lvl + 8); - case "Legendary": - return Math.round(4.5*lvl + 12); - case "Fabled": - return Math.round(12*lvl + 26); - case "Mythic": - return Math.round(18*lvl + 90); - case "Set": - return Math.round(1.5*lvl + 8) - default: - return -1; - } - } - - parent_elem.style = "display: visible"; - let lvl = item.get("lvl"); - if (typeof(lvl) === "string") { lvl = parseFloat(lvl); } - - let title_elem = document.createElement("div"); - title_elem.classList.add("big-title", "justify-content-center", "Set"); - title_elem.textContent = "Identification Costs"; - parent_elem.appendChild(title_elem); - - let grid_item = document.createElement("div"); - grid_item.classList.add("row", "g-3"); - parent_elem.appendChild(grid_item); - - let IDcost = getIDCost(tier, lvl); - let initIDcost = IDcost; - let invSpace = emsToInvSpace(IDcost); - let rerolls = 0; - - while(invSpace[0] <= 28 && IDcost > 0) { - let container_container = document.createElement("div"); - container_container.classList.add("col-lg-3", "col-sm-12"); - - let container = document.createElement("div"); - container.classList.add("col", "rounded", "border", "border-dark", "border-2"); - - container_container.appendChild(container); - - let container_title = document.createElement("div"); - container_title.classList.add("row", "box-title", "justify-content-center"); - - if (rerolls == 0) { - container_title.textContent = "Initial ID Cost: "; - } else { - container_title.textContent = "Reroll to [" + (rerolls+1) + "] Cost:"; - } - container.appendChild(container_title); - let total_cost_container = document.createElement("div"); - total_cost_container.classList.add("row"); - let total_cost_number = document.createElement("b"); - total_cost_number.classList.add("Set", "fw-bold", "col-6", "text-end"); - total_cost_number.textContent = IDcost + " "; - let total_cost_suffix = document.createElement("div"); - total_cost_suffix.classList.add("col-6", "text-start"); - total_cost_suffix.textContent = "emeralds." - total_cost_container.appendChild(total_cost_number); - total_cost_container.appendChild(total_cost_suffix); - container.appendChild(total_cost_container); - - let OR = document.createElement("div"); - OR.classList.add("row"); - container.appendChild(OR); - let OR_text = document.createElement("div"); - OR_text.classList.add("col", "text-center"); - OR_text.textContent = "OR"; - OR.appendChild(OR_text); - - let esuffixes = ["", "emeralds.", "EB.", "LE.", "stacks of LE."]; - for (let i = 4; i > 0; i--) { - let n_container = document.createElement("div"); - n_container.classList.add("row"); - let n_number = document.createElement("b"); - n_number.classList.add("Set", "fw-bold", "col-6", "text-end"); - n_number.textContent = invSpace[i] + " "; - let n_suffix = document.createElement("div"); - n_suffix.classList.add("col-6", "text-start"); - n_suffix.textContent = esuffixes[i]; - n_container.appendChild(n_number); - n_container.appendChild(n_suffix); - container.appendChild(n_container); - } - grid_item.appendChild(container_container); - - rerolls += 1; - IDcost = Math.round(initIDcost * (5 ** rerolls)); - invSpace = emsToInvSpace(IDcost); - } - } -} - -/** Displays all set bonuses (0/n, 1/n, ... n/n) for a given set - * - * @param {String} parent_id - id of the parent element - * @param {String} setName - the name of the set - */ - function displaysq2AllSetBonuses(parent_id, setName) { - let parent_elem = document.getElementById(parent_id); - parent_elem.style.display = ""; - let set = sets[setName]; - let title_elem = document.createElement("div"); - title_elem.textContent = setName + " Set Bonuses"; - title_elem.classList.add("Set", "big-title", "justify-content-center"); - parent_elem.appendChild(title_elem); - - let grid_elem = document.createElement("div"); - grid_elem.classList.add("row"); - parent_elem.appendChild(grid_elem); - - for (let i = 0; i < set.items.length; i++) { - - let set_elem = document.createElement('div'); - set_elem.classList.add("col-lg-3", "col-sm-12", "py-2", "my-1"); - grid_elem.appendChild(set_elem); - const bonus = set.bonuses[i]; - - let set_elem_display = document.createElement("div"); - set_elem_display.classList.add("rounded", "col", "g-0", "scaled-font", "border", "border-3", "border-dark", "dark-shadow", "dark-7", "p-3"); - set_elem_display.id = "set-"+setName+"-"+i; - set_elem.appendChild(set_elem_display); - - let mock_item = new Map(); - mock_item.set("fixID", true); - mock_item.set("tier", "Set"); - mock_item.set("displayName", setName+" Set: " + (i+1) + "/"+sets[setName].items.length); - // set_elem.textContent = mock_item.get("displayName"); - let mock_minRolls = new Map(); - let mock_maxRolls = new Map(); - mock_item.set("minRolls", mock_minRolls); - mock_item.set("maxRolls", mock_maxRolls); - for (const id in bonus) { - if (rolledIDs.includes(id)) { - mock_minRolls.set(id, bonus[id]); - mock_maxRolls.set(id, bonus[id]); - } - else { - mock_item.set(id, bonus[id]); - } - } - mock_item.set("powders", []); - displaysq2ExpandedItem(mock_item, set_elem_display.id); - } - -} - -/** Displays the individual probabilities of each possible value of each rollable ID for this item. - * - * @param {String} parent_id the document id of the parent element - * @param {String} item expandedItem object - * @param {String} amp the level of corkian amplifier used. 0 means no amp, 1 means Corkian Amplifier I, etc. [0,3] - */ -function displaysq2IDProbabilities(parent_id, item, amp) { - if (item.has("fixID") && item.get("fixID")) {return} - let parent_elem = document.getElementById(parent_id); - parent_elem.style.display = ""; - parent_elem.innerHTML = ""; - let title_elem = document.createElement("div"); - title_elem.textContent = "Identification Probabilities"; - title_elem.classList.add("row", "Legendary", "big-title", "justify-content-center"); - parent_elem.appendChild(title_elem); - - let disclaimer_elem = document.createElement("div"); - disclaimer_elem.classList.add("row", "justify-content-center"); - disclaimer_elem.textContent = "IDs are rolled on a uniform distribution. A chance of 0% means that either the minimum or maximum possible multiplier must be rolled to get this value." - parent_elem.appendChild(disclaimer_elem); - - let amp_row = document.createElement("div"); - amp_row.classList.add("row", "justify-content-center"); - amp_row.id = "amp_row"; - let amp_text = document.createElement("div"); - amp_text.classList.add("col-lg-2", "col-sm-3"); - amp_text.textContent = "Corkian Amplifier Used: " - amp_row.appendChild(amp_text); - - let amp_1 = document.createElement("button"); - amp_1.classList.add("col-lg-1", "col-sm-3", "border-dark", "text-light", "dark-5", "rounded", "scaled-font"); - amp_1.id = "cork_amp_1"; - amp_1.textContent = "I"; - amp_row.appendChild(amp_1); - let amp_2 = document.createElement("button"); - amp_2.classList.add("col-lg-1", "col-sm-3", "border-dark", "text-light", "dark-5", "rounded", "scaled-font"); - amp_2.id = "cork_amp_2"; - amp_2.textContent = "II"; - amp_row.appendChild(amp_2); - let amp_3 = document.createElement("button"); - amp_3.classList.add("col-lg-1", "col-sm-3", "border-dark", "text-light", "dark-5", "rounded", "scaled-font"); - amp_3.id = "cork_amp_3"; - amp_3.textContent = "III"; - amp_row.appendChild(amp_3); - amp_1.addEventListener("click", (event) => {toggleAmps(1)}); - amp_2.addEventListener("click", (event) => {toggleAmps(2)}); - amp_3.addEventListener("click", (event) => {toggleAmps(3)}); - parent_elem.appendChild(amp_row); - - if (amp != 0) {toggleButton("cork_amp_" + amp)} - - item_name = item.get("displayName"); - for (const [id,val] of Object.entries(itemMap.get(item_name))) { - if (rolledIDs.includes(id)) { - let min = item.get("minRolls").get(id); - let max = item.get("maxRolls").get(id); - - if (min != 0 || max != 0) { - //Apply corkian amps - if (val > 0) { - let base = itemMap.get(item_name)[id]; - if (reversedIDs.includes(id)) {max = Math.max( Math.round((0.3 + 0.05*amp) * base), 1)} - else {min = Math.max( Math.round((0.3 + 0.05*amp) * base), 1)} - } - - let row_elem = document.createElement("div"); - row_elem.classList.add("row"); - parent_elem.appendChild(row_elem); - - let base_and_range = document.createElement("div"); - base_and_range.classList.add("col-lg-4", "col-sm-12"); - - - let base_elem = document.createElement("div"); - let base_val = document.createElement("div"); - base_elem.classList.add("row"); - base_prefix = document.createElement("div"); - base_prefix.classList.add("col-auto"); - base_val.classList.add("col-auto"); - - base_prefix.textContent = idPrefixes[id] + "Base "; - base_val.textContent = val + idSuffixes[id]; - if (val > 0 == !reversedIDs.includes(id)) { - base_val.classList.add("positive"); - } else if (val > 0 == reversedIDs.includes(id)) { - base_val.classList.add("negative"); - } - base_elem.appendChild(base_prefix); - base_elem.appendChild(base_val); - - let range_elem = document.createElement("div"); - range_elem.classList.add("row", "justify-content-center"); - - range_elem.textContent = "[ " + min + idSuffixes[id] + ", " + max + idSuffixes[id] + " ]"; - if ( (min > 0 && max > 0 && !reversedIDs.includes(id)) || (min < 0 && max < 0 && reversedIDs.includes(id)) ) { - range_elem.classList.add("positive"); - } else if ( (min < 0 && max < 0 && !reversedIDs.includes(id)) || (min > 0 && max > 0 && reversedIDs.includes(id)) ) { - range_elem.classList.add("negative"); - } - - base_and_range.appendChild(base_elem); - base_and_range.appendChild(range_elem); - row_elem.appendChild(base_and_range); - - - let pdf_and_cdf = document.createElement("div"); - pdf_and_cdf.classList.add("col-lg-4", "col-sm-12"); - - let pdf_elem = document.createElement("div"); - pdf_elem.id = id + "-pdf"; - let cdf_elem = document.createElement("div"); - cdf_elem.id = id + "-cdf"; - pdf_elem.classList.add("row"); - cdf_elem.classList.add("row"); - - pdf_and_cdf.appendChild(pdf_elem); - pdf_and_cdf.appendChild(cdf_elem); - row_elem.appendChild(pdf_and_cdf); - - let input_sec = document.createElement("div"); - input_sec.classList.add("col-lg-4", "col-sm-12"); - - let title_input_slider = document.createElement("input"); - title_input_slider.classList.add("row"); - title_input_slider.type = "range"; - title_input_slider.id = id+"-slider"; - if (!reversedIDs.includes(id)) { - title_input_slider.step = 1; - title_input_slider.min = `${min}`; - title_input_slider.max = `${max}`; - title_input_slider.value = `${max}`; - } else { - title_input_slider.step = 1; - title_input_slider.min = `${-1*min}`; - title_input_slider.max = `${-1*max}`; - title_input_slider.value = `${-1*max}`; - } - let title_input_textbox = document.createElement("input"); - title_input_textbox.classList.add("row"); - title_input_textbox.type = "text"; - title_input_textbox.value = `${max}`; - title_input_textbox.id = id+"-textbox"; - title_input_textbox.classList.add("rounded", "border", "border-dark", "border-2", "dark-5", "text-light"); - input_sec.appendChild(title_input_slider); - input_sec.appendChild(title_input_textbox); - - row_elem.appendChild(input_sec); - - sq2StringPDF(id, max, val, amp); //val is base roll - sq2StringCDF(id, max, val, amp); //val is base roll - title_input_slider.addEventListener("change", (event) => { - let id_name = event.target.id.split("-")[0]; - let textbox_elem = document.getElementById(id_name+"-textbox"); - - if (reversedIDs.includes(id_name)) { - if (event.target.value < -1*min) { event.target.value = -1*min} - if (event.target.value > -1*max) { event.target.value = -1*max} - sq2StringPDF(id_name, -1*event.target.value, val, amp); //val is base roll - sq2StringCDF(id_name, -1*event.target.value, val, amp); //val is base roll - } else { - if (event.target.value < min) { event.target.value = min} - if (event.target.value > max) { event.target.value = max} - sq2StringPDF(id_name, 1*event.target.value, val, amp); //val is base roll - sq2StringCDF(id_name, 1*event.target.value, val, amp); //val is base roll - } - - if (textbox_elem && textbox_elem.value !== event.target.value) { - if (reversedIDs.includes(id_name)) { - textbox_elem.value = -event.target.value; - } else { - textbox_elem.value = event.target.value; - } - } - - - }); - title_input_textbox.addEventListener("change", (event) => { - let id_name = event.target.id.split("-")[0]; - if (reversedIDs.includes(id_name)) { - if (event.target.value > min) { event.target.value = min} - if (event.target.value < max) { event.target.value = max} - } else { - if (event.target.value < min) { event.target.value = min} - if (event.target.value > max) { event.target.value = max} - } - let slider_elem = document.getElementById(id_name+"-slider"); - if (slider_elem.value !== event.target.value) { - slider_elem.value = -event.target.value; - } - - sq2StringPDF(id_name, 1*event.target.value, val, amp); - sq2StringCDF(id_name, 1*event.target.value, val, amp); - }); - } - } - } -} - -//helper functions. id - the string of the id's name, val - the value of the id, base - the base value of the item for this id -function sq2StringPDF(id,val,base,amp) { - /** [0.3b,1.3b] positive normal - * [1.3b,0.3b] positive reversed - * [1.3b,0.7b] negative normal - * [0.7b,1.3b] negative reversed - * - * [0.3, 1.3] minr, maxr [0.3b, 1.3b] min, max - * the minr/maxr decimal roll that corresponds to val -> minround, maxround - */ - let p; let min; let max; let minr; let maxr; let minround; let maxround; - if (base > 0) { - minr = 0.3 + 0.05*amp; maxr = 1.3; - min = Math.max(1, Math.round(minr*base)); max = Math.max(1, Math.round(maxr*base)); - minround = (min == max) ? (minr) : ( Math.max(minr, (val-0.5) / base) ); - maxround = (min == max) ? (maxr) : ( Math.min(maxr, (val+0.5) / base) ); - } else { - minr = 1.3; maxr = 0.7; - min = Math.min(-1, Math.round(minr*base)); max = Math.min(-1, Math.round(maxr*base)); - minround = (min == max) ? (minr) : ( Math.min(minr, (val-0.5) / base) ); - maxround = (min == max) ? (maxr) : ( Math.max(maxr, (val+0.5) / base) ); - } - - p = Math.abs(maxround-minround)/Math.abs(maxr-minr)*100; - p = p.toFixed(3); - - let div1 = document.createElement("div"); - div1.textContent = "Roll exactly "; - div1.classList.add("col-auto", "px-0"); - let div2 = document.createElement("div"); - div2.textContent = val + idSuffixes[id]; - div2.classList.add("col-auto", "px-1"); - if (val > 0 == !reversedIDs.includes(id)) {div2.classList.add("positive")} - if (val > 0 == reversedIDs.includes(id)) {div2.classList.add("negative")} - let div3 = document.createElement("div"); - div3.textContent = ": " + p + "%"; - div3.classList.add("col-auto", "px-0"); - document.getElementById(id + "-pdf").innerHTML = ""; - document.getElementById(id + "-pdf").appendChild(div1); - document.getElementById(id + "-pdf").appendChild(div2); - document.getElementById(id + "-pdf").appendChild(div3); -} -function sq2StringCDF(id,val,base,amp) { - let p; let min; let max; let minr; let maxr; let minround; let maxround; - if (base > 0) { - minr = 0.3 + 0.05*amp; maxr = 1.3; - min = Math.max(1, Math.round(minr*base)); max = Math.max(1, Math.round(maxr*base)); - minround = (min == max) ? (minr) : ( Math.max(minr, (val-0.5) / base) ); - maxround = (min == max) ? (maxr) : ( Math.min(maxr, (val+0.5) / base) ); - } else { - minr = 1.3; maxr = 0.7; - min = Math.min(-1, Math.round(minr*base)); max = Math.min(-1, Math.round(maxr*base)); - minround = (min == max) ? (minr) : ( Math.min(minr, (val-0.5) / base) ); - maxround = (min == max) ? (maxr) : ( Math.max(maxr, (val+0.5) / base) ); - } - - if (reversedIDs.includes(id)) { - p = Math.abs(minr-maxround)/Math.abs(maxr-minr)*100; - } else { - p = Math.abs(maxr-minround)/Math.abs(maxr-minr)*100; - } - p = p.toFixed(3); - - let div1 = document.createElement("div"); - div1.textContent = "Roll "; - div1.classList.add("col-auto", "px-0"); - let div2 = document.createElement("div"); - div2.textContent = val + idSuffixes[id]; - div2.classList.add("col-auto", "px-1"); - if (val > 0 == !reversedIDs.includes(id)) {div2.classList.add("positive")} - if (val > 0 == reversedIDs.includes(id)) {div2.classList.add("negative")} - let div3 = document.createElement("div"); - div3.textContent= " or better: " + p + "%"; - div3.classList.add("col-auto", "px-0"); - document.getElementById(id + "-cdf").innerHTML = ""; - document.getElementById(id + "-cdf").appendChild(div1); - document.getElementById(id + "-cdf").appendChild(div2); - document.getElementById(id + "-cdf").appendChild(div3); -} diff --git a/js/sq2display_constants.js b/js/sq2display_constants.js deleted file mode 100644 index 6f352fd..0000000 --- a/js/sq2display_constants.js +++ /dev/null @@ -1,191 +0,0 @@ -let powder_chars = [ - '\u2724', - '\u2726', - '\u2749', - '\u2739', - '\u274b' -] -let subscript_nums = [ - '\u2081', - '\u2082', - '\u2083', - '\u2084', - '\u2085', - '\u2086', -] - -let skp_names = [ - 'str', - 'dex', - 'int', - 'def', - 'agi' -] - -let elem_chars = [ - 'e', - 't', - 'w', - 'f', - 'a' -] - -let elem_names = [ - 'earth', - 'thunder', - 'water', - 'fire', - 'air' -] - -let elem_colors = [ - "#00AA00", - "#FFFF55", - "#55FFFF", - "#FF5555", - "#FFFFFF" -] - -let item_types = [ - "Helmet", - "Chestplate", - "Leggings", - "Boots", - "Ring", - "Bracelet", - "Necklace", - "Dagger", - "Spear", - "Wand", - "Relik", - "Bow", - "Potion", - "Scroll", - "Food", - "Weapon Tome", - "Armor Tome", - "Guild Tome" -] - -let tome_types = ['weaponTome', 'armorTome', 'guildTome']; -let tome_keys = ['weaponTome1', 'weaponTome2', 'armorTome1', 'armorTome2', 'armorTome3', 'armorTome4', 'guildTome1']; - -/* - * Display commands - */ -let build_all_display_commands = [ - "#defense-stats", - "str", "dex", "int", "def", "agi", - "mr", "ms", - "hprRaw", "hprPct", - "sdRaw", "sdPct", - "mdRaw", "mdPct", - "ref", "thorns", - "ls", - "poison", - "expd", - "spd", - "atkTier", - "!elemental", - "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", - "!elemental", - "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", - "rainbowRaw", - "sprint", "sprintReg", - "jh", - "xpb", "lb", "lq", - "spRegen", - "eSteal", - "gXp", "gSpd", -]; - -let build_offensive_display_commands = [ - "str", "dex", "int", "def", "agi", - "mr", "ms", - "sdRaw", "sdPct", - "mdRaw", "mdPct", - "ref", "thorns", - "ls", - "poison", - "expd", - "spd", - "atkTier", - "rainbowRaw", - "!elemental", - "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", - "!elemental", - "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", -]; - -let build_basic_display_commands = [ - '#defense-stats', - // defense stats [hp, ehp, hpr, ] - // "sPot", // base * atkspd + spell raws - // melee potential - // "mPot", // melee% * (base * atkspd) + melee raws - "mr", "ms", - "ls", - "poison", - "spd", - "atkTier", -] - -let sq2_item_display_commands = [ - "displayName", - "atkSpd", - "!elemental", - "hp", - "nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_", - "!spacer", - "fDef", "wDef", "aDef", "tDef", "eDef", - "!elemental", - "classReq", - "lvl", - "strReq", "dexReq", "intReq", "defReq","agiReq", - "!spacer", - "str", "dex", "int", "def", "agi", - "hpBonus", - "hprRaw", "hprPct", - "sdRaw", "sdPct", - "mdRaw", "mdPct", - "mr", "ms", - "ref", "thorns", - "ls", - "poison", - "expd", - "spd", - "atkTier", - "!elemental", - "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", - "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", - "!elemental", - "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", - "rainbowRaw", - "sprint", "sprintReg", - "jh", - "xpb", "lb", "lq", - "spRegen", - "eSteal", - "gXp", "gSpd", - "majorIds", - "!spacer", - "slots", - "!spacer", - "set", - "lore", - "quest", - "restrict" -]; - -let sq2_ing_display_order = [ - "displayName", //tier will be displayed w/ name - "!spacer", - "ids", - "!spacer", - "posMods", - "itemIDs", - "consumableIDs", - "!spacer", - "lvl", - "skills", -] \ No newline at end of file From 564e4709cd4f7635ce7d88f8ae90dee6f9df54dc Mon Sep 17 00:00:00 2001 From: reschan Date: Wed, 22 Jun 2022 09:34:06 +0700 Subject: [PATCH 36/68] change clickable arrow to a function --- js/sq2display.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/js/sq2display.js b/js/sq2display.js index 7dd1d27..ef8f77e 100644 --- a/js/sq2display.js +++ b/js/sq2display.js @@ -827,12 +827,7 @@ function displaysq2MeleeDamage(parent_elem, overallparent_elem, meleeStats){ parent_elem.append(critStats); - //up and down arrow - done ugly - let arrow = document.createElement("img"); - arrow.id = "arrow_" + overallparent_elem.id; - arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem"; - arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png"; - overallparent_elem.appendChild(arrow); + addClickableArrow(overallparent_elem); } function displaysq2ArmorStats(build) { @@ -1401,12 +1396,7 @@ function displaysq2SpellDamage(parent_elem, overallparent_elem, build, spell, sp } } - //up and down arrow - done ugly - let arrow = document.createElement("img"); - arrow.id = "arrow_" + overallparent_elem.id; - arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem"; - arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png"; - overallparent_elem.appendChild(arrow); + addClickableArrow(overallparent_elem); } function displaysq2EquipOrder(parent_elem, buildOrder){ @@ -2390,3 +2380,12 @@ function sq2StringCDF(id,val,base,amp) { document.getElementById(id + "-cdf").appendChild(div2); document.getElementById(id + "-cdf").appendChild(div3); } + +function addClickableArrow(elem) { + //up and down arrow - done ugly + let arrow = document.createElement("img"); + arrow.id = "arrow_" + elem.id; + arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem"; + arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png"; + elem.appendChild(arrow); +} From 6620d011e5877ca93be44a93fef78a10ab829da0 Mon Sep 17 00:00:00 2001 From: hppeng Date: Tue, 21 Jun 2022 20:41:40 -0700 Subject: [PATCH 37/68] Fix edit id resetting --- builder/index.html | 2 +- js/builder_graph.js | 35 ++++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/builder/index.html b/builder/index.html index bc5c7d3..7b4f294 100644 --- a/builder/index.html +++ b/builder/index.html @@ -431,7 +431,7 @@
-
diff --git a/js/builder_graph.js b/js/builder_graph.js index ae36651..688615a 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -758,19 +758,40 @@ class AggregateStatsNode extends ComputeNode { } } +let edit_id_output; +function resetEditableIDs() { + edit_id_output.notify(); +} /** * Set the editble id fields. * * Signature: EditableIDSetterNode(build: Build) => null */ class EditableIDSetterNode extends ComputeNode { - constructor() { super("builder-id-setter"); } + constructor(notify_nodes) { + super("builder-id-setter"); + this.notify_nodes = notify_nodes.slice(); + } compute_func(input_map) { if (input_map.size !== 1) { throw "EditableIDSetterNode accepts exactly one input (build)"; } const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element for (const id of editable_item_fields) { - document.getElementById(id).value = build.statMap.get(id); + const val = build.statMap.get(id); + document.getElementById(id).value = val; + document.getElementById(id+'-base').textContent = 'Original Value: ' + val; + } + } + + notify() { + this.mark_dirty(); + this.update(); + // NOTE: DO NOT merge these loops for performance reasons!!! + for (const node of this.notify_nodes) { + node.mark_dirty(); + } + for (const node of this.notify_nodes) { + node.update(); } } } @@ -784,7 +805,7 @@ class EditableIDSetterNode extends ComputeNode { class SkillPointSetterNode extends ComputeNode { constructor(notify_nodes) { super("builder-skillpoint-setter"); - this.notify_nodes = notify_nodes; + this.notify_nodes = notify_nodes.slice(); } compute_func(input_map) { @@ -889,10 +910,6 @@ function builder_graph_init() { item_nodes[3].link_to(powder_nodes[3], 'powdering'); item_nodes[8].link_to(powder_nodes[4], 'powdering'); - // Edit IDs setter declared up here to set ids so they will be populated by default. - let edit_id_output = new EditableIDSetterNode(); - edit_id_output.link_to(build_node); - // Phase 2/2: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage let build_disp_node = new BuildDisplayNode() @@ -911,6 +928,10 @@ function builder_graph_init() { stat_agg_node.link_to(node, field); edit_input_nodes.push(node); } + // Edit IDs setter declared up here to set ids so they will be populated by default. + edit_id_output = new EditableIDSetterNode(edit_input_nodes); // Makes shallow copy of list. + edit_id_output.link_to(build_node); + for (const skp of skp_order) { const elem = document.getElementById(skp+'-skp'); const node = new SumNumberInputNode('builder-'+skp+'-input', elem); From 361d4cca542c94b48b5d3c39e557185c09d2e7d9 Mon Sep 17 00:00:00 2001 From: ferricles Date: Tue, 21 Jun 2022 22:09:54 -0700 Subject: [PATCH 38/68] bruh --- atlas/index.html | 2 +- js/atlas.js | 4 ++++ media/audio/bruh_sound_effect.mp3 | Bin 0 -> 23853 bytes 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 media/audio/bruh_sound_effect.mp3 diff --git a/atlas/index.html b/atlas/index.html index 9275ce7..8bbc658 100644 --- a/atlas/index.html +++ b/atlas/index.html @@ -50,7 +50,7 @@
- +
diff --git a/js/atlas.js b/js/atlas.js index 7724c93..f901935 100644 --- a/js/atlas.js +++ b/js/atlas.js @@ -119,6 +119,10 @@ function runAtlas() { let center = [(at1[0]+at2[0])/2, (at1[1]+at2[1])/2 ]; if (Math.sqrt(((at2[1]+atlas2.vy) - (at1[1]+atlas1.vy))**2 + ((at2[0]+atlas2.vx) - (at1[0]+atlas1.vx))**2) < 2*r) { + //Play bruh sound effect + document.getElementById('bruh_sound_effect').play(); + document.getElementById('bruh_sound_effect').currentTime = 0; + if(Math.sqrt( (at2[1]-at1[1])**2 + (at2[0]-at1[0])**2 ) < 2*r ) {//check for collision //Move both away slightly - correct alg this time :) atlas1.style.left = parseFloat(atlas1.style.left.replace("px","")) + (at1[0]-center[0]) * 2 * r / Math.sqrt(dx**2 + dy**2) + "px"; diff --git a/media/audio/bruh_sound_effect.mp3 b/media/audio/bruh_sound_effect.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..2b245a321d004029cdf9ff02f698d4f31151d6d5 GIT binary patch literal 23853 zcmeFZS6h?M7x$Zl5JCtLAV8>wD&2$*QbKP5LQy&<^xj1fh0wdSfT5`LCMsP~loEQA zE-EUDfPkQaiin!c@4rv>!E>-*!1F!Gon)>n*W7b|X3bi&*2K(E2@Lp8V)k}+MyFSH z0Du8ciuO@aS5Q__z+y1}YyE#;=qHQ+pHKhiw`L(ep{FgUKLZd30Nelo1OkCVp+rPP zq)&y#Vl^~0^iO4OZfwGFtPtU->z{pd@#l@whq~x56LZMVtRMeg7-o1NWU0qL4 zH8L_XGc&V%s<&_7Zf$LSIn~k85uHx|uP6WY`M>M`S#AKVUhD7wbKC#@_WzE+|GyD9 z<=^XU#v|8D_`sfpm`HQK3qhVCYr~;x3Wux%BMKzFf!v<(%5ByRAT$GxxZzV$VY<`2 z!}j-Pg`cVkdmmb47UE6uYW9&MGi1`nzt&}%7ee<-ld9o+MOx$`fZ{Z3d;eg)H1MXV zmq{rH$uoii*K-XXe{P@Tq$!Cz{*gf`|j`t=ag68s8W4tO%U=V5Bn-6!p8TC1ZSp?^7p^h59oJaKNQ&K z`TG1v^y-7I6FU9(8?`@gzJdYTHPD8GG`6vHPtlZy!JSZMCSIiX#0Ro9YXHGK)gRli zzioD;nb4;r*!ixPt`HkFcI-?49EHD~m44Ih$+Vij#xb(iVkM1vUC1qka*$7*1KvBSbZz{~$#3r#A@g zN%kTk@Wg@?zgIjS)c_ywXJO0=-sB)}JjWQ1k1d|hJtO+#Ko;OG4vJu6=E)=*?$krX zQ@9sC2t*fFspsG@n6D`W86qMxWvHHslgu~*fdGKetga>8GZx6e`&83Qqp1c9AgSjr zGQOBvLA{~#mG9g2)4hqFCBM2d$1*ac9`L>L1nScj$5i&cq(8NZVIQ)g3MOlw2e({K zi0a@x{*|I3l(^e>w1F7T?)Vj6Jh(^W@rZ0cZ&n*Wb~e!!zBxB1;NS_~;2 zbvfvR>zSr2`XPIJ1ErNBxbMv2{21$RIj!M@7Qu}MkSvoQrXgScv-`9oC$Inj$S|dT z#p!vb+)@#hv(g`bv*G&GEjmFV>P2(8Hgf-?o~ zdy7D%wG;b>hBgY1mB=4Fzigb&4&DA_B^hZYvN|Q$K+md@qd>1_ zIL7roqITV8?~Ad~t+~C@-J@%bN1iry?=1FnZp;UV*0+7XT-tV)U+!B}u{2xZ{}%3rE&9WdMMhN5cqH+F-_zo@p2Xv#OwV>k>xGK|>G+ zNP5o@OH9LnA=qGc>?biyMJ%@JkX1;Lb73KzK}6J+jZZ3{Tt&kGLEn<}938_bRt$Ec zxLJMLK?P%xcSegKs@fk}^>f*%9$W+O0j)`n%)k1sSx#bE#Js zKk^yW6!bb9&|Z?T3pswK@-O6V$Q|gwCwzK3I*Q{_5N14E^Q}n_OsF9%w3?7= z_O?B~-@diJkac7(g`WjsIaH7mg)(e&*R?lAENIOxUTYo=k#Y2Q06QI|H;qSpt`HKv zDJ|=9v7E(NKNgUp@aX^|r-4ffxO-EI9Ek|S0uESaU4tLAxZ=ODeB&6pIxRC_n{HyD z^J%+BV1;5hW4AahCGsVqz0b>nbJsZVq8CX5$?woTB>kl0#?pfzjRfS2Po_tnee{iz zH_?sJY`hFkt-5;-X|~k>fL@rOJfHppRPt$kM?HnL_)3sWCnM-6|) zjSb@m=RQw{eOmHerwFp5?EyF8)nWhfFAPzabh9q+jSF*yjoE`eLp&_6=vtlHe|)kC z4v}GIfWb*kkkCYe9Eb5&j3Ejx$V5e>X?nP}d{cmqC!s1Obq&!7Mj8U`S`#PDeXG$7 zLh)#-oFKCZp#3uK+(kwU(5MKTpdg)wpBLmqgPDdJGQ3f+_ehaueYn3s$`X$$2$jOt z9OV@0dlqBXp7!upUC&7DvqO-07mr;|2ovPR^YCX};%Q4Y`f5 zE}mXNtUx^!$f?NHhA?Na}D zlc+c6P;@3G1qCaZSrS8pczsgEm#wXT3rjz>8I5(>YAeVfYm^5o)i}A-4}u~qqw0}o z*wm;k)%;&Osozymm1j^sLC_>I5tJDF5RMZ~dOt88A)3T+O?U)qY^IoMV$`niiLBtc z)=Wb!UX%h02f_19lRSY7oMb#s1InF~ls#oAFT{vQYex_ayWMHe3TcE1qy;g^qJ1U05Kw9T-Rkh$~b6L+3m zi(b3>q_=xZ;<*5Kc+j;W4GpK+Q2+HSf7=Qf?mVYFKJM&Abr??`B%Dcm=VUmNE|mCy zMku>yHczuOE5*9%w(p*gUP*hVenG}uMoqzy+!ar$Vyy38Y;;Ez=~rHwE9bcXT532N zJpA>tKuPS%jQakD@%=BYeG{)}RIj=9w|YLR@5UeBd#)0DhME0~)el~XWeqc(;_xdl z!D;cl&XLCxwRhuPOSgWtyxV;E+#muZV*zBU}IlVl kNk*qA8yPEP$!9Ujm*~E`4{6cH%>N8qY0Lx8X?-KRw#@q zHFh;9CnH@2{3i=4FAe04h6#L`d<}kA06-j1g z+vtfD9|I@ZOJ0fZ{wT@4(xNG?%xW&OFP_hy#9I5pZ{0Jmu~jd2na7=uCPRE} zP7vhQln4_8Z@nPv>R&`YK;H(O%K4-!;m7#%2TxJ}3TFp=lQ+xG{s1xQezQtz%c+C; zU(ksQC~kkcTLid9k`nivC3|JSL=c9FMVm8`qW((jPUNs|pD;}iGk)KEw9lfX2U*yY z3iH9VU(<7hIl-A2hooL6Ru-gt{j!H*1TA1J^~zvl7+^#xDKRxXw?8(i8cme)AVibC zmXOBK9<6JhVdBJ$Tl`S=DNf)34k6Bpxdz~70FuL@pic^^2Sg0#B_l&(jDic904WM+ zAwq3Y0w_(;Ls0-40c0)UX4}C!LJUoUa13|hPV+7^`fME+u9%PUX3gZQ=H}JC(mx3d zk2+hljtua3T17BT?OXOy=qt6dkk2>mr0-lOD)H*^R-1+qOT^O5hDG%+1(r z)>F<^g!{5i$f5jrR_R%@9PKc2(E==GK{!Q;aZtoI1A$Cq0g_z%(S$)l&IkxINJNiD z=1%~l918+BIr10`_|gCv0uKfc;%3N^1e%e_riut52-FT&$$W__4ocLs0|0PqLYG?) zfjEhzcnd8?FnQV|`I1!_!=&k2PHDBq>|qUx!P29{tKkB~v2uKKn>ka&)(I4(dYG$_ zv>en$URTP~{pPC?if=ahcDF;eeuOXK{nWQfEww*?t(Ky0_rT*~=JBE59=~4L|D5#z z+vWS?E-NqP;Mbqwdkc>VW%1K*@64zvKfU{oPJK#03hhgf2%1azP5wL*Xw8>Vxu>#^ z>*%}^aF(z1y2x(9!>zc~jj2zvc(D&T6aHuW{)u*!-SCF)Rb^@;@Q=C!y{4$~C~4on zBC^?sp529?Yn=;a&no>aI2Y5*bVjtVQg&cKBUhp)rGL9Hi8;q8{61LY_T$nahOhQM z7_fsoNFY`H?R`jU@UaeA7l*0vwS~Alie|D!&o09c1g<6VHN;!N|0P@O^mhRDXYdAuma^f zG$QWGJ$`#b79!n<@| zOSUESp^p9Z&ZAeGAW>tHwRZ90f0_UQSMMMJNBdw1woc3o>k$t$Q}n#B6O5Z70@e_c zr5OUKP9)zoa2CLr3o=r8z8w4?|9otpYA($R^wPq-)6A-M;jWh$um8{g=g9NU@#K>- z4De~AT8_9d7aRL<@CSB*s1_AiF&*$xwK2wxBq!DhKHK%)*C~||q-4Vu*)t{bhVoQi#!jZpVmT77w^WT;FFKdzx`D5}u}uhcoKg1< z<$#>|WP>}hz-~LX#kpb?`#ZAeAws$n`TZhZpuNO8A?usJ{GLksL;e5HGQ=&}VNxn>S|gNC=@%I8t0qtB++x zE(beN&qPn3}ieNbz z;Xc!rO%K*t-Vc0vn76V?L54o%1^}|c1lie?P*hHD4vi#ytpNjf)N}}k;;ieSzLIK5 z4#*tHelh>6*T#8FH@l2o?$?K@I>O zuO-j(^6K>mjq`IaYDPn^R+qNR^LF@NdX2>(0078+A)t%}JedUubVTNwc2h|g41q9s zbFFVQr^^Pp$;Q(txk_>Hw_!7B+W{6HW%PrGXHsHFI&oUFfNvCrd|a3Zo|xoRCufAw z=U&Xi!UiijlBhWlkN&R!5AhYng_e(Nc5G-$K@~_`T<<*!N+1!KQ^_pYTUR+mD!-bS zGnhh5CZmZP^*Zt13?JsmA{hn|!D(qZV8Hgbgdc1qn11W7CFb9KzU&_-yNLR49AyjRU#9{Mk32R|pPV ze-}4+>8HRj`9XmG<;8**HcJm3FFG@%NEK|nRT#Tly(xC5fR_^_JoYn`>5TlP1(&2t zRLyP6$PMAn*+&=l*^_zc>mh7X^F-=!;V^=aC`H{6ZG)&4sV>_R=Q>@p^{4I{9b%6d zgFoufvqJH<^!eJE)mF z=zmQTyWsP!Zod2T@QJotj2u=(y@V>im8Z)3H00M?A6rGG7`spZ@$Z@zo$j=-8ygo! zlJZjrd%C(*g<4qo?+v6EyeCsirE+|b&VT7g5B`3w^R87FGIzD)CDgE-l`#bXfWcxS ze0+6{8Q810B-?T_zaPUazhC|vy?u7$T3>|(6Z7+Z=h#c)vI2Xe*`|A&R3^s4 z11tiMlH+4plKhJWPopYfc*R>^6WeCrlw(p~L#8^E1T8`c14jW-H_lBdc)92QavOSCL?nZ3W^$p_RrM4{L zEEAqOL4)l@WgF*@Yp+@eQhboYMCrF$F{z57^jm!iXX1t5sJsoHW`M`Jl^p3loGS03 z0LVnMh)FTlXVNkYEaRyN7+lOYp;d&D>){hT!Y2v+`yb-Q@$E4Y)H1$Qv4POJrVzs3 zQrU68M|kdm`r-R>19s=p>i3@A%5FJ2Dk@7bq{rg-o9M*PqN%#`4PAa}AOHYnrw}P| z1sMkI0!o*fV$|gU&#(FQ1DVKmMh!Rn;BWt4qpm!&K9F$K^c8Sex3Kdt?RTcF*;^(MVDek09+sQvX;92O$|%|yddK6Ldv>Z- z)Z=iH6@Z~e{QCrK^A+*U0hhY$*73XItN-y|2-*^Nv3(Lp3nSH`4zxex`AOd9d)s>h z4@&BR724R6+^bNiTt+|+I5{%I;a_V7z{DQkh00xXe61s4tei6@4b}Iv zlrf9>Dh38a%w(piThN1Rrguz$dZL9fKOx(q*vlsSWZSCZy?q=tNyt%fc+}gSB zW$`O^w{|BFyVy~#vnn|Fj8LF?V&@l zuy^l?cJ40sl~T|4FX}uEZz2%jYNVXuMuc8Ee}XtVp9=oQnZm=dqWN5sD@nrhu1nE5 zul}|Q{rNz#N(X{ZXVw-<^SW;Nl?b1URc#9Qvf`T`WZb}Pr3|}1O+*SC6rwXHu7-?jd%*AA}!^k0=tvRfH+;>&N=OmVQ)Wr@fGA8c{@*08)p;}!6HG~S^c@bpd*|&p|MrU*DxE=gdu_BWHMjjUQaS$wm*U-LiHF!lKeD1ULr3; zd5E(*#0-cB^MCw5y>S%eWFt@>7v^uwx8}*>O1$8yAM%BDpsd7qAV(X+FUBy%#e|n* z9byD()+0Z2-%*~6Ag+-;`DJWtcnmQG0D?%y<3icAb1j?>RCN@ZJT!pUFdR*uRxwmp zrs*>>NRkaaXb>LI>h0Z9lh=hK365Dy9-P0JEw&xF&`vG#3I?4enX6yhxmW)-RDgbI zbH3Oh`IrA7`^`6O)VY7C>{4DO9tWR~x+;)OYP&oCDE?W(Bi0u^8Z8n(yJv(XnWdvX zSYD{sZZ%LZCtMjR0d)xEPR`z5T(Jn|6c#%dBNSh^epB+rmvi-B==6;XNGhOZ?D0pvYh27#xb`0xEJaPI#=uz2;D*=sOMsqi5Zc^5>C=YR z=Ncu)#LD?WSBliY>tOxYacUa;yf$-eih&xo#uo4KF0&N3oIi_%*jA- z)mZx-RB7WpkUvMHa=JWMVr_~ovpBURC&+Jw0`w3!+#g6z=5N}*QO}QUE7IaqJTA_Y zHA_$A^Sf%u0z-MA?JaS^dNAmIlLy_*@TM?>*%(<0sQe;Js%6o^ZM~Xj;Ac&>&HTc- zF2lOK9pmQL+(qN}hdum%_U=j;vOtvh13t?~oS&_CU@3l<|7Mw6jl0A;#XN>*l*eehuCi;X$dmC~(uf9bk(`n7pf z`?>7a<)@U$uG^|CnCVhJG>4zD+A~Yd&r?2c>UOHLYq^;C-CL3yd~Y}@=R;H2|4xiS zJekQUWXGyO;Tie4FYyJ_?8j6wG;%ae`Pjq@@CLTPKB?A-W4A*J&{KdK|4{8RC|Dwx(OMln`~#KEH2{I|dV<9{;k zlYztY@+Y(~qIc6*Pj!s|%S5;_t*QG6sGWp-g;tR-tXr^@kO@j{RLsim4X@?K3 zs@?a?QA;+0#S=Hr-fQ{%oDJFB!yz7%d6)^)H7j#tC%NGY!fwR0g!x`g_%NqS|D$=% z;VRC0^txT&pCT0dW3wiZ(Ib|813Oo}U4!OjJ23u=D4b(8f+J=g8cg#w8VUQ5XOZ%`#+HX6_hAMwW+7(Os~(?CZ=cCBNR(7=X(@2{TK$=O6LsM_wxyWcZhm^o_H_x zpflOuV@#6ms=A*jj0dBLP(tx4;;=TJ5CtXRauXK_+)iZ9AU~o_=E9{i{Go<8Keh_f zM8f6fd(H0)$)F8Z7F0(VF>X4#x!XK7@1ZZ8X$1i_<%X_*A4JenTn? zA4@Ag=to~Bk`yexbLe>=w8Srs@xy-lMHM_-r!G-R; z-^2lc{k6(>A~%}tI52p8TX%W6tT2xBmb))J70HI4dz&>I#cBaQj~~>t-R)rO4K=8R z7Ym)?x@UZR)74kr@6fZvt@$N>A||tQRIb~{_u}FYuSxy0>8AEq=#I<_03!yB+d3Mk z5r&Mym~tySs;FfDQXIlzaE#c>4K{(PFlI6U15nX5;?fKdzM__5cZNjCO5@WAm_nqW zGGg?lv#U7yn1LfBX7G&s6g&;8XI8HWAc^kY`;Y%OH$K5G5f)DEzpJ;|f-fA8_P>t% z&;DPdZVvZ~L^z{|g9?(8T;ftk&e`gnrDXVN@|$Kk>}vqpctu|Nd}#+2qq^^@4*@JL zyn5#NWN7O_hA5xw4DC5*2m)N9%}4bQ={WEGDELm_m)&dz*Ie0a4_VwkE;(G~KAgWb z`?A4HCenG){N(e$cho4Nhh+!#|+Z}7{F$kQRd@Yc3J(xMIVzWpcv z&XfzdyET6KcZp8_b{E6VcnDJC!ej%*AgzAbsxya(${_4-QLZZ;c1j`0b;y7@hMh}{ z6&H><;A3E^&cd4W*D#uN$i%HPanXuO@;6L}5#nQ5IU0#Fg^lqW!ipOxN=c~@9tg7O znZM5(Ag3^64LucusxgcHN*Dty(S|{IgW*?xJgJhC=-BJonnKJ_*HR)I88DL95@-FK zFpf-!#P@^Di2$1nG7FCoBP6-C)lUR~1AG;i$HXwOF;1OD!pjSwVxB?)#Z4d;79ORu z@EDN+;1IWeIba%BKT2qB?ZJQXg@;yq=D-1ooHhwZvdS2H;tRZjIL-ExMpSt09J+Vw0M=Jvb4rw@z!;2h% zFixT3PXu%gPESnikyxe7P3tAT!!e_Rc>WXj_{Hv$-ZTC$!%HQwZ7FbH znW5A>Dk3SYo4KIib8&RNGuAUtuHwEJx0L<;_h3Z#P?vs8qB4L;*%4tVS%4lHA&4r2 zv?U@!P`)7l=D9ZNfBf&>*rB*uB)?gR@P$Z|1EpQX<>QpkMamB(P`7d`M8YDOGIRVy zK-m=oolfORcaaE$Zb!z2*%J z7^cv?B3!81eKAMsVg!i5s4uAvP7^qdv%qCjI&4@C!>;!Odihy50ZHPx9#NP2#xYcP zAOr?1Ax0^R!K;&*P%t@i|3EEz41IlYTV$Hy&`nmBBPX-J(1s3p=m)d7N$ zvgVuUZ3#Aw51F%NKf)fl`E{-K(f{q0-8`f(8ZB+nM;rP&9M+`W;1N$2&MnqW_9?|j zFQ3ngE4~5^QTS$5W7FIhyIU|F%~$tCy2SX7t91BCVds*Hg23R_4SF*pkisC^P;Zs; z-T1I@4kh`Ynm)Od6tqNjsYc|5lXf7$)u_ zl4S3H4$}+5c!evvUc^qNE7`wMn!SVVm1dAJMy44P(?;YOHw99S`7sld<+Kt>2%FD2x|;7)+lWEJyu=58PFARQu!I|@xu>K!nor)MKAx<% zq|<*L$~E1iuiSfl{AD+O-({5WQTZ=Mic_R3`cb98mz1)9)m?RkyZIK0$W&`ukzle! zck7_m7t^MIfZY>cL4{~}?c#)D^BT%aJ(nS94v6IP94NJ?VFw*2SqfPrAY*Ilr?*tG zOp%lqO1kCi!%CW&uxgQK5zf?pE~f&wyi}E`cmDI@l*U}c-4+o-&B_GZ7CbB2n#X1kB#n)zu5v_@Q=vmzl|JHb zPF9v;W94v70WCIdpxy#a1;S5*x>imix*guDc>B5-xf6~Qz%*VHgq~{*GfGU>M_1#J z#)ixs_HrHEhTK8PV?p@XLy=5rV>z5kj;~{;suRI=FZC<|a0&ENZf#pqhFRaVYcd~# z=%9>AUCwu8nL*Vj`rbLs!~CvFy;K)}WP;9gY-l?vTCF7?Ty=i++&XckglFAAdU5h> z`{1AI%dfldY|m0#wB4jYw`-FsO4{F>qTMI<{K4hrWVfcQ;eF3{l;&-Y$xL@XK;|LRD9;b*L!e_qGTC5K-0Zrz_)Em_s_) zj~rO_p9|9HE5%g`3|RAbGM59G1lCZcAxJ2A4sE*n)iY3yl}!cP*gu4$K{A*VH#Hei z=m}5k>>8!tgQhN*lvcz*fyW~;Uy&3Hv2~7UGKn= zKa1|s7xyjZW66CJvU1V?o+Sw9Jh^wheZoaT)3xWh_sY;MdWdomU+Lg#8dJ#b0D!g^ zUOZW068tjzaULuG(w(r1GE-Hze!zeBznG2LkT_Y?ON$~&Y07Kox{9_XV*c}gVrrzM z(~DyOSpXF9gsFc|2Qn^h$OsneZzQ1ZGTMf5LTJ@L@=Nl{8Yvg9jm^49xU1{*Fq*p? zr}xQ3O0cBJSe6cb{bWT%hX5t{=M5TSBpHw?##7hf~MLR$8MobC$?f&rOSpm`gUPKFV@GiJspaio%90B6wC zTPs-_FQNYdZ?~F0exv96ikscCvzE_ey1;kfF8mt~WVzrJvNI}p8O7z-bL&d<>*eJ) zD@BpMFh2ypNrX5za{|$iG+};ZZmt)Yn>4qEl<)2nkfA&I)6a6ydu7 zgD{uE8`&7BL(X25xf@eHqCnIv!AB(X22+h8nNVOtZFtcGW&naPYsxx|0LK0e9KWxE z5;udH(WWAA$qHA}M+p(ny6w)ZYGQL~{Zff_@B?_mU^38D$(A8#JR`{>QcZuK>ohBv zUcm$pFy@Q0!w{0aX-41tCr7*mgLgQNV>@b3AJX6LwR05`SIEg8_aaIiv|i}O(f_@P z{*V6@wrzR4(d^a9s7uEF?WE09{?7|rN3zVNl+^>%>%!vV!t8P%{F;s(3s`m;Ew!|G zM-7l)eIk9+yV(6&D}YU11bW?x%dc3H;0GUFaBBs zOXxC;jN|H4)Lp#dXypL;^%RNe^B682ub74(>A8hlULz%;AiQ2$o>P43%p#>6KEZ?A zd2H#>4eB7wTN=QJ^eLbciT!#cv=ma25gk%R&HuJL%Z_Y}b- z`H(20v*%aR9l&NDH{W6i2^?Muf?(N8O=lqNIf*y7BAb7s^q_1RX%e=gjJBv#c*>bpJSNMU);GN zDuT@^hG_LL_vURh#Wl8`4n(LGg>Bb#UpmN3M&hr+^V2+G5$hoN+HDGwl0s>Pc`q3t8!hu)>@@gGiP#M2l!A%5SER1S2T~WCgVL36 z3dJQX%)cK;h7o$*9%^i#?tOiIR2odVC*Q>D_)>OBtIDg^pGEbP^-Vq7CCk>t9z}iP zN%qTUdq1?kT>t=zI5;6{Ou7a_Hm#WNhz?EcW$ZOkE&-CBsx$78jpB=a0O5kq4k*g7 z(sU}P{s1&yKkBdL66JzF$yOAJ8P_l3V6ynlD@a{U@y0Ocni)$=qLe+nxI_~5eRQO$ zn4g|x=EQ*!F;2d!XC8?*N>gK13_?xtW)BVo-b)5!s*Ynau2IOy4`=Xp@QiwssXl`X zD+IvgkN}qfKJHc*B2hk$Zl5c1ySFv=mD%kUggL@41Yap(`%M&ShV4Hs$ z7#2xA7{R!egu+l=(C_mOj>@>!|K-1;*!(_P8*{DBfAt*c!TE2yqU)r2 z6b@wv;>$L=N~FX51bGB9CpV}h`pK{FX)4yXx3kLrouSj4UsHbWDTFQ3H*Egarrn?J zQ(U2Bm35w+ELq2=QM?Av7NVGVSeUFCC{6sQ=^a)kD38kfp}`ieqVT7B*P1977aN_q z6pe=2Y3}^)T!$$e8upcp>~uO`ZmBQ*c$uBki}0#-T&(WRpBet%j<@;}I8P6?j0*Ke zaiYF=etW|dDX7HnWuf0%dq~mOSiV&s8<_9mV(=8eS2)898sL#Q@0%gXbEb4Qj8v6L z9ydM;EPMRh`)r0(ncGJ;K}NF4hhd5sCIdN?bciX>S-zpX21c=Chy)o`r8&dFY%Vn> z{JbU;PmYx_X0EayNR!TkgF2Oo#QC!rQ~RM*TXe<8dZUspn~EO2M8K8ID!j+v7>@@?NICa^?uoV}CXl2^N8taAy`o=Bu;7`!g zYP0gvvYkk<$PM8wW^2?jo8N|061l$3TL5kiEZqc>h#8 zzm6;i8eXu@kDD6w5+Ue?;k+Q0VPPM-AV8a{i$pwvmCqcc<6>tForVSa{4$kH>}QL7 z10th54fI;xbfK~)%s|zxIWXfc4iVyE%K=2KH~t_qCi_0j18%eB zD~C;KPGV}-FnDQs8+GYnp8wZ(Te;KmC&ed@_Dz98j+4jVYX`|=V*m&(5$6H6l8m}2 zcjLBj>*QO2mce=#Uyg!v&8LWz+0!Q z{nF)H?v_3~ch!>;8THvf=cEIkZn}K=(2GVQ8uKZoPg3>LdE`J=1gK%43`pKi{CWy8 z|K%;^HNwMtj~W@wOlz-rk3CDzxpQ9V)FNNG7ph?Qvv5mk!ntCzrLB@53;;`AqH$^s zLlImWTRAY!+O}b)QRYlRQPEKeWuZTJ|6RHK^X}x2S0`I>??#V)Ilj5J_xF8{pwaI> zIsuTsxmT;=JlopYq<`THZ0hNdApX%Za_I!#(uL^+k8XNCuD(&~sQL2RnpC7>eGWub zi7+-;N7K)M+ZHK%*bf%k0omZNi=2`L^7sf|QD>?lycX_=!89VrHa)eN48;bBey`j7uBkjx?% zT5`o?n4OQ*{Hgzc)-o7=KU98zpISrx$sHyTCWc7Mf?%8^A{<3z9ov&KfU1lL;R_;I>U#Cs){*+1Lh5KK&_a$GkyPR^8U0eAv2Zz zM0a#6v@MnH-V{7|ccSiXroc}+N3Tr+S{P@u@fGs;fqU*7ZpJ#5t$d1mG`GCDdDWt< z73K>dw|aSTdd!wKtf81@VbCMy>2J6h=AzzG1kzjv0O7%MiJ984>7sd5y4UPqqJj5n zGB~FigN6*J#sEXWDf^gNz5LrUa-QI(YJa_Y5C{e|2JJX;B#Am=(1IYm^6D_aT{buP z3>aj}WQ2-}BigP77KRI!;glT@pzseb^v|cGpv9u(L~`jlu)PdadbY_UF;5P`&qhrW zNKFeUsXD{J&O-6c5|L@UHlIRhh$SeQ|C#)@?}0T)zb_>tAnvDec?ck069I58Q%p+Z zLO%{A&Te0$T6EoST{&d)uu)v=ZAm?I={4V0Z-VQxZu;Q~{myRLZ%Tn82c84TVG13T z3@J~CLoQv`;{*aSfT?PXE6DcFegNoou}Smg>0F%PkbJ6lVfMZ@b5<5icEM?k=WS{z zb>~aZa$Ix^OGtTwTEap5!-F4Hjx`f8M|3uukCn}Dj^oQlGgw8yX{9kv`Rsf;1N}Gn z6~j}Xpel}MJA_fk-I5iq%n$PcX44q_LL(up zN-7IQ{JM(C23U{`N(nfS^aE9xp<#~#A=%`QL&uXA(1XmZjN%!o6n6d$+AEDD4ptz8 z0f;OD+Q}bGeqG+(?z{B+la-IEqq$9bYCKJz(cv1>Gaabig#anUmRYx8Bof%n!KdShkiP%_U-r9m3HSim9B$dx$mZa+X3JJ z(96naz_%hCpQ0){b?+(Z^c0C+7M0j%GD1AvotFJ4*n9ozOij%tUI#jTF5}HT9PZ6k zjpFRk1EqvxA_{~3^S<4T%)Xf7wB<*f!j$r zHCfhHr7%*FPx<4*_Dii-9XCKqBAksx+^^1;*=ZSg1I`>RsY!DX3!F?vZWhR4VZvl$ zjC#sq>Ig7}WL|n6EddV;c+9S(5n5ji^HJa3&Km-`UAeL$>n9HD)=VJ&EmAY<>r@N zXZkz(gUz&wH_VPoP-#q*46vIe8BCmgJEOb*t$hcO26$d3x4#Z6svpajm-O23?Vbo2 z^(m|Ohxz>O4?7xx8$$K5dO6hkAc*vnN*_ixs_8XFpeO;%MAjp7JfcUHjh^SYQ<(&3iBkzMqJ?S*MR%*l62GYV+1Pn!V1if|@iF22OyEi0q1$ueou@rU+g?lp zFZLaD%~)wa5B^;_H>#{}#-(ALpA?HX@>p=c+*PyKL~zZM)vajLov4>QdXqvX0GI&? zBztAkO2x$y<%q4~bxZDR*_;$aT9{}3f~4aw>#}>&f(|}+Ux|6`>VA3FNB?BfoW&CH zTCw(y&Cl`gf3@^lG*XP)frbwB-_Q#)Xz3wn*jklNI@MIH&Oj;Oj?+eycz6zr6}1!X z(7Y_h8^Ok>fsTaes^aq3I%I<83$S&Tbye-V>Qa~|&DZ$D{GIfZCcuDOY?Ys}oHd3^ z{E{w}`U_B^5%faXA$p_($Z>-aQ6nZS_&9h-BO_ljBo!(%)IV4v&Ll1gc2BE3Wm>2p zf294SsVwd>6mFfWqxjue7-rAwdbtLXZcnK?hrEY3;180bBvzwvI1|bjh!>%M9cvb# zRF;%N$#P9L_9CNg_G!tv2I=CC`s#ZMomx0k3Ah8x>9J1#PjV4YA6HDe z{^@!!E`nD3Ct#=9E)(~C?@j*&GyaRuzSLd&F%@bgIhn)=O=&#qvp0~~r13aeI?TTn z!2R&I5bW^lcb{_M&HxAl00F~k%JQ^i9gK&HBBopXLzQ}5kB_a+UDRE5vc*n^)Nk^Y zAVd9#whU)Tz_ea5mEDs@=Nqd8dLA2P_;E&4+Z%5=QTqWFYmHl1=pP39RllLi?2cMpkW|7SSlp^>U%3iNh{iZNzvLi6In=pXxw}T)s#Z3gI(s{zf0 z7UsX>Z%LdE5i<*gv;MdLDeD>ZBlD5+&75HVlyu`|N&E4umpCW@uQ)#tB2}N+z&$db zk|+Y@Tryr$D*#rTd$ZU-3fuO1U07-|nncz_*e%YH6q;*QyZMpfzNzGZPULs zxBhl5sj#u&{AYPf<~UyV_@QOpbDs0id*Uzj_lAdgx`z9KrU=-F9X}2;>grWz`|iIx zh3sV|OI^NqPJYghyEEp>9hz1|oyj{CzIS3Z6aV>Nar_heTTw-icMaF=yO$?d-Z0W% z21VEQHNNS)>vGSF+Kuz&Oq>w_Ir579Kkb}#TNKRKfR~0Pq*+0PMZjg3?h@$+$)#%% zB%~Vz>F(|ZsTGheX^_rEO1dQlK@`{@&zE@rdgmL=T=#Y6oHKK7PHJ6pxuSC@i3?X* zV&()vdKzehTi@>mKDj`hB@IAQDl8H+YzzZ<*F40{44~y(?st7h>#OceszdUOduh2R zk!d0O8KzY%Sh6pWST$5&)BbRX8iN+VUC5Itjmq@4krHJnuY4b4S6scs=-5a#noN}D z))-d4DVrY=EpGbhk4Zwe*^|rr~ab^`HeGL07HOUeWDdcSSg=u63 zOh(vSuN+ZCscHQ5V)F1yF)A2UiA%F!k5{cRk+;G$<(C9zVkKITS(7_gm2C6s<(-PnVYPdpydE``M%A_l8$c0p3ax*X}-^FK>=#Q=YcZOXwCe>YOl4rncT|w?W@pu_YM5|;?3e7g<#@`++1*C(H z=pDOvU{#L{0E3&Ib7qvYNVoe*0hxpzfx9LoR=b*yvy8X$h0yml#oTvtM|n~%#}A0= zsywHH`Db3*Ume5u|9y1QxIs_Bze{Q0Gry9?`k_+TZiYU>b7Oye5$lhPlw2%T6^2Yn zgSZPt0>-&}CZhZLWEjmif6s;vU_@u)R_#0B%<`7&6H>Da+|T1F;|KcAOS?i_Ga7KdwBaVPN9}=$XA8Xy*`B znhDopME@;g_z!Do^p}Vr4^%ic@JS?_;dXlWfBbV3;K#W4^&9j%8xn#lnado(E=2?! zn^QQp`4`Yf=k{+#RfQA2XZ+97E63F_GXXkAE(S|yBA|J~CCso|_L3KH^> zv{ephs;*Jl)j8qaH$#?2IcwsjHUCwrwc}>NISuGP7sgOpo$0IEyt*sC$P@BA_o8yv zQ%%;Y>KA=PxjJhvg`UKOUyoR-N%&PU1ly?_&-?uZQpz~<6aW3lscY? zm6Oj<@bKtLdA(rP_1HwYtwo=q9}A72zt%7F*17-n#NhP=;$ResTKiTp1%C=^V6~KQ zD>Bb;6Ul))N=fR8JsZ$&pDUe;Ou&FY_X0BvjEczsr%jAap8JxZ57EEHBJN3}2c*YC zWVRakF46sGukUQGg7I`w_vVxUNp3u665GTc$uc(>6|Y5L;5(16xY0PhvSHGJYN%`8 z%(j}mRIqPn`L0iPrAAMlkm2)kcO#t40IeOd1Wf<1@TzGev!Vbq_}*C?ei>byVcg`3 zr?XbpMg#<3G6x^!VGEyupcO&ASOQteBHsYEeK zk$3^-M5BsHIVA^4EF)=S8jd^ZYCNUM ze>pyx*e8<9=eSL+0zAb60PykR`CAtUvMppudp5aMj6Hb_tmX&2{Kr2(2Hv(|-h;e;FtZq+HPS{NUs+xBLhhmxrxWrppu3+uA7WFU?GQHd!?wE(7bpDFNQW0_w`WcQZ8x@zZH^2=S7+ z99P;Jt_Ys!NFglj);taq%Jh({jG|#1dmlW_6GT&%Y7n!X zh~u?y#1CfRR1A0$zVjt?$nwxgc#7VhOGn654rVh_{^^NRW$fb&6x8GON$2Oe#tTWc zAA}h{&9s>X%?|C>x1Dag+|qsWMK&Wg!&n(^_di<|>=$fLIF=#%igsuvoTcnfYybca z^SG20hlOtsmqJC>+RI2}?}Pz_QJoorrIw2us{FYWzg~IA@#dy|wkBI+Sx-tz5L%f< z<2qGl&}4GDrM57)LpvWOt7=WVG;hVd{_|wc9Dp%{)e<6*+!wgb{)7-v{^e)z)eCZ& zx^tZ89yPVd7zEYtgaT$lpX`m9YrA3G&Y9sEA z|7&89pt}@>Q@^Wev`sX-oFgN00$c5of3M17n+Af`IuK@uzMJuAS;6rFPa_sb>XDJX z6b?nNXFdOinvK^kO`NJN*kI-H&2r~Zg81D}(Cl{i@g{w`QV}Y0UrVI?YwW49m@Oc? zX1Z#tc1eg^*7QZLh`U;0>~o3r$mE;4`CqB!DHFa!4QFm^-RJ9K=&{TPzuh-yez%tB zw1PVyru5 z{~j^+;5ou6{kIqf>y<|mUGCh;Y*>lvL8DM6YW{fS87dDT8F#HWAX}N$>+$yW?@fBO zjbrM%kM1nMR`CbHj^sR6Mpzj>`ip=ZE=FMslnKEW7>n^8GWN^+$`o#z z!gh`yHA;HhC%IBMis0M&W`aYc=^oo2>MzC4Fe(98(k1e{*4nB2CFsxWEJ5e8o=ynmfr-t=)rh~c$+n@fe4 zsB*2dr6?PYNW)W`DbDT_xhY@%dssU%nmBXV%yjI4XOdBoy+29f({FT@pVmUtBkUll z+|x{Ra42P%95#SM)!cyZ>c)cjW~1AN~ z_C80(T@)tqWBwmOwYhqp#Umrz>H?9jnSv@jEqBDuoZvFI3Z@H5e3u05Nyarm0JqgT zbD&BtKBIA|0|<7Wc)=Oa8~TAi{++K#8$zdF29NRhi}$yQN`t~;6Jk{Fx5;YN`fnDj zkw1Z|D`(#|FHhbh=0hs}pnkuXk0WBnmb-BV02s~GqF$z(2A=+PTG`HO{ntT_`W^q3 z2KVW^lwMEVup+d>FQq$W18L41j>{d)$vz1&y2eIr0V5mhREQl($C8U;ANt2L-^=;e5b;ha4@9%! z_hkOkYJMrO_|{IpgGq&|ni$8HZ=Wra#>cml{L@muNxTfNcd`qxC_?J`<99n$<8N4K zh_^~NU{0Bgq(A&`XI5|gtQA`Wmz%!yZ3}rIy)e(B=dsp}AL?I|2RgrD!N7}tKOYXe z^q=}TroSEcaM`;1=V#s-r)eTao8+!`oPg1H>e%LMi)aGPM=+Z;7x4DW8PbJ+;W47; zJ`7KZd`6$eIZW-JYMCmao76JLwkPxtQj4|xjg!{oyd+l@Mm`>R zEu(6O(d9R8zo)X~m;so8e&+&mD{kJto~S5bGmW0`R&-^%-Pk^NDORcwz1HGnfZI>@ zH4ghUX2rcfL764~qBIapK|l81nh%x3F{CaryGqGmX-iDG~E%t$FlYYV0$~tvp;FQd5^vEaG_Kqb*`P7S^>idm(uh zo2wGdJZVq$hV=m&}G#i`shva1R9$-mwN>#WwHO};NZOBkwz*Am~ z*=No7jD>wxJq^3_X29j2J*@3lx5q)8M*CIavEi)^)Jm5|1 zsZCaYCzFEAs>7{VrTEyKm--+uQtpGT2!0VrMC|uT<+GYMKf-SGnDE zVb|C6!`b@*gZAxfAM+CKPA`dgYHwE;jJqS8*M;J0!*dGdg@Yt{DS1ntB4ojAtHD7ij-uqd z%~&n4M1+S9(yZ#bi=mi`3sm51qy$RuPjHf{EaMvaqh$+75aY*DtWEde%hH(kqHjm? zF!RP}dpl)bcA!>kj&?a;k9U|_6t$6TBRR1+<|S1ERu>zSx;)|H5?z{IALikML5>3< z>nuD4Dr%O&{?<|gcoO99 zp3dt`Ztl=W0;t?Ms9(WuO|vp6EImajvz<42o*wm+TO6hQxS}QEibk zF{d^>AN&IZGl8V4r5Tyvv1L9w3Xs%vcI(WjPFp`h=L?me!eQ;EwP$f!nNbOJUgb_* zBY9dg4a(>TB<;qdaZ2)$a`H$_w{<~7lSxw}xlIdfJ!ZPy$%$n{dwU`x}?rgphN@Rjreoyk-)WnDb&w|=V+8T6Gn z-KXRWw}cHc!&pT1UXEzJ6*C^m z^5Lau>$jMX6dJlrx?5#n>2-K?;`!W@7W2ez^3zS>d~+G*9b3A0b(MbO+UG&>3z(8@vfz8*?HT~ zB;hpdv9;ggao?8T)C?1g-e>2Bmm>}LX=h)}MzHJFkd?Q9IDfmXwWc2)X5E`w{GhMh0_Z z5-@FcxP8h1DM(rt(&n6 Date: Tue, 21 Jun 2022 22:39:10 -0700 Subject: [PATCH 39/68] Small refactor to prepare for atree; fix reset health/lvl --- js/build.js | 4 - js/build2.js | 550 -------------------------------------- js/build_encode_decode.js | 18 +- js/builder.js | 6 +- js/builder_graph.js | 57 ++-- js/craft.js | 3 - js/display.js | 8 +- 7 files changed, 51 insertions(+), 595 deletions(-) delete mode 100644 js/build2.js diff --git a/js/build.js b/js/build.js index 78cf2ac..1760004 100644 --- a/js/build.js +++ b/js/build.js @@ -211,9 +211,5 @@ class Build{ let statMap = this.statMap; let weapon_stats = this.weapon.statMap; statMap.set("damageRaw", [weapon_stats.get("nDam"), weapon_stats.get("eDam"), weapon_stats.get("tDam"), weapon_stats.get("wDam"), weapon_stats.get("fDam"), weapon_stats.get("aDam")]); - statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]); - statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]); - statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]); - statMap.set("defMult", classDefenseMultipliers.get(weapon_stats.get("type"))); } } diff --git a/js/build2.js b/js/build2.js deleted file mode 100644 index b74e403..0000000 --- a/js/build2.js +++ /dev/null @@ -1,550 +0,0 @@ - - -const classDefenseMultipliers = new Map([ ["relik",0.50], ["bow",0.60], ["wand", 0.80], ["dagger", 1.0], ["spear",1.20] ]); - -/** - * @description Error to catch items that don't exist. - * @module ItemNotFound - */ -class ItemNotFound { - /** - * @class - * @param {String} item the item name entered - * @param {String} type the type of item - * @param {Boolean} genElement whether to generate an element from inputs - * @param {String} override override for item type - */ - constructor(item, type, genElement, override) { - /** - * @public - * @type {String} - */ - this.message = `Cannot find ${override||type} named ${item}`; - if (genElement) - /** - * @public - * @type {Element} - */ - this.element = document.getElementById(`${type}-choice`).parentElement.querySelectorAll("p.error")[0]; - else - this.element = document.createElement("div"); - } -} - -/** - * @description Error to catch incorrect input. - * @module IncorrectInput - */ -class IncorrectInput { - /** - * @class - * @param {String} input the inputted text - * @param {String} format the correct format - * @param {String} sibling the id of the error node's sibling - */ - constructor(input, format, sibling) { - /** - * @public - * @type {String} - */ - this.message = `${input} is incorrect. Example: ${format}`; - /** - * @public - * @type {String} - */ - this.id = sibling; - } -} - -/** - * @description Error that inputs an array of items to generate errors of. - * @module ListError - * @extends Error - */ -class ListError extends Error { - /** - * @class - * @param {Array} errors array of errors - */ - constructor(errors) { - let ret = []; - if (typeof errors[0] == "string") { - super(errors[0]); - } else { - super(errors[0].message); - } - for (let i of errors) { - if (typeof i == "string") { - ret.push(new Error(i)); - } else { - ret.push(i); - } - } - /** - * @public - * @type {Object[]} - */ - this.errors = ret; - } -} - -/*Class that represents a wynn player's build. -*/ -class Build{ - - /** - * @description Construct a build. - * @param {Number} level : Level of the player. - * @param {String[]} equipment : List of equipment names that make up the build. - * In order: boots, Chestplate, Leggings, Boots, Ring1, Ring2, Brace, Neck, Weapon. - * @param {Number[]} powders : Powder application. List of lists of integers (powder IDs). - * In order: boots, Chestplate, Leggings, Boots, Weapon. - * @param {Object[]} inputerrors : List of instances of error-like classes. - */ - constructor(level,equipment, powders, externalStats, inputerrors=[]){ - - let errors = inputerrors; - //this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem). - this.craftedItems = []; - this.customItems = []; - // NOTE: powders is just an array of arrays of powder IDs. Not powder objects. - this.powders = powders; - if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") { - const helmet = itemMap.get(equipment[0]); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - } else { - try { - //let boots = getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : (getCustomFromHash(equipment[0])? getCustomFromHash(equipment[0]) : undefined); - let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined); - if (helmet.statMap.get("type") !== "helmet") { - throw new Error("Not a helmet"); - } - this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots")); - helmet.statMap.set("powders",this.powders[0].slice()); - helmet.applyPowders(); - this.helmet = helmet.statMap; - if (this.helmet.get("custom")) { - this.customItems.push(helmet); - } else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(helmet); - } - - } catch (Error) { - //console.log(Error); //fix - const helmet = itemMap.get("No Helmet"); - this.powders[0] = this.powders[0].slice(0,helmet.slots); - this.helmet = expandItem(helmet, this.powders[0]); - errors.push(new ItemNotFound(equipment[0], "helmet", true)); - } - } - if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") { - const chestplate = itemMap.get(equipment[1]); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - } else { - try { - let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined); - if (chestplate.statMap.get("type") !== "chestplate") { - throw new Error("Not a chestplate"); - } - this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots")); - chestplate.statMap.set("powders",this.powders[1].slice()); - chestplate.applyPowders(); - this.chestplate = chestplate.statMap; - if (this.chestplate.get("custom")) { - this.customItems.push(chestplate); - } else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(chestplate); - } - } catch (Error) { - const chestplate = itemMap.get("No Chestplate"); - this.powders[1] = this.powders[1].slice(0,chestplate.slots); - this.chestplate = expandItem(chestplate, this.powders[1]); - errors.push(new ItemNotFound(equipment[1], "chestplate", true)); - } - } - if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") { - const leggings = itemMap.get(equipment[2]); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - } else { - try { - let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined); - if (leggings.statMap.get("type") !== "leggings") { - throw new Error("Not a leggings"); - } - this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots")); - leggings.statMap.set("powders",this.powders[2].slice()); - leggings.applyPowders(); - this.leggings = leggings.statMap; - if (this.leggings.get("custom")) { - this.customItems.push(leggings); - } else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(leggings); - } - } catch (Error) { - const leggings = itemMap.get("No Leggings"); - this.powders[2] = this.powders[2].slice(0,leggings.slots); - this.leggings = expandItem(leggings, this.powders[2]); - errors.push(new ItemNotFound(equipment[2], "leggings", true)); - } - } - if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") { - const boots = itemMap.get(equipment[3]); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - } else { - try { - let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined); - if (boots.statMap.get("type") !== "boots") { - throw new Error("Not a boots"); - } - this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots")); - boots.statMap.set("powders",this.powders[3].slice()); - boots.applyPowders(); - this.boots = boots.statMap; - console.log(boots); - if (this.boots.get("custom")) { - this.customItems.push(boots); - } else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(boots); - } - } catch (Error) { - const boots = itemMap.get("No Boots"); - this.powders[3] = this.powders[3].slice(0,boots.slots); - this.boots = expandItem(boots, this.powders[3]); - errors.push(new ItemNotFound(equipment[3], "boots", true)); - } - } - if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") { - const ring = itemMap.get(equipment[4]); - this.ring1 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring1 = ring.statMap; - if (this.ring1.get("custom")) { - this.customItems.push(ring); - } else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 1"); - this.ring1 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring")); - } - } - if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") { - const ring = itemMap.get(equipment[5]); - this.ring2 = expandItem(ring, []); - }else{ - try { - let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined); - if (ring.statMap.get("type") !== "ring") { - throw new Error("Not a ring"); - } - this.ring2 = ring.statMap; - if (this.ring2.get("custom")) { - this.customItems.push(ring); - } else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(ring); - } - } catch (Error) { - const ring = itemMap.get("No Ring 2"); - this.ring2 = expandItem(ring, []); - errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring")); - } - } - if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") { - const bracelet = itemMap.get(equipment[6]); - this.bracelet = expandItem(bracelet, []); - }else{ - try { - let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined); - if (bracelet.statMap.get("type") !== "bracelet") { - throw new Error("Not a bracelet"); - } - this.bracelet = bracelet.statMap; - if (this.bracelet.get("custom")) { - this.customItems.push(bracelet); - } else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(bracelet); - } - } catch (Error) { - const bracelet = itemMap.get("No Bracelet"); - this.bracelet = expandItem(bracelet, []); - errors.push(new ItemNotFound(equipment[6], "bracelet", true)); - } - } - if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") { - const necklace = itemMap.get(equipment[7]); - this.necklace = expandItem(necklace, []); - }else{ - try { - let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined); - if (necklace.statMap.get("type") !== "necklace") { - throw new Error("Not a necklace"); - } - this.necklace = necklace.statMap; - if (this.necklace.get("custom")) { - this.customItems.push(necklace); - } else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(necklace); - } - } catch (Error) { - const necklace = itemMap.get("No Necklace"); - this.necklace = expandItem(necklace, []); - errors.push(new ItemNotFound(equipment[7], "necklace", true)); - } - } - if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") { - const weapon = itemMap.get(equipment[8]); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - if (equipment[8] !== "No Weapon") { - document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } else { - document.getElementsByClassName("powder-specials")[0].style.display = "none"; - } - }else{ - try { - let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined); - if (weapon.statMap.get("category") !== "weapon") { - throw new Error("Not a weapon"); - } - this.weapon = weapon.statMap; - if (this.weapon.get("custom")) { - this.customItems.push(weapon); - } else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority. - this.craftedItems.push(weapon); - } - this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots")); - this.weapon.set("powders",this.powders[4].slice()); - document.getElementsByClassName("powder-specials")[0].style.display = "grid"; - } catch (Error) { - const weapon = itemMap.get("No Weapon"); - this.powders[4] = this.powders[4].slice(0,weapon.slots); - this.weapon = expandItem(weapon, this.powders[4]); - document.getElementsByClassName("powder-specials")[0].style.display = "none"; - errors.push(new ItemNotFound(equipment[8], "weapon", true)); - } - } - //console.log(this.craftedItems) - - if (level < 1) { //Should these be constants? - this.level = 1; - } else if (level > 106) { - this.level = 106; - } else if (level <= 106 && level >= 1) { - 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."); - } - document.getElementById("level-choice").value = this.level; - - this.availableSkillpoints = levelToSkillPoints(this.level); - this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ]; - this.items = this.equipment.concat([this.weapon]); - // return [equip_order, best_skillpoints, final_skillpoints, best_total]; - let result = calculate_skillpoints(this.equipment, this.weapon); - console.log(result); - this.equip_order = result[0]; - this.base_skillpoints = result[1]; - this.total_skillpoints = result[2]; - this.assigned_skillpoints = result[3]; - this.activeSetCounts = result[4]; - - // For strength boosts like warscream, vanish, etc. - this.damageMultiplier = 1.0; - this.defenseMultiplier = 1.0; - - // For other external boosts ;-; - this.externalStats = externalStats; - - this.initBuildStats(); - - // Remove every error before adding specific ones - for (let i of document.getElementsByClassName("error")) { - i.textContent = ""; - } - this.errors = errors; - if (errors.length > 0) this.errored = true; - } - - /*Returns build in string format - */ - toString(){ - return [this.equipment,this.weapon].flat(); - } - - /* Getters */ - - /* Get total health for build. - */ - - getSpellCost(spellIdx, cost) { - cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2]))); - cost = Math.max(0, Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100))); - return Math.max(1, cost + this.statMap.get("spRaw"+spellIdx)); - } - - - /* Get melee stats for build. - Returns an array in the order: - */ - getMeleeStats(){ - const stats = this.statMap; - if (this.weapon.get("tier") === "Crafted") { - stats.set("damageBases", [this.weapon.get("nDamBaseHigh"),this.weapon.get("eDamBaseHigh"),this.weapon.get("tDamBaseHigh"),this.weapon.get("wDamBaseHigh"),this.weapon.get("fDamBaseHigh"),this.weapon.get("aDamBaseHigh")]); - } - let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier"); - if(adjAtkSpd > 6){ - adjAtkSpd = 6; - }else if(adjAtkSpd < 0){ - adjAtkSpd = 0; - } - - let damage_mult = 1; - if (this.weapon.get("type") === "relik") { - damage_mult = 0.99; // CURSE YOU WYNNCRAFT - //One day we will create WynnWynn and no longer have shaman 99% melee injustice. - //In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams. - } - // 0spellmult for melee damage. - let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct") + this.externalStats.get("mdPct"), 0, this.weapon, this.total_skillpoints, damage_mult * this.damageMultiplier, this.externalStats); - - let dex = this.total_skillpoints[1]; - - let totalDamNorm = results[0]; - let totalDamCrit = results[1]; - totalDamNorm.push(1-skillPointsToPercentage(dex)); - totalDamCrit.push(skillPointsToPercentage(dex)); - let damages_results = results[2]; - - let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2]) - +(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2; - - //Now do math - let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd]; - let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd]; - let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex))); - //[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit] - return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]); - } - - /* - Get all defensive stats for this build. - */ - getDefenseStats(){ - const stats = this.statMap; - let defenseStats = []; - let def_pct = skillPointsToPercentage(this.total_skillpoints[3]); - let agi_pct = skillPointsToPercentage(this.total_skillpoints[4]); - //total hp - let totalHp = stats.get("hp") + stats.get("hpBonus"); - if (totalHp < 5) totalHp = 5; - defenseStats.push(totalHp); - //EHP - let ehp = [totalHp, totalHp]; - let defMult = classDefenseMultipliers.get(this.weapon.get("type")); - ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); - ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); - defenseStats.push(ehp); - //HPR - let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.); - defenseStats.push(totalHpr); - //EHPR - let ehpr = [totalHpr, totalHpr]; - ehpr[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier)); - ehpr[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier)); - defenseStats.push(ehpr); - //skp stats - defenseStats.push([ (1 - ((1-def_pct) * (2 - this.defenseMultiplier)))*100, agi_pct*100]); - //eledefs - TODO POWDERS - let eledefs = [0, 0, 0, 0, 0]; - for(const i in skp_elements){ //kinda jank but ok - eledefs[i] = rawToPct(stats.get(skp_elements[i] + "Def"), stats.get(skp_elements[i] + "DefPct")/100.); - } - defenseStats.push(eledefs); - - //[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]] - return defenseStats; - } - - /* Get all stats for this build. Stores in this.statMap. - @pre The build itself should be valid. No checking of validity of pieces is done here. - */ - initBuildStats(){ - - let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef"]; - - //Create a map of this build's stats - let statMap = new Map(); - - for (const staticID of staticIDs) { - statMap.set(staticID, 0); - } - statMap.set("hp", levelToHPBase(this.level)); - - let major_ids = new Set(); - for (const item of this.items){ - for (let [id, value] of item.get("maxRolls")) { - statMap.set(id,(statMap.get(id) || 0)+value); - } - for (const staticID of staticIDs) { - if (item.get(staticID)) { - statMap.set(staticID, statMap.get(staticID) + item.get(staticID)); - } - } - if (item.get("majorIds")) { - for (const majorID of item.get("majorIds")) { - major_ids.add(majorID); - } - } - } - statMap.set("activeMajorIDs", major_ids); - for (const [setName, count] of this.activeSetCounts) { - const bonus = sets[setName].bonuses[count-1]; - for (const id in bonus) { - if (skp_order.includes(id)) { - // pass. Don't include skillpoints in ids - } - else { - statMap.set(id,(statMap.get(id) || 0)+bonus[id]); - } - } - } - statMap.set("poisonPct", 100); - - // The stuff relevant for damage calculation!!! @ferricles - statMap.set("atkSpd", this.weapon.get("atkSpd")); - - for (const x of skp_elements) { - this.externalStats.set(x + "DamPct", 0); - } - this.externalStats.set("mdPct", 0); - this.externalStats.set("sdPct", 0); - this.externalStats.set("damageBonus", [0, 0, 0, 0, 0]); - this.externalStats.set("defBonus",[0, 0, 0, 0, 0]); - this.externalStats.set("poisonPct", 0); - this.statMap = statMap; - - this.aggregateStats(); - } - - aggregateStats() { - let statMap = this.statMap; - statMap.set("damageRaw", [this.weapon.get("nDam"), this.weapon.get("eDam"), this.weapon.get("tDam"), this.weapon.get("wDam"), this.weapon.get("fDam"), this.weapon.get("aDam")]); - statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]); - statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]); - statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]); - statMap.set("defMult", classDefenseMultipliers.get(this.weapon.get("type"))); - } -} diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 2601830..86aabd5 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -202,15 +202,15 @@ function shareBuild(build) { if (build) { let text = url_base+location.hash+"\n"+ "WynnBuilder build:\n"+ - "> "+build.helmet.statMap.get("displayName")+"\n"+ - "> "+build.chestplate.statMap.get("displayName")+"\n"+ - "> "+build.leggings.statMap.get("displayName")+"\n"+ - "> "+build.boots.statMap.get("displayName")+"\n"+ - "> "+build.ring1.statMap.get("displayName")+"\n"+ - "> "+build.ring2.statMap.get("displayName")+"\n"+ - "> "+build.bracelet.statMap.get("displayName")+"\n"+ - "> "+build.necklace.statMap.get("displayName")+"\n"+ - "> "+build.weapon.statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]"; + "> "+build.items[0].statMap.get("displayName")+"\n"+ + "> "+build.items[1].statMap.get("displayName")+"\n"+ + "> "+build.items[2].statMap.get("displayName")+"\n"+ + "> "+build.items[3].statMap.get("displayName")+"\n"+ + "> "+build.items[4].statMap.get("displayName")+"\n"+ + "> "+build.items[5].statMap.get("displayName")+"\n"+ + "> "+build.items[6].statMap.get("displayName")+"\n"+ + "> "+build.items[7].statMap.get("displayName")+"\n"+ + "> "+build.items[8].statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]"; copyTextToClipboard(text); document.getElementById("share-button").textContent = "Copied!"; } diff --git a/js/builder.js b/js/builder.js index ec4f93f..34f7eab 100644 --- a/js/builder.js +++ b/js/builder.js @@ -96,18 +96,14 @@ function resetFields(){ } } - const nodes_to_reset = item_nodes.concat(powder_nodes.concat(edit_input_nodes)); + const nodes_to_reset = item_nodes.concat(powder_nodes).concat(edit_input_nodes).concat([powder_special_input, boosts_node]); for (const node of nodes_to_reset) { node.mark_dirty(); } - powder_special_input.mark_dirty(); - boosts_node.mark_dirty(); for (const node of nodes_to_reset) { node.update(); } - powder_special_input.update(); - boosts_node.update(); setValue("level-choice", "106"); location.hash = ""; diff --git a/js/builder_graph.js b/js/builder_graph.js index 688615a..dd2a66e 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -206,6 +206,11 @@ class ItemInputDisplayNode extends ComputeNode { this.input_field.classList.add("text-light"); this.image.classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow'); + if (this.health_field) { + // Doesn't exist for weapons. + this.health_field.textContent = "0"; + } + this.level_field.textContent = "0"; if (!item) { this.input_field.classList.add("is-invalid"); return null; @@ -443,7 +448,7 @@ function getDefenseStats(stats) { defenseStats.push(totalHp); //EHP let ehp = [totalHp, totalHp]; - let defMult = (2 - stats.get("classDef")) * (1 - stats.get("defBonus")); + let defMult = (2 - stats.get("classDef")) * (2 - stats.get("defMultiplier")); ehp[0] /= (1-def_pct)*(1-agi_pct)*defMult; ehp[1] /= (1-def_pct)*defMult; defenseStats.push(ehp); @@ -725,33 +730,47 @@ class DisplayBuildWarningsNode extends ComputeNode { } /** - * Aggregate stats from the build and from inputs. + * Aggregate stats from all inputs (merges statmaps). * - * Signature: AggregateStatsNode(build: Build, *args) => StatMap + * Signature: AggregateStatsNode(*args) => StatMap */ class AggregateStatsNode extends ComputeNode { constructor() { super("builder-aggregate-stats"); } + compute_func(input_map) { + const output_stats = new Map(); + for (const [k, v] of input_map.entries()) { + for (const [k2, v2] of v.entries()) { + if (output_stats.has(k2)) { + output_stats.set(k2, v2 + output_stats.get(k2)); + } + else { + output_stats.set(k2, v2); + } + } + } + return output_stats; + } +} + +/** + * Aggregate editable ID stats with build and weapon type. + * + * Signature: AggregateEditableIDNode(build: Build, weapon: Item, *args) => StatMap + */ +class AggregateEditableIDNode extends ComputeNode { + constructor() { super("builder-aggregate-inputs"); } + compute_func(input_map) { const build = input_map.get('build'); input_map.delete('build'); - const powder_boost = input_map.get('powder-boost'); input_map.delete('powder-boost'); - const potion_boost = input_map.get('potion-boost'); input_map.delete('potion-boost'); const weapon = input_map.get('weapon'); input_map.delete('weapon'); const output_stats = new Map(build.statMap); - output_stats.set("damageMultiplier", 1 + potion_boost[0]); - output_stats.set("defBonus", potion_boost[1]); + output_stats.set("damageMultiplier", 1); + output_stats.set("defMultiplier", 1); for (const [k, v] of input_map.entries()) { output_stats.set(k, v); } - for (const [k, v] of powder_boost.entries()) { - if (output_stats.has(k)) { - output_stats.set(k, v + output_stats.get(k)); - } - else { - output_stats.set(k, v); - } - } output_stats.set('classDef', classDefenseMultipliers.get(weapon.statMap.get("type"))); return output_stats; @@ -919,13 +938,14 @@ function builder_graph_init() { // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) let stat_agg_node = new AggregateStatsNode(); - stat_agg_node.link_to(build_node, 'build').link_to(item_nodes[8], 'weapon'); + let edit_agg_node = new AggregateEditableIDNode(); + edit_agg_node.link_to(build_node, 'build').link_to(item_nodes[8], 'weapon'); for (const field of editable_item_fields) { // Create nodes that listens to each editable id input, the node name should match the "id" const elem = document.getElementById(field); const node = new SumNumberInputNode('builder-'+field+'-input', elem); - stat_agg_node.link_to(node, field); + edit_agg_node.link_to(node, field); edit_input_nodes.push(node); } // Edit IDs setter declared up here to set ids so they will be populated by default. @@ -936,11 +956,12 @@ function builder_graph_init() { const elem = document.getElementById(skp+'-skp'); const node = new SumNumberInputNode('builder-'+skp+'-input', elem); - stat_agg_node.link_to(node, skp); + edit_agg_node.link_to(node, skp); build_encode_node.link_to(node, skp); build_warnings_node.link_to(node, skp); edit_input_nodes.push(node); } + stat_agg_node.link_to(edit_agg_node); build_disp_node.link_to(stat_agg_node, 'stats'); for (const input_node of item_nodes.concat(powder_nodes)) { diff --git a/js/craft.js b/js/craft.js index 1d3ae41..db30aef 100644 --- a/js/craft.js +++ b/js/craft.js @@ -170,7 +170,6 @@ class Craft{ statMap.set(e + "Dam", "0-0"); statMap.set(e + "DamLow", "0-0"); } - //statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]); statMap.set("category","weapon"); statMap.set("atkSpd",this.atkSpd); } @@ -381,12 +380,10 @@ class Craft{ statMap.set("reqs",[0,0,0,0,0]); statMap.set("skillpoints", [0,0,0,0,0]); - statMap.set("damageBonus",[0,0,0,0,0]); for (const e in skp_order) { statMap.set(skp_order[e], statMap.get("maxRolls").has(skp_order[e]) ? statMap.get("maxRolls").get(skp_order[e]) : 0); statMap.get("skillpoints")[e] = statMap.get("maxRolls").has(skp_order[e]) ? statMap.get("maxRolls").get(skp_order[e]) : 0; statMap.get("reqs")[e] = statMap.has(skp_order[e]+"Req") && !consumableTypes.includes(statMap.get("type"))? statMap.get(skp_order[e]+"Req") : 0; - statMap.get("damageBonus")[e] = statMap.has(skp_order[e]+"DamPct") ? statMap.get(skp_order[e]+"DamPct") : 0; } for (const id of rolledIDs) { if (statMap.get("minRolls").has(id)) { diff --git a/js/display.js b/js/display.js index 61c57f0..9ecb378 100644 --- a/js/display.js +++ b/js/display.js @@ -182,7 +182,6 @@ function displayExpandedItem(item, parent_id){ stats.set("wDamPct", 0); stats.set("fDamPct", 0); stats.set("aDamPct", 0); - stats.set("damageBonus", [0, 0, 0, 0, 0]); //SUPER JANK @HPP PLS FIX let damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ]; @@ -1293,9 +1292,6 @@ function displayDefenseStats(parent_elem, statMap, insertSummary){ statsTable.appendChild(hpRow); } - let defMult = statMap.get("defMult"); - if (!defMult) {defMult = 1} - //EHP let ehpRow = document.createElement("div"); ehpRow.classList.add("row"); @@ -1405,8 +1401,8 @@ function displayDefenseStats(parent_elem, statMap, insertSummary){ boost.classList.add("col"); boost.classList.add("text-end"); - let defRaw = statMap.get("defRaw")[i]; - let defPct = statMap.get("defBonus")[i]/100; + let defRaw = statMap.get(skp_elements[i]+"Def"); + let defPct = statMap.get(skp_elements[i]+"DefPct")/100; if (defRaw < 0) { defPct >= 0 ? defPct = "- " + defPct: defPct = "+ " + defPct; } else { From e4466d80952bd7d1b037d95bfe1a9dc9f0b1a216 Mon Sep 17 00:00:00 2001 From: hppeng Date: Tue, 21 Jun 2022 22:53:04 -0700 Subject: [PATCH 40/68] Add back morph easter egg --- js/builder_graph.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/js/builder_graph.js b/js/builder_graph.js index dd2a66e..9528be9 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -178,6 +178,24 @@ class ItemInputNode extends InputNode { return item; } } + else if (this.none_item.statMap.get('category') === 'weapon' && item_text.startsWith("Morph-")) { + let replace_items = [ "Morph-Stardust", + "Morph-Steel", + "Morph-Iron", + "Morph-Gold", + "Morph-Topaz", + "Morph-Emerald", + "Morph-Amethyst", + "Morph-Ruby", + item_text.substring(6) + ] + + for (const [i, x] of zip2(equipment_inputs, replace_items)) { setValue(i, x); } + + // NOTE: DO NOT REORDER FOR PERFORMANCE REASONS + for (const node of item_nodes) { node.mark_dirty(); } + for (const node of item_nodes) { node.update(); } + } return null; } } From 9227dd0f56ab1672daa198b54fba8a66c6d0d4b1 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 01:25:19 -0700 Subject: [PATCH 41/68] Working armor special sliders (don't reset properly i think) --- builder/index.html | 20 ++++---- js/builder_graph.js | 105 +++++++++++++++++++++++++--------------- js/computation_graph.js | 2 + js/display_constants.js | 21 +++++--- 4 files changed, 94 insertions(+), 54 deletions(-) diff --git a/builder/index.html b/builder/index.html index 7b4f294..c30e554 100644 --- a/builder/index.html +++ b/builder/index.html @@ -944,27 +944,27 @@
-
-
-
-
-
@@ -1031,7 +1031,7 @@ Rage (Passive)
- +
@@ -1075,7 +1075,7 @@ Kill Streak (Passive)
- +
@@ -1119,7 +1119,7 @@ Concentration (Passive)
- +
@@ -1163,7 +1163,7 @@ Endurance (Passive)
- +
@@ -1207,7 +1207,7 @@ Dodge (Passive)
- +
diff --git a/js/builder_graph.js b/js/builder_graph.js index 9528be9..ed0ac00 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -1,18 +1,42 @@ -let boosts_node; -/* Updates all spell boosts -*/ -function updateBoosts(buttonId) { - let elem = document.getElementById(buttonId); - if (elem.classList.contains("toggleOn")) { - elem.classList.remove("toggleOn"); - } else { - elem.classList.add("toggleOn"); +let armor_powder_node = new (class extends ComputeNode { + constructor() { super('builder-armor-powder-input'); } + + compute_func(input_map) { + let damage_boost = 0; + let def_boost = 0; + let statMap = new Map(); + for (const [e, elem] of zip2(skp_elements, skp_order)) { + let val = parseInt(document.getElementById(elem+"_boost_armor").value); + statMap.set(e+'DamPct', val); + } + return statMap; } - boosts_node.mark_dirty(); - boosts_node.update(); +})().update(); + +/* Updates PASSIVE powder special boosts (armors) +*/ +function update_armor_powder_specials(elem_id) { + //we only update the powder special + external stats if the player has a build + let wynn_elem = elem_id.split("_")[0]; //str, dex, int, def, agi + + //update the label associated w/ the slider + let elem = document.getElementById(elem_id); + let label = document.getElementById(elem_id + "_label"); + + let value = elem.value; + + label.textContent = label.textContent.split(":")[0] + ": " + value + + //update the slider's graphics + let bg_color = elem_colors[skp_order.indexOf(wynn_elem)]; + let pct = Math.round(100 * value / powderSpecialStats[skp_order.indexOf(wynn_elem)].cap); + elem.style.background = `linear-gradient(to right, ${bg_color}, ${bg_color} ${pct}%, #AAAAAA ${pct}%, #AAAAAA 100%)`; + + armor_powder_node.mark_dirty().update(); } -class BoostsInputNode extends ComputeNode { + +let boosts_node = new (class extends ComputeNode { constructor() { super('builder-boost-input'); } compute_func(input_map) { @@ -28,14 +52,40 @@ class BoostsInputNode extends ComputeNode { } return [damage_boost, def_boost]; } +})().update(); + +/* Updates all spell boosts +*/ +function update_boosts(buttonId) { + let elem = document.getElementById(buttonId); + if (elem.classList.contains("toggleOn")) { + elem.classList.remove("toggleOn"); + } else { + elem.classList.add("toggleOn"); + } + boosts_node.mark_dirty().update(); } -let powder_special_input; let specialNames = ["Quake", "Chain Lightning", "Curse", "Courage", "Wind Prison"]; +let powder_special_input = new (class extends ComputeNode { + constructor() { super('builder-powder-special-input'); } + + compute_func(input_map) { + let powder_specials = []; // [ [special, power], [special, power]] + for (const sName of specialNames) { + for (let i = 1;i < 6; i++) { + if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) { + let powder_special = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))]; + powder_specials.push([powder_special, i]); + break; + } + } + } + return powder_specials; + } +})(); function updatePowderSpecials(buttonId) { - //console.log(player_build.statMap); - let name = (buttonId).split("-")[0]; let power = (buttonId).split("-")[1]; // [1, 5] @@ -53,26 +103,7 @@ function updatePowderSpecials(buttonId) { elem.classList.add("toggleOn"); } - powder_special_input.mark_dirty(); - powder_special_input.update(); -} - -class PowderSpecialInputNode extends ComputeNode { - constructor() { super('builder-powder-special-input'); } - - compute_func(input_map) { - let powder_specials = []; // [ [special, power], [special, power]] - for (const sName of specialNames) { - for (let i = 1;i < 6; i++) { - if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) { - let powder_special = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))]; - powder_specials.push([powder_special, i]); - break; - } - } - } - return powder_specials; - } + powder_special_input.mark_dirty().update(); } class PowderSpecialCalcNode extends ComputeNode { @@ -988,17 +1019,15 @@ function builder_graph_init() { level_input.update(); // Powder specials. - powder_special_input = new PowderSpecialInputNode(); let powder_special_calc = new PowderSpecialCalcNode().link_to(powder_special_input, 'powder-specials'); new PowderSpecialDisplayNode().link_to(powder_special_input, 'powder-specials') .link_to(stat_agg_node, 'stats').link_to(item_nodes[8], 'weapon'); stat_agg_node.link_to(powder_special_calc, 'powder-boost'); + stat_agg_node.link_to(armor_powder_node, 'armor-powder'); powder_special_input.update(); // Potion boost. - boosts_node = new BoostsInputNode(); stat_agg_node.link_to(boosts_node, 'potion-boost'); - boosts_node.update(); // Also do something similar for skill points diff --git a/js/computation_graph.js b/js/computation_graph.js index f99a966..160c802 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -37,6 +37,7 @@ class ComputeNode { for (const child of this.children) { child.mark_input_clean(this.name, this.value); } + return this; } /** @@ -69,6 +70,7 @@ class ComputeNode { child.mark_dirty(); } } + return this; } /** diff --git a/js/display_constants.js b/js/display_constants.js index 4901e88..3ad22fb 100644 --- a/js/display_constants.js +++ b/js/display_constants.js @@ -212,20 +212,21 @@ let posModSuffixes = { let build_all_display_commands = [ "#defense-stats", "str", "dex", "int", "def", "agi", + "!spacer", "mr", "ms", "hprRaw", "hprPct", + "ls", "sdRaw", "sdPct", "mdRaw", "mdPct", - "ref", "thorns", - "ls", - "poison", - "expd", - "spd", - "atkTier", "!elemental", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "!elemental", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", + "atkTier", + "poison", + "ref", "thorns", + "expd", + "spd", "rainbowRaw", "sprint", "sprintReg", "jh", @@ -325,3 +326,11 @@ let sq2_ing_display_order = [ "lvl", "skills", ] + +let elem_colors = [ + "#00AA00", + "#FFFF55", + "#55FFFF", + "#FF5555", + "#FFFFFF" +] From 0d702149068c95c4ef3edc46b01279346104cebc Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 23 Jun 2022 16:15:06 +0700 Subject: [PATCH 42/68] implement auto connectors --- js/sq2bs.js | 222 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 154 insertions(+), 68 deletions(-) diff --git a/js/sq2bs.js b/js/sq2bs.js index 41612bd..9a21daf 100644 --- a/js/sq2bs.js +++ b/js/sq2bs.js @@ -546,7 +546,7 @@ function init_autocomplete() { } } -// atree parsing +// placeholder name, follow new schema function construct_AT(elem, tree) { console.log("constructing ability tree UI"); document.getElementById("atree-active").innerHTML = ""; //reset all atree actives - should be done in a more general way later @@ -562,8 +562,8 @@ function construct_AT(elem, tree) { let node = tree[i]; // create rows if not exist - if (document.getElementById("atree-row-" + node.row) == null) { - for (let j = 0; j <= node.row; j++) { + if (document.getElementById("atree-row-" + node.display.row) == null) { + for (let j = 0; j <= node.display.row; j++) { if (document.getElementById("atree-row-" + j) == null) { let row = document.createElement('div'); row.classList.add("row"); @@ -585,77 +585,163 @@ function construct_AT(elem, tree) { }; }; + let connector_list = [] + // create connectors based on parent location + for (let parent of node.parents) { + let parent_node = tree.find(object => { + return object.display_name === parent; + }); + + let connect_elem = document.createElement("div"); + connect_elem.style = "background-size: cover; width: 100%; height: 100%;"; + // connect up + for (let i = node.display.row - 1; i > parent_node.display.row; i--) { + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; + connector.id = "r" + i + "-c" + node.display.col + "-line" + document.getElementById("atree-row-" + i).children[node.display.col].appendChild(connector); + resolve_connector(document.getElementById("atree-row-" + i).children[node.display.col]); + } + // connect left + for (let i = parent_node.display.col + 1; i < node.display.col; i++) { + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; + connector.classList.add("rotate-90"); + connector.id = "r" + parent_node.display.row + "-c" + i + "-line" + document.getElementById("atree-row-" + parent_node.display.row).children[i].appendChild(connector); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i]); + } + + // connect right + for (let i = parent_node.display.col - 1; i > node.display.col; i--) { + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; + connector.classList.add("rotate-90"); + connector.id = "r" + parent_node.display.row + "-c" + i + "-line" + document.getElementById("atree-row-" + parent_node.display.row).children[i].appendChild(connector); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i]); + } + + // connect corners + if (parent_node.display.col > node.display.col && (parent_node.display.row != node.display.row)) { + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; + connector.classList.add("rotate-180"); + connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" + document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); + } + + if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; + connector.classList.add("rotate-270"); + connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" + document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); + } + } + // create node let node_elem = document.createElement('div') - node_elem.style = "background-image: url('" + node.image + "'); background-size: cover; width: 100%; height: 100%;"; - - if (node.connector && node.rotate != 0) { - node_elem.classList.add("rotate-" + node.rotate); - }; + node_elem.style = "background-image: url('../media/atree/node.png'); background-size: cover; width: 100%; height: 100%;"; // add tooltip - if (!node.connector) { - node_elem.addEventListener('mouseover', function(e) { - if (e.target !== this) {return;} - let tooltip = this.children[0]; - tooltip.style.top = this.getBoundingClientRect().bottom + window.scrollY * 1.02 + "px"; - tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + (elem.getBoundingClientRect().width * .2 / 2) + "px"; - tooltip.style.display = "block"; - }); + node_elem.addEventListener('mouseover', function(e) { + if (e.target !== this) {return;} + let tooltip = this.children[0]; + tooltip.style.top = this.getBoundingClientRect().bottom + window.scrollY * 1.02 + "px"; + tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + (elem.getBoundingClientRect().width * .2 / 2) + "px"; + tooltip.style.display = "block"; + }); - node_elem.addEventListener('mouseout', function(e) { - if (e.target !== this) {return;} - let tooltip = this.children[0]; + node_elem.addEventListener('mouseout', function(e) { + if (e.target !== this) {return;} + let tooltip = this.children[0]; + tooltip.style.display = "none"; + }); + + node_elem.classList.add("fake-button"); + + let active_tooltip = document.createElement('div'); + active_tooltip.classList.add("rounded-bottom", "dark-7", "border", "mb-2", "mx-auto"); + //was causing active element boxes to be 0 width + // active_tooltip.style.width = elem.getBoundingClientRect().width * .80 + "px"; + active_tooltip.style.display = "none"; + + // tooltip text formatting + + let active_tooltip_title = document.createElement('b'); + active_tooltip_title.classList.add("scaled-font"); + active_tooltip_title.innerHTML = node.display_name; + + let active_tooltip_text = document.createElement('p'); + active_tooltip_text.classList.add("scaled-font-sm"); + active_tooltip_text.textContent = node.desc; + + active_tooltip.appendChild(active_tooltip_title); + active_tooltip.appendChild(active_tooltip_text); + + node_tooltip = active_tooltip.cloneNode(true); + + active_tooltip.id = "atree-ab-" + node.display_name.replaceAll(" ", ""); + + node_tooltip.style.position = "absolute"; + node_tooltip.style.zIndex = "100"; + + node_elem.appendChild(node_tooltip); + document.getElementById("atree-active").appendChild(active_tooltip); + + node_elem.addEventListener('click', function(e) { + if (e.target !== this) {return;} + let tooltip = document.getElementById("atree-ab-" + node.display_name.replaceAll(" ", "")); + if (tooltip.style.display == "block") { tooltip.style.display = "none"; - }); + this.classList.remove("atree-selected"); + this.style.backgroundImage = 'url("../media/atree/node.png")'; + } + else { + tooltip.style.display = "block"; + this.classList.add("atree-selected"); + this.style.backgroundImage = 'url("../media/atree/node-selected.png")'; + } - node_elem.classList.add("fake-button"); - - let active_tooltip = document.createElement('div'); - active_tooltip.classList.add("rounded-bottom", "dark-7", "border", "mb-2", "mx-auto"); - //was causing active element boxes to be 0 width - // active_tooltip.style.width = elem.getBoundingClientRect().width * .80 + "px"; - active_tooltip.style.display = "none"; - - // tooltip text formatting - - let active_tooltip_title = document.createElement('b'); - active_tooltip_title.classList.add("scaled-font"); - active_tooltip_title.innerHTML = node.title.replaceAll("\n", "
"); - - let active_tooltip_text = document.createElement('p'); - active_tooltip_text.classList.add("scaled-font-sm"); - active_tooltip_text.textContent = node.desc; - - active_tooltip.appendChild(active_tooltip_title); - active_tooltip.appendChild(active_tooltip_text); - - node_tooltip = active_tooltip.cloneNode(true); - - active_tooltip.id = "atree-ab-" + node.title.replaceAll(" ", ""); - - node_tooltip.style.position = "absolute"; - node_tooltip.style.zIndex = "100"; - - node_elem.appendChild(node_tooltip); - document.getElementById("atree-active").appendChild(active_tooltip); - - node_elem.addEventListener('click', function(e) { - if (e.target !== this) {return;} - let tooltip = document.getElementById("atree-ab-" + node.title.replaceAll(" ", "")); - if (tooltip.style.display == "block") { - tooltip.style.display = "none"; - this.classList.remove("atree-selected"); - this.style.backgroundImage = 'url("../media/atree/node.png")'; - } - else { - tooltip.style.display = "block"; - this.classList.add("atree-selected"); - this.style.backgroundImage = 'url("../media/atree/node-selected.png")'; - } - }); - }; - - document.getElementById("atree-row-" + node.row).children[node.col].appendChild(node_elem); + toggle_connectors(connector_list); + }); + document.getElementById("atree-row-" + node.display.row).children[node.display.col].appendChild(node_elem); }; }; + +// resolve connector conflict +function resolve_connector(elem) { + if (elem.children.length < 2) {return false;} + let line = 0; + let angle = 0; + let t = 0 + for (let child of elem.children) { + let type = child.id.split("-")[2] + if (type == "line") { + line += 1; + } else if (type == "angle") { + angle += 1; + } else if (type == "t") { + t += 1; + } + } + + let connect_elem = document.createElement("div"); + + if ((line == angle) || (angle == 1 && t == 1)) { + connect_elem.style = "background-image: url('../media/atree/connect_t.png'); background-size: cover; width: 100%; height: 100%;" + connect_elem.classList.add("rotate-180") + connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-t" + elem.replaceChildren(connect_elem); + } + if (line > 1 && angle == 0) { + elem.replaceChildren(elem.children[0]) + } + if (t == 1 && line == 1) { + connect_elem.style = "background-image: url('../media/atree/connect_c.png'); background-size: cover; width: 100%; height: 100%;" + elem.replaceChildren(connect_elem); + } +} From 3939f83ffc747f63fce5d5592b80288ef16bdf90 Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 23 Jun 2022 16:15:51 +0700 Subject: [PATCH 43/68] partially change to new format --- js/atree_constants.js | 4237 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 3991 insertions(+), 246 deletions(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 31c22eb..41cfb16 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -1,185 +1,1993 @@ const atrees = { "Archer": [ - {"title": "Arrow Bomb", "desc": "Throw a long-ranged arrow that explodes and deal high damage in a large area (self-damage for 30% of DPS) Mana cost: 50 Total damage: 180% (of DPS) - Neutral: 160% - Fire: 20% Range: 26 blocks AoE: 2.5 blocks (circular)", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 1, "col": 4, "rotate": 0}, - {"title": "Bow Proficiency", "desc": "Improve Main Attack damage and range w/ bow Main attack damage: +5% Main attack range: +6 blocks AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 2, "col": 5, "rotate": 90}, - {"title": "Cheaper Arrow Bomb", "desc": "Reduce Mana cost of Arrow Bomb Mana cost: -10 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 3, "col": 4, "rotate": 0}, - {"title": "Heart Shatter", "desc": "If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage Total damage: +100% (of DPS) - Neutral: 100% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 4, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 5, "col": 4, "rotate": 0}, - {"title": "Escape", "desc": "Throw yourself backward to avoid danger (hold shift to cancel) Mana cost: 25 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 3, "rotate": 90}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 6, "col": 5, "rotate": 90}, - {"title": "Double Shots Ability\nBoltslinger Archetype", "desc": "Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies) Blocks: - Power Shots AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 2}, - {"title": "Power Shots Ability\nSharpshooter Archetype", "desc": "Main Attack arrows have increased speed and knockback Blocks: - Double Shots AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 6, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 7, "col": 2, "rotate": 0}, - {"title": "Arrow Storm", "desc": "Shoots 2 streams of 8 arrows, dealing damage to close mobs and pushing them back Mana cost: 40 Total damage: 40% (of DPS per arrow) - Neutral: 30% - Thunder: 10% Range: 16 blocks AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 2}, - {"title": "Cheaper Escape", "desc": "Reduce mana cost of Escape Mana cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 4}, - {"title": "Arrow Shield", "desc": "Create a shield around you that deal damage and knockback mobs when triggered (2 charges) Mana cost: 30 Total damage: 100% (of DPS) - Neutral: 90% - Air: 10% Duration: 60s AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 3, "rotate": 90}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 5, "rotate": 90}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 8, "col": 1, "rotate": 180}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 0, "rotate": 180}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 8, "col": 7, "rotate": 90}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 8, "col": 8, "rotate": 270}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 0, "rotate": 0}, - {"title": "Windy Feet Ability\nBoltslinger Archetype", "desc": "When casting Escape, give speed to yourself and nearby allies Effect: +20% Walk Speed Duration: 120s AoE: 8 blocks (circular) AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 9, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 2, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 6, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 9, "col": 8, "rotate": 0}, - {"title": "Air Mastery Ability\nBoltslinger Archetype", "desc": "Increases base damage from all Air attacks Air Damage: +3-4 Air Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 0}, - {"title": "Thunder Mastery Ability\nBoltslinger Archetype", "desc": "Increases base damage from all Thunder attacks Thunder Damage: +1-8 Thunder Damage: +10% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 10, "col": 3, "rotate": 90}, + { + "display_name": "Arrow Shield", + "desc": "Create a shield around you that deal damage and knockback mobs when triggered. (2 Charges)", + "archetype": "", + "archetype_req": 0, + "parents": ["Power Shots", "Cheaper Escape"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 9, + "col": 6 + }, + "properties": { + "duration": 60 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Arrow Shield", + "cost": 30, + "display_text": "Max Damage", + "base_spell": 4, + "spell_type": "damage", + "scaling": "spell", + "display": "", + "parts": [ + { + "name": "Shield Damage", + "type": "damage", + "multipliers": [90, 0, 0, 0, 0, 10] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Shield Damage": 2 + } + } + ] + } + ] + }, + + { + "display_name": "Escape", + "desc": "Throw yourself backward to avoid danger. (Hold shift while escaping to cancel)", + "archetype": "", + "archetype_req": 0, + "parents": ["Heart Shatter"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 7, + "col": 4 + }, + "properties": { + "aoe": 0, + "range": 0 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Escape", + "cost": 25, + "display_text": "Max Damage", + "base_spell": 2, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "None", + "type": "damage", + "multipliers": [0, 0, 0, 0, 0, 0] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "None": 0 + } + } + ] + } + ] + }, + { + "display_name": "Arrow Bomb", + "desc": "Throw a long-range arrow that explodes and deal high damage in a large area. (Self-damage for 25% of your DPS)", + "archetype": "", + "archetype_req": 0, + "parents": [], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 0, + "col": 4 + }, + "properties": { + "aoe": 4.5, + "range": 26 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Arrow Bomb", + "cost": 50, + "display_text": "Average Damage", + "base_spell": 3, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "Arrow Bomb", + "type": "damage", + "multipliers": [160, 0, 0, 0, 20, 0] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Arrow Bomb": 1 + } + } + ] + } + ] + }, + { + "display_name": "Heart Shatter", + "desc": "If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage.", + "archetype": "", + "archetype_req": 0, + "parents": ["Bow Proficiency I"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 4, + "col": 4 + }, + "properties": {}, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Arrow Bomb", + "cost": 0, + "multipliers": [100, 0, 0, 0, 0, 0] + }, + { - {"image": "../media/atree/connect_c.png", "connector": true, "row": 10, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 10, "col": 5, "rotate": 90}, + } + ] + }, + { + "display_name": "Fire Creep", + "desc": "Arrow Bomb will leak a trail of fire for 6s, Damaging enemies that walk into it every 0.4s.", + "archetype": "", + "archetype_req": 0, + "parents": ["Phantom Ray", "Fire Mastery", "Bryophyte Roots"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 16, + "col": 6 + }, + "properties": { + "aoe": 0.8, + "duration": 6 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Fire Creep", + "cost": 0, + "multipliers": [30, 0, 0, 0, 20, 0] + }, + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Fire Creep": 15 + } + } + ] + }, + { + "display_name": "Bryophyte Roots", + "desc": "When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s.", + "archetype": "Trapper", + "archetype_req": 1, + "parents": ["Fire Creep", "Earth Mastery"], + "dependencies": ["Arrow Storm"], + "blockers": [], + "cost": 2, + "display": { + "row": 16, + "col": 8 + }, + "properties": { + "aoe": 2, + "duration": 5, + "slowness": 0.4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Bryophyte Roots", + "cost": 0, + "multipliers": [40, 20, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "Nimble String", + "desc": "Arrow Storm throw out +8 arrows per stream and shoot twice as fast.", + "archetype": "", + "archetype_req": 0, + "parents": ["Thunder Mastery", "Arrow Rain"], + "dependencies": ["Arrow Storm"], + "blockers": ["Phantom Ray"], + "cost": 2, + "display": { + "row": 15, + "col": 2 + }, + "properties": { + "shootspeed": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Single Arrow", + "cost": 0, + "multipliers": [-15, 0, 0, 0, 0, 0] + }, + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Single Stream", + "cost": 0, + "hits": { + "Single Arrow": 8 + } + } + ] + }, + { + "display_name": "Arrow Storm", + "desc": "Shoot two stream of 8 arrows, dealing significant damage to close mobs and pushing them back.", + "archetype": "", + "archetype_req": 0, + "parents": ["Double Shots", "Cheaper Escape"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 9, + "col": 2 + }, + "properties": { + "aoe": 0, + "range": 16 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Arrow Storm", + "cost": 40, + "display_text": "Max Damage", + "base_spell": 1, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "Single Arrow", + "type": "damage", + "multipliers": [30, 0, 10, 0, 0, 0] + }, + { + "name": "Single Stream", + "type": "total", + "hits": { + "Single Arrow": 8 + } + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Single Stream": 2 + } + } + ] + } + ] + }, + { + "display_name": "Guardian Angels", + "desc": "Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies. (Arrow Shield will no longer push nearby enemies)", + "archetype": "Boltslinger", + "archetype_req": 3, + "parents": ["Triple Shots", "Frenzy"], + "dependencies": ["Arrow Shield"], + "blockers": [], + "cost": 2, + "display": { + "row": 19, + "col": 1 + }, + "properties": { + "range": 4, + "duration": 60, + "shots": 8, + "count": 2 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Guardian Angels", + "cost": 30, + "display_text": "Total Damage Average", + "base_spell": 4, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "Single Arrow", + "type": "damage", + "multipliers": [40, 0, 0, 0, 0, 20] + }, + { + "name": "Single Bow", + "type": "total", + "hits": { + "Single Arrow": 8 + } + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Single Bow": 2 + } + } + ] + } + ] + }, + { + "display_name": "Windy Feet", + "base_abil": "Escape", + "desc": "When casting Escape, give speed to yourself and nearby allies.", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Arrow Storm"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 10, + "col": 1 + }, + "properties": { + "aoe": 8, + "duration": 120 + }, + "type": "stat_bonus", + "bonuses": [ + { + "type": "stat", + "name": "spd", + "value": 20 + } + ] + }, + { + "display_name": "Basaltic Trap", + "desc": "When you hit the ground with Arrow Bomb, leave a Trap that damages enemies. (Max 2 Traps)", + "archetype": "Trapper", + "archetype_req": 2, + "parents": ["Bryophyte Roots"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 19, + "col": 8 + }, + "properties": { + "aoe": 7, + "traps": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Basaltic Trap", + "cost": 0, + "multipliers": [140, 30, 0, 0, 30, 0] + } + ] + }, + { + "display_name": "Windstorm", + "desc": "Arrow Storm shoot +1 stream of arrows, effectively doubling its damage.", + "archetype": "", + "archetype_req": 0, + "parents": ["Guardian Angels"], + "dependencies": [], + "blockers": ["Phantom Ray"], + "cost": 2, + "display": { + "row": 21, + "col": 1 + }, + "properties": {}, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Single Arrow", + "cost": 0, + "multipliers": [-11, 0, -7, 0, 0, 3] + }, + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Single Stream": 1 + } + } + ] + }, + { + "display_name": "Grappling Hook", + "base_abil": "Escape", + "desc": "When casting Escape, throw a hook that pulls you when hitting a block. If you hit an enemy, pull them towards you instead. (Escape will not throw you backward anymore)", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Focus", "More Shields", "Cheaper Arrow Storm"], + "dependencies": [], + "blockers": ["Escape Artist"], + "cost": 2, + "display": { + "row": 21, + "col": 5 + }, + "properties": { + "range": 20 + }, + "effects": [ + ] + }, + { + "display_name": "Implosion", + "desc": "Arrow bomb will pull enemies towards you. If a trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage.", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Grappling Hook", "More Shields"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 23, + "col": 6 + }, + "properties": {}, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Arrow Bomb", + "cost": 0, + "multipliers": [40, 0, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "Twain's Arc", + "desc": "When you have 2+ Focus, holding shift will summon the Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage)", + "archetype": "Sharpshooter", + "archetype_req": 4, + "parents": ["More Focus"], + "dependencies": ["Focus"], + "blockers": [], + "cost": 2, + "display": { + "row": 26, + "col": 4 + }, + "properties": { + "range": 64, + "focusReq": 2 + }, + "effects": [ - {"title": "Fire Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Fire attacks Fire Damage: +3-5 Fire Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 6}, - {"title": "Earth Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Earth attacks Earth Damage: +2-4 Earth Damage: +20% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 0, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 2, "rotate": 0}, - {"title": "Water Mastery Ability\nSharpshooter Archetype", "desc": "Increases base damage from all Water attacks Water Damage: +2-4 Water Damage: +15% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 6, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 11, "col": 8, "rotate": 0}, - {"title": "Arrow Rain", "desc": "When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies Total Damage: 200% (of DPS per arrow) - Neutral: 120% - Air: 80% AP: 2 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 12, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 1, "rotate": 90}, - {"title": "Nimble String", "desc": "Arrow Storm throws out +8 arrows per stream and shoot twice as fast Total Damage: -15% (of DPS per arrow) - Neutral: -15% Blocks: - Phantom Ray AP: 2 Req: Arrow Storm", "image": "../media/atree/node.png", "connector": false, "row": 12, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 6, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 12, "col": 8, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 0, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 2, "rotate": 0}, - {"title": "Phantom Ray", "desc": "Condense Arrow Storm into a single ray that damages enemies 10 times per second Mana cost: -10 Total Damage: 30% (of DPS per hit) - Neutral: 25% - Water: 5% Range: 12 blocks Blocks: - Windstorm - Nimble String - Arrow Hurricane AP: 2 Req: Arrow Storm Min Sharpshooter: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 5, "rotate": 90}, - {"title": "Fire Creep\nSharpshooter Archetype", "desc": "Arrow Bomb will leak a trail of fire for 6s, damaging enemies that walk into it every 0.4s Total Damage: 50% (of DPS) - Neutral: 30% - Fire: 20% Duration: 6s AoE: 2 blocks (linear) AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 13, "col": 7, "rotate": 90}, - {"title": "Bryophyte Roots\nTrapper Archetype", "desc": "When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s Total Damage: 60% (of DPS) - Neutral: 40% - Earth: 20% Effect: 40% Slowness to Enemies Duration: 5s AoE: 2 blocks (circular) AP: 2 Req: Arrow Storm Min Trapper: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 8}, - {"title": "Triple Shot\nBoltslinger Archetype", "desc": "Triple Main Attack arrows, but they deal -20% damage per arrow AP: 1 Req: Double Shots", "image": "../media/atree/node.png", "connector": false, "row": 14, "col": 0}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 14, "col": 1, "rotate": 180}, - {"title": "Frenzy\nBoltslinger Archetype", "desc": "Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 14, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 14, "col": 8, "rotate": 0}, - {"title": "Guardian Angels Ability", "desc": "Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies (Arrow Shield will no longer push nearby enemies) Total Damage: 60% (of DPS per arrow) - Neutral: 40% - Air: 20% Range: 4 Blocks Duration: 60s AP: 2 Req: Arrow Shield Min Boltslinger: 0/3", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 1}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 3, "rotate": 180}, - {"title": "Focus Ability\nSharpshooter Archetype", "desc": "When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once Damage Bonus: +35% (per focus) AP: 2 Min Sharpshooter: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 4}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 5, "rotate": 270}, - {"title": "Basaltic Trap Ability\nTrapper Archetype", "desc": "When you hit the ground with Arrow Bomb, leave a Trap that damages enemies (Max 2 Traps) Total Damage: 200% (of DPS) - Neutral: 140% - Earth: 30% - Fire: 30% AoE: 7 Blocks (Circular) AP: 2 Min Trapper: 0/1", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 8}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 15, "col": 7, "rotate": 180}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 0, "rotate": 180}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 16, "col": 2, "rotate": 90}, - {"title": "Cheaper Arrow Storm", "desc": "Reduces the Mana cost of Arrow Storm. Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 3}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 4, "rotate": 180}, - {"title": "Grappling Hook Ability\nTrapper Archetype", "desc": "When casting Escape, you throw a hook that pulls you when hitting a block. If you hit a mob, pull them towards you instead. (Escape will not throw you backward anymore) Range: 20 blocks Blocks: - Escape Artist AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 5}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 16, "col": 6, "rotate": 180}, - {"title": "More Shields Ability", "desc": "Give +2 charges to Arrow Shield AP: 1 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 16, "col": 8, "rotate": 270}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 0, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 1, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 4, "rotate": 0}, - {"title": "Implosion Ability\nTrapper Archetype", "desc": "Arrow Bomb will pull enemies towards you. If a Trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage Total Damage: +40% (of DPS) - Neutral: +40% AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 17, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 17, "col": 7, "rotate": 0}, - {"title": "Patient Hunter Ability\nTrapper Archetype", "desc": "Your Traps will deal +20% more damage for every second they are active (Max +80%) AP: 2 Req: Basaltic Trap", "image": "../media/atree/node.png", "connector": false, "row": 17, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 0, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 1}, - {"title": "More Focus Ability\nSharpshooter Archetype", "desc": "Add +2 max Focus Damage Bonus: -5% (per focus) AP: 1 Req: Focus", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 18, "col": 7, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 0, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 19, "col": 7, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 0}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 1, "rotate": 180}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 20, "col": 3, "rotate": 90}, - {"title": "Twain's Arc Ability\nSharpshooter Archetype", "desc": "If you have 2+ Focus, holding shift will summon Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage) Total Damage: 200% (of DPS) - Neutral: 200% Range: 64 blocks AP: 2 Min Sharpshooter: 0/4 Req: Focus", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 4}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 20, "col": 5, "rotate": 180}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 6, "rotate": 270}, - {"title": "Bouncing Bomb Ability", "desc": "Arrow Bomb will bounce once when hitting a block or mob AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 20, "col": 8, "rotate": 270}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 0, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 4, "rotate": 0}, - {"title": "Scorched Earth Ability\nSharpshooter Archetype", "desc": "Fire Creep becomes much stronger Total Damage: +15% (of DPS) - Neutral: +10% - Fire: +5% Duration: 2s AoE: +0.4 Blocks (linear) AP: 1 Req: Fire Creep", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 5}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 21, "col": 6, "rotate": 0}, - {"title": "More Traps Ability\nTrapper Archetype", "desc": "Increase the maximum amount of active Traps you can have by +2 AP: 1 Req: Basaltic Trap", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 0, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 6, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 22, "col": 8, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 0}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 1, "rotate": 180}, - {"title": "Homing Shots Ability", "desc": "Your Main Attack arrows will follow nearby enemies and not be affected by gravity AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 3, "rotate": 90}, - {"title": "Shocking Bomb Ability\nSharpshooter Archetype", "desc": "Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder AP: 2 Min Sharpshooter: 0/5 Req: Arrow Bomb", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 4}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 23, "col": 5, "rotate": 180}, - {"title": "Better Arrow Shield Ability", "desc": "Arrow Shield will gain additonal AoE, knockback, and damage Total Damage: +40% (of DPS) - Neutral: +40% AoE: +1 Blocks (Circular) AP: 1 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 23, "col": 7, "rotate": 90}, - {"title": "Mana Trap Ability\nTrapper Archetype", "desc": "Your Traps will give you 4 Mana per second when you stay close to them Mana Cost: +10 Range: 12 Blocks AP: 2 Min Trapper: 0/5", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 0, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 2, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 5, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 24, "col": 8, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 1, "rotate": 90}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 2}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 25, "col": 4, "rotate": 180}, - {"title": "Initiator Ability\nSharpshooter Archetype", "desc": "If you do not damage an enemy for 5s for more, your next successful hit will deal +50% damage and add +1 Focus AP: 2 Req: Focus Min Sharpshooter: 0/5", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 5}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 25, "col": 6, "rotate": 90}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 25, "col": 7, "rotate": 180}, - {"title": "Cheaper Arrow Storm Ability", "desc": "Reduce the Mana cost of Arrow Storm Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 0, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 2, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 26, "col": 4, "rotate": 0}, - {"title": "Call of the Hound Ability\nTrapper Archetype", "desc": "Arrow Shield summons a Hound that will attack and drag aggressive mobs towards your traps Total Damage: 40% (of DPS) - Neutral: 40% AP: 2 Req: Arrow Shield", "image": "../media/atree/node.png", "connector": false, "row": 26, "col": 7}, - {"title": "Arrow Hurricane Ability\nBoltslinger Archetype", "desc": "Arrow Storm will shoot +2 stream of arrows Blocks: - Phantom Ray AP: 2 Min Boltslinger: 0/8", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 0}, - {"image": "../media/atree/connect_t.png", "connector": true, "row": 27, "col": 1, "rotate": 180}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 3, "rotate": 90}, - {"title": "Cheaper Arrow Shield Ability", "desc": "Reduce the Mana cost of Arrow Shield Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 4}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 27, "col": 5, "rotate": 270}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 27, "col": 7, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 4, "rotate": 0}, - {"title": "Decimator Ability\nSharpshooter Archetype", "desc": "Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%) AP: 2 Req: Phantom Ray", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 5}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 28, "col": 6, "rotate": 90}, - {"title": "Cheaper Escape Ability", "desc": "Reduce the Mana cost of Escape Mana Cost: -5 AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 28, "col": 8, "rotate": 270}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 1, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 29, "col": 7, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 29, "col": 8}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 0, "rotate": 180}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 1}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 2, "rotate": 270}, - {"title": "Crepuscular Ray Ability\nSharpshooter Archetype", "desc": "If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus While in that state, you will lose 1 Focus per second Total Damage: 15% (of DPS per arrrow) - Neutral: 10% - Water: 5% AP: 2 Req: Arrow Storm Min Sharpshooter: 0/8", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 4}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 6, "rotate": 180}, - {"title": "Grape Bomb Ability", "desc": "Arrow Bomb will throw 3 additional smaller bombs when exploding Total Damage: 40% (of DPS) - Neutral: 30% - Fire: 10% AoE: 2 Blocks (Circular) AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 30, "col": 8, "rotate": 270}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 2, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 4, "rotate": 0}, - {"title": "Tangled Traps Ability\nTrapper Archetype", "desc": "Your Traps will be connected by a rope that deals damage to enemies every 0.2s Total Damage: 40% (of DPS) - Neutral: 20% - Air: 20% AP: 2", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 31, "col": 7, "rotate": 0}, - {"title": "Stringer Patient Hunter Ability\nTrapper Archetype", "desc": "Add +80% Max Damage to Patient Hunter AP: 1", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 2, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 4, "rotate": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 32, "col": 7, "rotate": 0}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 1, "rotate": 180}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 3, "rotate": 90}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 4}, - {"image": "../media/atree/connect_angle.png", "connector": true, "row": 33, "col": 5, "rotate": 270}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 33, "col": 7, "rotate": 0}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 1}, - {"title": "text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 5}, - {"image": "../media/atree/connect_line.png", "connector": true, "row": 34, "col": 6, "rotate": 90}, - {"title": "Minefield Ability\nTrapper Archetype", "desc": "Allow you to place +6 Traps, but with reduced damage and range Total Damage: -80% (of DPS) - Neutral: -80% AoE: -2 Blocks (Circular) AP: 2 Req: Basaltic Trap Min Trapper: 0/10", "image": "../media/atree/node.png", "connector": false, "row": 34, "col": 7}, + { + "type": "replace_spell", + "name": "Twain's Arc", + "cost": 0, + "display_text": "Twain's Arc", + "base_spell": 5, + "spell_type": "damage", + "scaling": "melee", + "display": "Twain's Arc Damage", + "parts": [ + { + "name": "Twain's Arc Damage", + "type": "damage", + "multipliers": [200, 0, 0, 0, 0, 0] + } + ] + } + ] + }, + { + "display_name": "Fierce Stomp", + "desc": "When using Escape, hold shift to quickly drop down and deal damage.", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Refined Gunpowder", "Traveler"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 27, + "col": 1 + }, + "properties": { + "aoe": 4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Fierce Stomp", + "cost": 0, + "multipliers": [100, 0, 0, 0, 0, 0] + }, + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Fierce Stomp": 1 + } + } + ] + }, + { + "display_name": "Scorched Earth", + "desc": "Fire Creep become much stronger.", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Twain's Arc"], + "dependencies": ["Fire Creep"], + "blockers": [], + "cost": 1, + "display": { + "row": 27, + "col": 5 + }, + "properties": { + "duration": 2, + "aoe": 0.4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Fire Creep", + "cost": 0, + "multipliers": [10, 0, 0, 0, 5, 0] + } + ] + }, + { + "display_name": "Leap", + "desc": "When you double tap jump, leap foward. (2s Cooldown)", + "archetype": "Boltslinger", + "archetype_req": 5, + "parents": ["Refined Gunpowder", "Homing Shots"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 28, + "col": 0 + }, + "properties": { + "cooldown": 2 + }, + "effects": [ + + ] + }, + { + "display_name": "Shocking Bomb", + "desc": "Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder.", + "archetype": "Sharpshooter", + "archetype_req": 5, + "parents": ["Twain's Arc", "Better Arrow Shield", "Homing Shots"], + "dependencies": ["Arrow Bomb"], + "blockers": [], + "cost": 2, + "display": { + "row": 29, + "col": 4 + }, + "properties": { + "gravity": 0 + }, + "effects": [ + { + "type": "convert_spell_conv", + "target_part": "all", + "conversion": "thunder" + } + ] + }, + { + "display_name": "Mana Trap", + "desc": "Your Traps will give you 4 Mana per second when you stay close to them.", + "archetype": "Trapper", + "archetype_req": 5, + "parents": ["More Traps", "Better Arrow Shield"], + "dependencies": ["Fire Creep"], + "blockers": [], + "cost": 2, + "display": { + "row": 29, + "col": 8 + }, + "properties": { + "range": 12, + "manaRegen": 4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Basaltic Trap", + "cost": 10, + "multipliers": [0, 0, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "Escape Artist", + "desc": "When casting Escape, release 100 arrows towards the ground.", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Better Guardian Angels", "Leap"], + "dependencies": [], + "blockers": ["Grappling Hook"], + "cost": 2, + "display": { + "row": 32, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Escape Artist", + "cost": 0, + "multipliers": [30, 0, 10, 0, 0, 0] + } + ] + }, + { + "display_name": "Initiator", + "desc": "If you do not damage an enemy for 5s or more, your next sucessful hit will deal +50% damage and add +1 Focus.", + "archetype": "Sharpshooter", + "archetype_req": 5, + "parents": ["Shocking Bomb", "Better Arrow Shield"], + "dependencies": ["Focus"], + "blockers": [], + "cost": 2, + "display": { + "row": 32, + "col": 5 + }, + "properties": { + "focus": 1, + "timer": 5 + }, + "type": "stat_bonus", + "bonuses": [ + { + "type": "stat", + "name": "damPct", + "value": 50 + } + ] + }, + { + "display_name": "Call of the Hound", + "desc": "Arrow Shield summon a Hound that will attack and drag aggressive enemies towards your traps.", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Initiator", "Cheaper Arrow Storm"], + "dependencies": ["Arrow Shield"], + "blockers": [], + "cost": 2, + "display": { + "row": 33, + "col": 7 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Call of the Hound", + "cost": 0, + "multipliers": [40, 0, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "Arrow Hurricane", + "desc": "Arrow Storm will shoot +2 stream of arrows.", + "archetype": "Boltslinger", + "archetype_req": 8, + "parents": ["Precise Shot", "Escape Artist"], + "dependencies": [], + "blockers": ["Phantom Ray"], + "cost": 2, + "display": { + "row": 34, + "col": 0 + }, + "properties": {}, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Single Stream": 2 + } + } + ] + }, + { + "display_name": "Geyser Stomp", + "desc": "Fierce Stomp will create geysers, dealing more damage and vertical knockback.", + "archetype": "", + "archetype_req": 0, + "parents": ["Shrapnel Bomb"], + "dependencies": ["Fierce Stomp"], + "blockers": [], + "cost": 2, + "display": { + "row": 38, + "col": 1 + }, + "properties": { + "aoe": 1 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Fierce Stomp", + "cost": 0, + "multipliers": [0, 0, 0, 50, 0, 0] + } + ] + }, + { + "display_name": "Crepuscular Ray", + "desc": "If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus. While in that state, you will lose 1 Focus per second.", + "archetype": "Sharpshooter", + "archetype_req": 10, + "parents": ["Cheaper Arrow Shield"], + "dependencies": ["Arrow Storm"], + "blockers": [], + "cost": 2, + "display": { + "row": 38, + "col": 4 + }, + "properties": { + "focusReq": 5, + "focusRegen": -1 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Crepuscular Ray", + "base_spell": 5, + "spell_type": "damage", + "scaling": "spell", + "display": "One Focus", + "cost": 0, + + "parts": [ + { + "name": "Single Arrow", + "type": "damage", + "multipliers": [10, 0, 0, 5, 0, 0] + }, + { + "name": "One Focus", + "type": "total", + "hits": { + "Single Arrow": 20 + } + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "One Focus": 7 + } + } + ] + } + ] + }, + { + "display_name": "Grape Bomb", + "desc": "Arrow bomb will throw 3 additional smaller bombs when exploding.", + "archetype": "", + "archetype_req": 0, + "parents": ["Grappling Hook", "More Shields"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 38, + "col": 7 + }, + "properties": { + "miniBombs": 3, + "aoe": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Grape Bomb", + "cost": 0, + "multipliers": [30, 0, 0, 0, 10, 0] + } + ] + }, + { + "display_name": "Tangled Traps", + "desc": "Your Traps will be connected by a rope that deals damage to enemies every 0.2s.", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Grape Bomb"], + "dependencies": ["Basaltic Trap"], + "blockers": [], + "cost": 2, + "display": { + "row": 39, + "col": 6 + }, + "properties": { + "attackSpeed": 0.2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Tangled Traps", + "cost": 0, + "multipliers": [20, 0, 0, 0, 0, 20] + } + ] + }, + { + "display_name": "Snow Storm", + "desc": "Enemies near you will be slowed down.", + "archetype": "", + "archetype_req": 0, + "parents": ["Geyser Stomp", "More Focus"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 40, + "col": 4 + }, + "properties": { + "range": 2.5, + "slowness": 0.3 + } + }, + { + "display_name": "All-Seeing Panoptes", + "desc": "Your bows from Guardian Angels become all-seeing, increasing their range, damage and letting them shoot up to +5 times each.", + "archetype": "Boltslinger", + "archetype_req": 11, + "parents": ["Snow Storm"], + "dependencies": ["Guardian Angels"], + "blockers": [], + "cost": 2, + "display": { + "row": 41, + "col": 1 + }, + "properties": { + "range": 10, + "shots": 5 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Single Arrow", + "cost": 0, + "multipliers": [0, 0, 0, 0, 20, 0] + }, + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Single Bow", + "cost": 0, + "hits": { + "Single Arrow": 5 + } + } + ] + }, + { + "display_name": "Minefield", + "desc": "Allow you to place +6 Traps, but with reduced damage and range.", + "archetype": "Trapper", + "archetype_req": 10, + "parents": ["Grape Bomb", "Cheaper Arrow Bomb"], + "dependencies": ["Basaltic Trap"], + "blockers": [], + "cost": 2, + "display": { + "row": 41, + "col": 7 + }, + "properties": { + "aoe": -2, + "traps": 6 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Basaltic Trap", + "cost": 0, + "multipliers": [-80, 0, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "Bow Proficiency I", + "desc": "Improve your Main Attack's damage and range when using a bow.", + "archetype": "", + "archetype_req": 0, + "parents": ["Arrow Bomb"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 2, + "col": 4 + }, + "properties": { + "mainAtk_range": 6 + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "mdPct", + "value": 5 + } + ] + } + ] + }, + { + "display_name": "Cheaper Arrow Bomb", + "desc": "Reduce the Mana cost of Arrow Bomb.", + "archetype": "", + "archetype_req": 0, + "parents": ["Bow Proficiency I"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 2, + "col": 6 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "cost": -10 + } + ] + }, + { + "display_name": "Cheaper Arrow Storm", + "desc": "Reduce the Mana cost of Arrow Storm.", + "archetype": "", + "archetype_req": 0, + "parents": ["Grappling Hook", "Windstorm", "Focus"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 21, + "col": 3 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "cost": -5 + } + ] + }, + { + "display_name": "Cheaper Escape", + "desc": "Reduce the Mana cost of Escape.", + "archetype": "", + "archetype_req": 0, + "parents": ["Arrow Storm", "Arrow Shield"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 9, + "col": 4 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "cost": -5 + } + ] + }, + { + "display_name": "Earth Mastery", + "desc": "Increases your base damage from all Earth attacks", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Arrow Shield"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 13, + "col": 8 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "eDamPct", + "value": 20 + }, + { + "type": "stat", + "name": "eDam", + "value": [2, 4] + } + ] + } + ] + }, + { + "display_name": "Thunder Mastery", + "desc": "Increases your base damage from all Thunder attacks", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Arrow Storm", "Fire Mastery"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 13, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "tDamPct", + "value": 10 + }, + { + "type": "stat", + "name": "tDam", + "value": [1, 8] + } + ] + } + ] + }, + { + "display_name": "Water Mastery", + "desc": "Increases your base damage from all Water attacks", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Cheaper Escape", "Thunder Mastery", "Fire Mastery"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 14, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "wDamPct", + "value": 15 + }, + { + "type": "stat", + "name": "wDam", + "value": [2, 4] + } + ] + } + ] + }, + { + "display_name": "Air Mastery", + "desc": "Increases base damage from all Air attacks", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["Arrow Storm"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 13, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "aDamPct", + "value": 15 + }, + { + "type": "stat", + "name": "aDam", + "value": [3, 4] + } + ] + } + ] + }, + { + "display_name": "Fire Mastery", + "desc": "Increases base damage from all Earth attacks", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Thunder Mastery", "Arrow Shield"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 13, + "col": 6 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "fDamPct", + "value": 15 + }, + { + "type": "stat", + "name": "fDam", + "value": [3, 5] + } + ] + } + ] + }, + { + "display_name": "More Shields", + "desc": "Give +2 charges to Arrow Shield.", + "archetype": "", + "archetype_req": 0, + "parents": ["Grappling Hook", "Basaltic Trap"], + "dependencies": ["Arrow Shield"], + "blockers": [], + "cost": 1, + "display": { + "row": 21, + "col": 7 + }, + "properties": { + "shieldCharges": 2 + } + }, + { + "display_name": "Stormy Feet", + "desc": "Windy Feet will last longer and add more speed.", + "archetype": "", + "archetype_req": 0, + "parents": ["Windstorm"], + "dependencies": ["Windy Feet"], + "blockers": [], + "cost": 1, + "display": { + "row": 23, + "col": 1 + }, + "properties": { + "duration": 60 + }, + "effects": [ + { + "type": "stat_bonus", + "bonuses": [ + { + "type": "stat", + "name": "spdPct", + "value": 20 + } + ] + } + ] + }, + { + "display_name": "Refined Gunpowder", + "desc": "Increase the damage of Arrow Bomb.", + "archetype": "", + "archetype_req": 0, + "parents": ["Windstorm"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 26, + "col": 0 + }, + "properties": {}, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Arrow Bomb", + "cost": 0, + "multipliers": [50, 0, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "More Traps", + "desc": "Increase the maximum amount of active Traps you can have by +2.", + "archetype": "Trapper", + "archetype_req": 10, + "parents": ["Bouncing Bomb"], + "dependencies": ["Basaltic Trap"], + "blockers": [], + "cost": 1, + "display": { + "row": 27, + "col": 8 + }, + "properties": { + "traps": 2 + } + }, + { + "display_name": "Better Arrow Shield", + "desc": "Arrow Shield will gain additional area of effect, knockback and damage.", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Mana Trap", "Shocking Bomb", "Twain's Arc"], + "dependencies": ["Arrow Shield"], + "blockers": [], + "cost": 1, + "display": { + "row": 29, + "col": 6 + }, + "properties": { + "aoe": 1 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Arrow Shield", + "multipliers": [40, 0, 0, 0, 0, 0] + } + ] + }, + { + "display_name": "Better Leap", + "desc": "Reduce leap's cooldown by 1s.", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Leap", "Homing Shots"], + "dependencies": ["Leap"], + "blockers": [], + "cost": 1, + "display": { + "row": 30, + "col": 1 + }, + "properties": { + "cooldown": -1 + } + }, + { + "display_name": "Better Guardian Angels", + "desc": "Your Guardian Angels can shoot +4 arrows before disappearing.", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Escape Artist", "Homing Shots"], + "dependencies": ["Guardian Angels"], + "blockers": [], + "cost": 1, + "display": { + "row": 32, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Single Bow", + "cost": 0, + "hits": { + "Single Arrow": 4 + } + } + ] + }, + { + "display_name": "Cheaper Arrow Storm (2)", + "desc": "Reduce the Mana cost of Arrow Storm.", + "archetype": "", + "archetype_req": 0, + "parents": ["Initiator", "Mana Trap"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 32, + "col": 8 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "cost": -5 + } + ] + }, + { + "display_name": "Precise Shot", + "desc": "+30% Critical Hit Damage", + "archetype": "", + "archetype_req": 0, + "parents": ["Arrow Bomb"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 34, + "col": 2 + }, + "properties": { + "mainAtk_range": 6 + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "mdCritPct", + "value": 30 + } + ] + } + ] + }, + { + "display_name": "Cheaper Arrow Shield (2)", + "desc": "Reduce the Mana cost of Arrow Shield.", + "archetype": "", + "archetype_req": 0, + "parents": ["Precise Shot", "Initiator"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 34, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "cost": -5 + } + ] + }, + { + "display_name": "Rocket Jump", + "desc": "Arrow Bomb's self-damage will knockback you farther away.", + "archetype": "", + "archetype_req": 0, + "parents": ["Cheaper Arrow Storm", "Initiator"], + "dependencies": ["Arrow Bomb"], + "blockers": [], + "cost": 1, + "display": { + "row": 34, + "col": 6 + }, + "properties": { + } + }, + { + "display_name": "Cheaper Escape (2)", + "desc": "Reduce the Mana cost of Escape.", + "archetype": "", + "archetype_req": 0, + "parents": ["Arrow Storm", "Arrow Shield"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 35, + "col": 7 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "cost": -5 + } + ] + }, + { + "display_name": "Stronger Hook", + "desc": "Increase your Grappling Hook's range, speed and strength.", + "archetype": "Trapper", + "archetype_req": 5, + "parents": ["Cheaper Escape"], + "dependencies": ["Grappling Hook"], + "blockers": [], + "cost": 1, + "display": { + "row": 36, + "col": 8 + }, + "properties": { + "range": 8 + } + }, + { + "display_name": "Cheaper Arrow Bomb (2)", + "desc": "Reduce the Mana cost of Arrow Bomb.", + "archetype": "", + "archetype_req": 0, + "parents": ["More Focus (2)", "Minefield"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 41, + "col": 5 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "cost": -5 + } + ] + }, + { + "display_name": "Bouncing Bomb", + "desc": "Arrow Bomb will bounce once when hitting a block or enemy", + "archetype": "", + "archetype_req": 0, + "parents": ["More Shields"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 20, + "col": 7 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Arrow Bomb": 2 + } + } + ] + }, + { + "display_name": "Homing Shots", + "desc": "Your Main Attack arrows will follow nearby enemies and not be affected by gravity", + "archetype": "", + "archetype_req": 0, + "parents": ["Leap", "Shocking Bomb"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 23, + "col": 2 + }, + "properties": { + + }, + "effects": [ + + ] + }, + { + "display_name": "Shrapnel Bomb", + "desc": "Arrow Bomb's explosion will fling 15 shrapnel, dealing damage in a large area", + "archetype": "Boltslinger", + "archetype_req": 8, + "parents": ["Arrow Hurricane", "Precise Shot"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 27, + "col": 1 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Shrapnel Bomb", + "cost": 0, + "multipliers": [40, 0, 0, 0, 20, 0] + } + ] + }, + { + "display_name": "Elusive", + "desc": "If you do not get hit for 8+ seconds, become immune to self-damage and remove Arrow Storm's recoil. (Dodging counts as not getting hit)", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Geyser Stomp"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 30, + "col": 0 + }, + "properties": { + + }, + "effects": [ + + ] + }, + { + "display_name": "Double Shots", + "desc": "Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies)", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Escape"], + "dependencies": [], + "blockers": ["Power Shots"], + "cost": 1, + "display": { + "row": 7, + "col": 2 + }, + "properties": { + "arrow": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 0, + "target_part": "Melee Damage", + "cost": 0, + "multipliers": 0.7 + } + ] + }, + { + "display_name": "Triple Shots", + "desc": "Triple Main Attack arrows, but they deal -20% damage per arrow", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Arrow Rain", "Frenzy"], + "dependencies": ["Double Shots"], + "blockers": [], + "cost": 1, + "display": { + "row": 14, + "col": 0 + }, + "properties": { + "arrow": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 0, + "target_part": "Melee Damage", + "cost": 0, + "multipliers": 0.7 + } + ] + }, + { + "display_name": "Power Shots", + "desc": "Main Attack arrows have increased speed and knockback", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Escape"], + "dependencies": [], + "blockers": ["Double Shots"], + "cost": 1, + "display": { + "row": 7, + "col": 6 + }, + "properties": { + + }, + "effects": [ + + ] + }, + { + "display_name": "Focus", + "desc": "When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once", + "archetype": "Sharpshooter", + "archetype_req": 2, + "parents": ["Phantom Ray"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 15, + "col": 4 + }, + "properties": { + + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Focus", + "output": { + "type": "stat", + "abil_name": "Focus", + "name": "dmgPct" + }, + "scaling": [35], + "max": 3 + } + ] + }, + { + "display_name": "More Focus", + "desc": "Add +2 max Focus", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Cheaper Arrow Storm", "Grappling Hook"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 18, + "col": 4 + }, + "properties": { + + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Focus", + "output": { + "type": "stat", + "abil_name": "Focus", + "name": "dmgPct" + }, + "scaling": [35], + "max": 5 + } + ] + }, + { + "display_name": "More Focus (2)", + "desc": "Add +2 max Focus", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Crepuscular Ray"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 32, + "col": 4 + }, + "properties": { + + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Focus", + "output": { + "type": "stat", + "abil_name": "Focus", + "name": "dmgPct" + }, + "scaling": [35], + "max": 7 + } + ] + }, + { + "display_name": "Traveler", + "desc": "For every 1% Walk Speed you have from items, gain +1 Raw Spell Damage (Max 100)", + "archetype": "", + "archetype_req": 0, + "parents": ["Refined Gunpowder", "Twain's Arc"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 20, + "col": 2 + }, + "properties": { + + }, + "effects": [ + { + "type": "stat_scaling", + "slider": false, + "inputs": [ + { + "type": "stat", + "name": "spd" + } + ], + "output": { + "type": "stat", + "name": "sdRaw" + }, + "scaling": [1], + "max": 100 + } + ] + }, + { + "display_name": "Patient Hunter", + "desc": "Your Traps will deal +20% more damage for every second they are active (Max +80%)", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["More Shields"], + "dependencies": ["Basaltic Trap"], + "blockers": [], + "cost": 2, + "display": { + "row": 17, + "col": 8 + }, + "properties": { + "max": 80 + }, + "effects": [ + + ] + }, + { + "display_name": "Stronger Patient Hunter", + "desc": "Add +80% Max Damage to Patient Hunter", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Grape Bomb"], + "dependencies": ["Patient Hunter"], + "blockers": [], + "cost": 1, + "display": { + "row": 31, + "col": 8 + }, + "properties": { + "max": 80 + }, + "effects": [ + + ] + }, + { + "display_name": "Frenzy", + "desc": "Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second", + "archetype": "Boltslinger", + "archetype_req": 0, + "parents": ["Triple Shots", "Nimble String"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 14, + "col": 2 + }, + "properties": { + + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Hits dealt", + "output": { + "type": "stat", + "name": "spd" + }, + "scaling": [6], + "max": 200 + } + ] + }, + { + "display_name": "Phantom Ray", + "desc": "Condense Arrow Storm into a single ray that damages enemies 10 times per second", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Water Mastery", "Fire Creep"], + "dependencies": ["Arrow Storm"], + "blockers": ["Windstorm", "Nimble String", "Arrow Hurricane"], + "cost": 2, + "display": { + "row": 16, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "replace_spell", + "name": "Phantom Ray", + "cost": 40, + "display_text": "Max Damage", + "base_spell": 1, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "Single Arrow", + "type": "damage", + "multipliers": [25, 0, 5, 0, 0, 0] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Single Arrow": 16 + } + } + ] + } + ] + }, + { + "display_name": "Arrow Rain", + "desc": "When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies", + "archetype": "Trapper", + "archetype_req": 0, + "parents": ["Nimble String", "Air Mastery"], + "dependencies": ["Arrow Shield"], + "blockers": [], + "cost": 2, + "display": { + "row": 15, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Arrow Rain", + "cost": 0, + "multipliers": [120, 0, 0, 0, 0, 80] + } + ] + } ], + "Assassin": [ {"title": "Spin Attack", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 1, "col": 4}, @@ -350,76 +2158,2013 @@ const atrees = {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 8}, ], "Warrior": [ - {"row": 0, "col": 4, "name": "Bash", "desc": "Violently bash the ground, dealing high damage in a large area"}, - {"row": 2, "col": 2, "name": "Cheaper Bash", "desc": "Reduce the Mana cost of Bash"}, - {"row": 2, "col": 4, "name": "Spear Proficiency 1", "desc": "Improve your Main Attack's damage and range w/ spear"}, - {"row": 4, "col": 4, "name": "Double Bash", "desc": "Bash will hit a second time at a farther range"}, - {"row": 6, "col": 2, "name": "Heavy Impact", "desc": "After using Charge, violently crash down into the ground and deal damage. (Does not work if Flying Kick is unlocked)"}, - {"row": 6, "col": 4, "name": "Charge", "desc": "Charge forward at high speed (hold shift to cancel)"}, - {"row": 6, "col": 6, "name": "Tougher Skin", "desc": "Harden your skin and become permanently +5% more resistant. For every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)"}, - {"row": 7, "col": 0, "name": "Vehement", "desc": "For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%). Damage Bonus: +5 (Raw)"}, - {"row": 8, "col": 2, "name": "Uppercut", "desc": "Rocket enemies in the air and deal massive damage"}, - {"row": 8, "col": 4, "name": "Cheaper Charge", "desc": "Reduce the Mana cost of Charge"}, - {"row": 8, "col": 6, "name": "War Scream", "desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies"}, - {"row": 10, "col": 0, "name": "Earth Mastery", "desc": "Increases base damage from all Earth attacks"}, - {"row": 10, "col": 2, "name": "Thunder Mastery", "desc": "Increases base damage from all Thunder attacks"}, - {"row": 10, "col": 6, "name": "Air Mastery", "desc": "Increases base damage from all Air attacks"}, - {"row": 10, "col": 8, "name": "Fire Mastery", "desc": "Increases base damage from all Fire attacks"}, - {"row": 11, "col": 4, "name": "Water Mastery", "desc": "Increases base damage from all Water attacks"}, - {"row": 12, "col": 0, "name": "Quadruple Bash", "desc": "Bash will hit 4 times at an even larger range"}, - {"row": 12, "col": 2, "name": "Fireworks", "desc": "Mobs hit by Uppercut will explode mid-air and receive additional damage"}, - {"row": 12, "col": 6, "name": "Flyby Jab", "desc": "Damage enemies in your way when using Charge"}, - {"row": 12, "col": 8, "name": "Flaming Uppercut", "desc": "Uppercut will light mobs on fire, dealing damage every 0.6 seconds"}, - {"row": 13, "col": 4, "name": "Half-Moon Swipe", "desc": "Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water"}, - {"row": 13, "col": 7, "name": "Iron Lungs", "desc": "War scream deals more damage"}, - {"row": 15, "col": 2, "name": "Generalist", "desc": "After casting 3 different spells in a row, your next spell will cost 5 mana"}, - {"row": 15, "col": 4, "name": "Counter", "desc": "When dodging a nearby enemy attack, get 30% chance to instantly attack back"}, - {"row": 15, "col": 7, "name": "Mantle of the Bovemists", "desc": "When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)"}, - {"row": 16, "col": 1, "name": "Bak'al's Grasp", "desc": "After casting war Scream, become Corrupted (15s Cooldown). You cannot heal while in that state. While Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)."}, - {"row": 17, "col": 0, "name": "Spear Proficiency 2", "desc": "Improve your Main Attack's damage and range w/ spear"}, - {"row": 17, "col": 3, "name": "Cheaper Uppercut", "desc": "Reduce the Mana Cost of Uppercut"}, - {"row": 17, "col": 5, "name": "Aerodynamics", "desc": "During Charge, you can steer and change direction"}, - {"row": 17, "col": 7, "name": "Provoke", "desc": "Mobs damaged by War Scream will target only you for at least 5s. Reduce the Mana cost of War Scream."}, - {"row": 18, "col": 2, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 18, "col": 6, "name": "Air Shout", "desc": "War Scream will fire a projectile that can go through walls and deal damage multiple times."}, - {"row": 20, "col": 0, "name": "Enraged Blow", "desc": "While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)"}, - {"row": 20, "col": 3, "name": "Flying Kick", "desc": "While using Charge, mobs hit will halt your momentum and get knocked back"}, - {"row": 20, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 20, "col": 8, "name": "Manachism", "desc": "If you receive a hit that's less than 5% of your max HP, gain 10 mana (1s Cooldown)"}, - {"row": 22, "col": 0, "name": "Boiling Blood", "desc": "Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds"}, - {"row": 22, "col": 2, "name": "Ragnarokkr", "desc": "War Scream becomes deafening, increasing its range and giving damage bonus to players"}, - {"row": 22, "col": 4, "name": "Ambidextrous", "desc": "Increase your change to attack with Counter by 30%"}, - {"row": 22, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 22, "col": 8, "name": "Stronger Bash", "desc": "Increase the damage of Bash"}, - {"row": 23, "col": 1, "name": "Massacre", "desc": "While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar"}, - {"row": 23, "col": 5, "name": "Collide", "desc": "Mobs thrown into walls from Flying Kick will explode and receive additonal damage"}, - {"row": 23, "col": 7, "name": "Rejuvenating Skin", "desc": "Regain back 30% of the damage you take as healing over 30s"}, - {"row": 24, "col": 2, "name": "Comet", "desc": "After being hit by Fireworks, enemies will crash into the ground and receive more damage"}, - {"row": 26, "col": 0, "name": "Uncontainable Corruption", "desc": "Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1"}, - {"row": 26, "col": 2, "name": "Radiant Devotee", "desc": "For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)"}, - {"row": 26, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 26, "col": 7, "name": "Mythril Skin", "desc": "Gain +5% Base Resistance and become immune to knockback"}, - {"row": 27, "col": 1, "name": "Armour Breaker", "desc": "While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage"}, - {"row": 27, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 27, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 27, "col": 8, "name": "Sparking Hope", "desc": "Everytime you heal 5% of your max health, deal damage to all nearby enemies"}, - {"row": 28, "col": 0, "name": "Massive Bash", "desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)"}, - {"row": 28, "col": 2, "name": "Tempest", "desc": "War Scream will ripple the ground and deal damage 3 times in a large area"}, - {"row": 28, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 29, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 29, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 29, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 31, "col": 0, "name": "Cheaper War Scream", "desc": "Reduce the Mana cost of War Scream"}, - {"row": 31, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 32, "col": 1, "name": "Blood KO", "desc": "Gonna have to rewrite this one chief"}, - {"row": 32, "col": 3, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 32, "col": 5, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 32, "col": 7, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 34, "col": 1, "name": "Blood Pact", "desc": "If you do not have enough mana to cast a spell, spend health instead (1% health per mana)"}, - {"row": 34, "col": 4, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 34, "col": 6, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 34, "col": 8, "name": "TEXT", "desc": "IDFK MMMM"}, - {"row": 35, "col": 0, "name": "TEXT", "desc": "IDFK MMMM"} + { + "display_name": "Bash", + "desc": "Violently bash the ground, dealing high damage in a large area", + "archetype": "", + "archetype_req": 0, + "parents": [], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 0, + "col": 4 + }, + "properties": { + "aoe": 4, + "range": 3 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Bash", + "cost": 45, + "display_text": "Total Damage Average", + "base_spell": 1, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "Single Hit", + "type": "damage", + "multipliers": [130, 20, 0, 0, 0, 0] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Single Hit": 1 + } + } + ] + } + ] + }, + { + "display_name": "Spear Proficiency 1", + "desc": "Improve your Main Attack's damage and range w/ spear", + "archetype": "", + "archetype_req": 0, + "parents": ["Bash"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 2, + "col": 4 + }, + "properties": { + "melee_range": 1 + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "mdPct", + "value": 5 + } + ] + } + ] + }, + + { + "display_name": "Cheaper Bash", + "desc": "Reduce the Mana cost of Bash", + "archetype": "", + "archetype_req": 0, + "parents": ["Spear Proficiency 1"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 2, + "col": 2 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "cost": -10 + } + ] + }, + { + "display_name": "Double Bash", + "desc": "Bash will hit a second time at a farther range", + "archetype": "", + "archetype_req": 0, + "parents": ["Spear Proficiency 1"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 4, + "col": 4 + }, + "properties": { + "range": 3 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "name": "Single Hit", + "value": 1 + } + }, + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Single Hit", + "cost": 0, + "multipliers": [-50, 0, 0, 0, 0, 0] + } + ] + }, + + { + "display_name": "Charge", + "desc": "Charge forward at high speed (hold shift to cancel)", + "archetype": "", + "archetype_req": 0, + "parents": ["Double Bash"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 6, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "replace_spell", + "name": "Charge", + "cost": 25, + "display_text": "Total Damage Average", + "base_spell": 2, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage", + "parts": [ + { + "name": "None", + "type": "damage", + "multipliers": [0, 0, 0, 0, 0, 0] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "None": 0 + } + } + ] + } + ] + }, + + { + "display_name": "Heavy Impact", + "desc": "After using Charge, violently crash down into the ground and deal damage", + "archetype": "", + "archetype_req": 0, + "parents": ["Charge"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 6, + "col": 2 + }, + "properties": { + "aoe": 4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Heavy Impact", + "cost": 0, + "multipliers": [100, 0, 0, 0, 0, 0] + } + ] + }, + + { + "display_name": "Vehement", + "desc": "For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%)", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Heavy Impact"], + "dependencies": [], + "blockers": ["Tougher Skin"], + "cost": 1, + "display": { + "row": 7, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "slider": false, + "inputs": [ + { + "type": "stat", + "name": "mdPct" + }, + { + "type": "stat", + "name": "mdRaw" + } + ], + "output": { + "type": "stat", + "name": "spd" + }, + "scaling": [1, 1], + "max": 20 + } + ] + }, + + { + "display_name": "Tougher Skin", + "desc": "Harden your skin and become permanently +5% more resistant\nFor every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Charge"], + "dependencies": [], + "blockers": ["Vehement"], + "cost": 1, + "display": { + "row": 6, + "col": 6 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "baseResist", + "value": "5" + } + ] + }, + { + "type": "stat_scaling", + "slider": false, + "inputs": [ + { + "type": "stat", + "name": "hprRaw" + }, + { + "type": "stat", + "name": "hprPct" + } + ], + "output": { + "type": "stat", + "name": "hpBonus" + }, + "scaling": [10, 10], + "max": 100 + } + ] + }, + + { + "display_name": "Uppercut", + "desc": "Rocket enemies in the air and deal massive damage", + "archetype": "", + "archetype_req": 0, + "parents": ["Vehement"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 8, + "col": 2 + }, + "properties": { + "aoe": 3, + "range": 5 + }, + "effects": [ + { + "type": "replace_spell", + "name": "Uppercut", + "cost": 45, + "display_text": "Total Damage Average", + "base_spell": 3, + "spell_type": "damage", + "scaling": "spell", + "display": "total", + "parts": [ + { + "name": "Uppercut", + "type": "damage", + "multipliers": [150, 50, 50, 0, 0, 0] + }, + { + "name": "Total Damage", + "type": "total", + "hits": { + "Uppercut": 1 + } + } + ] + } + ] + }, + + { + "display_name": "Cheaper Charge", + "desc": "Reduce the Mana cost of Charge", + "archetype": "", + "archetype_req": 0, + "parents": ["Uppercut", "War Scream"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 8, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "cost": -5 + } + ] + }, + + { + "display_name": "War Scream", + "desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies", + "archetype": "", + "archetype_req": 0, + "parents": ["Tougher Skin"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 8, + "col": 6 + }, + "properties": { + "duration": 30, + "aoe": 12, + "defense_bonus": 10 + }, + "effects": [ + { + "type": "replace_spell", + "name": "War Scream", + "cost": 35, + "display_text": "War Scream", + "base_spell": 4, + "spell_type": "damage", + "scaling": "spell", + "display": "Total Damage Average", + "parts": [ + { + "name": "War Scream", + "type": "damage", + "multipliers": [50, 0, 0, 0, 50, 0] + } + ] + } + ] + }, + + { + "display_name": "Earth Mastery", + "desc": "Increases base damage from all Earth attacks", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Uppercut"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 10, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "eDamPct", + "value": 20 + }, + { + "type": "stat", + "name": "eDam", + "value": [2, 4] + } + ] + } + ] + }, + + { + "display_name": "Thunder Mastery", + "desc": "Increases base damage from all Thunder attacks", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Uppercut", "Air Mastery"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 10, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "tDamPct", + "value": 10 + }, + { + "type": "stat", + "name": "tDam", + "value": [1, 8] + } + ] + } + ] + }, + + { + "display_name": "Water Mastery", + "desc": "Increases base damage from all Water attacks", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["Cheaper Charge", "Thunder Mastery", "Air Mastery"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 11, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "wDamPct", + "value": 15 + }, + { + "type": "stat", + "name": "wDam", + "value": [2, 4] + } + ] + } + ] + }, + + { + "display_name": "Air Mastery", + "desc": "Increases base damage from all Air attacks", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["War Scream", "Thunder Mastery"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 10, + "col": 6 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "aDamPct", + "value": 15 + }, + { + "type": "stat", + "name": "aDam", + "value": [3, 4] + } + ] + } + ] + }, + + { + "display_name": "Fire Mastery", + "desc": "Increases base damage from all Earth attacks", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["War Scream"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 10, + "col": 8 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "fDamPct", + "value": 15 + }, + { + "type": "stat", + "name": "fDam", + "value": [3, 5] + } + ] + } + ] + }, + + { + "display_name": "Quadruple Bash", + "desc": "Bash will hit 4 times at an even larger range", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Earth Mastery", "Fireworks"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 12, + "col": 0 + }, + "properties": { + "range": 6 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Single Hit": 2 + } + }, + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Single Hit", + "cost": 0, + "multipliers": [-20, 0, 0, 0, 0, 0] + } + ] + }, + + { + "display_name": "Fireworks", + "desc": "Mobs hit by Uppercut will explode mid-air and receive additional damage", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Thunder Mastery", "Quadruple Bash"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 12, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Fireworks", + "cost": 0, + "multipliers": [80, 0, 20, 0, 0, 0] + }, + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Fireworks": 1 + } + } + ] + }, + + { + "display_name": "Half-Moon Swipe", + "desc": "Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water", + "archetype": "Battle Monk", + "archetype_req": 1, + "parents": ["Water Mastery"], + "dependencies": ["Uppercut"], + "blockers": [], + "cost": 2, + "display": { + "row": 13, + "col": 4 + }, + "properties": { + "range": 4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Uppercut", + "cost": -10, + "multipliers": [-70, 0, 0, 0, 0, 0] + }, + { + "type": "convert_spell_conv", + "target_part": "all", + "conversion": "water" + } + ] + }, + + { + "display_name": "Flyby Jab", + "desc": "Damage enemies in your way when using Charge", + "archetype": "", + "archetype_req": 0, + "parents": ["Air Mastery", "Flaming Uppercut"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 12, + "col": 6 + }, + "properties": { + "aoe": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Flyby Jab", + "cost": 0, + "multipliers": [20, 0, 0, 0, 0, 40] + } + ] + }, + + { + "display_name": "Flaming Uppercut", + "desc": "Uppercut will light mobs on fire, dealing damage every 0.6 seconds", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Fire Mastery", "Flyby Jab"], + "dependencies": ["Uppercut"], + "blockers": [], + "cost": 2, + "display": { + "row": 12, + "col": 8 + }, + "properties": { + "duration": 3, + "tick": 0.6 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Flaming Uppercut", + "cost": 0, + "multipliers": [0, 0, 0, 0, 50, 0] + }, + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Flaming Uppercut Total Damage", + "cost": 0, + "hits": { + "Flaming Uppercut": 5 + } + }, + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Flaming Uppercut": 5 + } + } + ] + }, + + { + "display_name": "Iron Lungs", + "desc": "War Scream deals more damage", + "archetype": "", + "archetype_req": 0, + "parents": ["Flyby Jab", "Flaming Uppercut"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 13, + "col": 7 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "War Scream", + "cost": 0, + "multipliers": [30, 0, 0, 0, 0, 30] + } + ] + }, + + { + "display_name": "Generalist", + "desc": "After casting 3 different spells in a row, your next spell will cost 5 mana", + "archetype": "Battle Monk", + "archetype_req": 3, + "parents": ["Counter"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 15, + "col": 2 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Counter", + "desc": "When dodging a nearby enemy attack, get 30% chance to instantly attack back", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["Half-Moon Swipe"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 15, + "col": 4 + }, + "properties": { + "chance": 30 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 5, + "target_part": "Counter", + "cost": 0, + "multipliers": [60, 0, 20, 0, 0, 20] + } + ] + }, + + { + "display_name": "Mantle of the Bovemists", + "desc": "When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)", + "archetype": "Paladin", + "archetype_req": 3, + "parents": ["Iron Lungs"], + "dependencies": ["War Scream"], + "blockers": [], + "cost": 2, + "display": { + "row": 15, + "col": 7 + }, + "properties": { + "mantle_charge": 3 + }, + "effects": [ + + ] + }, + + { + "display_name": "Bak'al's Grasp", + "desc": "After casting War Scream, become Corrupted (15s Cooldown). You cannot heal while in that state\n\nWhile Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)", + "archetype": "Fallen", + "archetype_req": 2, + "parents": ["Quadruple Bash", "Fireworks"], + "dependencies": ["War Scream"], + "blockers": [], + "cost": 2, + "display": { + "row": 16, + "col": 1 + }, + "properties": { + "cooldown": 15 + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Corrupted", + "output": { + "type": "stat", + "name": "raw" + }, + "scaling": [4], + "slider_step": 2, + "max": 120 + } + ] + }, + + { + "display_name": "Spear Proficiency 2", + "desc": "Improve your Main Attack's damage and range w/ spear", + "archetype": "", + "archetype_req": 0, + "parents": ["Bak'al's Grasp", "Cheaper Uppercut"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 17, + "col": 0 + }, + "properties": { + "melee_range": 1 + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "mdPct", + "value": 5 + } + ] + } + ] + }, + + { + "display_name": "Cheaper Uppercut", + "desc": "Reduce the Mana Cost of Uppercut", + "archetype": "", + "archetype_req": 0, + "parents": ["Spear Proficiency 2", "Aerodynamics", "Counter"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 17, + "col": 3 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "cost": -5 + } + ] + }, + + { + "display_name": "Aerodynamics", + "desc": "During Charge, you can steer and change direction", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["Cheaper Uppercut", "Provoke"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 17, + "col": 5 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Provoke", + "desc": "Mobs damaged by War Scream will target only you for at least 5s \n\nReduce the Mana cost of War Scream", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Aerodynamics", "Mantle of the Bovemists"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 17, + "col": 7 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "cost": -5 + } + ] + }, + + { + "display_name": "Precise Strikes", + "desc": "+30% Critical Hit Damage", + "archetype": "", + "archetype_req": 0, + "parents": ["Cheaper Uppercut", "Spear Proficiency 2"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 18, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "critDmg", + "value": 30 + } + ] + } + ] + }, + + { + "display_name": "Air Shout", + "desc": "War Scream will fire a projectile that can go through walls and deal damage multiple times", + "archetype": "", + "archetype_req": 0, + "parents": ["Aerodynamics", "Provoke"], + "dependencies": ["War Scream"], + "blockers": [], + "cost": 2, + "display": { + "row": 18, + "col": 6 + }, + "properties": { + + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Air Shout", + "cost": 0, + "multipliers": [20, 0, 0, 0, 0, 5] + } + ] + }, + + { + "display_name": "Enraged Blow", + "desc": "While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Spear Proficiency 2"], + "dependencies": ["Bak'al's Grasp"], + "blockers": [], + "cost": 2, + "display": { + "row": 20, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "slider": false, + "inputs": [ + { + "type": "stat", + "name": "hpBonus" + } + ], + "output": { + "type": "stat", + "name": "dmgPct" + }, + "scaling": [2], + "max": 200 + } + ] + }, + + { + "display_name": "Flying Kick", + "desc": "When using Charge, mobs hit will halt your momentum and get knocked back", + "archetype": "Battle Monk", + "archetype_req": 1, + "parents": ["Cheaper Uppercut", "Stronger Mantle"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 20, + "col": 3 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Flying Kick", + "cost": 0, + "multipliers": [120, 0, 0, 10, 0, 20] + } + ] + }, + + { + "display_name": "Stronger Mantle", + "desc": "Add +2 additional charges to Mantle of the Bovemists", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Manachism", "Flying Kick"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 20, + "col": 6 + }, + "properties": { + "mantle_charge": 2 + }, + "effects": [ + + ] + }, + + { + "display_name": "Manachism", + "desc": "If you receive a hit that's less than 5% of your max HP, gain 10 Mana (1s Cooldown)", + "archetype": "Paladin", + "archetype_req": 3, + "parents": ["Stronger Mantle", "Provoke"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 20, + "col": 8 + }, + "properties": { + "cooldown": 1 + }, + "effects": [ + + ] + }, + + { + "display_name": "Boiling Blood", + "desc": "Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds", + "archetype": "", + "archetype_req": 0, + "parents": ["Enraged Blow", "Ragnarokkr"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 22, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Boiling Blood", + "cost": 0, + "multipliers": [25, 0, 0, 0, 5, 0] + } + ] + }, + + { + "display_name": "Ragnarokkr", + "desc": "War Scream become deafening, increasing its range and giving damage bonus to players", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Boiling Blood", "Flying Kick"], + "dependencies": ["War Scream"], + "blockers": [], + "cost": 2, + "display": { + "row": 22, + "col": 2 + }, + "properties": { + "damage_bonus": 30, + "aoe": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "cost": 10 + } + ] + }, + + { + "display_name": "Ambidextrous", + "desc": "Increase your chance to attack with Counter by +30%", + "archetype": "", + "archetype_req": 0, + "parents": ["Flying Kick", "Stronger Mantle", "Burning Heart"], + "dependencies": ["Counter"], + "blockers": [], + "cost": 1, + "display": { + "row": 22, + "col": 4 + }, + "properties": { + "chance": 30 + }, + "effects": [ + + ] + }, + + { + "display_name": "Burning Heart", + "desc": "For every 100 Health Bonus you have from item IDs, gain +2% Fire Damage (Max 100%)", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Ambidextrous", "Stronger Bash"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 22, + "col": 6 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "slider": false, + "inputs": [ + { + "type": "stat", + "name": "hpBonus" + } + ], + "output": { + "type": "stat", + "name": "fDamPct" + }, + "scaling": [2], + "max": 100, + "slider_step": 100 + } + ] + }, + + { + "display_name": "Stronger Bash", + "desc": "Increase the damage of Bash", + "archetype": "", + "archetype_req": 0, + "parents": ["Burning Heart", "Manachism"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 22, + "col": 8 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "target_part": "Single Hit", + "cost": 0, + "multipliers": [30, 0, 0, 0, 0, 0] + } + ] + }, + + { + "display_name": "Intoxicating Blood", + "desc": "After leaving Corrupted, gain 2% of the health lost back for each enemy killed while Corrupted", + "archetype": "Fallen", + "archetype_req": 5, + "parents": ["Ragnarokkr", "Boiling Blood"], + "dependencies": ["Bak'al's Grasp"], + "blockers": [], + "cost": 2, + "display": { + "row": 23, + "col": 1 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Comet", + "desc": "After being hit by Fireworks, enemies will crash into the ground and receive more damage", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Ragnarokkr"], + "dependencies": ["Fireworks"], + "blockers": [], + "cost": 2, + "display": { + "row": 24, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Comet", + "cost": 0, + "multipliers": [80, 20, 0, 0, 0, 0] + }, + { + "type":"add_spell_prop", + "base_spell": 3, + "target_part": "Total Damage", + "cost": 0, + "hits": { + "Comet": 1 + } + } + ] + }, + + { + "display_name": "Collide", + "desc": "Mobs thrown into walls from Flying Kick will explode and receive additonal damage", + "archetype": "Battle Monk", + "archetype_req": 4, + "parents": ["Ambidextrous", "Burning Heart"], + "dependencies": ["Flying Kick"], + "blockers": [], + "cost": 2, + "display": { + "row": 23, + "col": 5 + }, + "properties": { + "aoe": 4 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "target_part": "Collide", + "cost": 0, + "multipliers": [100, 0, 0, 0, 50, 0] + } + ] + }, + + { + "display_name": "Rejuvenating Skin", + "desc": "Regain back 30% of the damage you take as healing over 30s", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Burning Heart", "Stronger Bash"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 23, + "col": 7 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Uncontainable Corruption", + "desc": "Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1", + "archetype": "", + "archetype_req": 0, + "parents": ["Boiling Blood", "Radiant Devotee"], + "dependencies": ["Bak'al's Grasp"], + "blockers": [], + "cost": 1, + "display": { + "row": 26, + "col": 0 + }, + "properties": { + "cooldown": -5 + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Corrupted", + "output": { + "type": "stat", + "name": "raw" + }, + "scaling": [1], + "slider_step": 2, + "max": 50 + } + ] + }, + + { + "display_name": "Radiant Devotee", + "desc": "For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)", + "archetype": "Battle Monk", + "archetype_req": 1, + "parents": ["Whirlwind Strike", "Uncontainable Corruption"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 26, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "inputs": [ + { + "type": "stat", + "name": "ref" + } + ], + "output": { + "type": "stat", + "name": "mr" + }, + "scaling": [1], + "max": 10, + "slider_step": 4 + } + ] + }, + + { + "display_name": "Whirlwind Strike", + "desc": "Uppercut will create a strong gust of air, launching you upward with enemies (Hold shift to stay grounded)", + "archetype": "Battle Monk", + "archetype_req": 5, + "parents": ["Ambidextrous", "Radiant Devotee"], + "dependencies": ["Uppercut"], + "blockers": [], + "cost": 2, + "display": { + "row": 26, + "col": 4 + }, + "properties": { + "range": 2 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Uppercut", + "cost": 0, + "multipliers": [0, 0, 0, 0, 0, 50] + } + ] + }, + + { + "display_name": "Mythril Skin", + "desc": "Gain +5% Base Resistance and become immune to knockback", + "archetype": "Paladin", + "archetype_req": 6, + "parents": ["Rejuvenating Skin"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 26, + "col": 7 + }, + "properties": { + }, + "effects": [ + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "baseResist", + "value": 5 + } + ] + } + ] + }, + + { + "display_name": "Armour Breaker", + "desc": "While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Uncontainable Corruption", "Radiant Devotee"], + "dependencies": ["Bak'al's Grasp"], + "blockers": [], + "cost": 2, + "display": { + "row": 27, + "col": 1 + }, + "properties": { + "duration": 5 + }, + "effects": [ + + ] + }, + + { + "display_name": "Shield Strike", + "desc": "When your Mantle of the Bovemist loses all charges, deal damage around you for each Mantle individually lost", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Mythril Skin", "Sparkling Hope"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 27, + "col": 6 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 5, + "target_part": "Shield Strike", + "cost": 0, + "multipliers": [60, 0, 20, 0, 0, 0] + } + ] + }, + + { + "display_name": "Sparkling Hope", + "desc": "Everytime you heal 5% of your max health, deal damage to all nearby enemies", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Mythril Skin"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 27, + "col": 8 + }, + "properties": { + "aoe": 6 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 5, + "target_part": "Sparkling Hope", + "cost": 0, + "multipliers": [10, 0, 5, 0, 0, 0] + } + ] + }, + + { + "display_name": "Massive Bash", + "desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)", + "archetype": "Fallen", + "archetype_req": 8, + "parents": ["Tempest", "Uncontainable Corruption"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 28, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Corrupted", + "output": { + "type": "stat", + "name": "bashAoE" + }, + "scaling": [1], + "max": 10, + "slider_step": 3 + } + ] + }, + + { + "display_name": "Tempest", + "desc": "War Scream will ripple the ground and deal damage 3 times in a large area", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["Massive Bash", "Spirit of the Rabbit"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 28, + "col": 2 + }, + "properties": { + "aoe": 16 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Tempest", + "cost": "0", + "multipliers": [30, 10, 0, 0, 0, 10] + }, + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Tempest Total Damage", + "cost": "0", + "hits": { + "Tempest": 3 + } + }, + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Total Damage", + "cost": "0", + "hits": { + "Tempest": 3 + } + } + ] + }, + + { + "display_name": "Spirit of the Rabbit", + "desc": "Reduce the Mana cost of Charge and increase your Walk Speed by +20%", + "archetype": "Battle Monk", + "archetype_req": 5, + "parents": ["Tempest", "Whirlwind Strike"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 28, + "col": 4 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 2, + "cost": -5 + }, + { + "type": "raw_stat", + "bonuses": [ + { + "type": "stat", + "name": "spd", + "value": 20 + } + ] + } + ] + }, + + { + "display_name": "Massacre", + "desc": "While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar", + "archetype": "Fallen", + "archetype_req": 5, + "parents": ["Tempest", "Massive Bash"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 29, + "col": 1 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Axe Kick", + "desc": "Increase the damage of Uppercut, but also increase its mana cost", + "archetype": "", + "archetype_req": 0, + "parents": ["Tempest", "Spirit of the Rabbit"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 29, + "col": 3 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "target_part": "Uppercut", + "cost": 10, + "multipliers": [100, 0, 0, 0, 0, 0] + } + ] + }, + + { + "display_name": "Radiance", + "desc": "Bash will buff your allies' positive IDs. (15s Cooldown)", + "archetype": "Paladin", + "archetype_req": 2, + "parents": ["Spirit of the Rabbit", "Cheaper Bash 2"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 29, + "col": 5 + }, + "properties": { + "cooldown": 15 + }, + "effects": [ + + ] + }, + + { + "display_name": "Cheaper Bash 2", + "desc": "Reduce the Mana cost of Bash", + "archetype": "", + "archetype_req": 0, + "parents": ["Radiance", "Shield Strike", "Sparkling Hope"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 29, + "col": 7 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 1, + "cost": -5 + } + ] + }, + + { + "display_name": "Cheaper War Scream", + "desc": "Reduce the Mana cost of War Scream", + "archetype": "", + "archetype_req": 0, + "parents": ["Massive Bash"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 31, + "col": 0 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "cost": -5 + } + ] + }, + + { + "display_name": "Discombobulate", + "desc": "Every time you hit an enemy, briefly increase your elemental damage dealt to them by +2 (Additive, Max +50). This bonus decays -5 every second", + "archetype": "Battle Monk", + "archetype_req": 12, + "parents": ["Thunderclap"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 31, + "col": 2 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Hits dealt", + "output": { + "type": "stat", + "name": "rainrawButDifferent" + }, + "scaling": [2], + "max": 50 + } + ] + }, + + { + "display_name": "Thunderclap", + "desc": "Bash will cast at the player's position and gain additional AoE.\n\n All elemental conversions become Thunder", + "archetype": "Battle Monk", + "archetype_req": 8, + "parents": ["Spirit of the Rabbit"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 31, + "col": 4 + }, + "properties": { + "aoe": 2 + }, + "effects": [ + { + "type": "convert_spell_conv", + "target_part": "all", + "conversion": "thunder" + } + ] + }, + + { + "display_name": "Cyclone", + "desc": "After casting War Scream, envelop yourself with a vortex that damages nearby enemies every 0.5s", + "archetype": "Battle Monk", + "archetype_req": 0, + "parents": ["Thunderclap"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 32, + "col": 5 + }, + "properties": { + "aoe": 4, + "duration": 20 + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Cyclone", + "cost": 0, + "multipliers": [10, 0, 0, 0, 5, 10] + }, + { + "type": "add_spell_prop", + "base_spell": 4, + "target_part": "Cyclone Total Damage", + "cost": 0, + "hits": { + "Cyclone": 40 + } + + } + ] + }, + + { + "display_name": "Second Chance", + "desc": "When you receive a fatal blow, survive and regain 30% of your Health (10m Cooldown)", + "archetype": "Paladin", + "archetype_req": 12, + "parents": ["Cheaper Bash 2"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 32, + "col": 7 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Blood Pact", + "desc": "If you do not have enough mana to cast a spell, spend health instead (1% health per mana)", + "archetype": "", + "archetype_req": 10, + "parents": ["Cheaper War Scream"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 34, + "col": 1 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Haemorrhage", + "desc": "Reduce Blood Pact's health cost. (0.5% health per mana)", + "archetype": "Fallen", + "archetype_req": 0, + "parents": ["Blood Pact"], + "dependencies": ["Blood Pact"], + "blockers": [], + "cost": 1, + "display": { + "row": 35, + "col": 2 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Brink of Madness", + "desc": "If your health is 25% full or less, gain +40% Resistance", + "archetype": "", + "archetype_req": 0, + "parents": ["Blood Pact", "Cheaper Uppercut 2"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 35, + "col": 4 + }, + "properties": { + }, + "effects": [ + + ] + }, + + { + "display_name": "Cheaper Uppercut 2", + "desc": "Reduce the Mana cost of Uppercut", + "archetype": "", + "archetype_req": 0, + "parents": ["Second Chance", "Brink of Madness"], + "dependencies": [], + "blockers": [], + "cost": 1, + "display": { + "row": 35, + "col": 6 + }, + "properties": { + }, + "effects": [ + { + "type": "add_spell_prop", + "base_spell": 3, + "cost": -5 + } + ] + }, + + { + "display_name": "Martyr", + "desc": "When you receive a fatal blow, all nearby allies become invincible", + "archetype": "Paladin", + "archetype_req": 0, + "parents": ["Second Chance"], + "dependencies": [], + "blockers": [], + "cost": 2, + "display": { + "row": 35, + "col": 8 + }, + "properties": { + "duration": 3, + "aoe": 12 + }, + "effects": [ + + ] + } ], "Mage": [], "Shaman": [] From 6ea34708a6de2ed906efe382aa4dda57105d3de9 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 02:23:56 -0700 Subject: [PATCH 44/68] Fully working tomes !!! --- js/build.js | 14 ++++++++++++-- js/build_encode_decode.js | 2 +- js/builder.js | 10 +++++++++- js/builder_graph.js | 40 ++++++++++++++++++++++++++++++++------- js/load_tome.js | 24 +++++++++++------------ tomes.json | 20 ++++++++++++++++++++ 6 files changed, 87 insertions(+), 23 deletions(-) diff --git a/js/build.js b/js/build.js index 1760004..987aac3 100644 --- a/js/build.js +++ b/js/build.js @@ -155,10 +155,12 @@ class Build{ */ initBuildStats(){ - let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "def", "agi"]; + let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "def", "agi", "dmgMobs", "defMobs"]; //Create a map of this build's stats let statMap = new Map(); + statMap.set("damageMultiplier", 1); + statMap.set("defMultiplier", 1); for (const staticID of staticIDs) { statMap.set(staticID, 0); @@ -176,7 +178,15 @@ class Build{ } for (const staticID of staticIDs) { if (item_stats.get(staticID)) { - statMap.set(staticID, statMap.get(staticID) + item_stats.get(staticID)); + if (staticID === "dmgMobs") { + statMap.set('damageMultiplier', statMap.get('damageMultiplier') * item_stats.get(staticID)); + } + else if (staticID === "defMobs") { + statMap.set('defMultiplier', statMap.get('defMultiplier') * item_stats.get(staticID)); + } + else { + statMap.set(staticID, statMap.get(staticID) + item_stats.get(staticID)); + } } } if (item_stats.get("majorIds")) { diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 86aabd5..c12dae0 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -157,7 +157,7 @@ function encodeBuild(build, powders, skillpoints) { } else if (item.statMap.get("crafted")) { build_string += "CR-"+encodeCraft(item); } else if (item.statMap.get("category") === "tome") { - let tome_id = item.get("id"); + let tome_id = item.statMap.get("id"); if (tome_id <= 60) { // valid normal tome. ID 61-63 is for NONE tomes. build_version = Math.max(build_version, 6); diff --git a/js/builder.js b/js/builder.js index 34f7eab..cf997f3 100644 --- a/js/builder.js +++ b/js/builder.js @@ -95,8 +95,15 @@ function resetFields(){ elem.classList.remove("toggleOn"); } } + for (const elem of skp_order) { + console.log(document.getElementById(elem + "_boost_armor").value); + document.getElementById(elem + "_boost_armor").value = 0; + document.getElementById(elem + "_boost_armor_prev").value = 0; + document.getElementById(elem + "_boost_armor").style.background = `linear-gradient(to right, #AAAAAA, #AAAAAA 0%, #AAAAAA 100%)`; + document.getElementById(elem + "_boost_armor_label").textContent = `% ${damageClasses[skp_order.indexOf(elem)+1]} Damage Boost: 0`; + } - const nodes_to_reset = item_nodes.concat(powder_nodes).concat(edit_input_nodes).concat([powder_special_input, boosts_node]); + const nodes_to_reset = item_nodes.concat(powder_nodes).concat(edit_input_nodes).concat([powder_special_input, boosts_node, armor_powder_node]); for (const node of nodes_to_reset) { node.mark_dirty(); } @@ -317,6 +324,7 @@ function init_autocomplete() { if (event.detail.selection.value) { event.target.value = event.detail.selection.value; } + event.target.dispatchEvent(new Event('input')); }, }, } diff --git a/js/builder_graph.js b/js/builder_graph.js index ed0ac00..cfb8491 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -259,7 +259,10 @@ class ItemInputDisplayNode extends ComputeNode { // Doesn't exist for weapons. this.health_field.textContent = "0"; } - this.level_field.textContent = "0"; + if (this.level_field) { + // Doesn't exist for tomes. + this.level_field.textContent = "0"; + } if (!item) { this.input_field.classList.add("is-invalid"); return null; @@ -274,7 +277,10 @@ class ItemInputDisplayNode extends ComputeNode { // Doesn't exist for weapons. this.health_field.textContent = item.statMap.get('hp'); } - this.level_field.textContent = item.statMap.get('lvl'); + if (this.level_field) { + // Doesn't exist for tomes. + this.level_field.textContent = item.statMap.get('lvl'); + } this.image.classList.add(tier + "-shadow"); return null; } @@ -401,7 +407,14 @@ class BuildAssembleNode extends ComputeNode { input_map.get('ring1-input'), input_map.get('ring2-input'), input_map.get('bracelet-input'), - input_map.get('necklace-input') + input_map.get('necklace-input'), + input_map.get('weaponTome1-input'), + input_map.get('weaponTome2-input'), + input_map.get('armorTome1-input'), + input_map.get('armorTome2-input'), + input_map.get('armorTome3-input'), + input_map.get('armorTome4-input'), + input_map.get('guildTome1-input') ]; let weapon = input_map.get('weapon-input'); let level = input_map.get('level-input'); @@ -759,7 +772,7 @@ class DisplayBuildWarningsNode extends ComputeNode { } let baditem = document.createElement("p"); baditem.classList.add("nocolor"); baditem.classList.add("itemp"); - baditem.textContent = item.get("displayName") + " requires level " + item_lvl + " to use."; + baditem.textContent = item.statMap.get("displayName") + " requires level " + item_lvl + " to use."; lvlWarning.appendChild(baditem); } } @@ -791,7 +804,13 @@ class AggregateStatsNode extends ComputeNode { for (const [k, v] of input_map.entries()) { for (const [k2, v2] of v.entries()) { if (output_stats.has(k2)) { - output_stats.set(k2, v2 + output_stats.get(k2)); + // TODO: ugly AF + if (k2 === 'damageMultiplier' || k2 === 'defMultiplier') { + output_stats.set(k2, v2 * output_stats.get(k2)); + } + else { + output_stats.set(k2, v2 + output_stats.get(k2)); + } } else { output_stats.set(k2, v2); @@ -815,8 +834,6 @@ class AggregateEditableIDNode extends ComputeNode { const weapon = input_map.get('weapon'); input_map.delete('weapon'); const output_stats = new Map(build.statMap); - output_stats.set("damageMultiplier", 1); - output_stats.set("defMultiplier", 1); for (const [k, v] of input_map.entries()) { output_stats.set(k, v); } @@ -944,6 +961,15 @@ function builder_graph_init() { //new PrintNode(eq+'-debug').link_to(item_input); //document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm'); } + console.log(none_tomes); + for (const [eq, none_item] of zip2(tome_fields, [none_tomes[0], none_tomes[0], none_tomes[1], none_tomes[1], none_tomes[1], none_tomes[1], none_tomes[2]])) { + let input_field = document.getElementById(eq+"-choice"); + let item_image = document.getElementById(eq+"-img"); + + let item_input = new ItemInputNode(eq+'-input', input_field, none_item); + item_nodes.push(item_input); + new ItemInputDisplayNode(eq+'-input-display', eq, item_image).link_to(item_input); + } // weapon image changer node. let weapon_image = document.getElementById("weapon-img"); diff --git a/js/load_tome.js b/js/load_tome.js index e6fded5..3b44842 100644 --- a/js/load_tome.js +++ b/js/load_tome.js @@ -1,4 +1,4 @@ -const TOME_DB_VERSION = 1; +const TOME_DB_VERSION = 2; // @See https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/video-store/index.jsA let tdb; @@ -120,6 +120,11 @@ async function load_tome_init() { }); } +let none_tomes = [ + ["tome", "weaponTome", "No Weapon Tome"], + ["tome", "armorTome", "No Armor Tome"], + ["tome", "guildTome", "No Guild Tome"] +]; function init_tome_maps() { //warp tomeMap = new Map(); @@ -131,17 +136,12 @@ function init_tome_maps() { tomeLists.set(it, []); } - let noneTomes = [ - ["tome", "weaponTome", "No Weapon Tome"], - ["tome", "armorTome", "No Armor Tome"], - ["tome", "guildTome", "No Guild Tome"] - ]; for (let i = 0; i < 3; i++) { let tome = Object(); tome.slots = 0; - tome.category = noneTomes[i][0]; - tome.type = noneTomes[i][1]; - tome.name = noneTomes[i][2]; + tome.category = none_tomes[i][0]; + tome.type = none_tomes[i][1]; + tome.name = none_tomes[i][2]; tome.displayName = tome.name; tome.set = null; tome.quest = null; @@ -160,14 +160,14 @@ function init_tome_maps() { //dependency - load.js clean_item(tome); - noneTomes[i] = tome; + none_tomes[i] = tome; } - tomes = tomes.concat(noneTomes); + tomes = tomes.concat(none_tomes); for (const tome of tomes) { if (tome.remapID === undefined) { tomeLists.get(tome.type).push(tome.displayName); tomeMap.set(tome.displayName, tome); - if (noneTomes.includes(tome)) { + if (none_tomes.includes(tome)) { tomeIDMap.set(tome.id, ""); } else { diff --git a/tomes.json b/tomes.json index 913efa4..0b4c213 100644 --- a/tomes.json +++ b/tomes.json @@ -9,6 +9,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defmobs": 3, "thorns": 6, "ref": 6, "hpBonus": 120, @@ -23,6 +24,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "thorns": 8, "ref": 8, "fixID": false, @@ -36,6 +38,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defMobs": 3, "exploding": 5, "mdPct": 5, "hpBonus": 120, @@ -50,6 +53,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "thorns": 6, "reflection": 6, "fixID": false, @@ -63,6 +67,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defMobs": 3, "sdPct": 5, "hpBonus": 120, "fixID": false, @@ -76,6 +81,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "sdPct": 6, "fixID": false, "id": 5 @@ -88,6 +94,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defMobs": 3, "hprRaw": 15, "hpBonus": 120, "fixID": false, @@ -101,6 +108,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "hprRaw": 60, "fixID": false, "id": 7 @@ -113,6 +121,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defMobs": 3, "ls": 25, "hpBonus": 120, "fixID": false, @@ -126,6 +135,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "ls": 85, "fixID": false, "id": 9 @@ -138,6 +148,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defMobs": 3, "lb": 5, "hpBonus": 120, "fixID": false, @@ -151,6 +162,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "lb": 6, "fixID": false, "id": 11 @@ -163,6 +175,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 60, + "defMobs": 3, "spd": 5, "hpBonus": 120, "fixID": false, @@ -176,6 +189,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 5, "spd": 6, "fixID": false, "id": 13 @@ -188,6 +202,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 8, "eDefPct": 10, "hpBonus": 150, "fixID": false, @@ -201,6 +216,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 8, "tDefPct": 10, "hpBonus": 150, "fixID": false, @@ -214,6 +230,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 8, "wDefPct": 10, "hpBonus": 150, "fixID": false, @@ -227,6 +244,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 8, "fDefPct": 10, "hpBonus": 150, "fixID": false, @@ -240,6 +258,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 8, "aDefPct": 10, "hpBonus": 150, "fixID": false, @@ -253,6 +272,7 @@ "drop": "never", "restrict": "Soulbound Item", "lvl": 100, + "defMobs": 8, "eDefPct": 6, "tDefPct": 6, "wDefPct": 6, From 12e4c1e88c976670d1c72ca0a13bac58a74a07be Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 03:16:36 -0700 Subject: [PATCH 45/68] Revert to current dev branch behaviour somewhat --- js/builder.js | 2 +- js/builder_graph.js | 1 - js/computation_graph.js | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/js/builder.js b/js/builder.js index f497224..96513d8 100644 --- a/js/builder.js +++ b/js/builder.js @@ -293,7 +293,7 @@ function init_autocomplete() { resultsList: { maxResults: 1000, tabSelect: true, - noResults: true, + noResults: false, class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm", element: (list, data) => { // dynamic result loc diff --git a/js/builder_graph.js b/js/builder_graph.js index cfb8491..51a4d6c 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -894,7 +894,6 @@ class SkillPointSetterNode extends ComputeNode { } compute_func(input_map) { - console.log("a"); if (input_map.size !== 1) { throw "SkillPointSetterNode accepts exactly one input (build)"; } const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element for (const [idx, elem] of skp_order.entries()) { diff --git a/js/computation_graph.js b/js/computation_graph.js index c0337ec..90fe08c 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -105,7 +105,7 @@ class ComputeNode { * * @param node : ComputeNode to schedule an update for. */ -function calcSchedule(node) { +function calcSchedule(node, timeout) { if (node.update_task !== null) { clearTimeout(node.update_task); } @@ -113,7 +113,7 @@ function calcSchedule(node) { node.update_task = setTimeout(function() { node.update(); node.update_task = null; - }, 500); + }, timeout); } class PrintNode extends ComputeNode { @@ -139,8 +139,8 @@ class InputNode extends ComputeNode { constructor(name, input_field) { super(name); this.input_field = input_field; - //this.input_field.addEventListener("input", () => calcSchedule(this)); - this.input_field.addEventListener("change", () => calcSchedule(this)); + this.input_field.addEventListener("input", () => calcSchedule(this, 5000)); + this.input_field.addEventListener("change", () => calcSchedule(this, 500)); //calcSchedule(this); Manually fire first update for better control } From ef5dd8f6c6065dc38b085f5af075a4d301c93345 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 03:42:18 -0700 Subject: [PATCH 46/68] HOTFIX: tome patch --- js/build.js | 9 ++++----- js/build_encode_decode.js | 8 ++++---- js/builder_graph.js | 16 ++++++++++------ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/js/build.js b/js/build.js index b95a569..ccd25a6 100644 --- a/js/build.js +++ b/js/build.js @@ -105,7 +105,7 @@ class Build{ * In order: 2x Weapon Mastery Tome, 4x Armor Mastery Tome, 1x Guild Tome. * 2x Slaying Mastery Tome, 2x Dungeoneering Mastery Tome, 2x Gathering Mastery Tome are in game, but do not have "useful" stats (those that affect damage calculations or building) */ - constructor(level, items, tomes, weapon){ + constructor(level, items, weapon){ if (level < 1) { //Should these be constants? this.level = 1; @@ -123,13 +123,12 @@ class Build{ this.availableSkillpoints = levelToSkillPoints(this.level); this.equipment = items; - this.tomes = tomes; this.weapon = weapon; - this.items = this.equipment.concat([this.weapon]).concat(this.tomes); + this.items = this.equipment.concat([this.weapon]); // return [equip_order, best_skillpoints, final_skillpoints, best_total]; // calc skillpoints requires statmaps only - let result = calculate_skillpoints(this.equipment.concat(this.tomes).map((x) => x.statMap), this.weapon.statMap); + let result = calculate_skillpoints(this.equipment.map((x) => x.statMap), this.weapon.statMap); this.equip_order = result[0]; // How many skillpoints the player had to assign (5 number) this.base_skillpoints = result[1]; @@ -145,7 +144,7 @@ class Build{ /*Returns build in string format */ toString(){ - return [this.equipment,this.weapon,this.tomes].flat(); + return [this.equipment,this.weapon].flat(); } diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index c12dae0..618bb71 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -119,11 +119,11 @@ function decodeBuild(url_tag) { // Tomes. if (version == 6) { //tome values do not appear in anything before v6. - for (let i = 0; i < 7; ++i) { + for (let i in tomes) { let tome_str = info[1].charAt(i); - for (let i in tomes) { - setValue(tomeInputs[i], getTomeNameFromID(Base64.toInt(tome_str))); - } + let tome_name = getTomeNameFromID(Base64.toInt(tome_str)); + console.log(tome_name); + setValue(tomeInputs[i], tome_name); } info[1] = info[1].slice(7); } diff --git a/js/builder_graph.js b/js/builder_graph.js index 51a4d6c..f4d41ea 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -382,8 +382,6 @@ class URLUpdateNode extends ComputeNode { * Create a "build" object from a set of equipments. * Returns a new Build object, or null if all items are NONE items. * - * TODO: add tomes - * * Signature: BuildAssembleNode(helmet-input: Item, * chestplate-input: Item, * leggings-input: Item, @@ -426,7 +424,7 @@ class BuildAssembleNode extends ComputeNode { if (all_none && !location.hash) { return null; } - return new Build(level, equipments, [], weapon); + return new Build(level, equipments, weapon); } } @@ -1007,8 +1005,6 @@ function builder_graph_init() { let build_disp_node = new BuildDisplayNode() build_disp_node.link_to(build_node, 'build'); - let build_warnings_node = new DisplayBuildWarningsNode(); - build_warnings_node.link_to(build_node, 'build'); // Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap) let stat_agg_node = new AggregateStatsNode(); @@ -1026,14 +1022,15 @@ function builder_graph_init() { edit_id_output = new EditableIDSetterNode(edit_input_nodes); // Makes shallow copy of list. edit_id_output.link_to(build_node); + let skp_inputs = []; for (const skp of skp_order) { const elem = document.getElementById(skp+'-skp'); const node = new SumNumberInputNode('builder-'+skp+'-input', elem); edit_agg_node.link_to(node, skp); build_encode_node.link_to(node, skp); - build_warnings_node.link_to(node, skp); edit_input_nodes.push(node); + skp_inputs.push(node); } stat_agg_node.link_to(edit_agg_node); build_disp_node.link_to(stat_agg_node, 'stats'); @@ -1079,6 +1076,13 @@ function builder_graph_init() { let skp_output = new SkillPointSetterNode(edit_input_nodes); skp_output.link_to(build_node); + let build_warnings_node = new DisplayBuildWarningsNode(); + build_warnings_node.link_to(build_node, 'build'); + for (const [skp_input, skp] of zip2(skp_inputs, skp_order)) { + build_warnings_node.link_to(skp_input, skp); + } + build_warnings_node.update(); + // call node.update() for each skillpoint node and stat edit listener node manually // NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them // this will propagate the update to the `stat_agg_node`, and then to damage calc From 5f9c0d5185ea08b8dfa43b8513df5715f37b2073 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 03:55:13 -0700 Subject: [PATCH 47/68] HOTFIX: str/dex optimizer working now --- js/builder_graph.js | 7 +++---- js/display.js | 1 - js/optimize.js | 30 +++++++++--------------------- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/js/builder_graph.js b/js/builder_graph.js index f4d41ea..5cddf8d 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -223,9 +223,8 @@ class ItemInputNode extends InputNode { for (const [i, x] of zip2(equipment_inputs, replace_items)) { setValue(i, x); } - // NOTE: DO NOT REORDER FOR PERFORMANCE REASONS - for (const node of item_nodes) { node.mark_dirty(); } - for (const node of item_nodes) { node.update(); } + for (const node of item_nodes) { calcSchedule(node, 10); } + return this.compute_func(input_map); } return null; } @@ -942,6 +941,7 @@ let item_nodes = []; let powder_nodes = []; let spelldmg_nodes = []; let edit_input_nodes = []; +let skp_inputs = []; function builder_graph_init() { // Phase 1/2: Set up item input, propagate updates, etc. @@ -1022,7 +1022,6 @@ function builder_graph_init() { edit_id_output = new EditableIDSetterNode(edit_input_nodes); // Makes shallow copy of list. edit_id_output.link_to(build_node); - let skp_inputs = []; for (const skp of skp_order) { const elem = document.getElementById(skp+'-skp'); const node = new SumNumberInputNode('builder-'+skp+'-input', elem); diff --git a/js/display.js b/js/display.js index 11afa6a..8f7017d 100644 --- a/js/display.js +++ b/js/display.js @@ -51,7 +51,6 @@ function displaySetBonuses(parent_id,build) { } mock_item.set("powders", []); displayExpandedItem(mock_item, set_elem.id); - console.log(mock_item); } } diff --git a/js/optimize.js b/js/optimize.js index fdce392..2343d47 100644 --- a/js/optimize.js +++ b/js/optimize.js @@ -19,7 +19,7 @@ function optimizeStrDex() { total_skillpoints[1] += Math.min(max_dex_boost, dex_bonus); // Calculate total 3rd spell damage - let spell = spell_table[player_build.weapon.get("type")][2]; + let spell = spell_table[player_build.weapon.statMap.get("type")][2]; const stats = player_build.statMap; let critChance = skillPointsToPercentage(total_skillpoints[1]); let save_damages = []; @@ -41,8 +41,7 @@ function optimizeStrDex() { if (part.type === "damage") { let _results = calculateSpellDamage(stats, part.conversion, stats.get("sdRaw"), stats.get("sdPct"), - part.multiplier / 100, player_build.weapon, total_skillpoints, - player_build.damageMultiplier, player_build.externalStats); + part.multiplier / 100, player_build.weapon.statMap, total_skillpoints, 1); let totalDamNormal = _results[0]; let totalDamCrit = _results[1]; let results = _results[2]; @@ -75,27 +74,16 @@ function optimizeStrDex() { str_bonus -= 1; dex_bonus += 1; - } - // TODO: reduce duplicated code, @calculateBuild - let skillpoints = player_build.total_skillpoints; - let delta_total = 0; + console.log(best_skillpoints); + + // TODO do not merge for performance reasons for (let i in skp_order) { - let manual_assigned = best_skillpoints[i]; - let delta = manual_assigned - skillpoints[i]; - skillpoints[i] = manual_assigned; - player_build.base_skillpoints[i] += delta; - delta_total += delta; + skp_inputs[i].input_field.value = best_skillpoints[i]; + skp_inputs[i].mark_dirty(); } - player_build.assigned_skillpoints += delta_total; - - try { - calculateBuildStats(); - if (player_build.errored) - throw new ListError(player_build.errors); - } - catch (error) { - handleBuilderError(error); + for (let i in skp_order) { + skp_inputs[i].update(); } } From d8d5b6caf643b6b9abb05e643a4229b7d74a66df Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 07:29:25 -0700 Subject: [PATCH 48/68] Minor code cleanup combine code in corners case combine cases in horiz. drawing --- js/display_atree.js | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/js/display_atree.js b/js/display_atree.js index 0758189..551a233 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -54,18 +54,10 @@ function construct_AT(elem, tree) { document.getElementById("atree-row-" + i).children[node.display.col].appendChild(connector); resolve_connector(document.getElementById("atree-row-" + i).children[node.display.col]); } - // connect left - for (let i = parent_node.display.col + 1; i < node.display.col; i++) { - let connector = connect_elem.cloneNode() - connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; - connector.classList.add("rotate-90"); - connector.id = "r" + parent_node.display.row + "-c" + i + "-line" - document.getElementById("atree-row-" + parent_node.display.row).children[i].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i]); - } - - // connect right - for (let i = parent_node.display.col - 1; i > node.display.col; i--) { + // connect horizontally + let left = Math.min(parent_node.display.col, node.display.col); + let max = Math.max(parent_node.display.col, node.display.col); + for (let i = min + 1; i < max; i++) { let connector = connect_elem.cloneNode() connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; connector.classList.add("rotate-90"); @@ -75,23 +67,18 @@ function construct_AT(elem, tree) { } // connect corners - if (parent_node.display.col > node.display.col && (parent_node.display.row != node.display.row)) { - let connector = connect_elem.cloneNode() - connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; - connector.classList.add("rotate-180"); - connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" - document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); - } - if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { - let connector = connect_elem.cloneNode() - connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; - connector.classList.add("rotate-270"); - connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" - document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; + connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" + document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); + if (parent_node.display.col > node.display.col && (parent_node.display.row != node.display.row)) { + connector.classList.add("rotate-180"); } + else {//if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { + connector.classList.add("rotate-270"); + } + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); } // create node From e0f2dde9535d896016bcb79ee8c10912f91b2977 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 07:42:58 -0700 Subject: [PATCH 49/68] Fix typo --- js/display_atree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/display_atree.js b/js/display_atree.js index 551a233..162488e 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -55,7 +55,7 @@ function construct_AT(elem, tree) { resolve_connector(document.getElementById("atree-row-" + i).children[node.display.col]); } // connect horizontally - let left = Math.min(parent_node.display.col, node.display.col); + let min = Math.min(parent_node.display.col, node.display.col); let max = Math.max(parent_node.display.col, node.display.col); for (let i = min + 1; i < max; i++) { let connector = connect_elem.cloneNode() From b5f05cb1e6b23ff6187e0a8134d346c415c67db5 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 07:43:32 -0700 Subject: [PATCH 50/68] Whoops -- missing case --- js/display_atree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/display_atree.js b/js/display_atree.js index 162488e..85c2872 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -75,7 +75,7 @@ function construct_AT(elem, tree) { if (parent_node.display.col > node.display.col && (parent_node.display.row != node.display.row)) { connector.classList.add("rotate-180"); } - else {//if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { + else if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { connector.classList.add("rotate-270"); } resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); From c42e96d3607f62165330c5828e3807fa1a1e59a9 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 07:53:55 -0700 Subject: [PATCH 51/68] Finally fixed bugs -- please test your code before pushing bad hpp --- js/display_atree.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/js/display_atree.js b/js/display_atree.js index 85c2872..8d2cf7a 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -68,17 +68,19 @@ function construct_AT(elem, tree) { // connect corners - let connector = connect_elem.cloneNode() - connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; - connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" - document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); - if (parent_node.display.col > node.display.col && (parent_node.display.row != node.display.row)) { - connector.classList.add("rotate-180"); + if (parent_node.display.row != node.display.row && parent_node.display.col != node.display.col) { + let connector = connect_elem.cloneNode() + connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; + connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" + document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); + if (parent_node.display.col > node.display.col) { + connector.classList.add("rotate-180"); + } + else {// if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { + connector.classList.add("rotate-270"); + } + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); } - else if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { - connector.classList.add("rotate-270"); - } - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); } // create node From d68af2f4c4ef837281e7f124b4d1fbc5a69fa4a5 Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 23 Jun 2022 22:00:04 +0700 Subject: [PATCH 52/68] fix archer tree --- js/atree_constants.js | 175 ++++++++++++++++++++++++------------------ 1 file changed, 102 insertions(+), 73 deletions(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 41cfb16..55e0032 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -1,15 +1,15 @@ const atrees = { "Archer": [ - { + { "display_name": "Arrow Shield", "desc": "Create a shield around you that deal damage and knockback mobs when triggered. (2 Charges)", - "archetype": "", - "archetype_req": 0, - "parents": ["Power Shots", "Cheaper Escape"], + "archetype": "", + "archetype_req": 0, + "parents": ["Power Shots", "Cheaper Escape"], "dependencies": [], "blockers": [], - "cost": 1, + "cost": 1, "display": { "row": 9, "col": 6 @@ -18,17 +18,17 @@ const atrees = "duration": 60 }, "effects": [ - { + { "type": "replace_spell", "name": "Arrow Shield", "cost": 30, - "display_text": "Max Damage", - "base_spell": 4, - "spell_type": "damage", + "display_text": "Max Damage", + "base_spell": 4, + "spell_type": "damage", "scaling": "spell", - "display": "", + "display": "", "parts": [ - { + { "name": "Shield Damage", "type": "damage", "multipliers": [90, 0, 0, 0, 0, 10] @@ -44,7 +44,7 @@ const atrees = } ] }, - + { "display_name": "Escape", "desc": "Throw yourself backward to avoid danger. (Hold shift while escaping to cancel)", @@ -422,7 +422,7 @@ const atrees = "desc": "Arrow Storm shoot +1 stream of arrows, effectively doubling its damage.", "archetype": "", "archetype_req": 0, - "parents": ["Guardian Angels"], + "parents": ["Guardian Angels", "Cheaper Arrow Storm"], "dependencies": [], "blockers": ["Phantom Ray"], "cost": 2, @@ -480,7 +480,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 23, + "row": 22, "col": 6 }, "properties": {}, @@ -499,12 +499,12 @@ const atrees = "desc": "When you have 2+ Focus, holding shift will summon the Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage)", "archetype": "Sharpshooter", "archetype_req": 4, - "parents": ["More Focus"], + "parents": ["More Focus", "Traveler"], "dependencies": ["Focus"], "blockers": [], "cost": 2, "display": { - "row": 26, + "row": 25, "col": 4 }, "properties": { @@ -542,7 +542,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 27, + "row": 26, "col": 1 }, "properties": { @@ -577,7 +577,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 27, + "row": 26 , "col": 5 }, "properties": { @@ -624,7 +624,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 29, + "row": 28, "col": 4 }, "properties": { @@ -648,7 +648,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 29, + "row": 28, "col": 8 }, "properties": { @@ -675,7 +675,7 @@ const atrees = "blockers": ["Grappling Hook"], "cost": 2, "display": { - "row": 32, + "row": 31, "col": 0 }, "properties": { @@ -695,12 +695,12 @@ const atrees = "desc": "If you do not damage an enemy for 5s or more, your next sucessful hit will deal +50% damage and add +1 Focus.", "archetype": "Sharpshooter", "archetype_req": 5, - "parents": ["Shocking Bomb", "Better Arrow Shield"], + "parents": ["Shocking Bomb", "Better Arrow Shield", "Cheaper Arrow Storm (2)"], "dependencies": ["Focus"], "blockers": [], "cost": 2, "display": { - "row": 32, + "row": 31, "col": 5 }, "properties": { @@ -721,12 +721,12 @@ const atrees = "desc": "Arrow Shield summon a Hound that will attack and drag aggressive enemies towards your traps.", "archetype": "Trapper", "archetype_req": 0, - "parents": ["Initiator", "Cheaper Arrow Storm"], + "parents": ["Initiator", "Cheaper Arrow Storm (2)"], "dependencies": ["Arrow Shield"], "blockers": [], "cost": 2, "display": { - "row": 33, + "row": 32, "col": 7 }, "properties": { @@ -751,7 +751,7 @@ const atrees = "blockers": ["Phantom Ray"], "cost": 2, "display": { - "row": 34, + "row": 33, "col": 0 }, "properties": {}, @@ -777,7 +777,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 38, + "row": 37, "col": 1 }, "properties": { @@ -803,7 +803,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 38, + "row": 37, "col": 4 }, "properties": { @@ -849,12 +849,12 @@ const atrees = "desc": "Arrow bomb will throw 3 additional smaller bombs when exploding.", "archetype": "", "archetype_req": 0, - "parents": ["Grappling Hook", "More Shields"], + "parents": ["Cheaper Escape (2)"], "dependencies": [], "blockers": [], "cost": 2, "display": { - "row": 38, + "row": 37, "col": 7 }, "properties": { @@ -881,7 +881,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 39, + "row": 38, "col": 6 }, "properties": { @@ -902,13 +902,13 @@ const atrees = "desc": "Enemies near you will be slowed down.", "archetype": "", "archetype_req": 0, - "parents": ["Geyser Stomp", "More Focus"], + "parents": ["Geyser Stomp", "More Focus (2)"], "dependencies": [], "blockers": [], "cost": 2, "display": { - "row": 40, - "col": 4 + "row": 39, + "col": 2 }, "properties": { "range": 2.5, @@ -925,7 +925,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 41, + "row": 40, "col": 1 }, "properties": { @@ -956,12 +956,12 @@ const atrees = "desc": "Allow you to place +6 Traps, but with reduced damage and range.", "archetype": "Trapper", "archetype_req": 10, - "parents": ["Grape Bomb", "Cheaper Arrow Bomb"], + "parents": ["Grape Bomb", "Cheaper Arrow Bomb (2)"], "dependencies": ["Basaltic Trap"], "blockers": [], "cost": 2, "display": { - "row": 41, + "row": 40, "col": 7 }, "properties": { @@ -1299,7 +1299,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 26, + "row": 25, "col": 0 }, "properties": {}, @@ -1323,7 +1323,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 27, + "row": 26, "col": 8 }, "properties": { @@ -1340,7 +1340,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 29, + "row": 28, "col": 6 }, "properties": { @@ -1365,7 +1365,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 30, + "row": 29, "col": 1 }, "properties": { @@ -1382,7 +1382,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 32, + "row": 31, "col": 2 }, "properties": { @@ -1409,7 +1409,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 32, + "row": 31, "col": 8 }, "properties": { @@ -1427,12 +1427,12 @@ const atrees = "desc": "+30% Critical Hit Damage", "archetype": "", "archetype_req": 0, - "parents": ["Arrow Bomb"], + "parents": ["Better Guardian Angels", "Cheaper Arrow Shield", "Arrow Hurricane"], "dependencies": [], "blockers": [], "cost": 1, "display": { - "row": 34, + "row": 33, "col": 2 }, "properties": { @@ -1452,7 +1452,7 @@ const atrees = ] }, { - "display_name": "Cheaper Arrow Shield (2)", + "display_name": "Cheaper Arrow Shield", "desc": "Reduce the Mana cost of Arrow Shield.", "archetype": "", "archetype_req": 0, @@ -1461,7 +1461,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 34, + "row": 33, "col": 4 }, "properties": { @@ -1479,12 +1479,12 @@ const atrees = "desc": "Arrow Bomb's self-damage will knockback you farther away.", "archetype": "", "archetype_req": 0, - "parents": ["Cheaper Arrow Storm", "Initiator"], + "parents": ["Cheaper Arrow Storm (2)", "Initiator"], "dependencies": ["Arrow Bomb"], "blockers": [], "cost": 1, "display": { - "row": 34, + "row": 33, "col": 6 }, "properties": { @@ -1495,12 +1495,12 @@ const atrees = "desc": "Reduce the Mana cost of Escape.", "archetype": "", "archetype_req": 0, - "parents": ["Arrow Storm", "Arrow Shield"], + "parents": ["Call of the Hound", "Decimator"], "dependencies": [], "blockers": [], "cost": 1, "display": { - "row": 35, + "row": 34, "col": 7 }, "properties": { @@ -1519,12 +1519,12 @@ const atrees = "desc": "Increase your Grappling Hook's range, speed and strength.", "archetype": "Trapper", "archetype_req": 5, - "parents": ["Cheaper Escape"], + "parents": ["Cheaper Escape (2)"], "dependencies": ["Grappling Hook"], "blockers": [], "cost": 1, "display": { - "row": 36, + "row": 35, "col": 8 }, "properties": { @@ -1541,7 +1541,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 41, + "row": 40, "col": 5 }, "properties": { @@ -1565,7 +1565,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 20, + "row": 25, "col": 7 }, "properties": { @@ -1593,7 +1593,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 23, + "row": 28, "col": 2 }, "properties": { @@ -1613,7 +1613,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 27, + "row": 34, "col": 1 }, "properties": { @@ -1639,7 +1639,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 30, + "row": 38, "col": 0 }, "properties": { @@ -1685,7 +1685,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 14, + "row": 17, "col": 0 }, "properties": { @@ -1731,7 +1731,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 15, + "row": 19, "col": 4 }, "properties": { @@ -1762,7 +1762,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 18, + "row": 22, "col": 4 }, "properties": { @@ -1788,12 +1788,12 @@ const atrees = "desc": "Add +2 max Focus", "archetype": "Sharpshooter", "archetype_req": 0, - "parents": ["Crepuscular Ray"], + "parents": ["Crepuscular Ray", "Snow Storm"], "dependencies": [], "blockers": [], "cost": 1, "display": { - "row": 32, + "row": 39, "col": 4 }, "properties": { @@ -1824,7 +1824,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 20, + "row": 25, "col": 2 }, "properties": { @@ -1859,7 +1859,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 17, + "row": 22, "col": 8 }, "properties": { @@ -1879,7 +1879,7 @@ const atrees = "blockers": [], "cost": 1, "display": { - "row": 31, + "row": 38, "col": 8 }, "properties": { @@ -1899,7 +1899,7 @@ const atrees = "blockers": [], "cost": 2, "display": { - "row": 14, + "row": 17, "col": 2 }, "properties": { @@ -1985,6 +1985,35 @@ const atrees = "multipliers": [120, 0, 0, 0, 0, 80] } ] + }, + { + "display_name": "Decimator", + "desc": "Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%)", + "archetype": "Sharpshooter", + "archetype_req": 0, + "parents": ["Cheaper Arrow Shield"], + "dependencies": ["Phantom Ray"], + "blockers": [], + "cost": 1, + "display": { + "row": 34, + "col": 5 + }, + "properties": { + }, + "effects": [ + { + "type": "stat_scaling", + "slider": true, + "slider_name": "Phantom Ray hits", + "output": { + "type": "stat", + "name": "PhRayDmg" + }, + "scaling": 10, + "max": 50 + } + ] } ], @@ -2341,13 +2370,13 @@ const atrees = "desc": "After using Charge, violently crash down into the ground and deal damage", "archetype": "", "archetype_req": 0, - "parents": ["Charge"], + "parents": ["Uppercut"], "dependencies": [], "blockers": [], "cost": 1, "display": { - "row": 6, - "col": 2 + "row": 9, + "col": 1 }, "properties": { "aoe": 4 @@ -2368,13 +2397,13 @@ const atrees = "desc": "For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%)", "archetype": "Fallen", "archetype_req": 0, - "parents": ["Heavy Impact"], + "parents": ["Charge"], "dependencies": [], "blockers": ["Tougher Skin"], "cost": 1, "display": { - "row": 7, - "col": 0 + "row": 6, + "col": 2 }, "properties": { }, From 39bbf45b12c0d57af2afd25a0dcb284987bf85af Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 23 Jun 2022 22:00:15 +0700 Subject: [PATCH 53/68] change connector logic --- js/display_atree.js | 81 +++++++++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/js/display_atree.js b/js/display_atree.js index 0758189..19e3f22 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -14,28 +14,36 @@ function construct_AT(elem, tree) { let node = tree[i]; // create rows if not exist - if (document.getElementById("atree-row-" + node.display.row) == null) { - for (let j = 0; j <= node.display.row; j++) { - if (document.getElementById("atree-row-" + j) == null) { - let row = document.createElement('div'); - row.classList.add("row"); - row.id = "atree-row-" + j; - //was causing atree rows to be 0 height - console.log(elem.scrollWidth / 9); - row.style.minHeight = elem.scrollWidth / 9 + "px"; - //row.style.minHeight = elem.getBoundingClientRect().width / 9 + "px"; + let missing_rows = [node.display.row]; + + for (let parent of node.parents) { + missing_rows.push(tree.find(object => {return object.display_name === parent;}).display.row); + } + for (let missing_row of missing_rows) { + if (document.getElementById("atree-row-" + missing_row) == null) { + for (let j = 0; j <= missing_row; j++) { + if (document.getElementById("atree-row-" + j) == null) { + let row = document.createElement('div'); + row.classList.add("row"); + row.id = "atree-row-" + j; + //was causing atree rows to be 0 height + console.log(elem.scrollWidth / 9); + row.style.minHeight = elem.scrollWidth / 9 + "px"; + //row.style.minHeight = elem.getBoundingClientRect().width / 9 + "px"; - for (let k = 0; k < 9; k++) { - col = document.createElement('div'); - col.classList.add('col', 'px-0'); - col.style.minHeight = elem.scrollWidth / 9 + "px"; - row.appendChild(col); + for (let k = 0; k < 9; k++) { + col = document.createElement('div'); + col.classList.add('col', 'px-0'); + col.style.minHeight = elem.scrollWidth / 9 + "px"; + row.appendChild(col); + }; + elem.appendChild(row); }; - elem.appendChild(row); }; }; - }; + } + let connector_list = [] // create connectors based on parent location @@ -52,7 +60,7 @@ function construct_AT(elem, tree) { connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; connector.id = "r" + i + "-c" + node.display.col + "-line" document.getElementById("atree-row-" + i).children[node.display.col].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + i).children[node.display.col]); + resolve_connector(document.getElementById("atree-row-" + i).children[node.display.col], node); } // connect left for (let i = parent_node.display.col + 1; i < node.display.col; i++) { @@ -61,7 +69,7 @@ function construct_AT(elem, tree) { connector.classList.add("rotate-90"); connector.id = "r" + parent_node.display.row + "-c" + i + "-line" document.getElementById("atree-row-" + parent_node.display.row).children[i].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i]); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i], node); } // connect right @@ -71,7 +79,7 @@ function construct_AT(elem, tree) { connector.classList.add("rotate-90"); connector.id = "r" + parent_node.display.row + "-c" + i + "-line" document.getElementById("atree-row-" + parent_node.display.row).children[i].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i]); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i], node); } // connect corners @@ -81,7 +89,7 @@ function construct_AT(elem, tree) { connector.classList.add("rotate-180"); connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col], node); } if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { @@ -90,7 +98,7 @@ function construct_AT(elem, tree) { connector.classList.add("rotate-270"); connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col], node); } } @@ -165,11 +173,12 @@ function construct_AT(elem, tree) { }; // resolve connector conflict -function resolve_connector(elem) { +function resolve_connector(elem, node) { if (elem.children.length < 2) {return false;} let line = 0; let angle = 0; - let t = 0 + let t = 0; + let c = 0; for (let child of elem.children) { let type = child.id.split("-")[2] if (type == "line") { @@ -178,22 +187,36 @@ function resolve_connector(elem) { angle += 1; } else if (type == "t") { t += 1; + } else if (type == "c") { + c += 1; } } let connect_elem = document.createElement("div"); - if ((line == angle) || (angle == 1 && t == 1)) { + //if ((line == 1 && angle == 1) || (line == 2 && angle == 2) || (line == 1 && angle == 2) || (angle == 1 && t == 1)) { + // connect_elem.style = "background-image: url('../media/atree/connect_t.png'); background-size: cover; width: 100%; height: 100%;" + // connect_elem.classList.add("rotate-180") + // connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-t" + // elem.replaceChildren(connect_elem); + //} + //if (line > 1 && angle == 0) { + // elem.replaceChildren(elem.children[0]) + //} + //if (t == 1 && line == 1) { + // connect_elem.style = "background-image: url('../media/atree/connect_c.png'); background-size: cover; width: 100%; height: 100%;" + // elem.replaceChildren(connect_elem); + //} + if ((line == 1 && angle == 1)) { connect_elem.style = "background-image: url('../media/atree/connect_t.png'); background-size: cover; width: 100%; height: 100%;" connect_elem.classList.add("rotate-180") connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-t" elem.replaceChildren(connect_elem); } - if (line > 1 && angle == 0) { - elem.replaceChildren(elem.children[0]) - } - if (t == 1 && line == 1) { + if (node.parents.length == 3 && t == 1) { connect_elem.style = "background-image: url('../media/atree/connect_c.png'); background-size: cover; width: 100%; height: 100%;" + connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-c" elem.replaceChildren(connect_elem); } + elem.replaceChildren(elem.children[0]) } From bb98b2c5e43f382e0063e5622fc3cf818d951a49 Mon Sep 17 00:00:00 2001 From: reschan Date: Thu, 23 Jun 2022 22:11:47 +0700 Subject: [PATCH 54/68] fix archer tree (2) --- js/display_atree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/display_atree.js b/js/display_atree.js index 6e072cb..86d776b 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -87,7 +87,7 @@ function construct_AT(elem, tree) { else {// if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { connector.classList.add("rotate-270"); } - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col]); + resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col], node); } } From fe3445c16b1558785cfc52475b732a8543b215ed Mon Sep 17 00:00:00 2001 From: reschan Date: Fri, 24 Jun 2022 09:36:50 +0700 Subject: [PATCH 55/68] fix: auto connector logic --- js/display_atree.js | 116 +++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/js/display_atree.js b/js/display_atree.js index 86d776b..82b2a94 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -1,4 +1,5 @@ -// placeholder name, follow new schema +let atree_map; +let atree_connectors_map; function construct_AT(elem, tree) { console.log("constructing ability tree UI"); document.getElementById("atree-active").innerHTML = ""; //reset all atree actives - should be done in a more general way later @@ -10,6 +11,12 @@ function construct_AT(elem, tree) { active_row.textContent = "Active:"; document.getElementById("atree-active").appendChild(active_row); + atree_map = new Map(); + atree_connectors_map = new Map() + for (let i of tree) { + atree_map.set(i.display_name, {display: i.display, parents: i.parents, connectors: []}); + } + for (let i = 0; i < tree.length; i++) { let node = tree[i]; @@ -31,12 +38,13 @@ function construct_AT(elem, tree) { row.style.minHeight = elem.scrollWidth / 9 + "px"; //row.style.minHeight = elem.getBoundingClientRect().width / 9 + "px"; - for (let k = 0; k < 9; k++) { col = document.createElement('div'); col.classList.add('col', 'px-0'); col.style.minHeight = elem.scrollWidth / 9 + "px"; row.appendChild(col); + + atree_connectors_map.set(j + "," + k, []) }; elem.appendChild(row); }; @@ -45,49 +53,47 @@ function construct_AT(elem, tree) { } - let connector_list = [] + let connector_list = []; // create connectors based on parent location for (let parent of node.parents) { - let parent_node = tree.find(object => { - return object.display_name === parent; - }); + let parent_node = atree_map.get(parent); let connect_elem = document.createElement("div"); connect_elem.style = "background-size: cover; width: 100%; height: 100%;"; // connect up for (let i = node.display.row - 1; i > parent_node.display.row; i--) { - let connector = connect_elem.cloneNode() + let connector = connect_elem.cloneNode(); connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; - connector.id = "r" + i + "-c" + node.display.col + "-line" - document.getElementById("atree-row-" + i).children[node.display.col].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + i).children[node.display.col], node); + atree_map.get(node.display_name).connectors.push(i + "," + node.display.col); + atree_connectors_map.get(i + "," + node.display.col).push({connector: connector, type: "line"}); + resolve_connector(i + "," + node.display.col, node); } // connect horizontally let min = Math.min(parent_node.display.col, node.display.col); let max = Math.max(parent_node.display.col, node.display.col); for (let i = min + 1; i < max; i++) { - let connector = connect_elem.cloneNode() + let connector = connect_elem.cloneNode(); connector.style.backgroundImage = "url('../media/atree/connect_line.png')"; connector.classList.add("rotate-90"); - connector.id = "r" + parent_node.display.row + "-c" + i + "-line" - document.getElementById("atree-row-" + parent_node.display.row).children[i].appendChild(connector); - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[i], node); + atree_map.get(node.display_name).connectors.push(parent_node.display.row + "," + i); + atree_connectors_map.get(parent_node.display.row + "," + i).push({connector: connector, type: "line"}); + resolve_connector(parent_node.display.row + "," + i, node); } // connect corners if (parent_node.display.row != node.display.row && parent_node.display.col != node.display.col) { - let connector = connect_elem.cloneNode() + let connector = connect_elem.cloneNode(); connector.style.backgroundImage = "url('../media/atree/connect_angle.png')"; - connector.id = "r" + parent_node.display.row + "-c" + node.display.col + "-angle" - document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col].appendChild(connector); + atree_map.get(node.display_name).connectors.push(parent_node.display.row + "," + node.display.col); + atree_connectors_map.get(parent_node.display.row + "," + node.display.col).push({connector: connector, type: "angle"}); if (parent_node.display.col > node.display.col) { connector.classList.add("rotate-180"); } else {// if (parent_node.display.col < node.display.col && (parent_node.display.row != node.display.row)) { connector.classList.add("rotate-270"); } - resolve_connector(document.getElementById("atree-row-" + parent_node.display.row).children[node.display.col], node); + resolve_connector(parent_node.display.row + "," + node.display.col, node); } } @@ -159,53 +165,55 @@ function construct_AT(elem, tree) { }); document.getElementById("atree-row-" + node.display.row).children[node.display.col].appendChild(node_elem); }; + + atree_render_connection(); }; // resolve connector conflict -function resolve_connector(elem, node) { - if (elem.children.length < 2) {return false;} - let line = 0; - let angle = 0; - let t = 0; - let c = 0; - for (let child of elem.children) { - let type = child.id.split("-")[2] - if (type == "line") { - line += 1; - } else if (type == "angle") { - angle += 1; - } else if (type == "t") { - t += 1; - } else if (type == "c") { - c += 1; +function resolve_connector(pos, node) { + if (atree_connectors_map.get(pos).length < 2) {return false;} + + let line = false; + let angle = false; + let t = false; + for (let i of atree_connectors_map.get(pos)) { + if (i.type == "line") { + line += true; + } else if (i.type == "angle") { + angle += true; + } else if (i.type == "t") { + t += true; } } let connect_elem = document.createElement("div"); - //if ((line == 1 && angle == 1) || (line == 2 && angle == 2) || (line == 1 && angle == 2) || (angle == 1 && t == 1)) { - // connect_elem.style = "background-image: url('../media/atree/connect_t.png'); background-size: cover; width: 100%; height: 100%;" - // connect_elem.classList.add("rotate-180") - // connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-t" - // elem.replaceChildren(connect_elem); - //} - //if (line > 1 && angle == 0) { - // elem.replaceChildren(elem.children[0]) - //} - //if (t == 1 && line == 1) { - // connect_elem.style = "background-image: url('../media/atree/connect_c.png'); background-size: cover; width: 100%; height: 100%;" - // elem.replaceChildren(connect_elem); - //} - if ((line == 1 && angle == 1)) { + if ((line && angle)) { connect_elem.style = "background-image: url('../media/atree/connect_t.png'); background-size: cover; width: 100%; height: 100%;" connect_elem.classList.add("rotate-180") - connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-t" - elem.replaceChildren(connect_elem); + atree_connectors_map.set(pos, [{connector: connect_elem, type: "t"}]) } - if (node.parents.length == 3 && t == 1) { + if (node.parents.length == 3 && t && atree_same_row(node)) { connect_elem.style = "background-image: url('../media/atree/connect_c.png'); background-size: cover; width: 100%; height: 100%;" - connect_elem.id = elem.children[0].id.split("-")[0] + "-" + elem.children[0].id.split("-")[1] + "-c" - elem.replaceChildren(connect_elem); + atree_connectors_map.set(pos, [{connector: connect_elem, type: "c"}]) + } + // override the conflict with the first children + atree_connectors_map.set(pos, [atree_connectors_map.get(pos)[0]]) +} + +// check if a node doesn't have same row w/ its parents (used to solve conflict) +function atree_same_row(node) { + for (let i of node.parents) { + if (node.display.row == atree_map.get(i).display.row) { return false; } + } + return true; +} + +// draw the connector onto the screen +function atree_render_connection() { + for (let i of atree_connectors_map.keys()) { + if (atree_connectors_map.get(i).length != 0) { + document.getElementById("atree-row-" + i.split(",")[0]).children[i.split(",")[1]].appendChild(atree_connectors_map.get(i)[0].connector) + } } - elem.replaceChildren(elem.children[0]) } From debbeee9f81faaffd94ea517d4a56239d52735c2 Mon Sep 17 00:00:00 2001 From: reschan Date: Fri, 24 Jun 2022 09:49:24 +0700 Subject: [PATCH 56/68] for reference when creating beta atree data --- js/atree_constants_old.js | 171 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 js/atree_constants_old.js diff --git a/js/atree_constants_old.js b/js/atree_constants_old.js new file mode 100644 index 0000000..e325247 --- /dev/null +++ b/js/atree_constants_old.js @@ -0,0 +1,171 @@ +const atrees_old = { + "Assassin": [ + {"title": "Spin Attack", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 1, "col": 4}, + {"title": "Dagger Proficiency I", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 2, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 3, "col": 4}, + {"title": "Double Spin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 4, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 5, "col": 4}, + {"title": "Dash", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 6, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 6, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 7, "col": 2}, + {"title": "Smoke Bomb", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 7, "col": 6}, + {"title": "Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 1}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 8, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 6}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 8, "col": 8}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 8}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 8}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 10, "col": 1}, + {"title": "Backstab", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 90, "row": 10, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 10, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 10, "col": 7}, + {"title": "Fatality", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 11, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 0}, + {"title": "Violent Vortex", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 11, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 2}, + {"title": "Vanish", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 13, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 13, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 7}, + {"title": "Lacerate", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 15, "col": 1}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 1}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 15, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 5}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 15, "col": 8}, + {"title": "Wall of Smoke", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 8}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 16, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 0}, + {"title": "Silent Killer", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 16, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 2}, + {"title": "Shadow Travel", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 5}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 8}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 8}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 4}, + {"title": "Exploding Clones", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 19, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 18, "col": 6}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 7}, + {"title": "Weightless", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 7}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 20, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 20, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 1}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 20, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 4}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 20, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 21, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 6}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 20, "col": 8}, + {"title": "Dancing Blade", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 8}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 21, "col": 0}, + {"title": "Spin Attack Damage", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 22, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 21, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 22, "col": 3}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 22, "col": 1}, + {"title": "Marked", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 22, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 23, "col": 5}, + {"title": "Shurikens", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 6}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 23, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 22, "col": 8}, + {"title": "Far Reach", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 8}, + {"title": "Stronger Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 5}, + {"title": "Psithurism", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 24, "col": 1}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 1}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 3}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 24, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 4}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 25, "col": 5}, + {"title": "Choke Bomb", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 6}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 25, "col": 7}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 8}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 26, "col": 5}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 25, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 0}, + {"title": "Death Magnet", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 25, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 2}, + {"title": "Cheaper Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 2}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 7}, + {"title": "Parry", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 7}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 27, "col": 1}, + {"title": "Fatal Spin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 1}, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 27, "col": 3}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 3}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 27, "col": 6}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 6}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 27, "col": 8}, + {"title": "Wall Jump", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 8}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 28, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 29, "col": 0}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 1}, + {"title": "Harvester", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 28, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 4}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 28, "col": 7}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 7}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 7 }, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 30, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 2 }, + {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 30, "col": 5}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 30, "col": 6}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 5}, + {"title": "Ricochet", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 8}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 1}, + {"title": "Satsujin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 1}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 4}, + {"title": "Forbidden Art", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 4}, + {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 7}, + {"title": "Jasmine Bloom", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 7}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 32, "col": 0}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 0}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 32, "col": 2}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 2}, + {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 32, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 5}, + {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 8}, + ] +} \ No newline at end of file From 32ee15dc2b023cb0fe3c22026c5cb587087e0404 Mon Sep 17 00:00:00 2001 From: reschan Date: Fri, 24 Jun 2022 09:49:54 +0700 Subject: [PATCH 57/68] delete empty atrees, move assassin to old --- js/atree_constants.js | 172 ------------------------------------------ 1 file changed, 172 deletions(-) diff --git a/js/atree_constants.js b/js/atree_constants.js index 55e0032..add793f 100644 --- a/js/atree_constants.js +++ b/js/atree_constants.js @@ -2016,176 +2016,6 @@ const atrees = ] } ], - - "Assassin": [ - {"title": "Spin Attack", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 0, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 1, "col": 4}, - {"title": "Dagger Proficiency I", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 2, "col": 3}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 2, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 3, "col": 4}, - {"title": "Double Spin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 4, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 5, "col": 4}, - {"title": "Dash", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 6, "col": 3}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 6, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 6, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 7, "col": 2}, - {"title": "Smoke Bomb", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 7, "col": 6}, - {"title": "Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 3}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 8, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 1}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 8, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 0}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 2}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 6}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 8, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 8, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 8}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 10, "col": 8}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 10, "col": 1}, - {"title": "Backstab", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 9, "col": 4}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 90, "row": 10, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 10, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 4}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 10, "col": 7}, - {"title": "Fatality", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 11, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 11, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 0}, - {"title": "Violent Vortex", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 0}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 11, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 2}, - {"title": "Vanish", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 13, "col": 3}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 13, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 2}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 12, "col": 7}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 13, "col": 7}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 14, "col": 7}, - {"title": "Lacerate", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 15, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 15, "col": 1}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 1}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 15, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 5}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 15, "col": 8}, - {"title": "Wall of Smoke", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 16, "col": 8}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 16, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 0}, - {"title": "Silent Killer", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 0}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 16, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 2}, - {"title": "Shadow Travel", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 5}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 17, "col": 8}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 18, "col": 8}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 4}, - {"title": "Exploding Clones", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 19, "col": 4}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 3}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 0}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 3}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 3}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 18, "col": 6}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 18, "col": 7}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 19, "col": 7}, - {"title": "Weightless", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 20, "col": 7}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 20, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 20, "col": 2}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 1}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 20, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 4}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 20, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 21, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 6}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 20, "col": 8}, - {"title": "Dancing Blade", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 21, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 21, "col": 0}, - {"title": "Spin Attack Damage", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 22, "col": 0}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 21, "col": 3}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 22, "col": 3}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 22, "col": 1}, - {"title": "Marked", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 22, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 23, "col": 5}, - {"title": "Shurikens", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 6}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 23, "col": 7}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 22, "col": 8}, - {"title": "Far Reach", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 23, "col": 8}, - {"title": "Stronger Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 5}, - {"title": "Psithurism", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 24, "col": 7}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 24, "col": 1}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 1}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 3}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 24, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 4}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 25, "col": 5}, - {"title": "Choke Bomb", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 6}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 25, "col": 7}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 25, "col": 8}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 26, "col": 5}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 25, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 0}, - {"title": "Death Magnet", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 0}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 25, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 2}, - {"title": "Cheaper Multihit", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 2}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 26, "col": 7}, - {"title": "Parry", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 27, "col": 7}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 27, "col": 1}, - {"title": "Fatal Spin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 1}, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 27, "col": 3}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 3}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 27, "col": 6}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 6}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 27, "col": 8}, - {"title": "Wall Jump", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 28, "col": 8}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 28, "col": 0}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 29, "col": 0}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 1}, - {"title": "Harvester", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 28, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 4}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 28, "col": 7}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 29, "col": 7}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 30, "col": 7 }, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 30, "col": 2}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 2 }, - {"image": "../media/atree/connect_t.png", "connector": true, "rotate": 180, "row": 30, "col": 5}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 90, "row": 30, "col": 6}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 5}, - {"title": "Ricochet", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 31, "col": 8}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 1}, - {"title": "Satsujin", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 1}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 4}, - {"title": "Forbidden Art", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 4}, - {"image": "../media/atree/connect_line.png", "connector": true, "rotate": 0, "row": 31, "col": 7}, - {"title": "Jasmine Bloom", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 32, "col": 7}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 180, "row": 32, "col": 0}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 0}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 32, "col": 2}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 2}, - {"image": "../media/atree/connect_angle.png", "connector": true, "rotate": 270, "row": 32, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 5}, - {"title": "Text", "desc": "desc", "image": "../media/atree/node.png", "connector": false, "row": 33, "col": 8}, - ], "Warrior": [ { "display_name": "Bash", @@ -4195,8 +4025,6 @@ const atrees = ] } ], - "Mage": [], - "Shaman": [] } const atree_example = [ From cba1bb5eb09baca8095e1a5fecfa46b01687975a Mon Sep 17 00:00:00 2001 From: reschan Date: Fri, 24 Jun 2022 09:50:18 +0700 Subject: [PATCH 58/68] catch if given atree does not exist --- js/display_atree.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/display_atree.js b/js/display_atree.js index 82b2a94..8f0c08f 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -4,6 +4,8 @@ function construct_AT(elem, tree) { console.log("constructing ability tree UI"); document.getElementById("atree-active").innerHTML = ""; //reset all atree actives - should be done in a more general way later elem.innerHTML = ""; //reset the atree in the DOM + + if (tree === undefined) {return false;} // add in the "Active" title to atree let active_row = document.createElement("div"); From e86152af181b7b82f819c317648db05ab22dd09a Mon Sep 17 00:00:00 2001 From: reschan Date: Fri, 24 Jun 2022 09:51:43 +0700 Subject: [PATCH 59/68] delete nonexist function call --- js/display_atree.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/display_atree.js b/js/display_atree.js index 8f0c08f..0c5254a 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -162,8 +162,6 @@ function construct_AT(elem, tree) { this.classList.add("atree-selected"); this.style.backgroundImage = 'url("../media/atree/node-selected.png")'; } - - toggle_connectors(connector_list); }); document.getElementById("atree-row-" + node.display.row).children[node.display.col].appendChild(node_elem); }; From da97f593abd0b3face2b56e5aec4af206629e03e Mon Sep 17 00:00:00 2001 From: reschan Date: Fri, 24 Jun 2022 10:01:31 +0700 Subject: [PATCH 60/68] remove old script --- py_script/atree_csv_to_json.py | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 py_script/atree_csv_to_json.py diff --git a/py_script/atree_csv_to_json.py b/py_script/atree_csv_to_json.py deleted file mode 100644 index 4c63d06..0000000 --- a/py_script/atree_csv_to_json.py +++ /dev/null @@ -1,24 +0,0 @@ -import csv -import json -import re - -with open('atree.csv', newline='') as csvfile: - res = "" - reader = csv.DictReader(csvfile) - for row in reader: - if not row["connector"]: - row["connector"] = False - else: - row["connector"] = True - row["row"] = int(row["row"]) - row["col"] = int(row["col"]) - if row["rotate"].isdigit(): - row["rotate"] = int(row["rotate"]) - else: - row.pop("rotate") - row["desc"] = re.sub("\n", " ", row["desc"]) - - resjson = json.dumps(row) - res += str(resjson) + ",\n" - - print(res) From beb47c7f3d361a817b5b1bfdd162b02988eb160e Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 20:01:31 -0700 Subject: [PATCH 61/68] Minified atree constants file; remove unused (old) script --- builder/index.html | 2 +- js/atree_constants_min.js | 2 ++ py_script/atree_csv_to_json.py | 24 ------------------------ 3 files changed, 3 insertions(+), 25 deletions(-) create mode 100644 js/atree_constants_min.js delete mode 100644 py_script/atree_csv_to_json.py diff --git a/builder/index.html b/builder/index.html index b0c21b7..1a14ed3 100644 --- a/builder/index.html +++ b/builder/index.html @@ -1404,7 +1404,7 @@ - + diff --git a/js/atree_constants_min.js b/js/atree_constants_min.js new file mode 100644 index 0000000..9379944 --- /dev/null +++ b/js/atree_constants_min.js @@ -0,0 +1,2 @@ +// Minified version of js/atree_constants.js +const atrees={Archer:[{display_name:"Arrow Shield",desc:"Create a shield around you that deal damage and knockback mobs when triggered. (2 Charges)",archetype:"",archetype_req:0,parents:["Power Shots","Cheaper Escape"],dependencies:[],blockers:[],cost:1,display:{row:9,col:6},properties:{duration:60},effects:[{type:"replace_spell",name:"Arrow Shield",cost:30,display_text:"Max Damage",base_spell:4,spell_type:"damage",scaling:"spell",display:"",parts:[{name:"Shield Damage",type:"damage",multipliers:[90,0,0,0,0,10]},{name:"Total Damage",type:"total",hits:{"Shield Damage":2}}]}]},{display_name:"Escape",desc:"Throw yourself backward to avoid danger. (Hold shift while escaping to cancel)",archetype:"",archetype_req:0,parents:["Heart Shatter"],dependencies:[],blockers:[],cost:1,display:{row:7,col:4},properties:{aoe:0,range:0},effects:[{type:"replace_spell",name:"Escape",cost:25,display_text:"Max Damage",base_spell:2,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"None",type:"damage",multipliers:[0,0,0,0,0,0]},{name:"Total Damage",type:"total",hits:{None:0}}]}]},{display_name:"Arrow Bomb",desc:"Throw a long-range arrow that explodes and deal high damage in a large area. (Self-damage for 25% of your DPS)",archetype:"",archetype_req:0,parents:[],dependencies:[],blockers:[],cost:1,display:{row:0,col:4},properties:{aoe:4.5,range:26},effects:[{type:"replace_spell",name:"Arrow Bomb",cost:50,display_text:"Average Damage",base_spell:3,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Arrow Bomb",type:"damage",multipliers:[160,0,0,0,20,0]},{name:"Total Damage",type:"total",hits:{"Arrow Bomb":1}}]}]},{display_name:"Heart Shatter",desc:"If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage.",archetype:"",archetype_req:0,parents:["Bow Proficiency I"],dependencies:[],blockers:[],cost:1,display:{row:4,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[100,0,0,0,0,0]},{}]},{display_name:"Fire Creep",desc:"Arrow Bomb will leak a trail of fire for 6s, Damaging enemies that walk into it every 0.4s.",archetype:"",archetype_req:0,parents:["Phantom Ray","Fire Mastery","Bryophyte Roots"],dependencies:[],blockers:[],cost:2,display:{row:16,col:6},properties:{aoe:.8,duration:6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fire Creep",cost:0,multipliers:[30,0,0,0,20,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Fire Creep":15}}]},{display_name:"Bryophyte Roots",desc:"When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s.",archetype:"Trapper",archetype_req:1,parents:["Fire Creep","Earth Mastery"],dependencies:["Arrow Storm"],blockers:[],cost:2,display:{row:16,col:8},properties:{aoe:2,duration:5,slowness:.4},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Bryophyte Roots",cost:0,multipliers:[40,20,0,0,0,0]}]},{display_name:"Nimble String",desc:"Arrow Storm throw out +8 arrows per stream and shoot twice as fast.",archetype:"",archetype_req:0,parents:["Thunder Mastery","Arrow Rain"],dependencies:["Arrow Storm"],blockers:["Phantom Ray"],cost:2,display:{row:15,col:2},properties:{shootspeed:2},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Arrow",cost:0,multipliers:[-15,0,0,0,0,0]},{type:"add_spell_prop",base_spell:1,target_part:"Single Stream",cost:0,hits:{"Single Arrow":8}}]},{display_name:"Arrow Storm",desc:"Shoot two stream of 8 arrows, dealing significant damage to close mobs and pushing them back.",archetype:"",archetype_req:0,parents:["Double Shots","Cheaper Escape"],dependencies:[],blockers:[],cost:1,display:{row:9,col:2},properties:{aoe:0,range:16},effects:[{type:"replace_spell",name:"Arrow Storm",cost:40,display_text:"Max Damage",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[30,0,10,0,0,0]},{name:"Single Stream",type:"total",hits:{"Single Arrow":8}},{name:"Total Damage",type:"total",hits:{"Single Stream":2}}]}]},{display_name:"Guardian Angels",desc:"Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies. (Arrow Shield will no longer push nearby enemies)",archetype:"Boltslinger",archetype_req:3,parents:["Triple Shots","Frenzy"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:19,col:1},properties:{range:4,duration:60,shots:8,count:2},effects:[{type:"replace_spell",name:"Guardian Angels",cost:30,display_text:"Total Damage Average",base_spell:4,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[40,0,0,0,0,20]},{name:"Single Bow",type:"total",hits:{"Single Arrow":8}},{name:"Total Damage",type:"total",hits:{"Single Bow":2}}]}]},{display_name:"Windy Feet",base_abil:"Escape",desc:"When casting Escape, give speed to yourself and nearby allies.",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Storm"],dependencies:[],blockers:[],cost:1,display:{row:10,col:1},properties:{aoe:8,duration:120},type:"stat_bonus",bonuses:[{type:"stat",name:"spd",value:20}]},{display_name:"Basaltic Trap",desc:"When you hit the ground with Arrow Bomb, leave a Trap that damages enemies. (Max 2 Traps)",archetype:"Trapper",archetype_req:2,parents:["Bryophyte Roots"],dependencies:[],blockers:[],cost:2,display:{row:19,col:8},properties:{aoe:7,traps:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:0,multipliers:[140,30,0,0,30,0]}]},{display_name:"Windstorm",desc:"Arrow Storm shoot +1 stream of arrows, effectively doubling its damage.",archetype:"",archetype_req:0,parents:["Guardian Angels","Cheaper Arrow Storm"],dependencies:[],blockers:["Phantom Ray"],cost:2,display:{row:21,col:1},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Arrow",cost:0,multipliers:[-11,0,-7,0,0,3]},{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Stream":1}}]},{display_name:"Grappling Hook",base_abil:"Escape",desc:"When casting Escape, throw a hook that pulls you when hitting a block. If you hit an enemy, pull them towards you instead. (Escape will not throw you backward anymore)",archetype:"Trapper",archetype_req:0,parents:["Focus","More Shields","Cheaper Arrow Storm"],dependencies:[],blockers:["Escape Artist"],cost:2,display:{row:21,col:5},properties:{range:20},effects:[]},{display_name:"Implosion",desc:"Arrow bomb will pull enemies towards you. If a trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage.",archetype:"Trapper",archetype_req:0,parents:["Grappling Hook","More Shields"],dependencies:[],blockers:[],cost:2,display:{row:22,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[40,0,0,0,0,0]}]},{display_name:"Twain's Arc",desc:"When you have 2+ Focus, holding shift will summon the Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage)",archetype:"Sharpshooter",archetype_req:4,parents:["More Focus","Traveler"],dependencies:["Focus"],blockers:[],cost:2,display:{row:25,col:4},properties:{range:64,focusReq:2},effects:[{type:"replace_spell",name:"Twain's Arc",cost:0,display_text:"Twain's Arc",base_spell:5,spell_type:"damage",scaling:"melee",display:"Twain's Arc Damage",parts:[{name:"Twain's Arc Damage",type:"damage",multipliers:[200,0,0,0,0,0]}]}]},{display_name:"Fierce Stomp",desc:"When using Escape, hold shift to quickly drop down and deal damage.",archetype:"Boltslinger",archetype_req:0,parents:["Refined Gunpowder","Traveler"],dependencies:[],blockers:[],cost:2,display:{row:26,col:1},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Fierce Stomp",cost:0,multipliers:[100,0,0,0,0,0]},{type:"add_spell_prop",base_spell:2,target_part:"Total Damage",cost:0,hits:{"Fierce Stomp":1}}]},{display_name:"Scorched Earth",desc:"Fire Creep become much stronger.",archetype:"Sharpshooter",archetype_req:0,parents:["Twain's Arc"],dependencies:["Fire Creep"],blockers:[],cost:1,display:{row:26,col:5},properties:{duration:2,aoe:.4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fire Creep",cost:0,multipliers:[10,0,0,0,5,0]}]},{display_name:"Leap",desc:"When you double tap jump, leap foward. (2s Cooldown)",archetype:"Boltslinger",archetype_req:5,parents:["Refined Gunpowder","Homing Shots"],dependencies:[],blockers:[],cost:2,display:{row:28,col:0},properties:{cooldown:2},effects:[]},{display_name:"Shocking Bomb",desc:"Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder.",archetype:"Sharpshooter",archetype_req:5,parents:["Twain's Arc","Better Arrow Shield","Homing Shots"],dependencies:["Arrow Bomb"],blockers:[],cost:2,display:{row:28,col:4},properties:{gravity:0},effects:[{type:"convert_spell_conv",target_part:"all",conversion:"thunder"}]},{display_name:"Mana Trap",desc:"Your Traps will give you 4 Mana per second when you stay close to them.",archetype:"Trapper",archetype_req:5,parents:["More Traps","Better Arrow Shield"],dependencies:["Fire Creep"],blockers:[],cost:2,display:{row:28,col:8},properties:{range:12,manaRegen:4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:10,multipliers:[0,0,0,0,0,0]}]},{display_name:"Escape Artist",desc:"When casting Escape, release 100 arrows towards the ground.",archetype:"Boltslinger",archetype_req:0,parents:["Better Guardian Angels","Leap"],dependencies:[],blockers:["Grappling Hook"],cost:2,display:{row:31,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Escape Artist",cost:0,multipliers:[30,0,10,0,0,0]}]},{display_name:"Initiator",desc:"If you do not damage an enemy for 5s or more, your next sucessful hit will deal +50% damage and add +1 Focus.",archetype:"Sharpshooter",archetype_req:5,parents:["Shocking Bomb","Better Arrow Shield","Cheaper Arrow Storm (2)"],dependencies:["Focus"],blockers:[],cost:2,display:{row:31,col:5},properties:{focus:1,timer:5},type:"stat_bonus",bonuses:[{type:"stat",name:"damPct",value:50}]},{display_name:"Call of the Hound",desc:"Arrow Shield summon a Hound that will attack and drag aggressive enemies towards your traps.",archetype:"Trapper",archetype_req:0,parents:["Initiator","Cheaper Arrow Storm (2)"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:32,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Call of the Hound",cost:0,multipliers:[40,0,0,0,0,0]}]},{display_name:"Arrow Hurricane",desc:"Arrow Storm will shoot +2 stream of arrows.",archetype:"Boltslinger",archetype_req:8,parents:["Precise Shot","Escape Artist"],dependencies:[],blockers:["Phantom Ray"],cost:2,display:{row:33,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Stream":2}}]},{display_name:"Geyser Stomp",desc:"Fierce Stomp will create geysers, dealing more damage and vertical knockback.",archetype:"",archetype_req:0,parents:["Shrapnel Bomb"],dependencies:["Fierce Stomp"],blockers:[],cost:2,display:{row:37,col:1},properties:{aoe:1},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Fierce Stomp",cost:0,multipliers:[0,0,0,50,0,0]}]},{display_name:"Crepuscular Ray",desc:"If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus. While in that state, you will lose 1 Focus per second.",archetype:"Sharpshooter",archetype_req:10,parents:["Cheaper Arrow Shield"],dependencies:["Arrow Storm"],blockers:[],cost:2,display:{row:37,col:4},properties:{focusReq:5,focusRegen:-1},effects:[{type:"replace_spell",name:"Crepuscular Ray",base_spell:5,spell_type:"damage",scaling:"spell",display:"One Focus",cost:0,parts:[{name:"Single Arrow",type:"damage",multipliers:[10,0,0,5,0,0]},{name:"One Focus",type:"total",hits:{"Single Arrow":20}},{name:"Total Damage",type:"total",hits:{"One Focus":7}}]}]},{display_name:"Grape Bomb",desc:"Arrow bomb will throw 3 additional smaller bombs when exploding.",archetype:"",archetype_req:0,parents:["Cheaper Escape (2)"],dependencies:[],blockers:[],cost:2,display:{row:37,col:7},properties:{miniBombs:3,aoe:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Grape Bomb",cost:0,multipliers:[30,0,0,0,10,0]}]},{display_name:"Tangled Traps",desc:"Your Traps will be connected by a rope that deals damage to enemies every 0.2s.",archetype:"Trapper",archetype_req:0,parents:["Grape Bomb"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:38,col:6},properties:{attackSpeed:.2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Tangled Traps",cost:0,multipliers:[20,0,0,0,0,20]}]},{display_name:"Snow Storm",desc:"Enemies near you will be slowed down.",archetype:"",archetype_req:0,parents:["Geyser Stomp","More Focus (2)"],dependencies:[],blockers:[],cost:2,display:{row:39,col:2},properties:{range:2.5,slowness:.3}},{display_name:"All-Seeing Panoptes",desc:"Your bows from Guardian Angels become all-seeing, increasing their range, damage and letting them shoot up to +5 times each.",archetype:"Boltslinger",archetype_req:11,parents:["Snow Storm"],dependencies:["Guardian Angels"],blockers:[],cost:2,display:{row:40,col:1},properties:{range:10,shots:5},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Single Arrow",cost:0,multipliers:[0,0,0,0,20,0]},{type:"add_spell_prop",base_spell:4,target_part:"Single Bow",cost:0,hits:{"Single Arrow":5}}]},{display_name:"Minefield",desc:"Allow you to place +6 Traps, but with reduced damage and range.",archetype:"Trapper",archetype_req:10,parents:["Grape Bomb","Cheaper Arrow Bomb (2)"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:40,col:7},properties:{aoe:-2,traps:6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:0,multipliers:[-80,0,0,0,0,0]}]},{display_name:"Bow Proficiency I",desc:"Improve your Main Attack's damage and range when using a bow.",archetype:"",archetype_req:0,parents:["Arrow Bomb"],dependencies:[],blockers:[],cost:1,display:{row:2,col:4},properties:{mainAtk_range:6},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Arrow Bomb",desc:"Reduce the Mana cost of Arrow Bomb.",archetype:"",archetype_req:0,parents:["Bow Proficiency I"],dependencies:[],blockers:[],cost:1,display:{row:2,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-10}]},{display_name:"Cheaper Arrow Storm",desc:"Reduce the Mana cost of Arrow Storm.",archetype:"",archetype_req:0,parents:["Grappling Hook","Windstorm","Focus"],dependencies:[],blockers:[],cost:1,display:{row:21,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Cheaper Escape",desc:"Reduce the Mana cost of Escape.",archetype:"",archetype_req:0,parents:["Arrow Storm","Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:9,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"Earth Mastery",desc:"Increases your base damage from all Earth attacks",archetype:"Trapper",archetype_req:0,parents:["Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:13,col:8},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"eDamPct",value:20},{type:"stat",name:"eDam",value:[2,4]}]}]},{display_name:"Thunder Mastery",desc:"Increases your base damage from all Thunder attacks",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Storm","Fire Mastery"],dependencies:[],blockers:[],cost:1,display:{row:13,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"tDamPct",value:10},{type:"stat",name:"tDam",value:[1,8]}]}]},{display_name:"Water Mastery",desc:"Increases your base damage from all Water attacks",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Escape","Thunder Mastery","Fire Mastery"],dependencies:[],blockers:[],cost:1,display:{row:14,col:4},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"wDamPct",value:15},{type:"stat",name:"wDam",value:[2,4]}]}]},{display_name:"Air Mastery",desc:"Increases base damage from all Air attacks",archetype:"Battle Monk",archetype_req:0,parents:["Arrow Storm"],dependencies:[],blockers:[],cost:1,display:{row:13,col:0},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"aDamPct",value:15},{type:"stat",name:"aDam",value:[3,4]}]}]},{display_name:"Fire Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Sharpshooter",archetype_req:0,parents:["Thunder Mastery","Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:13,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"fDamPct",value:15},{type:"stat",name:"fDam",value:[3,5]}]}]},{display_name:"More Shields",desc:"Give +2 charges to Arrow Shield.",archetype:"",archetype_req:0,parents:["Grappling Hook","Basaltic Trap"],dependencies:["Arrow Shield"],blockers:[],cost:1,display:{row:21,col:7},properties:{shieldCharges:2}},{display_name:"Stormy Feet",desc:"Windy Feet will last longer and add more speed.",archetype:"",archetype_req:0,parents:["Windstorm"],dependencies:["Windy Feet"],blockers:[],cost:1,display:{row:23,col:1},properties:{duration:60},effects:[{type:"stat_bonus",bonuses:[{type:"stat",name:"spdPct",value:20}]}]},{display_name:"Refined Gunpowder",desc:"Increase the damage of Arrow Bomb.",archetype:"",archetype_req:0,parents:["Windstorm"],dependencies:[],blockers:[],cost:1,display:{row:25,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[50,0,0,0,0,0]}]},{display_name:"More Traps",desc:"Increase the maximum amount of active Traps you can have by +2.",archetype:"Trapper",archetype_req:10,parents:["Bouncing Bomb"],dependencies:["Basaltic Trap"],blockers:[],cost:1,display:{row:26,col:8},properties:{traps:2}},{display_name:"Better Arrow Shield",desc:"Arrow Shield will gain additional area of effect, knockback and damage.",archetype:"Sharpshooter",archetype_req:0,parents:["Mana Trap","Shocking Bomb","Twain's Arc"],dependencies:["Arrow Shield"],blockers:[],cost:1,display:{row:28,col:6},properties:{aoe:1},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Shield",multipliers:[40,0,0,0,0,0]}]},{display_name:"Better Leap",desc:"Reduce leap's cooldown by 1s.",archetype:"Boltslinger",archetype_req:0,parents:["Leap","Homing Shots"],dependencies:["Leap"],blockers:[],cost:1,display:{row:29,col:1},properties:{cooldown:-1}},{display_name:"Better Guardian Angels",desc:"Your Guardian Angels can shoot +4 arrows before disappearing.",archetype:"Boltslinger",archetype_req:0,parents:["Escape Artist","Homing Shots"],dependencies:["Guardian Angels"],blockers:[],cost:1,display:{row:31,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Single Bow",cost:0,hits:{"Single Arrow":4}}]},{display_name:"Cheaper Arrow Storm (2)",desc:"Reduce the Mana cost of Arrow Storm.",archetype:"",archetype_req:0,parents:["Initiator","Mana Trap"],dependencies:[],blockers:[],cost:1,display:{row:31,col:8},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Precise Shot",desc:"+30% Critical Hit Damage",archetype:"",archetype_req:0,parents:["Better Guardian Angels","Cheaper Arrow Shield","Arrow Hurricane"],dependencies:[],blockers:[],cost:1,display:{row:33,col:2},properties:{mainAtk_range:6},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdCritPct",value:30}]}]},{display_name:"Cheaper Arrow Shield",desc:"Reduce the Mana cost of Arrow Shield.",archetype:"",archetype_req:0,parents:["Precise Shot","Initiator"],dependencies:[],blockers:[],cost:1,display:{row:33,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Rocket Jump",desc:"Arrow Bomb's self-damage will knockback you farther away.",archetype:"",archetype_req:0,parents:["Cheaper Arrow Storm (2)","Initiator"],dependencies:["Arrow Bomb"],blockers:[],cost:1,display:{row:33,col:6},properties:{}},{display_name:"Cheaper Escape (2)",desc:"Reduce the Mana cost of Escape.",archetype:"",archetype_req:0,parents:["Call of the Hound","Decimator"],dependencies:[],blockers:[],cost:1,display:{row:34,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"Stronger Hook",desc:"Increase your Grappling Hook's range, speed and strength.",archetype:"Trapper",archetype_req:5,parents:["Cheaper Escape (2)"],dependencies:["Grappling Hook"],blockers:[],cost:1,display:{row:35,col:8},properties:{range:8}},{display_name:"Cheaper Arrow Bomb (2)",desc:"Reduce the Mana cost of Arrow Bomb.",archetype:"",archetype_req:0,parents:["More Focus (2)","Minefield"],dependencies:[],blockers:[],cost:1,display:{row:40,col:5},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Bouncing Bomb",desc:"Arrow Bomb will bounce once when hitting a block or enemy",archetype:"",archetype_req:0,parents:["More Shields"],dependencies:[],blockers:[],cost:2,display:{row:25,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Arrow Bomb":2}}]},{display_name:"Homing Shots",desc:"Your Main Attack arrows will follow nearby enemies and not be affected by gravity",archetype:"",archetype_req:0,parents:["Leap","Shocking Bomb"],dependencies:[],blockers:[],cost:2,display:{row:28,col:2},properties:{},effects:[]},{display_name:"Shrapnel Bomb",desc:"Arrow Bomb's explosion will fling 15 shrapnel, dealing damage in a large area",archetype:"Boltslinger",archetype_req:8,parents:["Arrow Hurricane","Precise Shot"],dependencies:[],blockers:[],cost:2,display:{row:34,col:1},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Shrapnel Bomb",cost:0,multipliers:[40,0,0,0,20,0]}]},{display_name:"Elusive",desc:"If you do not get hit for 8+ seconds, become immune to self-damage and remove Arrow Storm's recoil. (Dodging counts as not getting hit)",archetype:"Boltslinger",archetype_req:0,parents:["Geyser Stomp"],dependencies:[],blockers:[],cost:2,display:{row:38,col:0},properties:{},effects:[]},{display_name:"Double Shots",desc:"Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies)",archetype:"Boltslinger",archetype_req:0,parents:["Escape"],dependencies:[],blockers:["Power Shots"],cost:1,display:{row:7,col:2},properties:{arrow:2},effects:[{type:"add_spell_prop",base_spell:0,target_part:"Melee Damage",cost:0,multipliers:.7}]},{display_name:"Triple Shots",desc:"Triple Main Attack arrows, but they deal -20% damage per arrow",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Rain","Frenzy"],dependencies:["Double Shots"],blockers:[],cost:1,display:{row:17,col:0},properties:{arrow:2},effects:[{type:"add_spell_prop",base_spell:0,target_part:"Melee Damage",cost:0,multipliers:.7}]},{display_name:"Power Shots",desc:"Main Attack arrows have increased speed and knockback",archetype:"Sharpshooter",archetype_req:0,parents:["Escape"],dependencies:[],blockers:["Double Shots"],cost:1,display:{row:7,col:6},properties:{},effects:[]},{display_name:"Focus",desc:"When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once",archetype:"Sharpshooter",archetype_req:2,parents:["Phantom Ray"],dependencies:[],blockers:[],cost:2,display:{row:19,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:3}]},{display_name:"More Focus",desc:"Add +2 max Focus",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Arrow Storm","Grappling Hook"],dependencies:[],blockers:[],cost:1,display:{row:22,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:5}]},{display_name:"More Focus (2)",desc:"Add +2 max Focus",archetype:"Sharpshooter",archetype_req:0,parents:["Crepuscular Ray","Snow Storm"],dependencies:[],blockers:[],cost:1,display:{row:39,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:7}]},{display_name:"Traveler",desc:"For every 1% Walk Speed you have from items, gain +1 Raw Spell Damage (Max 100)",archetype:"",archetype_req:0,parents:["Refined Gunpowder","Twain's Arc"],dependencies:[],blockers:[],cost:1,display:{row:25,col:2},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"spd"}],output:{type:"stat",name:"sdRaw"},scaling:[1],max:100}]},{display_name:"Patient Hunter",desc:"Your Traps will deal +20% more damage for every second they are active (Max +80%)",archetype:"Trapper",archetype_req:0,parents:["More Shields"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:22,col:8},properties:{max:80},effects:[]},{display_name:"Stronger Patient Hunter",desc:"Add +80% Max Damage to Patient Hunter",archetype:"Trapper",archetype_req:0,parents:["Grape Bomb"],dependencies:["Patient Hunter"],blockers:[],cost:1,display:{row:38,col:8},properties:{max:80},effects:[]},{display_name:"Frenzy",desc:"Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second",archetype:"Boltslinger",archetype_req:0,parents:["Triple Shots","Nimble String"],dependencies:[],blockers:[],cost:2,display:{row:17,col:2},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Hits dealt",output:{type:"stat",name:"spd"},scaling:[6],max:200}]},{display_name:"Phantom Ray",desc:"Condense Arrow Storm into a single ray that damages enemies 10 times per second",archetype:"Sharpshooter",archetype_req:0,parents:["Water Mastery","Fire Creep"],dependencies:["Arrow Storm"],blockers:["Windstorm","Nimble String","Arrow Hurricane"],cost:2,display:{row:16,col:4},properties:{},effects:[{type:"replace_spell",name:"Phantom Ray",cost:40,display_text:"Max Damage",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[25,0,5,0,0,0]},{name:"Total Damage",type:"total",hits:{"Single Arrow":16}}]}]},{display_name:"Arrow Rain",desc:"When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies",archetype:"Trapper",archetype_req:0,parents:["Nimble String","Air Mastery"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:15,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Arrow Rain",cost:0,multipliers:[120,0,0,0,0,80]}]},{display_name:"Decimator",desc:"Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%)",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Arrow Shield"],dependencies:["Phantom Ray"],blockers:[],cost:1,display:{row:34,col:5},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Phantom Ray hits",output:{type:"stat",name:"PhRayDmg"},scaling:10,max:50}]}],Assassin:[{title:"Spin Attack",desc:"desc",image:"../media/atree/node.png",connector:!1,row:0,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:1,col:4},{title:"Dagger Proficiency I",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:2,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:3,col:4},{title:"Double Spin",desc:"desc",image:"../media/atree/node.png",connector:!1,row:4,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:5,col:4},{title:"Dash",desc:"desc",image:"../media/atree/node.png",connector:!1,row:6,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:6,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:6,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:6,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:6,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:7,col:2},{title:"Smoke Bomb",desc:"desc",image:"../media/atree/node.png",connector:!1,row:8,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:7,col:6},{title:"Multihit",desc:"desc",image:"../media/atree/node.png",connector:!1,row:8,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:8,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:1},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:8,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:6},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:8,col:8},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:8},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:8},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:10,col:1},{title:"Backstab",desc:"desc",image:"../media/atree/node.png",connector:!1,row:11,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:90,row:10,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:10,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:11,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:10,col:7},{title:"Fatality",desc:"desc",image:"../media/atree/node.png",connector:!1,row:11,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:11,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:0},{title:"Violent Vortex",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:11,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:2},{title:"Vanish",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:13,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:14,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:15,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:14,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:15,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:13,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:14,col:7},{title:"Lacerate",desc:"desc",image:"../media/atree/node.png",connector:!1,row:15,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:15,col:1},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:16,col:1},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:15,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:16,col:5},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:15,col:8},{title:"Wall of Smoke",desc:"desc",image:"../media/atree/node.png",connector:!1,row:16,col:8},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:16,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:0},{title:"Silent Killer",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:16,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:2},{title:"Shadow Travel",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:5},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:8},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:8},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:18,col:4},{title:"Exploding Clones",desc:"desc",image:"../media/atree/node.png",connector:!1,row:19,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:18,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:19,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:20,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:19,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:20,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:18,col:6},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:18,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:19,col:7},{title:"Weightless",desc:"desc",image:"../media/atree/node.png",connector:!1,row:20,col:7},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:20,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:20,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:1},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:20,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:4},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:20,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:21,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:6},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:20,col:8},{title:"Dancing Blade",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:8},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:21,col:0},{title:"Spin Attack Damage",desc:"desc",image:"../media/atree/node.png",connector:!1,row:22,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:21,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:22,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:22,col:1},{title:"Marked",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:22,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:23,col:5},{title:"Shurikens",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:23,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:22,col:8},{title:"Far Reach",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:8},{title:"Stronger Multihit",desc:"desc",image:"../media/atree/node.png",connector:!1,row:24,col:5},{title:"Psithurism",desc:"desc",image:"../media/atree/node.png",connector:!1,row:24,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:24,col:1},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:1},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:24,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:25,col:5},{title:"Choke Bomb",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:6},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:25,col:7},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:8},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:26,col:5},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:25,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:0},{title:"Death Magnet",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:25,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:2},{title:"Cheaper Multihit",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:7},{title:"Parry",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:7},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:27,col:1},{title:"Fatal Spin",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:1},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:27,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:27,col:6},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:6},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:27,col:8},{title:"Wall Jump",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:8},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:28,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:29,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:29,col:1},{title:"Harvester",desc:"desc",image:"../media/atree/node.png",connector:!1,row:30,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:28,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:29,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:30,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:28,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:29,col:7},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:30,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:30,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:31,col:2},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:30,col:5},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:30,col:6},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:31,col:5},{title:"Ricochet",desc:"desc",image:"../media/atree/node.png",connector:!1,row:31,col:8},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:31,col:1},{title:"Satsujin",desc:"desc",image:"../media/atree/node.png",connector:!1,row:32,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:31,col:4},{title:"Forbidden Art",desc:"desc",image:"../media/atree/node.png",connector:!1,row:32,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:31,col:7},{title:"Jasmine Bloom",desc:"desc",image:"../media/atree/node.png",connector:!1,row:32,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:32,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:32,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:2},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:32,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:8},],Warrior:[{display_name:"Bash",desc:"Violently bash the ground, dealing high damage in a large area",archetype:"",archetype_req:0,parents:[],dependencies:[],blockers:[],cost:1,display:{row:0,col:4},properties:{aoe:4,range:3},effects:[{type:"replace_spell",name:"Bash",cost:45,display_text:"Total Damage Average",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Hit",type:"damage",multipliers:[130,20,0,0,0,0]},{name:"Total Damage",type:"total",hits:{"Single Hit":1}}]}]},{display_name:"Spear Proficiency 1",desc:"Improve your Main Attack's damage and range w/ spear",archetype:"",archetype_req:0,parents:["Bash"],dependencies:[],blockers:[],cost:1,display:{row:2,col:4},properties:{melee_range:1},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Bash",desc:"Reduce the Mana cost of Bash",archetype:"",archetype_req:0,parents:["Spear Proficiency 1"],dependencies:[],blockers:[],cost:1,display:{row:2,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-10}]},{display_name:"Double Bash",desc:"Bash will hit a second time at a farther range",archetype:"",archetype_req:0,parents:["Spear Proficiency 1"],dependencies:[],blockers:[],cost:1,display:{row:4,col:4},properties:{range:3},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{name:"Single Hit",value:1}},{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[-50,0,0,0,0,0]}]},{display_name:"Charge",desc:"Charge forward at high speed (hold shift to cancel)",archetype:"",archetype_req:0,parents:["Double Bash"],dependencies:[],blockers:[],cost:1,display:{row:6,col:4},properties:{},effects:[{type:"replace_spell",name:"Charge",cost:25,display_text:"Total Damage Average",base_spell:2,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"None",type:"damage",multipliers:[0,0,0,0,0,0]},{name:"Total Damage",type:"total",hits:{None:0}}]}]},{display_name:"Heavy Impact",desc:"After using Charge, violently crash down into the ground and deal damage",archetype:"",archetype_req:0,parents:["Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:9,col:1},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Heavy Impact",cost:0,multipliers:[100,0,0,0,0,0]}]},{display_name:"Vehement",desc:"For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%)",archetype:"Fallen",archetype_req:0,parents:["Charge"],dependencies:[],blockers:["Tougher Skin"],cost:1,display:{row:6,col:2},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"mdPct"},{type:"stat",name:"mdRaw"}],output:{type:"stat",name:"spd"},scaling:[1,1],max:20}]},{display_name:"Tougher Skin",desc:"Harden your skin and become permanently +5% more resistant\nFor every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)",archetype:"Paladin",archetype_req:0,parents:["Charge"],dependencies:[],blockers:["Vehement"],cost:1,display:{row:6,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"baseResist",value:"5"}]},{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hprRaw"},{type:"stat",name:"hprPct"}],output:{type:"stat",name:"hpBonus"},scaling:[10,10],max:100}]},{display_name:"Uppercut",desc:"Rocket enemies in the air and deal massive damage",archetype:"",archetype_req:0,parents:["Vehement"],dependencies:[],blockers:[],cost:1,display:{row:8,col:2},properties:{aoe:3,range:5},effects:[{type:"replace_spell",name:"Uppercut",cost:45,display_text:"Total Damage Average",base_spell:3,spell_type:"damage",scaling:"spell",display:"total",parts:[{name:"Uppercut",type:"damage",multipliers:[150,50,50,0,0,0]},{name:"Total Damage",type:"total",hits:{Uppercut:1}}]}]},{display_name:"Cheaper Charge",desc:"Reduce the Mana cost of Charge",archetype:"",archetype_req:0,parents:["Uppercut","War Scream"],dependencies:[],blockers:[],cost:1,display:{row:8,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"War Scream",desc:"Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies",archetype:"",archetype_req:0,parents:["Tougher Skin"],dependencies:[],blockers:[],cost:1,display:{row:8,col:6},properties:{duration:30,aoe:12,defense_bonus:10},effects:[{type:"replace_spell",name:"War Scream",cost:35,display_text:"War Scream",base_spell:4,spell_type:"damage",scaling:"spell",display:"Total Damage Average",parts:[{name:"War Scream",type:"damage",multipliers:[50,0,0,0,50,0]}]}]},{display_name:"Earth Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Fallen",archetype_req:0,parents:["Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:10,col:0},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"eDamPct",value:20},{type:"stat",name:"eDam",value:[2,4]}]}]},{display_name:"Thunder Mastery",desc:"Increases base damage from all Thunder attacks",archetype:"Fallen",archetype_req:0,parents:["Uppercut","Air Mastery"],dependencies:[],blockers:[],cost:1,display:{row:10,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"tDamPct",value:10},{type:"stat",name:"tDam",value:[1,8]}]}]},{display_name:"Water Mastery",desc:"Increases base damage from all Water attacks",archetype:"Battle Monk",archetype_req:0,parents:["Cheaper Charge","Thunder Mastery","Air Mastery"],dependencies:[],blockers:[],cost:1,display:{row:11,col:4},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"wDamPct",value:15},{type:"stat",name:"wDam",value:[2,4]}]}]},{display_name:"Air Mastery",desc:"Increases base damage from all Air attacks",archetype:"Battle Monk",archetype_req:0,parents:["War Scream","Thunder Mastery"],dependencies:[],blockers:[],cost:1,display:{row:10,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"aDamPct",value:15},{type:"stat",name:"aDam",value:[3,4]}]}]},{display_name:"Fire Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Paladin",archetype_req:0,parents:["War Scream"],dependencies:[],blockers:[],cost:1,display:{row:10,col:8},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"fDamPct",value:15},{type:"stat",name:"fDam",value:[3,5]}]}]},{display_name:"Quadruple Bash",desc:"Bash will hit 4 times at an even larger range",archetype:"Fallen",archetype_req:0,parents:["Earth Mastery","Fireworks"],dependencies:[],blockers:[],cost:2,display:{row:12,col:0},properties:{range:6},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Hit":2}},{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[-20,0,0,0,0,0]}]},{display_name:"Fireworks",desc:"Mobs hit by Uppercut will explode mid-air and receive additional damage",archetype:"Fallen",archetype_req:0,parents:["Thunder Mastery","Quadruple Bash"],dependencies:[],blockers:[],cost:2,display:{row:12,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fireworks",cost:0,multipliers:[80,0,20,0,0,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{Fireworks:1}}]},{display_name:"Half-Moon Swipe",desc:"Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water",archetype:"Battle Monk",archetype_req:1,parents:["Water Mastery"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:13,col:4},properties:{range:4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:-10,multipliers:[-70,0,0,0,0,0]},{type:"convert_spell_conv",target_part:"all",conversion:"water"}]},{display_name:"Flyby Jab",desc:"Damage enemies in your way when using Charge",archetype:"",archetype_req:0,parents:["Air Mastery","Flaming Uppercut"],dependencies:[],blockers:[],cost:2,display:{row:12,col:6},properties:{aoe:2},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Flyby Jab",cost:0,multipliers:[20,0,0,0,0,40]}]},{display_name:"Flaming Uppercut",desc:"Uppercut will light mobs on fire, dealing damage every 0.6 seconds",archetype:"Paladin",archetype_req:0,parents:["Fire Mastery","Flyby Jab"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:12,col:8},properties:{duration:3,tick:.6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Flaming Uppercut",cost:0,multipliers:[0,0,0,0,50,0]},{type:"add_spell_prop",base_spell:3,target_part:"Flaming Uppercut Total Damage",cost:0,hits:{"Flaming Uppercut":5}},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Flaming Uppercut":5}}]},{display_name:"Iron Lungs",desc:"War Scream deals more damage",archetype:"",archetype_req:0,parents:["Flyby Jab","Flaming Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:13,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"War Scream",cost:0,multipliers:[30,0,0,0,0,30]}]},{display_name:"Generalist",desc:"After casting 3 different spells in a row, your next spell will cost 5 mana",archetype:"Battle Monk",archetype_req:3,parents:["Counter"],dependencies:[],blockers:[],cost:2,display:{row:15,col:2},properties:{},effects:[]},{display_name:"Counter",desc:"When dodging a nearby enemy attack, get 30% chance to instantly attack back",archetype:"Battle Monk",archetype_req:0,parents:["Half-Moon Swipe"],dependencies:[],blockers:[],cost:2,display:{row:15,col:4},properties:{chance:30},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Counter",cost:0,multipliers:[60,0,20,0,0,20]}]},{display_name:"Mantle of the Bovemists",desc:"When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)",archetype:"Paladin",archetype_req:3,parents:["Iron Lungs"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:15,col:7},properties:{mantle_charge:3},effects:[]},{display_name:"Bak'al's Grasp",desc:"After casting War Scream, become Corrupted (15s Cooldown). You cannot heal while in that state\n\nWhile Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)",archetype:"Fallen",archetype_req:2,parents:["Quadruple Bash","Fireworks"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:16,col:1},properties:{cooldown:15},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"raw"},scaling:[4],slider_step:2,max:120}]},{display_name:"Spear Proficiency 2",desc:"Improve your Main Attack's damage and range w/ spear",archetype:"",archetype_req:0,parents:["Bak'al's Grasp","Cheaper Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:17,col:0},properties:{melee_range:1},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Uppercut",desc:"Reduce the Mana Cost of Uppercut",archetype:"",archetype_req:0,parents:["Spear Proficiency 2","Aerodynamics","Counter"],dependencies:[],blockers:[],cost:1,display:{row:17,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Aerodynamics",desc:"During Charge, you can steer and change direction",archetype:"Battle Monk",archetype_req:0,parents:["Cheaper Uppercut","Provoke"],dependencies:[],blockers:[],cost:2,display:{row:17,col:5},properties:{},effects:[]},{display_name:"Provoke",desc:"Mobs damaged by War Scream will target only you for at least 5s \n\nReduce the Mana cost of War Scream",archetype:"Paladin",archetype_req:0,parents:["Aerodynamics","Mantle of the Bovemists"],dependencies:[],blockers:[],cost:1,display:{row:17,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Precise Strikes",desc:"+30% Critical Hit Damage",archetype:"",archetype_req:0,parents:["Cheaper Uppercut","Spear Proficiency 2"],dependencies:[],blockers:[],cost:1,display:{row:18,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"critDmg",value:30}]}]},{display_name:"Air Shout",desc:"War Scream will fire a projectile that can go through walls and deal damage multiple times",archetype:"",archetype_req:0,parents:["Aerodynamics","Provoke"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:18,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Air Shout",cost:0,multipliers:[20,0,0,0,0,5]}]},{display_name:"Enraged Blow",desc:"While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)",archetype:"Fallen",archetype_req:0,parents:["Spear Proficiency 2"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:20,col:0},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hpBonus"}],output:{type:"stat",name:"dmgPct"},scaling:[2],max:200}]},{display_name:"Flying Kick",desc:"When using Charge, mobs hit will halt your momentum and get knocked back",archetype:"Battle Monk",archetype_req:1,parents:["Cheaper Uppercut","Stronger Mantle"],dependencies:[],blockers:[],cost:2,display:{row:20,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Flying Kick",cost:0,multipliers:[120,0,0,10,0,20]}]},{display_name:"Stronger Mantle",desc:"Add +2 additional charges to Mantle of the Bovemists",archetype:"Paladin",archetype_req:0,parents:["Manachism","Flying Kick"],dependencies:[],blockers:[],cost:1,display:{row:20,col:6},properties:{mantle_charge:2},effects:[]},{display_name:"Manachism",desc:"If you receive a hit that's less than 5% of your max HP, gain 10 Mana (1s Cooldown)",archetype:"Paladin",archetype_req:3,parents:["Stronger Mantle","Provoke"],dependencies:[],blockers:[],cost:2,display:{row:20,col:8},properties:{cooldown:1},effects:[]},{display_name:"Boiling Blood",desc:"Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds",archetype:"",archetype_req:0,parents:["Enraged Blow","Ragnarokkr"],dependencies:[],blockers:[],cost:2,display:{row:22,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Boiling Blood",cost:0,multipliers:[25,0,0,0,5,0]}]},{display_name:"Ragnarokkr",desc:"War Scream become deafening, increasing its range and giving damage bonus to players",archetype:"Fallen",archetype_req:0,parents:["Boiling Blood","Flying Kick"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:22,col:2},properties:{damage_bonus:30,aoe:2},effects:[{type:"add_spell_prop",base_spell:4,cost:10}]},{display_name:"Ambidextrous",desc:"Increase your chance to attack with Counter by +30%",archetype:"",archetype_req:0,parents:["Flying Kick","Stronger Mantle","Burning Heart"],dependencies:["Counter"],blockers:[],cost:1,display:{row:22,col:4},properties:{chance:30},effects:[]},{display_name:"Burning Heart",desc:"For every 100 Health Bonus you have from item IDs, gain +2% Fire Damage (Max 100%)",archetype:"Paladin",archetype_req:0,parents:["Ambidextrous","Stronger Bash"],dependencies:[],blockers:[],cost:1,display:{row:22,col:6},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hpBonus"}],output:{type:"stat",name:"fDamPct"},scaling:[2],max:100,slider_step:100}]},{display_name:"Stronger Bash",desc:"Increase the damage of Bash",archetype:"",archetype_req:0,parents:["Burning Heart","Manachism"],dependencies:[],blockers:[],cost:1,display:{row:22,col:8},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[30,0,0,0,0,0]}]},{display_name:"Intoxicating Blood",desc:"After leaving Corrupted, gain 2% of the health lost back for each enemy killed while Corrupted",archetype:"Fallen",archetype_req:5,parents:["Ragnarokkr","Boiling Blood"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:23,col:1},properties:{},effects:[]},{display_name:"Comet",desc:"After being hit by Fireworks, enemies will crash into the ground and receive more damage",archetype:"Fallen",archetype_req:0,parents:["Ragnarokkr"],dependencies:["Fireworks"],blockers:[],cost:2,display:{row:24,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Comet",cost:0,multipliers:[80,20,0,0,0,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{Comet:1}}]},{display_name:"Collide",desc:"Mobs thrown into walls from Flying Kick will explode and receive additonal damage",archetype:"Battle Monk",archetype_req:4,parents:["Ambidextrous","Burning Heart"],dependencies:["Flying Kick"],blockers:[],cost:2,display:{row:23,col:5},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Collide",cost:0,multipliers:[100,0,0,0,50,0]}]},{display_name:"Rejuvenating Skin",desc:"Regain back 30% of the damage you take as healing over 30s",archetype:"Paladin",archetype_req:0,parents:["Burning Heart","Stronger Bash"],dependencies:[],blockers:[],cost:2,display:{row:23,col:7},properties:{},effects:[]},{display_name:"Uncontainable Corruption",desc:"Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1",archetype:"",archetype_req:0,parents:["Boiling Blood","Radiant Devotee"],dependencies:["Bak'al's Grasp"],blockers:[],cost:1,display:{row:26,col:0},properties:{cooldown:-5},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"raw"},scaling:[1],slider_step:2,max:50}]},{display_name:"Radiant Devotee",desc:"For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)",archetype:"Battle Monk",archetype_req:1,parents:["Whirlwind Strike","Uncontainable Corruption"],dependencies:[],blockers:[],cost:1,display:{row:26,col:2},properties:{},effects:[{type:"stat_scaling",inputs:[{type:"stat",name:"ref"}],output:{type:"stat",name:"mr"},scaling:[1],max:10,slider_step:4}]},{display_name:"Whirlwind Strike",desc:"Uppercut will create a strong gust of air, launching you upward with enemies (Hold shift to stay grounded)",archetype:"Battle Monk",archetype_req:5,parents:["Ambidextrous","Radiant Devotee"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:26,col:4},properties:{range:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:0,multipliers:[0,0,0,0,0,50]}]},{display_name:"Mythril Skin",desc:"Gain +5% Base Resistance and become immune to knockback",archetype:"Paladin",archetype_req:6,parents:["Rejuvenating Skin"],dependencies:[],blockers:[],cost:2,display:{row:26,col:7},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"baseResist",value:5}]}]},{display_name:"Armour Breaker",desc:"While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage",archetype:"Fallen",archetype_req:0,parents:["Uncontainable Corruption","Radiant Devotee"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:27,col:1},properties:{duration:5},effects:[]},{display_name:"Shield Strike",desc:"When your Mantle of the Bovemist loses all charges, deal damage around you for each Mantle individually lost",archetype:"Paladin",archetype_req:0,parents:["Mythril Skin","Sparkling Hope"],dependencies:[],blockers:[],cost:2,display:{row:27,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Shield Strike",cost:0,multipliers:[60,0,20,0,0,0]}]},{display_name:"Sparkling Hope",desc:"Everytime you heal 5% of your max health, deal damage to all nearby enemies",archetype:"Paladin",archetype_req:0,parents:["Mythril Skin"],dependencies:[],blockers:[],cost:2,display:{row:27,col:8},properties:{aoe:6},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Sparkling Hope",cost:0,multipliers:[10,0,5,0,0,0]}]},{display_name:"Massive Bash",desc:"While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)",archetype:"Fallen",archetype_req:8,parents:["Tempest","Uncontainable Corruption"],dependencies:[],blockers:[],cost:2,display:{row:28,col:0},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"bashAoE"},scaling:[1],max:10,slider_step:3}]},{display_name:"Tempest",desc:"War Scream will ripple the ground and deal damage 3 times in a large area",archetype:"Battle Monk",archetype_req:0,parents:["Massive Bash","Spirit of the Rabbit"],dependencies:[],blockers:[],cost:2,display:{row:28,col:2},properties:{aoe:16},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Tempest",cost:"0",multipliers:[30,10,0,0,0,10]},{type:"add_spell_prop",base_spell:4,target_part:"Tempest Total Damage",cost:"0",hits:{Tempest:3}},{type:"add_spell_prop",base_spell:4,target_part:"Total Damage",cost:"0",hits:{Tempest:3}}]},{display_name:"Spirit of the Rabbit",desc:"Reduce the Mana cost of Charge and increase your Walk Speed by +20%",archetype:"Battle Monk",archetype_req:5,parents:["Tempest","Whirlwind Strike"],dependencies:[],blockers:[],cost:1,display:{row:28,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5},{type:"raw_stat",bonuses:[{type:"stat",name:"spd",value:20}]}]},{display_name:"Massacre",desc:"While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar",archetype:"Fallen",archetype_req:5,parents:["Tempest","Massive Bash"],dependencies:[],blockers:[],cost:2,display:{row:29,col:1},properties:{},effects:[]},{display_name:"Axe Kick",desc:"Increase the damage of Uppercut, but also increase its mana cost",archetype:"",archetype_req:0,parents:["Tempest","Spirit of the Rabbit"],dependencies:[],blockers:[],cost:1,display:{row:29,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:10,multipliers:[100,0,0,0,0,0]}]},{display_name:"Radiance",desc:"Bash will buff your allies' positive IDs. (15s Cooldown)",archetype:"Paladin",archetype_req:2,parents:["Spirit of the Rabbit","Cheaper Bash 2"],dependencies:[],blockers:[],cost:2,display:{row:29,col:5},properties:{cooldown:15},effects:[]},{display_name:"Cheaper Bash 2",desc:"Reduce the Mana cost of Bash",archetype:"",archetype_req:0,parents:["Radiance","Shield Strike","Sparkling Hope"],dependencies:[],blockers:[],cost:1,display:{row:29,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Cheaper War Scream",desc:"Reduce the Mana cost of War Scream",archetype:"",archetype_req:0,parents:["Massive Bash"],dependencies:[],blockers:[],cost:1,display:{row:31,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Discombobulate",desc:"Every time you hit an enemy, briefly increase your elemental damage dealt to them by +2 (Additive, Max +50). This bonus decays -5 every second",archetype:"Battle Monk",archetype_req:12,parents:["Thunderclap"],dependencies:[],blockers:[],cost:2,display:{row:31,col:2},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Hits dealt",output:{type:"stat",name:"rainrawButDifferent"},scaling:[2],max:50}]},{display_name:"Thunderclap",desc:"Bash will cast at the player's position and gain additional AoE.\n\n All elemental conversions become Thunder",archetype:"Battle Monk",archetype_req:8,parents:["Spirit of the Rabbit"],dependencies:[],blockers:[],cost:2,display:{row:31,col:4},properties:{aoe:2},effects:[{type:"convert_spell_conv",target_part:"all",conversion:"thunder"}]},{display_name:"Cyclone",desc:"After casting War Scream, envelop yourself with a vortex that damages nearby enemies every 0.5s",archetype:"Battle Monk",archetype_req:0,parents:["Thunderclap"],dependencies:[],blockers:[],cost:1,display:{row:32,col:5},properties:{aoe:4,duration:20},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Cyclone",cost:0,multipliers:[10,0,0,0,5,10]},{type:"add_spell_prop",base_spell:4,target_part:"Cyclone Total Damage",cost:0,hits:{Cyclone:40}}]},{display_name:"Second Chance",desc:"When you receive a fatal blow, survive and regain 30% of your Health (10m Cooldown)",archetype:"Paladin",archetype_req:12,parents:["Cheaper Bash 2"],dependencies:[],blockers:[],cost:2,display:{row:32,col:7},properties:{},effects:[]},{display_name:"Blood Pact",desc:"If you do not have enough mana to cast a spell, spend health instead (1% health per mana)",archetype:"",archetype_req:10,parents:["Cheaper War Scream"],dependencies:[],blockers:[],cost:2,display:{row:34,col:1},properties:{},effects:[]},{display_name:"Haemorrhage",desc:"Reduce Blood Pact's health cost. (0.5% health per mana)",archetype:"Fallen",archetype_req:0,parents:["Blood Pact"],dependencies:["Blood Pact"],blockers:[],cost:1,display:{row:35,col:2},properties:{},effects:[]},{display_name:"Brink of Madness",desc:"If your health is 25% full or less, gain +40% Resistance",archetype:"",archetype_req:0,parents:["Blood Pact","Cheaper Uppercut 2"],dependencies:[],blockers:[],cost:2,display:{row:35,col:4},properties:{},effects:[]},{display_name:"Cheaper Uppercut 2",desc:"Reduce the Mana cost of Uppercut",archetype:"",archetype_req:0,parents:["Second Chance","Brink of Madness"],dependencies:[],blockers:[],cost:1,display:{row:35,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Martyr",desc:"When you receive a fatal blow, all nearby allies become invincible",archetype:"Paladin",archetype_req:0,parents:["Second Chance"],dependencies:[],blockers:[],cost:2,display:{row:35,col:8},properties:{duration:3,aoe:12},effects:[]}],Mage:[],Shaman:[]},atree_example=[{title:"skill",desc:"desc",image:"../media/atree/node.png",connector:!1,row:5,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:4,col:3},{title:"skill2",desc:"desc",image:"../media/atree/node.png",connector:!1,row:0,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:1,col:2},{title:"skill3",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:2,col:3},{title:"skill4",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:3,col:2},{title:"skill5",desc:"desc",image:"../media/atree/node.png",connector:!1,row:4,col:2},] diff --git a/py_script/atree_csv_to_json.py b/py_script/atree_csv_to_json.py deleted file mode 100644 index 4c63d06..0000000 --- a/py_script/atree_csv_to_json.py +++ /dev/null @@ -1,24 +0,0 @@ -import csv -import json -import re - -with open('atree.csv', newline='') as csvfile: - res = "" - reader = csv.DictReader(csvfile) - for row in reader: - if not row["connector"]: - row["connector"] = False - else: - row["connector"] = True - row["row"] = int(row["row"]) - row["col"] = int(row["col"]) - if row["rotate"].isdigit(): - row["rotate"] = int(row["rotate"]) - else: - row.pop("rotate") - row["desc"] = re.sub("\n", " ", row["desc"]) - - resjson = json.dumps(row) - res += str(resjson) + ",\n" - - print(res) From 3f3ed18455946a04431d5c498ba05d51fe2ac933 Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 20:10:02 -0700 Subject: [PATCH 62/68] Fix minified file / update --- js/atree_constants_min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/atree_constants_min.js b/js/atree_constants_min.js index 9379944..714b6d0 100644 --- a/js/atree_constants_min.js +++ b/js/atree_constants_min.js @@ -1,2 +1,2 @@ // Minified version of js/atree_constants.js -const atrees={Archer:[{display_name:"Arrow Shield",desc:"Create a shield around you that deal damage and knockback mobs when triggered. (2 Charges)",archetype:"",archetype_req:0,parents:["Power Shots","Cheaper Escape"],dependencies:[],blockers:[],cost:1,display:{row:9,col:6},properties:{duration:60},effects:[{type:"replace_spell",name:"Arrow Shield",cost:30,display_text:"Max Damage",base_spell:4,spell_type:"damage",scaling:"spell",display:"",parts:[{name:"Shield Damage",type:"damage",multipliers:[90,0,0,0,0,10]},{name:"Total Damage",type:"total",hits:{"Shield Damage":2}}]}]},{display_name:"Escape",desc:"Throw yourself backward to avoid danger. (Hold shift while escaping to cancel)",archetype:"",archetype_req:0,parents:["Heart Shatter"],dependencies:[],blockers:[],cost:1,display:{row:7,col:4},properties:{aoe:0,range:0},effects:[{type:"replace_spell",name:"Escape",cost:25,display_text:"Max Damage",base_spell:2,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"None",type:"damage",multipliers:[0,0,0,0,0,0]},{name:"Total Damage",type:"total",hits:{None:0}}]}]},{display_name:"Arrow Bomb",desc:"Throw a long-range arrow that explodes and deal high damage in a large area. (Self-damage for 25% of your DPS)",archetype:"",archetype_req:0,parents:[],dependencies:[],blockers:[],cost:1,display:{row:0,col:4},properties:{aoe:4.5,range:26},effects:[{type:"replace_spell",name:"Arrow Bomb",cost:50,display_text:"Average Damage",base_spell:3,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Arrow Bomb",type:"damage",multipliers:[160,0,0,0,20,0]},{name:"Total Damage",type:"total",hits:{"Arrow Bomb":1}}]}]},{display_name:"Heart Shatter",desc:"If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage.",archetype:"",archetype_req:0,parents:["Bow Proficiency I"],dependencies:[],blockers:[],cost:1,display:{row:4,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[100,0,0,0,0,0]},{}]},{display_name:"Fire Creep",desc:"Arrow Bomb will leak a trail of fire for 6s, Damaging enemies that walk into it every 0.4s.",archetype:"",archetype_req:0,parents:["Phantom Ray","Fire Mastery","Bryophyte Roots"],dependencies:[],blockers:[],cost:2,display:{row:16,col:6},properties:{aoe:.8,duration:6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fire Creep",cost:0,multipliers:[30,0,0,0,20,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Fire Creep":15}}]},{display_name:"Bryophyte Roots",desc:"When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s.",archetype:"Trapper",archetype_req:1,parents:["Fire Creep","Earth Mastery"],dependencies:["Arrow Storm"],blockers:[],cost:2,display:{row:16,col:8},properties:{aoe:2,duration:5,slowness:.4},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Bryophyte Roots",cost:0,multipliers:[40,20,0,0,0,0]}]},{display_name:"Nimble String",desc:"Arrow Storm throw out +8 arrows per stream and shoot twice as fast.",archetype:"",archetype_req:0,parents:["Thunder Mastery","Arrow Rain"],dependencies:["Arrow Storm"],blockers:["Phantom Ray"],cost:2,display:{row:15,col:2},properties:{shootspeed:2},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Arrow",cost:0,multipliers:[-15,0,0,0,0,0]},{type:"add_spell_prop",base_spell:1,target_part:"Single Stream",cost:0,hits:{"Single Arrow":8}}]},{display_name:"Arrow Storm",desc:"Shoot two stream of 8 arrows, dealing significant damage to close mobs and pushing them back.",archetype:"",archetype_req:0,parents:["Double Shots","Cheaper Escape"],dependencies:[],blockers:[],cost:1,display:{row:9,col:2},properties:{aoe:0,range:16},effects:[{type:"replace_spell",name:"Arrow Storm",cost:40,display_text:"Max Damage",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[30,0,10,0,0,0]},{name:"Single Stream",type:"total",hits:{"Single Arrow":8}},{name:"Total Damage",type:"total",hits:{"Single Stream":2}}]}]},{display_name:"Guardian Angels",desc:"Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies. (Arrow Shield will no longer push nearby enemies)",archetype:"Boltslinger",archetype_req:3,parents:["Triple Shots","Frenzy"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:19,col:1},properties:{range:4,duration:60,shots:8,count:2},effects:[{type:"replace_spell",name:"Guardian Angels",cost:30,display_text:"Total Damage Average",base_spell:4,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[40,0,0,0,0,20]},{name:"Single Bow",type:"total",hits:{"Single Arrow":8}},{name:"Total Damage",type:"total",hits:{"Single Bow":2}}]}]},{display_name:"Windy Feet",base_abil:"Escape",desc:"When casting Escape, give speed to yourself and nearby allies.",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Storm"],dependencies:[],blockers:[],cost:1,display:{row:10,col:1},properties:{aoe:8,duration:120},type:"stat_bonus",bonuses:[{type:"stat",name:"spd",value:20}]},{display_name:"Basaltic Trap",desc:"When you hit the ground with Arrow Bomb, leave a Trap that damages enemies. (Max 2 Traps)",archetype:"Trapper",archetype_req:2,parents:["Bryophyte Roots"],dependencies:[],blockers:[],cost:2,display:{row:19,col:8},properties:{aoe:7,traps:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:0,multipliers:[140,30,0,0,30,0]}]},{display_name:"Windstorm",desc:"Arrow Storm shoot +1 stream of arrows, effectively doubling its damage.",archetype:"",archetype_req:0,parents:["Guardian Angels","Cheaper Arrow Storm"],dependencies:[],blockers:["Phantom Ray"],cost:2,display:{row:21,col:1},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Arrow",cost:0,multipliers:[-11,0,-7,0,0,3]},{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Stream":1}}]},{display_name:"Grappling Hook",base_abil:"Escape",desc:"When casting Escape, throw a hook that pulls you when hitting a block. If you hit an enemy, pull them towards you instead. (Escape will not throw you backward anymore)",archetype:"Trapper",archetype_req:0,parents:["Focus","More Shields","Cheaper Arrow Storm"],dependencies:[],blockers:["Escape Artist"],cost:2,display:{row:21,col:5},properties:{range:20},effects:[]},{display_name:"Implosion",desc:"Arrow bomb will pull enemies towards you. If a trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage.",archetype:"Trapper",archetype_req:0,parents:["Grappling Hook","More Shields"],dependencies:[],blockers:[],cost:2,display:{row:22,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[40,0,0,0,0,0]}]},{display_name:"Twain's Arc",desc:"When you have 2+ Focus, holding shift will summon the Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage)",archetype:"Sharpshooter",archetype_req:4,parents:["More Focus","Traveler"],dependencies:["Focus"],blockers:[],cost:2,display:{row:25,col:4},properties:{range:64,focusReq:2},effects:[{type:"replace_spell",name:"Twain's Arc",cost:0,display_text:"Twain's Arc",base_spell:5,spell_type:"damage",scaling:"melee",display:"Twain's Arc Damage",parts:[{name:"Twain's Arc Damage",type:"damage",multipliers:[200,0,0,0,0,0]}]}]},{display_name:"Fierce Stomp",desc:"When using Escape, hold shift to quickly drop down and deal damage.",archetype:"Boltslinger",archetype_req:0,parents:["Refined Gunpowder","Traveler"],dependencies:[],blockers:[],cost:2,display:{row:26,col:1},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Fierce Stomp",cost:0,multipliers:[100,0,0,0,0,0]},{type:"add_spell_prop",base_spell:2,target_part:"Total Damage",cost:0,hits:{"Fierce Stomp":1}}]},{display_name:"Scorched Earth",desc:"Fire Creep become much stronger.",archetype:"Sharpshooter",archetype_req:0,parents:["Twain's Arc"],dependencies:["Fire Creep"],blockers:[],cost:1,display:{row:26,col:5},properties:{duration:2,aoe:.4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fire Creep",cost:0,multipliers:[10,0,0,0,5,0]}]},{display_name:"Leap",desc:"When you double tap jump, leap foward. (2s Cooldown)",archetype:"Boltslinger",archetype_req:5,parents:["Refined Gunpowder","Homing Shots"],dependencies:[],blockers:[],cost:2,display:{row:28,col:0},properties:{cooldown:2},effects:[]},{display_name:"Shocking Bomb",desc:"Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder.",archetype:"Sharpshooter",archetype_req:5,parents:["Twain's Arc","Better Arrow Shield","Homing Shots"],dependencies:["Arrow Bomb"],blockers:[],cost:2,display:{row:28,col:4},properties:{gravity:0},effects:[{type:"convert_spell_conv",target_part:"all",conversion:"thunder"}]},{display_name:"Mana Trap",desc:"Your Traps will give you 4 Mana per second when you stay close to them.",archetype:"Trapper",archetype_req:5,parents:["More Traps","Better Arrow Shield"],dependencies:["Fire Creep"],blockers:[],cost:2,display:{row:28,col:8},properties:{range:12,manaRegen:4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:10,multipliers:[0,0,0,0,0,0]}]},{display_name:"Escape Artist",desc:"When casting Escape, release 100 arrows towards the ground.",archetype:"Boltslinger",archetype_req:0,parents:["Better Guardian Angels","Leap"],dependencies:[],blockers:["Grappling Hook"],cost:2,display:{row:31,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Escape Artist",cost:0,multipliers:[30,0,10,0,0,0]}]},{display_name:"Initiator",desc:"If you do not damage an enemy for 5s or more, your next sucessful hit will deal +50% damage and add +1 Focus.",archetype:"Sharpshooter",archetype_req:5,parents:["Shocking Bomb","Better Arrow Shield","Cheaper Arrow Storm (2)"],dependencies:["Focus"],blockers:[],cost:2,display:{row:31,col:5},properties:{focus:1,timer:5},type:"stat_bonus",bonuses:[{type:"stat",name:"damPct",value:50}]},{display_name:"Call of the Hound",desc:"Arrow Shield summon a Hound that will attack and drag aggressive enemies towards your traps.",archetype:"Trapper",archetype_req:0,parents:["Initiator","Cheaper Arrow Storm (2)"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:32,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Call of the Hound",cost:0,multipliers:[40,0,0,0,0,0]}]},{display_name:"Arrow Hurricane",desc:"Arrow Storm will shoot +2 stream of arrows.",archetype:"Boltslinger",archetype_req:8,parents:["Precise Shot","Escape Artist"],dependencies:[],blockers:["Phantom Ray"],cost:2,display:{row:33,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Stream":2}}]},{display_name:"Geyser Stomp",desc:"Fierce Stomp will create geysers, dealing more damage and vertical knockback.",archetype:"",archetype_req:0,parents:["Shrapnel Bomb"],dependencies:["Fierce Stomp"],blockers:[],cost:2,display:{row:37,col:1},properties:{aoe:1},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Fierce Stomp",cost:0,multipliers:[0,0,0,50,0,0]}]},{display_name:"Crepuscular Ray",desc:"If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus. While in that state, you will lose 1 Focus per second.",archetype:"Sharpshooter",archetype_req:10,parents:["Cheaper Arrow Shield"],dependencies:["Arrow Storm"],blockers:[],cost:2,display:{row:37,col:4},properties:{focusReq:5,focusRegen:-1},effects:[{type:"replace_spell",name:"Crepuscular Ray",base_spell:5,spell_type:"damage",scaling:"spell",display:"One Focus",cost:0,parts:[{name:"Single Arrow",type:"damage",multipliers:[10,0,0,5,0,0]},{name:"One Focus",type:"total",hits:{"Single Arrow":20}},{name:"Total Damage",type:"total",hits:{"One Focus":7}}]}]},{display_name:"Grape Bomb",desc:"Arrow bomb will throw 3 additional smaller bombs when exploding.",archetype:"",archetype_req:0,parents:["Cheaper Escape (2)"],dependencies:[],blockers:[],cost:2,display:{row:37,col:7},properties:{miniBombs:3,aoe:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Grape Bomb",cost:0,multipliers:[30,0,0,0,10,0]}]},{display_name:"Tangled Traps",desc:"Your Traps will be connected by a rope that deals damage to enemies every 0.2s.",archetype:"Trapper",archetype_req:0,parents:["Grape Bomb"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:38,col:6},properties:{attackSpeed:.2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Tangled Traps",cost:0,multipliers:[20,0,0,0,0,20]}]},{display_name:"Snow Storm",desc:"Enemies near you will be slowed down.",archetype:"",archetype_req:0,parents:["Geyser Stomp","More Focus (2)"],dependencies:[],blockers:[],cost:2,display:{row:39,col:2},properties:{range:2.5,slowness:.3}},{display_name:"All-Seeing Panoptes",desc:"Your bows from Guardian Angels become all-seeing, increasing their range, damage and letting them shoot up to +5 times each.",archetype:"Boltslinger",archetype_req:11,parents:["Snow Storm"],dependencies:["Guardian Angels"],blockers:[],cost:2,display:{row:40,col:1},properties:{range:10,shots:5},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Single Arrow",cost:0,multipliers:[0,0,0,0,20,0]},{type:"add_spell_prop",base_spell:4,target_part:"Single Bow",cost:0,hits:{"Single Arrow":5}}]},{display_name:"Minefield",desc:"Allow you to place +6 Traps, but with reduced damage and range.",archetype:"Trapper",archetype_req:10,parents:["Grape Bomb","Cheaper Arrow Bomb (2)"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:40,col:7},properties:{aoe:-2,traps:6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:0,multipliers:[-80,0,0,0,0,0]}]},{display_name:"Bow Proficiency I",desc:"Improve your Main Attack's damage and range when using a bow.",archetype:"",archetype_req:0,parents:["Arrow Bomb"],dependencies:[],blockers:[],cost:1,display:{row:2,col:4},properties:{mainAtk_range:6},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Arrow Bomb",desc:"Reduce the Mana cost of Arrow Bomb.",archetype:"",archetype_req:0,parents:["Bow Proficiency I"],dependencies:[],blockers:[],cost:1,display:{row:2,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-10}]},{display_name:"Cheaper Arrow Storm",desc:"Reduce the Mana cost of Arrow Storm.",archetype:"",archetype_req:0,parents:["Grappling Hook","Windstorm","Focus"],dependencies:[],blockers:[],cost:1,display:{row:21,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Cheaper Escape",desc:"Reduce the Mana cost of Escape.",archetype:"",archetype_req:0,parents:["Arrow Storm","Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:9,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"Earth Mastery",desc:"Increases your base damage from all Earth attacks",archetype:"Trapper",archetype_req:0,parents:["Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:13,col:8},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"eDamPct",value:20},{type:"stat",name:"eDam",value:[2,4]}]}]},{display_name:"Thunder Mastery",desc:"Increases your base damage from all Thunder attacks",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Storm","Fire Mastery"],dependencies:[],blockers:[],cost:1,display:{row:13,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"tDamPct",value:10},{type:"stat",name:"tDam",value:[1,8]}]}]},{display_name:"Water Mastery",desc:"Increases your base damage from all Water attacks",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Escape","Thunder Mastery","Fire Mastery"],dependencies:[],blockers:[],cost:1,display:{row:14,col:4},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"wDamPct",value:15},{type:"stat",name:"wDam",value:[2,4]}]}]},{display_name:"Air Mastery",desc:"Increases base damage from all Air attacks",archetype:"Battle Monk",archetype_req:0,parents:["Arrow Storm"],dependencies:[],blockers:[],cost:1,display:{row:13,col:0},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"aDamPct",value:15},{type:"stat",name:"aDam",value:[3,4]}]}]},{display_name:"Fire Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Sharpshooter",archetype_req:0,parents:["Thunder Mastery","Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:13,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"fDamPct",value:15},{type:"stat",name:"fDam",value:[3,5]}]}]},{display_name:"More Shields",desc:"Give +2 charges to Arrow Shield.",archetype:"",archetype_req:0,parents:["Grappling Hook","Basaltic Trap"],dependencies:["Arrow Shield"],blockers:[],cost:1,display:{row:21,col:7},properties:{shieldCharges:2}},{display_name:"Stormy Feet",desc:"Windy Feet will last longer and add more speed.",archetype:"",archetype_req:0,parents:["Windstorm"],dependencies:["Windy Feet"],blockers:[],cost:1,display:{row:23,col:1},properties:{duration:60},effects:[{type:"stat_bonus",bonuses:[{type:"stat",name:"spdPct",value:20}]}]},{display_name:"Refined Gunpowder",desc:"Increase the damage of Arrow Bomb.",archetype:"",archetype_req:0,parents:["Windstorm"],dependencies:[],blockers:[],cost:1,display:{row:25,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[50,0,0,0,0,0]}]},{display_name:"More Traps",desc:"Increase the maximum amount of active Traps you can have by +2.",archetype:"Trapper",archetype_req:10,parents:["Bouncing Bomb"],dependencies:["Basaltic Trap"],blockers:[],cost:1,display:{row:26,col:8},properties:{traps:2}},{display_name:"Better Arrow Shield",desc:"Arrow Shield will gain additional area of effect, knockback and damage.",archetype:"Sharpshooter",archetype_req:0,parents:["Mana Trap","Shocking Bomb","Twain's Arc"],dependencies:["Arrow Shield"],blockers:[],cost:1,display:{row:28,col:6},properties:{aoe:1},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Shield",multipliers:[40,0,0,0,0,0]}]},{display_name:"Better Leap",desc:"Reduce leap's cooldown by 1s.",archetype:"Boltslinger",archetype_req:0,parents:["Leap","Homing Shots"],dependencies:["Leap"],blockers:[],cost:1,display:{row:29,col:1},properties:{cooldown:-1}},{display_name:"Better Guardian Angels",desc:"Your Guardian Angels can shoot +4 arrows before disappearing.",archetype:"Boltslinger",archetype_req:0,parents:["Escape Artist","Homing Shots"],dependencies:["Guardian Angels"],blockers:[],cost:1,display:{row:31,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Single Bow",cost:0,hits:{"Single Arrow":4}}]},{display_name:"Cheaper Arrow Storm (2)",desc:"Reduce the Mana cost of Arrow Storm.",archetype:"",archetype_req:0,parents:["Initiator","Mana Trap"],dependencies:[],blockers:[],cost:1,display:{row:31,col:8},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Precise Shot",desc:"+30% Critical Hit Damage",archetype:"",archetype_req:0,parents:["Better Guardian Angels","Cheaper Arrow Shield","Arrow Hurricane"],dependencies:[],blockers:[],cost:1,display:{row:33,col:2},properties:{mainAtk_range:6},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdCritPct",value:30}]}]},{display_name:"Cheaper Arrow Shield",desc:"Reduce the Mana cost of Arrow Shield.",archetype:"",archetype_req:0,parents:["Precise Shot","Initiator"],dependencies:[],blockers:[],cost:1,display:{row:33,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Rocket Jump",desc:"Arrow Bomb's self-damage will knockback you farther away.",archetype:"",archetype_req:0,parents:["Cheaper Arrow Storm (2)","Initiator"],dependencies:["Arrow Bomb"],blockers:[],cost:1,display:{row:33,col:6},properties:{}},{display_name:"Cheaper Escape (2)",desc:"Reduce the Mana cost of Escape.",archetype:"",archetype_req:0,parents:["Call of the Hound","Decimator"],dependencies:[],blockers:[],cost:1,display:{row:34,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"Stronger Hook",desc:"Increase your Grappling Hook's range, speed and strength.",archetype:"Trapper",archetype_req:5,parents:["Cheaper Escape (2)"],dependencies:["Grappling Hook"],blockers:[],cost:1,display:{row:35,col:8},properties:{range:8}},{display_name:"Cheaper Arrow Bomb (2)",desc:"Reduce the Mana cost of Arrow Bomb.",archetype:"",archetype_req:0,parents:["More Focus (2)","Minefield"],dependencies:[],blockers:[],cost:1,display:{row:40,col:5},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Bouncing Bomb",desc:"Arrow Bomb will bounce once when hitting a block or enemy",archetype:"",archetype_req:0,parents:["More Shields"],dependencies:[],blockers:[],cost:2,display:{row:25,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Arrow Bomb":2}}]},{display_name:"Homing Shots",desc:"Your Main Attack arrows will follow nearby enemies and not be affected by gravity",archetype:"",archetype_req:0,parents:["Leap","Shocking Bomb"],dependencies:[],blockers:[],cost:2,display:{row:28,col:2},properties:{},effects:[]},{display_name:"Shrapnel Bomb",desc:"Arrow Bomb's explosion will fling 15 shrapnel, dealing damage in a large area",archetype:"Boltslinger",archetype_req:8,parents:["Arrow Hurricane","Precise Shot"],dependencies:[],blockers:[],cost:2,display:{row:34,col:1},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Shrapnel Bomb",cost:0,multipliers:[40,0,0,0,20,0]}]},{display_name:"Elusive",desc:"If you do not get hit for 8+ seconds, become immune to self-damage and remove Arrow Storm's recoil. (Dodging counts as not getting hit)",archetype:"Boltslinger",archetype_req:0,parents:["Geyser Stomp"],dependencies:[],blockers:[],cost:2,display:{row:38,col:0},properties:{},effects:[]},{display_name:"Double Shots",desc:"Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies)",archetype:"Boltslinger",archetype_req:0,parents:["Escape"],dependencies:[],blockers:["Power Shots"],cost:1,display:{row:7,col:2},properties:{arrow:2},effects:[{type:"add_spell_prop",base_spell:0,target_part:"Melee Damage",cost:0,multipliers:.7}]},{display_name:"Triple Shots",desc:"Triple Main Attack arrows, but they deal -20% damage per arrow",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Rain","Frenzy"],dependencies:["Double Shots"],blockers:[],cost:1,display:{row:17,col:0},properties:{arrow:2},effects:[{type:"add_spell_prop",base_spell:0,target_part:"Melee Damage",cost:0,multipliers:.7}]},{display_name:"Power Shots",desc:"Main Attack arrows have increased speed and knockback",archetype:"Sharpshooter",archetype_req:0,parents:["Escape"],dependencies:[],blockers:["Double Shots"],cost:1,display:{row:7,col:6},properties:{},effects:[]},{display_name:"Focus",desc:"When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once",archetype:"Sharpshooter",archetype_req:2,parents:["Phantom Ray"],dependencies:[],blockers:[],cost:2,display:{row:19,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:3}]},{display_name:"More Focus",desc:"Add +2 max Focus",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Arrow Storm","Grappling Hook"],dependencies:[],blockers:[],cost:1,display:{row:22,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:5}]},{display_name:"More Focus (2)",desc:"Add +2 max Focus",archetype:"Sharpshooter",archetype_req:0,parents:["Crepuscular Ray","Snow Storm"],dependencies:[],blockers:[],cost:1,display:{row:39,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:7}]},{display_name:"Traveler",desc:"For every 1% Walk Speed you have from items, gain +1 Raw Spell Damage (Max 100)",archetype:"",archetype_req:0,parents:["Refined Gunpowder","Twain's Arc"],dependencies:[],blockers:[],cost:1,display:{row:25,col:2},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"spd"}],output:{type:"stat",name:"sdRaw"},scaling:[1],max:100}]},{display_name:"Patient Hunter",desc:"Your Traps will deal +20% more damage for every second they are active (Max +80%)",archetype:"Trapper",archetype_req:0,parents:["More Shields"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:22,col:8},properties:{max:80},effects:[]},{display_name:"Stronger Patient Hunter",desc:"Add +80% Max Damage to Patient Hunter",archetype:"Trapper",archetype_req:0,parents:["Grape Bomb"],dependencies:["Patient Hunter"],blockers:[],cost:1,display:{row:38,col:8},properties:{max:80},effects:[]},{display_name:"Frenzy",desc:"Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second",archetype:"Boltslinger",archetype_req:0,parents:["Triple Shots","Nimble String"],dependencies:[],blockers:[],cost:2,display:{row:17,col:2},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Hits dealt",output:{type:"stat",name:"spd"},scaling:[6],max:200}]},{display_name:"Phantom Ray",desc:"Condense Arrow Storm into a single ray that damages enemies 10 times per second",archetype:"Sharpshooter",archetype_req:0,parents:["Water Mastery","Fire Creep"],dependencies:["Arrow Storm"],blockers:["Windstorm","Nimble String","Arrow Hurricane"],cost:2,display:{row:16,col:4},properties:{},effects:[{type:"replace_spell",name:"Phantom Ray",cost:40,display_text:"Max Damage",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[25,0,5,0,0,0]},{name:"Total Damage",type:"total",hits:{"Single Arrow":16}}]}]},{display_name:"Arrow Rain",desc:"When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies",archetype:"Trapper",archetype_req:0,parents:["Nimble String","Air Mastery"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:15,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Arrow Rain",cost:0,multipliers:[120,0,0,0,0,80]}]},{display_name:"Decimator",desc:"Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%)",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Arrow Shield"],dependencies:["Phantom Ray"],blockers:[],cost:1,display:{row:34,col:5},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Phantom Ray hits",output:{type:"stat",name:"PhRayDmg"},scaling:10,max:50}]}],Assassin:[{title:"Spin Attack",desc:"desc",image:"../media/atree/node.png",connector:!1,row:0,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:1,col:4},{title:"Dagger Proficiency I",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:2,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:3,col:4},{title:"Double Spin",desc:"desc",image:"../media/atree/node.png",connector:!1,row:4,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:5,col:4},{title:"Dash",desc:"desc",image:"../media/atree/node.png",connector:!1,row:6,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:6,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:6,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:6,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:6,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:7,col:2},{title:"Smoke Bomb",desc:"desc",image:"../media/atree/node.png",connector:!1,row:8,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:7,col:6},{title:"Multihit",desc:"desc",image:"../media/atree/node.png",connector:!1,row:8,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:8,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:1},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:8,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:6},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:8,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:8,col:8},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:8},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:10,col:8},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:10,col:1},{title:"Backstab",desc:"desc",image:"../media/atree/node.png",connector:!1,row:11,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:9,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:90,row:10,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:10,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:11,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:10,col:7},{title:"Fatality",desc:"desc",image:"../media/atree/node.png",connector:!1,row:11,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:11,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:0},{title:"Violent Vortex",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:11,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:2},{title:"Vanish",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:13,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:13,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:14,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:15,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:14,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:15,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:12,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:13,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:14,col:7},{title:"Lacerate",desc:"desc",image:"../media/atree/node.png",connector:!1,row:15,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:15,col:1},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:16,col:1},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:15,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:16,col:5},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:15,col:8},{title:"Wall of Smoke",desc:"desc",image:"../media/atree/node.png",connector:!1,row:16,col:8},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:16,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:0},{title:"Silent Killer",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:16,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:2},{title:"Shadow Travel",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:5},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:17,col:8},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:18,col:8},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:18,col:4},{title:"Exploding Clones",desc:"desc",image:"../media/atree/node.png",connector:!1,row:19,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:18,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:19,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:20,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:19,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:20,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:18,col:6},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:18,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:19,col:7},{title:"Weightless",desc:"desc",image:"../media/atree/node.png",connector:!1,row:20,col:7},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:20,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:20,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:1},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:20,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:4},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:20,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:21,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:6},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:20,col:8},{title:"Dancing Blade",desc:"desc",image:"../media/atree/node.png",connector:!1,row:21,col:8},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:21,col:0},{title:"Spin Attack Damage",desc:"desc",image:"../media/atree/node.png",connector:!1,row:22,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:21,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:22,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:22,col:1},{title:"Marked",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:22,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:23,col:5},{title:"Shurikens",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:6},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:23,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:22,col:8},{title:"Far Reach",desc:"desc",image:"../media/atree/node.png",connector:!1,row:23,col:8},{title:"Stronger Multihit",desc:"desc",image:"../media/atree/node.png",connector:!1,row:24,col:5},{title:"Psithurism",desc:"desc",image:"../media/atree/node.png",connector:!1,row:24,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:24,col:1},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:1},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:3},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:24,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:4},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:25,col:5},{title:"Choke Bomb",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:6},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:25,col:7},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:25,col:8},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:26,col:5},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:25,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:0},{title:"Death Magnet",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:25,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:2},{title:"Cheaper Multihit",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:26,col:7},{title:"Parry",desc:"desc",image:"../media/atree/node.png",connector:!1,row:27,col:7},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:27,col:1},{title:"Fatal Spin",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:1},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:27,col:3},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:27,col:6},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:6},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:27,col:8},{title:"Wall Jump",desc:"desc",image:"../media/atree/node.png",connector:!1,row:28,col:8},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:28,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:29,col:0},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:29,col:1},{title:"Harvester",desc:"desc",image:"../media/atree/node.png",connector:!1,row:30,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:28,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:29,col:4},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:30,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:28,col:7},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:29,col:7},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:30,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:30,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:31,col:2},{image:"../media/atree/connect_t.png",connector:!0,rotate:180,row:30,col:5},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:30,col:6},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:31,col:5},{title:"Ricochet",desc:"desc",image:"../media/atree/node.png",connector:!1,row:31,col:8},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:31,col:1},{title:"Satsujin",desc:"desc",image:"../media/atree/node.png",connector:!1,row:32,col:1},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:31,col:4},{title:"Forbidden Art",desc:"desc",image:"../media/atree/node.png",connector:!1,row:32,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:31,col:7},{title:"Jasmine Bloom",desc:"desc",image:"../media/atree/node.png",connector:!1,row:32,col:7},{image:"../media/atree/connect_angle.png",connector:!0,rotate:180,row:32,col:0},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:0},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:32,col:2},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:2},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:32,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:5},{title:"Text",desc:"desc",image:"../media/atree/node.png",connector:!1,row:33,col:8},],Warrior:[{display_name:"Bash",desc:"Violently bash the ground, dealing high damage in a large area",archetype:"",archetype_req:0,parents:[],dependencies:[],blockers:[],cost:1,display:{row:0,col:4},properties:{aoe:4,range:3},effects:[{type:"replace_spell",name:"Bash",cost:45,display_text:"Total Damage Average",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Hit",type:"damage",multipliers:[130,20,0,0,0,0]},{name:"Total Damage",type:"total",hits:{"Single Hit":1}}]}]},{display_name:"Spear Proficiency 1",desc:"Improve your Main Attack's damage and range w/ spear",archetype:"",archetype_req:0,parents:["Bash"],dependencies:[],blockers:[],cost:1,display:{row:2,col:4},properties:{melee_range:1},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Bash",desc:"Reduce the Mana cost of Bash",archetype:"",archetype_req:0,parents:["Spear Proficiency 1"],dependencies:[],blockers:[],cost:1,display:{row:2,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-10}]},{display_name:"Double Bash",desc:"Bash will hit a second time at a farther range",archetype:"",archetype_req:0,parents:["Spear Proficiency 1"],dependencies:[],blockers:[],cost:1,display:{row:4,col:4},properties:{range:3},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{name:"Single Hit",value:1}},{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[-50,0,0,0,0,0]}]},{display_name:"Charge",desc:"Charge forward at high speed (hold shift to cancel)",archetype:"",archetype_req:0,parents:["Double Bash"],dependencies:[],blockers:[],cost:1,display:{row:6,col:4},properties:{},effects:[{type:"replace_spell",name:"Charge",cost:25,display_text:"Total Damage Average",base_spell:2,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"None",type:"damage",multipliers:[0,0,0,0,0,0]},{name:"Total Damage",type:"total",hits:{None:0}}]}]},{display_name:"Heavy Impact",desc:"After using Charge, violently crash down into the ground and deal damage",archetype:"",archetype_req:0,parents:["Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:9,col:1},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Heavy Impact",cost:0,multipliers:[100,0,0,0,0,0]}]},{display_name:"Vehement",desc:"For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%)",archetype:"Fallen",archetype_req:0,parents:["Charge"],dependencies:[],blockers:["Tougher Skin"],cost:1,display:{row:6,col:2},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"mdPct"},{type:"stat",name:"mdRaw"}],output:{type:"stat",name:"spd"},scaling:[1,1],max:20}]},{display_name:"Tougher Skin",desc:"Harden your skin and become permanently +5% more resistant\nFor every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)",archetype:"Paladin",archetype_req:0,parents:["Charge"],dependencies:[],blockers:["Vehement"],cost:1,display:{row:6,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"baseResist",value:"5"}]},{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hprRaw"},{type:"stat",name:"hprPct"}],output:{type:"stat",name:"hpBonus"},scaling:[10,10],max:100}]},{display_name:"Uppercut",desc:"Rocket enemies in the air and deal massive damage",archetype:"",archetype_req:0,parents:["Vehement"],dependencies:[],blockers:[],cost:1,display:{row:8,col:2},properties:{aoe:3,range:5},effects:[{type:"replace_spell",name:"Uppercut",cost:45,display_text:"Total Damage Average",base_spell:3,spell_type:"damage",scaling:"spell",display:"total",parts:[{name:"Uppercut",type:"damage",multipliers:[150,50,50,0,0,0]},{name:"Total Damage",type:"total",hits:{Uppercut:1}}]}]},{display_name:"Cheaper Charge",desc:"Reduce the Mana cost of Charge",archetype:"",archetype_req:0,parents:["Uppercut","War Scream"],dependencies:[],blockers:[],cost:1,display:{row:8,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"War Scream",desc:"Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies",archetype:"",archetype_req:0,parents:["Tougher Skin"],dependencies:[],blockers:[],cost:1,display:{row:8,col:6},properties:{duration:30,aoe:12,defense_bonus:10},effects:[{type:"replace_spell",name:"War Scream",cost:35,display_text:"War Scream",base_spell:4,spell_type:"damage",scaling:"spell",display:"Total Damage Average",parts:[{name:"War Scream",type:"damage",multipliers:[50,0,0,0,50,0]}]}]},{display_name:"Earth Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Fallen",archetype_req:0,parents:["Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:10,col:0},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"eDamPct",value:20},{type:"stat",name:"eDam",value:[2,4]}]}]},{display_name:"Thunder Mastery",desc:"Increases base damage from all Thunder attacks",archetype:"Fallen",archetype_req:0,parents:["Uppercut","Air Mastery"],dependencies:[],blockers:[],cost:1,display:{row:10,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"tDamPct",value:10},{type:"stat",name:"tDam",value:[1,8]}]}]},{display_name:"Water Mastery",desc:"Increases base damage from all Water attacks",archetype:"Battle Monk",archetype_req:0,parents:["Cheaper Charge","Thunder Mastery","Air Mastery"],dependencies:[],blockers:[],cost:1,display:{row:11,col:4},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"wDamPct",value:15},{type:"stat",name:"wDam",value:[2,4]}]}]},{display_name:"Air Mastery",desc:"Increases base damage from all Air attacks",archetype:"Battle Monk",archetype_req:0,parents:["War Scream","Thunder Mastery"],dependencies:[],blockers:[],cost:1,display:{row:10,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"aDamPct",value:15},{type:"stat",name:"aDam",value:[3,4]}]}]},{display_name:"Fire Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Paladin",archetype_req:0,parents:["War Scream"],dependencies:[],blockers:[],cost:1,display:{row:10,col:8},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"fDamPct",value:15},{type:"stat",name:"fDam",value:[3,5]}]}]},{display_name:"Quadruple Bash",desc:"Bash will hit 4 times at an even larger range",archetype:"Fallen",archetype_req:0,parents:["Earth Mastery","Fireworks"],dependencies:[],blockers:[],cost:2,display:{row:12,col:0},properties:{range:6},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Hit":2}},{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[-20,0,0,0,0,0]}]},{display_name:"Fireworks",desc:"Mobs hit by Uppercut will explode mid-air and receive additional damage",archetype:"Fallen",archetype_req:0,parents:["Thunder Mastery","Quadruple Bash"],dependencies:[],blockers:[],cost:2,display:{row:12,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fireworks",cost:0,multipliers:[80,0,20,0,0,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{Fireworks:1}}]},{display_name:"Half-Moon Swipe",desc:"Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water",archetype:"Battle Monk",archetype_req:1,parents:["Water Mastery"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:13,col:4},properties:{range:4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:-10,multipliers:[-70,0,0,0,0,0]},{type:"convert_spell_conv",target_part:"all",conversion:"water"}]},{display_name:"Flyby Jab",desc:"Damage enemies in your way when using Charge",archetype:"",archetype_req:0,parents:["Air Mastery","Flaming Uppercut"],dependencies:[],blockers:[],cost:2,display:{row:12,col:6},properties:{aoe:2},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Flyby Jab",cost:0,multipliers:[20,0,0,0,0,40]}]},{display_name:"Flaming Uppercut",desc:"Uppercut will light mobs on fire, dealing damage every 0.6 seconds",archetype:"Paladin",archetype_req:0,parents:["Fire Mastery","Flyby Jab"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:12,col:8},properties:{duration:3,tick:.6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Flaming Uppercut",cost:0,multipliers:[0,0,0,0,50,0]},{type:"add_spell_prop",base_spell:3,target_part:"Flaming Uppercut Total Damage",cost:0,hits:{"Flaming Uppercut":5}},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Flaming Uppercut":5}}]},{display_name:"Iron Lungs",desc:"War Scream deals more damage",archetype:"",archetype_req:0,parents:["Flyby Jab","Flaming Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:13,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"War Scream",cost:0,multipliers:[30,0,0,0,0,30]}]},{display_name:"Generalist",desc:"After casting 3 different spells in a row, your next spell will cost 5 mana",archetype:"Battle Monk",archetype_req:3,parents:["Counter"],dependencies:[],blockers:[],cost:2,display:{row:15,col:2},properties:{},effects:[]},{display_name:"Counter",desc:"When dodging a nearby enemy attack, get 30% chance to instantly attack back",archetype:"Battle Monk",archetype_req:0,parents:["Half-Moon Swipe"],dependencies:[],blockers:[],cost:2,display:{row:15,col:4},properties:{chance:30},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Counter",cost:0,multipliers:[60,0,20,0,0,20]}]},{display_name:"Mantle of the Bovemists",desc:"When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)",archetype:"Paladin",archetype_req:3,parents:["Iron Lungs"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:15,col:7},properties:{mantle_charge:3},effects:[]},{display_name:"Bak'al's Grasp",desc:"After casting War Scream, become Corrupted (15s Cooldown). You cannot heal while in that state\n\nWhile Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)",archetype:"Fallen",archetype_req:2,parents:["Quadruple Bash","Fireworks"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:16,col:1},properties:{cooldown:15},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"raw"},scaling:[4],slider_step:2,max:120}]},{display_name:"Spear Proficiency 2",desc:"Improve your Main Attack's damage and range w/ spear",archetype:"",archetype_req:0,parents:["Bak'al's Grasp","Cheaper Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:17,col:0},properties:{melee_range:1},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Uppercut",desc:"Reduce the Mana Cost of Uppercut",archetype:"",archetype_req:0,parents:["Spear Proficiency 2","Aerodynamics","Counter"],dependencies:[],blockers:[],cost:1,display:{row:17,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Aerodynamics",desc:"During Charge, you can steer and change direction",archetype:"Battle Monk",archetype_req:0,parents:["Cheaper Uppercut","Provoke"],dependencies:[],blockers:[],cost:2,display:{row:17,col:5},properties:{},effects:[]},{display_name:"Provoke",desc:"Mobs damaged by War Scream will target only you for at least 5s \n\nReduce the Mana cost of War Scream",archetype:"Paladin",archetype_req:0,parents:["Aerodynamics","Mantle of the Bovemists"],dependencies:[],blockers:[],cost:1,display:{row:17,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Precise Strikes",desc:"+30% Critical Hit Damage",archetype:"",archetype_req:0,parents:["Cheaper Uppercut","Spear Proficiency 2"],dependencies:[],blockers:[],cost:1,display:{row:18,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"critDmg",value:30}]}]},{display_name:"Air Shout",desc:"War Scream will fire a projectile that can go through walls and deal damage multiple times",archetype:"",archetype_req:0,parents:["Aerodynamics","Provoke"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:18,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Air Shout",cost:0,multipliers:[20,0,0,0,0,5]}]},{display_name:"Enraged Blow",desc:"While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)",archetype:"Fallen",archetype_req:0,parents:["Spear Proficiency 2"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:20,col:0},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hpBonus"}],output:{type:"stat",name:"dmgPct"},scaling:[2],max:200}]},{display_name:"Flying Kick",desc:"When using Charge, mobs hit will halt your momentum and get knocked back",archetype:"Battle Monk",archetype_req:1,parents:["Cheaper Uppercut","Stronger Mantle"],dependencies:[],blockers:[],cost:2,display:{row:20,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Flying Kick",cost:0,multipliers:[120,0,0,10,0,20]}]},{display_name:"Stronger Mantle",desc:"Add +2 additional charges to Mantle of the Bovemists",archetype:"Paladin",archetype_req:0,parents:["Manachism","Flying Kick"],dependencies:[],blockers:[],cost:1,display:{row:20,col:6},properties:{mantle_charge:2},effects:[]},{display_name:"Manachism",desc:"If you receive a hit that's less than 5% of your max HP, gain 10 Mana (1s Cooldown)",archetype:"Paladin",archetype_req:3,parents:["Stronger Mantle","Provoke"],dependencies:[],blockers:[],cost:2,display:{row:20,col:8},properties:{cooldown:1},effects:[]},{display_name:"Boiling Blood",desc:"Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds",archetype:"",archetype_req:0,parents:["Enraged Blow","Ragnarokkr"],dependencies:[],blockers:[],cost:2,display:{row:22,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Boiling Blood",cost:0,multipliers:[25,0,0,0,5,0]}]},{display_name:"Ragnarokkr",desc:"War Scream become deafening, increasing its range and giving damage bonus to players",archetype:"Fallen",archetype_req:0,parents:["Boiling Blood","Flying Kick"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:22,col:2},properties:{damage_bonus:30,aoe:2},effects:[{type:"add_spell_prop",base_spell:4,cost:10}]},{display_name:"Ambidextrous",desc:"Increase your chance to attack with Counter by +30%",archetype:"",archetype_req:0,parents:["Flying Kick","Stronger Mantle","Burning Heart"],dependencies:["Counter"],blockers:[],cost:1,display:{row:22,col:4},properties:{chance:30},effects:[]},{display_name:"Burning Heart",desc:"For every 100 Health Bonus you have from item IDs, gain +2% Fire Damage (Max 100%)",archetype:"Paladin",archetype_req:0,parents:["Ambidextrous","Stronger Bash"],dependencies:[],blockers:[],cost:1,display:{row:22,col:6},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hpBonus"}],output:{type:"stat",name:"fDamPct"},scaling:[2],max:100,slider_step:100}]},{display_name:"Stronger Bash",desc:"Increase the damage of Bash",archetype:"",archetype_req:0,parents:["Burning Heart","Manachism"],dependencies:[],blockers:[],cost:1,display:{row:22,col:8},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[30,0,0,0,0,0]}]},{display_name:"Intoxicating Blood",desc:"After leaving Corrupted, gain 2% of the health lost back for each enemy killed while Corrupted",archetype:"Fallen",archetype_req:5,parents:["Ragnarokkr","Boiling Blood"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:23,col:1},properties:{},effects:[]},{display_name:"Comet",desc:"After being hit by Fireworks, enemies will crash into the ground and receive more damage",archetype:"Fallen",archetype_req:0,parents:["Ragnarokkr"],dependencies:["Fireworks"],blockers:[],cost:2,display:{row:24,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Comet",cost:0,multipliers:[80,20,0,0,0,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{Comet:1}}]},{display_name:"Collide",desc:"Mobs thrown into walls from Flying Kick will explode and receive additonal damage",archetype:"Battle Monk",archetype_req:4,parents:["Ambidextrous","Burning Heart"],dependencies:["Flying Kick"],blockers:[],cost:2,display:{row:23,col:5},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Collide",cost:0,multipliers:[100,0,0,0,50,0]}]},{display_name:"Rejuvenating Skin",desc:"Regain back 30% of the damage you take as healing over 30s",archetype:"Paladin",archetype_req:0,parents:["Burning Heart","Stronger Bash"],dependencies:[],blockers:[],cost:2,display:{row:23,col:7},properties:{},effects:[]},{display_name:"Uncontainable Corruption",desc:"Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1",archetype:"",archetype_req:0,parents:["Boiling Blood","Radiant Devotee"],dependencies:["Bak'al's Grasp"],blockers:[],cost:1,display:{row:26,col:0},properties:{cooldown:-5},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"raw"},scaling:[1],slider_step:2,max:50}]},{display_name:"Radiant Devotee",desc:"For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)",archetype:"Battle Monk",archetype_req:1,parents:["Whirlwind Strike","Uncontainable Corruption"],dependencies:[],blockers:[],cost:1,display:{row:26,col:2},properties:{},effects:[{type:"stat_scaling",inputs:[{type:"stat",name:"ref"}],output:{type:"stat",name:"mr"},scaling:[1],max:10,slider_step:4}]},{display_name:"Whirlwind Strike",desc:"Uppercut will create a strong gust of air, launching you upward with enemies (Hold shift to stay grounded)",archetype:"Battle Monk",archetype_req:5,parents:["Ambidextrous","Radiant Devotee"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:26,col:4},properties:{range:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:0,multipliers:[0,0,0,0,0,50]}]},{display_name:"Mythril Skin",desc:"Gain +5% Base Resistance and become immune to knockback",archetype:"Paladin",archetype_req:6,parents:["Rejuvenating Skin"],dependencies:[],blockers:[],cost:2,display:{row:26,col:7},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"baseResist",value:5}]}]},{display_name:"Armour Breaker",desc:"While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage",archetype:"Fallen",archetype_req:0,parents:["Uncontainable Corruption","Radiant Devotee"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:27,col:1},properties:{duration:5},effects:[]},{display_name:"Shield Strike",desc:"When your Mantle of the Bovemist loses all charges, deal damage around you for each Mantle individually lost",archetype:"Paladin",archetype_req:0,parents:["Mythril Skin","Sparkling Hope"],dependencies:[],blockers:[],cost:2,display:{row:27,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Shield Strike",cost:0,multipliers:[60,0,20,0,0,0]}]},{display_name:"Sparkling Hope",desc:"Everytime you heal 5% of your max health, deal damage to all nearby enemies",archetype:"Paladin",archetype_req:0,parents:["Mythril Skin"],dependencies:[],blockers:[],cost:2,display:{row:27,col:8},properties:{aoe:6},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Sparkling Hope",cost:0,multipliers:[10,0,5,0,0,0]}]},{display_name:"Massive Bash",desc:"While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)",archetype:"Fallen",archetype_req:8,parents:["Tempest","Uncontainable Corruption"],dependencies:[],blockers:[],cost:2,display:{row:28,col:0},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"bashAoE"},scaling:[1],max:10,slider_step:3}]},{display_name:"Tempest",desc:"War Scream will ripple the ground and deal damage 3 times in a large area",archetype:"Battle Monk",archetype_req:0,parents:["Massive Bash","Spirit of the Rabbit"],dependencies:[],blockers:[],cost:2,display:{row:28,col:2},properties:{aoe:16},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Tempest",cost:"0",multipliers:[30,10,0,0,0,10]},{type:"add_spell_prop",base_spell:4,target_part:"Tempest Total Damage",cost:"0",hits:{Tempest:3}},{type:"add_spell_prop",base_spell:4,target_part:"Total Damage",cost:"0",hits:{Tempest:3}}]},{display_name:"Spirit of the Rabbit",desc:"Reduce the Mana cost of Charge and increase your Walk Speed by +20%",archetype:"Battle Monk",archetype_req:5,parents:["Tempest","Whirlwind Strike"],dependencies:[],blockers:[],cost:1,display:{row:28,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5},{type:"raw_stat",bonuses:[{type:"stat",name:"spd",value:20}]}]},{display_name:"Massacre",desc:"While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar",archetype:"Fallen",archetype_req:5,parents:["Tempest","Massive Bash"],dependencies:[],blockers:[],cost:2,display:{row:29,col:1},properties:{},effects:[]},{display_name:"Axe Kick",desc:"Increase the damage of Uppercut, but also increase its mana cost",archetype:"",archetype_req:0,parents:["Tempest","Spirit of the Rabbit"],dependencies:[],blockers:[],cost:1,display:{row:29,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:10,multipliers:[100,0,0,0,0,0]}]},{display_name:"Radiance",desc:"Bash will buff your allies' positive IDs. (15s Cooldown)",archetype:"Paladin",archetype_req:2,parents:["Spirit of the Rabbit","Cheaper Bash 2"],dependencies:[],blockers:[],cost:2,display:{row:29,col:5},properties:{cooldown:15},effects:[]},{display_name:"Cheaper Bash 2",desc:"Reduce the Mana cost of Bash",archetype:"",archetype_req:0,parents:["Radiance","Shield Strike","Sparkling Hope"],dependencies:[],blockers:[],cost:1,display:{row:29,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Cheaper War Scream",desc:"Reduce the Mana cost of War Scream",archetype:"",archetype_req:0,parents:["Massive Bash"],dependencies:[],blockers:[],cost:1,display:{row:31,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Discombobulate",desc:"Every time you hit an enemy, briefly increase your elemental damage dealt to them by +2 (Additive, Max +50). This bonus decays -5 every second",archetype:"Battle Monk",archetype_req:12,parents:["Thunderclap"],dependencies:[],blockers:[],cost:2,display:{row:31,col:2},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Hits dealt",output:{type:"stat",name:"rainrawButDifferent"},scaling:[2],max:50}]},{display_name:"Thunderclap",desc:"Bash will cast at the player's position and gain additional AoE.\n\n All elemental conversions become Thunder",archetype:"Battle Monk",archetype_req:8,parents:["Spirit of the Rabbit"],dependencies:[],blockers:[],cost:2,display:{row:31,col:4},properties:{aoe:2},effects:[{type:"convert_spell_conv",target_part:"all",conversion:"thunder"}]},{display_name:"Cyclone",desc:"After casting War Scream, envelop yourself with a vortex that damages nearby enemies every 0.5s",archetype:"Battle Monk",archetype_req:0,parents:["Thunderclap"],dependencies:[],blockers:[],cost:1,display:{row:32,col:5},properties:{aoe:4,duration:20},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Cyclone",cost:0,multipliers:[10,0,0,0,5,10]},{type:"add_spell_prop",base_spell:4,target_part:"Cyclone Total Damage",cost:0,hits:{Cyclone:40}}]},{display_name:"Second Chance",desc:"When you receive a fatal blow, survive and regain 30% of your Health (10m Cooldown)",archetype:"Paladin",archetype_req:12,parents:["Cheaper Bash 2"],dependencies:[],blockers:[],cost:2,display:{row:32,col:7},properties:{},effects:[]},{display_name:"Blood Pact",desc:"If you do not have enough mana to cast a spell, spend health instead (1% health per mana)",archetype:"",archetype_req:10,parents:["Cheaper War Scream"],dependencies:[],blockers:[],cost:2,display:{row:34,col:1},properties:{},effects:[]},{display_name:"Haemorrhage",desc:"Reduce Blood Pact's health cost. (0.5% health per mana)",archetype:"Fallen",archetype_req:0,parents:["Blood Pact"],dependencies:["Blood Pact"],blockers:[],cost:1,display:{row:35,col:2},properties:{},effects:[]},{display_name:"Brink of Madness",desc:"If your health is 25% full or less, gain +40% Resistance",archetype:"",archetype_req:0,parents:["Blood Pact","Cheaper Uppercut 2"],dependencies:[],blockers:[],cost:2,display:{row:35,col:4},properties:{},effects:[]},{display_name:"Cheaper Uppercut 2",desc:"Reduce the Mana cost of Uppercut",archetype:"",archetype_req:0,parents:["Second Chance","Brink of Madness"],dependencies:[],blockers:[],cost:1,display:{row:35,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Martyr",desc:"When you receive a fatal blow, all nearby allies become invincible",archetype:"Paladin",archetype_req:0,parents:["Second Chance"],dependencies:[],blockers:[],cost:2,display:{row:35,col:8},properties:{duration:3,aoe:12},effects:[]}],Mage:[],Shaman:[]},atree_example=[{title:"skill",desc:"desc",image:"../media/atree/node.png",connector:!1,row:5,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:4,col:3},{title:"skill2",desc:"desc",image:"../media/atree/node.png",connector:!1,row:0,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:1,col:2},{title:"skill3",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:2,col:3},{title:"skill4",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:3,col:2},{title:"skill5",desc:"desc",image:"../media/atree/node.png",connector:!1,row:4,col:2},] +const atrees={Archer:[{display_name:"Arrow Shield",desc:"Create a shield around you that deal damage and knockback mobs when triggered. (2 Charges)",archetype:"",archetype_req:0,parents:["Power Shots","Cheaper Escape"],dependencies:[],blockers:[],cost:1,display:{row:9,col:6},properties:{duration:60},effects:[{type:"replace_spell",name:"Arrow Shield",cost:30,display_text:"Max Damage",base_spell:4,spell_type:"damage",scaling:"spell",display:"",parts:[{name:"Shield Damage",type:"damage",multipliers:[90,0,0,0,0,10]},{name:"Total Damage",type:"total",hits:{"Shield Damage":2}}]}]},{display_name:"Escape",desc:"Throw yourself backward to avoid danger. (Hold shift while escaping to cancel)",archetype:"",archetype_req:0,parents:["Heart Shatter"],dependencies:[],blockers:[],cost:1,display:{row:7,col:4},properties:{aoe:0,range:0},effects:[{type:"replace_spell",name:"Escape",cost:25,display_text:"Max Damage",base_spell:2,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"None",type:"damage",multipliers:[0,0,0,0,0,0]},{name:"Total Damage",type:"total",hits:{None:0}}]}]},{display_name:"Arrow Bomb",desc:"Throw a long-range arrow that explodes and deal high damage in a large area. (Self-damage for 25% of your DPS)",archetype:"",archetype_req:0,parents:[],dependencies:[],blockers:[],cost:1,display:{row:0,col:4},properties:{aoe:4.5,range:26},effects:[{type:"replace_spell",name:"Arrow Bomb",cost:50,display_text:"Average Damage",base_spell:3,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Arrow Bomb",type:"damage",multipliers:[160,0,0,0,20,0]},{name:"Total Damage",type:"total",hits:{"Arrow Bomb":1}}]}]},{display_name:"Heart Shatter",desc:"If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage.",archetype:"",archetype_req:0,parents:["Bow Proficiency I"],dependencies:[],blockers:[],cost:1,display:{row:4,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[100,0,0,0,0,0]},{}]},{display_name:"Fire Creep",desc:"Arrow Bomb will leak a trail of fire for 6s, Damaging enemies that walk into it every 0.4s.",archetype:"",archetype_req:0,parents:["Phantom Ray","Fire Mastery","Bryophyte Roots"],dependencies:[],blockers:[],cost:2,display:{row:16,col:6},properties:{aoe:.8,duration:6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fire Creep",cost:0,multipliers:[30,0,0,0,20,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Fire Creep":15}}]},{display_name:"Bryophyte Roots",desc:"When you hit an enemy with Arrow Storm, create an area that slows them down and deals damage every 0.4s.",archetype:"Trapper",archetype_req:1,parents:["Fire Creep","Earth Mastery"],dependencies:["Arrow Storm"],blockers:[],cost:2,display:{row:16,col:8},properties:{aoe:2,duration:5,slowness:.4},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Bryophyte Roots",cost:0,multipliers:[40,20,0,0,0,0]}]},{display_name:"Nimble String",desc:"Arrow Storm throw out +8 arrows per stream and shoot twice as fast.",archetype:"",archetype_req:0,parents:["Thunder Mastery","Arrow Rain"],dependencies:["Arrow Storm"],blockers:["Phantom Ray"],cost:2,display:{row:15,col:2},properties:{shootspeed:2},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Arrow",cost:0,multipliers:[-15,0,0,0,0,0]},{type:"add_spell_prop",base_spell:1,target_part:"Single Stream",cost:0,hits:{"Single Arrow":8}}]},{display_name:"Arrow Storm",desc:"Shoot two stream of 8 arrows, dealing significant damage to close mobs and pushing them back.",archetype:"",archetype_req:0,parents:["Double Shots","Cheaper Escape"],dependencies:[],blockers:[],cost:1,display:{row:9,col:2},properties:{aoe:0,range:16},effects:[{type:"replace_spell",name:"Arrow Storm",cost:40,display_text:"Max Damage",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[30,0,10,0,0,0]},{name:"Single Stream",type:"total",hits:{"Single Arrow":8}},{name:"Total Damage",type:"total",hits:{"Single Stream":2}}]}]},{display_name:"Guardian Angels",desc:"Your protective arrows from Arrow Shield will become sentient bows, dealing damage up to 8 times each to nearby enemies. (Arrow Shield will no longer push nearby enemies)",archetype:"Boltslinger",archetype_req:3,parents:["Triple Shots","Frenzy"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:19,col:1},properties:{range:4,duration:60,shots:8,count:2},effects:[{type:"replace_spell",name:"Guardian Angels",cost:30,display_text:"Total Damage Average",base_spell:4,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[40,0,0,0,0,20]},{name:"Single Bow",type:"total",hits:{"Single Arrow":8}},{name:"Total Damage",type:"total",hits:{"Single Bow":2}}]}]},{display_name:"Windy Feet",base_abil:"Escape",desc:"When casting Escape, give speed to yourself and nearby allies.",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Storm"],dependencies:[],blockers:[],cost:1,display:{row:10,col:1},properties:{aoe:8,duration:120},type:"stat_bonus",bonuses:[{type:"stat",name:"spd",value:20}]},{display_name:"Basaltic Trap",desc:"When you hit the ground with Arrow Bomb, leave a Trap that damages enemies. (Max 2 Traps)",archetype:"Trapper",archetype_req:2,parents:["Bryophyte Roots"],dependencies:[],blockers:[],cost:2,display:{row:19,col:8},properties:{aoe:7,traps:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:0,multipliers:[140,30,0,0,30,0]}]},{display_name:"Windstorm",desc:"Arrow Storm shoot +1 stream of arrows, effectively doubling its damage.",archetype:"",archetype_req:0,parents:["Guardian Angels","Cheaper Arrow Storm"],dependencies:[],blockers:["Phantom Ray"],cost:2,display:{row:21,col:1},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Arrow",cost:0,multipliers:[-11,0,-7,0,0,3]},{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Stream":1}}]},{display_name:"Grappling Hook",base_abil:"Escape",desc:"When casting Escape, throw a hook that pulls you when hitting a block. If you hit an enemy, pull them towards you instead. (Escape will not throw you backward anymore)",archetype:"Trapper",archetype_req:0,parents:["Focus","More Shields","Cheaper Arrow Storm"],dependencies:[],blockers:["Escape Artist"],cost:2,display:{row:21,col:5},properties:{range:20},effects:[]},{display_name:"Implosion",desc:"Arrow bomb will pull enemies towards you. If a trap is nearby, it will pull them towards it instead. Increase Heart Shatter's damage.",archetype:"Trapper",archetype_req:0,parents:["Grappling Hook","More Shields"],dependencies:[],blockers:[],cost:2,display:{row:22,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[40,0,0,0,0,0]}]},{display_name:"Twain's Arc",desc:"When you have 2+ Focus, holding shift will summon the Twain's Arc. Charge it up to shoot a destructive long-range beam. (Damage is dealt as Main Attack Damage)",archetype:"Sharpshooter",archetype_req:4,parents:["More Focus","Traveler"],dependencies:["Focus"],blockers:[],cost:2,display:{row:25,col:4},properties:{range:64,focusReq:2},effects:[{type:"replace_spell",name:"Twain's Arc",cost:0,display_text:"Twain's Arc",base_spell:5,spell_type:"damage",scaling:"melee",display:"Twain's Arc Damage",parts:[{name:"Twain's Arc Damage",type:"damage",multipliers:[200,0,0,0,0,0]}]}]},{display_name:"Fierce Stomp",desc:"When using Escape, hold shift to quickly drop down and deal damage.",archetype:"Boltslinger",archetype_req:0,parents:["Refined Gunpowder","Traveler"],dependencies:[],blockers:[],cost:2,display:{row:26,col:1},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Fierce Stomp",cost:0,multipliers:[100,0,0,0,0,0]},{type:"add_spell_prop",base_spell:2,target_part:"Total Damage",cost:0,hits:{"Fierce Stomp":1}}]},{display_name:"Scorched Earth",desc:"Fire Creep become much stronger.",archetype:"Sharpshooter",archetype_req:0,parents:["Twain's Arc"],dependencies:["Fire Creep"],blockers:[],cost:1,display:{row:26,col:5},properties:{duration:2,aoe:.4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fire Creep",cost:0,multipliers:[10,0,0,0,5,0]}]},{display_name:"Leap",desc:"When you double tap jump, leap foward. (2s Cooldown)",archetype:"Boltslinger",archetype_req:5,parents:["Refined Gunpowder","Homing Shots"],dependencies:[],blockers:[],cost:2,display:{row:28,col:0},properties:{cooldown:2},effects:[]},{display_name:"Shocking Bomb",desc:"Arrow Bomb will not be affected by gravity, and all damage conversions become Thunder.",archetype:"Sharpshooter",archetype_req:5,parents:["Twain's Arc","Better Arrow Shield","Homing Shots"],dependencies:["Arrow Bomb"],blockers:[],cost:2,display:{row:28,col:4},properties:{gravity:0},effects:[{type:"convert_spell_conv",target_part:"all",conversion:"thunder"}]},{display_name:"Mana Trap",desc:"Your Traps will give you 4 Mana per second when you stay close to them.",archetype:"Trapper",archetype_req:5,parents:["More Traps","Better Arrow Shield"],dependencies:["Fire Creep"],blockers:[],cost:2,display:{row:28,col:8},properties:{range:12,manaRegen:4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:10,multipliers:[0,0,0,0,0,0]}]},{display_name:"Escape Artist",desc:"When casting Escape, release 100 arrows towards the ground.",archetype:"Boltslinger",archetype_req:0,parents:["Better Guardian Angels","Leap"],dependencies:[],blockers:["Grappling Hook"],cost:2,display:{row:31,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Escape Artist",cost:0,multipliers:[30,0,10,0,0,0]}]},{display_name:"Initiator",desc:"If you do not damage an enemy for 5s or more, your next sucessful hit will deal +50% damage and add +1 Focus.",archetype:"Sharpshooter",archetype_req:5,parents:["Shocking Bomb","Better Arrow Shield","Cheaper Arrow Storm (2)"],dependencies:["Focus"],blockers:[],cost:2,display:{row:31,col:5},properties:{focus:1,timer:5},type:"stat_bonus",bonuses:[{type:"stat",name:"damPct",value:50}]},{display_name:"Call of the Hound",desc:"Arrow Shield summon a Hound that will attack and drag aggressive enemies towards your traps.",archetype:"Trapper",archetype_req:0,parents:["Initiator","Cheaper Arrow Storm (2)"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:32,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Call of the Hound",cost:0,multipliers:[40,0,0,0,0,0]}]},{display_name:"Arrow Hurricane",desc:"Arrow Storm will shoot +2 stream of arrows.",archetype:"Boltslinger",archetype_req:8,parents:["Precise Shot","Escape Artist"],dependencies:[],blockers:["Phantom Ray"],cost:2,display:{row:33,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Stream":2}}]},{display_name:"Geyser Stomp",desc:"Fierce Stomp will create geysers, dealing more damage and vertical knockback.",archetype:"",archetype_req:0,parents:["Shrapnel Bomb"],dependencies:["Fierce Stomp"],blockers:[],cost:2,display:{row:37,col:1},properties:{aoe:1},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Fierce Stomp",cost:0,multipliers:[0,0,0,50,0,0]}]},{display_name:"Crepuscular Ray",desc:"If you have 5 Focus, casting Arrow Storm will make you levitate and shoot 20 homing arrows per second until you run out of Focus. While in that state, you will lose 1 Focus per second.",archetype:"Sharpshooter",archetype_req:10,parents:["Cheaper Arrow Shield"],dependencies:["Arrow Storm"],blockers:[],cost:2,display:{row:37,col:4},properties:{focusReq:5,focusRegen:-1},effects:[{type:"replace_spell",name:"Crepuscular Ray",base_spell:5,spell_type:"damage",scaling:"spell",display:"One Focus",cost:0,parts:[{name:"Single Arrow",type:"damage",multipliers:[10,0,0,5,0,0]},{name:"One Focus",type:"total",hits:{"Single Arrow":20}},{name:"Total Damage",type:"total",hits:{"One Focus":7}}]}]},{display_name:"Grape Bomb",desc:"Arrow bomb will throw 3 additional smaller bombs when exploding.",archetype:"",archetype_req:0,parents:["Cheaper Escape (2)"],dependencies:[],blockers:[],cost:2,display:{row:37,col:7},properties:{miniBombs:3,aoe:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Grape Bomb",cost:0,multipliers:[30,0,0,0,10,0]}]},{display_name:"Tangled Traps",desc:"Your Traps will be connected by a rope that deals damage to enemies every 0.2s.",archetype:"Trapper",archetype_req:0,parents:["Grape Bomb"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:38,col:6},properties:{attackSpeed:.2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Tangled Traps",cost:0,multipliers:[20,0,0,0,0,20]}]},{display_name:"Snow Storm",desc:"Enemies near you will be slowed down.",archetype:"",archetype_req:0,parents:["Geyser Stomp","More Focus (2)"],dependencies:[],blockers:[],cost:2,display:{row:39,col:2},properties:{range:2.5,slowness:.3}},{display_name:"All-Seeing Panoptes",desc:"Your bows from Guardian Angels become all-seeing, increasing their range, damage and letting them shoot up to +5 times each.",archetype:"Boltslinger",archetype_req:11,parents:["Snow Storm"],dependencies:["Guardian Angels"],blockers:[],cost:2,display:{row:40,col:1},properties:{range:10,shots:5},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Single Arrow",cost:0,multipliers:[0,0,0,0,20,0]},{type:"add_spell_prop",base_spell:4,target_part:"Single Bow",cost:0,hits:{"Single Arrow":5}}]},{display_name:"Minefield",desc:"Allow you to place +6 Traps, but with reduced damage and range.",archetype:"Trapper",archetype_req:10,parents:["Grape Bomb","Cheaper Arrow Bomb (2)"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:40,col:7},properties:{aoe:-2,traps:6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Basaltic Trap",cost:0,multipliers:[-80,0,0,0,0,0]}]},{display_name:"Bow Proficiency I",desc:"Improve your Main Attack's damage and range when using a bow.",archetype:"",archetype_req:0,parents:["Arrow Bomb"],dependencies:[],blockers:[],cost:1,display:{row:2,col:4},properties:{mainAtk_range:6},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Arrow Bomb",desc:"Reduce the Mana cost of Arrow Bomb.",archetype:"",archetype_req:0,parents:["Bow Proficiency I"],dependencies:[],blockers:[],cost:1,display:{row:2,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-10}]},{display_name:"Cheaper Arrow Storm",desc:"Reduce the Mana cost of Arrow Storm.",archetype:"",archetype_req:0,parents:["Grappling Hook","Windstorm","Focus"],dependencies:[],blockers:[],cost:1,display:{row:21,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Cheaper Escape",desc:"Reduce the Mana cost of Escape.",archetype:"",archetype_req:0,parents:["Arrow Storm","Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:9,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"Earth Mastery",desc:"Increases your base damage from all Earth attacks",archetype:"Trapper",archetype_req:0,parents:["Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:13,col:8},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"eDamPct",value:20},{type:"stat",name:"eDam",value:[2,4]}]}]},{display_name:"Thunder Mastery",desc:"Increases your base damage from all Thunder attacks",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Storm","Fire Mastery"],dependencies:[],blockers:[],cost:1,display:{row:13,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"tDamPct",value:10},{type:"stat",name:"tDam",value:[1,8]}]}]},{display_name:"Water Mastery",desc:"Increases your base damage from all Water attacks",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Escape","Thunder Mastery","Fire Mastery"],dependencies:[],blockers:[],cost:1,display:{row:14,col:4},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"wDamPct",value:15},{type:"stat",name:"wDam",value:[2,4]}]}]},{display_name:"Air Mastery",desc:"Increases base damage from all Air attacks",archetype:"Battle Monk",archetype_req:0,parents:["Arrow Storm"],dependencies:[],blockers:[],cost:1,display:{row:13,col:0},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"aDamPct",value:15},{type:"stat",name:"aDam",value:[3,4]}]}]},{display_name:"Fire Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Sharpshooter",archetype_req:0,parents:["Thunder Mastery","Arrow Shield"],dependencies:[],blockers:[],cost:1,display:{row:13,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"fDamPct",value:15},{type:"stat",name:"fDam",value:[3,5]}]}]},{display_name:"More Shields",desc:"Give +2 charges to Arrow Shield.",archetype:"",archetype_req:0,parents:["Grappling Hook","Basaltic Trap"],dependencies:["Arrow Shield"],blockers:[],cost:1,display:{row:21,col:7},properties:{shieldCharges:2}},{display_name:"Stormy Feet",desc:"Windy Feet will last longer and add more speed.",archetype:"",archetype_req:0,parents:["Windstorm"],dependencies:["Windy Feet"],blockers:[],cost:1,display:{row:23,col:1},properties:{duration:60},effects:[{type:"stat_bonus",bonuses:[{type:"stat",name:"spdPct",value:20}]}]},{display_name:"Refined Gunpowder",desc:"Increase the damage of Arrow Bomb.",archetype:"",archetype_req:0,parents:["Windstorm"],dependencies:[],blockers:[],cost:1,display:{row:25,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Bomb",cost:0,multipliers:[50,0,0,0,0,0]}]},{display_name:"More Traps",desc:"Increase the maximum amount of active Traps you can have by +2.",archetype:"Trapper",archetype_req:10,parents:["Bouncing Bomb"],dependencies:["Basaltic Trap"],blockers:[],cost:1,display:{row:26,col:8},properties:{traps:2}},{display_name:"Better Arrow Shield",desc:"Arrow Shield will gain additional area of effect, knockback and damage.",archetype:"Sharpshooter",archetype_req:0,parents:["Mana Trap","Shocking Bomb","Twain's Arc"],dependencies:["Arrow Shield"],blockers:[],cost:1,display:{row:28,col:6},properties:{aoe:1},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Arrow Shield",multipliers:[40,0,0,0,0,0]}]},{display_name:"Better Leap",desc:"Reduce leap's cooldown by 1s.",archetype:"Boltslinger",archetype_req:0,parents:["Leap","Homing Shots"],dependencies:["Leap"],blockers:[],cost:1,display:{row:29,col:1},properties:{cooldown:-1}},{display_name:"Better Guardian Angels",desc:"Your Guardian Angels can shoot +4 arrows before disappearing.",archetype:"Boltslinger",archetype_req:0,parents:["Escape Artist","Homing Shots"],dependencies:["Guardian Angels"],blockers:[],cost:1,display:{row:31,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Single Bow",cost:0,hits:{"Single Arrow":4}}]},{display_name:"Cheaper Arrow Storm (2)",desc:"Reduce the Mana cost of Arrow Storm.",archetype:"",archetype_req:0,parents:["Initiator","Mana Trap"],dependencies:[],blockers:[],cost:1,display:{row:31,col:8},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Precise Shot",desc:"+30% Critical Hit Damage",archetype:"",archetype_req:0,parents:["Better Guardian Angels","Cheaper Arrow Shield","Arrow Hurricane"],dependencies:[],blockers:[],cost:1,display:{row:33,col:2},properties:{mainAtk_range:6},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdCritPct",value:30}]}]},{display_name:"Cheaper Arrow Shield",desc:"Reduce the Mana cost of Arrow Shield.",archetype:"",archetype_req:0,parents:["Precise Shot","Initiator"],dependencies:[],blockers:[],cost:1,display:{row:33,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Rocket Jump",desc:"Arrow Bomb's self-damage will knockback you farther away.",archetype:"",archetype_req:0,parents:["Cheaper Arrow Storm (2)","Initiator"],dependencies:["Arrow Bomb"],blockers:[],cost:1,display:{row:33,col:6},properties:{}},{display_name:"Cheaper Escape (2)",desc:"Reduce the Mana cost of Escape.",archetype:"",archetype_req:0,parents:["Call of the Hound","Decimator"],dependencies:[],blockers:[],cost:1,display:{row:34,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"Stronger Hook",desc:"Increase your Grappling Hook's range, speed and strength.",archetype:"Trapper",archetype_req:5,parents:["Cheaper Escape (2)"],dependencies:["Grappling Hook"],blockers:[],cost:1,display:{row:35,col:8},properties:{range:8}},{display_name:"Cheaper Arrow Bomb (2)",desc:"Reduce the Mana cost of Arrow Bomb.",archetype:"",archetype_req:0,parents:["More Focus (2)","Minefield"],dependencies:[],blockers:[],cost:1,display:{row:40,col:5},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Bouncing Bomb",desc:"Arrow Bomb will bounce once when hitting a block or enemy",archetype:"",archetype_req:0,parents:["More Shields"],dependencies:[],blockers:[],cost:2,display:{row:25,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Arrow Bomb":2}}]},{display_name:"Homing Shots",desc:"Your Main Attack arrows will follow nearby enemies and not be affected by gravity",archetype:"",archetype_req:0,parents:["Leap","Shocking Bomb"],dependencies:[],blockers:[],cost:2,display:{row:28,col:2},properties:{},effects:[]},{display_name:"Shrapnel Bomb",desc:"Arrow Bomb's explosion will fling 15 shrapnel, dealing damage in a large area",archetype:"Boltslinger",archetype_req:8,parents:["Arrow Hurricane","Precise Shot"],dependencies:[],blockers:[],cost:2,display:{row:34,col:1},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Shrapnel Bomb",cost:0,multipliers:[40,0,0,0,20,0]}]},{display_name:"Elusive",desc:"If you do not get hit for 8+ seconds, become immune to self-damage and remove Arrow Storm's recoil. (Dodging counts as not getting hit)",archetype:"Boltslinger",archetype_req:0,parents:["Geyser Stomp"],dependencies:[],blockers:[],cost:2,display:{row:38,col:0},properties:{},effects:[]},{display_name:"Double Shots",desc:"Double Main Attack arrows, but they deal -30% damage per arrow (harder to hit far enemies)",archetype:"Boltslinger",archetype_req:0,parents:["Escape"],dependencies:[],blockers:["Power Shots"],cost:1,display:{row:7,col:2},properties:{arrow:2},effects:[{type:"add_spell_prop",base_spell:0,target_part:"Melee Damage",cost:0,multipliers:.7}]},{display_name:"Triple Shots",desc:"Triple Main Attack arrows, but they deal -20% damage per arrow",archetype:"Boltslinger",archetype_req:0,parents:["Arrow Rain","Frenzy"],dependencies:["Double Shots"],blockers:[],cost:1,display:{row:17,col:0},properties:{arrow:2},effects:[{type:"add_spell_prop",base_spell:0,target_part:"Melee Damage",cost:0,multipliers:.7}]},{display_name:"Power Shots",desc:"Main Attack arrows have increased speed and knockback",archetype:"Sharpshooter",archetype_req:0,parents:["Escape"],dependencies:[],blockers:["Double Shots"],cost:1,display:{row:7,col:6},properties:{},effects:[]},{display_name:"Focus",desc:"When hitting an aggressive mob 5+ blocks away, gain +1 Focus (Max 3). Resets if you miss once",archetype:"Sharpshooter",archetype_req:2,parents:["Phantom Ray"],dependencies:[],blockers:[],cost:2,display:{row:19,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:3}]},{display_name:"More Focus",desc:"Add +2 max Focus",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Arrow Storm","Grappling Hook"],dependencies:[],blockers:[],cost:1,display:{row:22,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:5}]},{display_name:"More Focus (2)",desc:"Add +2 max Focus",archetype:"Sharpshooter",archetype_req:0,parents:["Crepuscular Ray","Snow Storm"],dependencies:[],blockers:[],cost:1,display:{row:39,col:4},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Focus",output:{type:"stat",abil_name:"Focus",name:"dmgPct"},scaling:[35],max:7}]},{display_name:"Traveler",desc:"For every 1% Walk Speed you have from items, gain +1 Raw Spell Damage (Max 100)",archetype:"",archetype_req:0,parents:["Refined Gunpowder","Twain's Arc"],dependencies:[],blockers:[],cost:1,display:{row:25,col:2},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"spd"}],output:{type:"stat",name:"sdRaw"},scaling:[1],max:100}]},{display_name:"Patient Hunter",desc:"Your Traps will deal +20% more damage for every second they are active (Max +80%)",archetype:"Trapper",archetype_req:0,parents:["More Shields"],dependencies:["Basaltic Trap"],blockers:[],cost:2,display:{row:22,col:8},properties:{max:80},effects:[]},{display_name:"Stronger Patient Hunter",desc:"Add +80% Max Damage to Patient Hunter",archetype:"Trapper",archetype_req:0,parents:["Grape Bomb"],dependencies:["Patient Hunter"],blockers:[],cost:1,display:{row:38,col:8},properties:{max:80},effects:[]},{display_name:"Frenzy",desc:"Every time you hit an enemy, briefly gain +6% Walk Speed (Max 200%). Decay -40% of the bonus every second",archetype:"Boltslinger",archetype_req:0,parents:["Triple Shots","Nimble String"],dependencies:[],blockers:[],cost:2,display:{row:17,col:2},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Hits dealt",output:{type:"stat",name:"spd"},scaling:[6],max:200}]},{display_name:"Phantom Ray",desc:"Condense Arrow Storm into a single ray that damages enemies 10 times per second",archetype:"Sharpshooter",archetype_req:0,parents:["Water Mastery","Fire Creep"],dependencies:["Arrow Storm"],blockers:["Windstorm","Nimble String","Arrow Hurricane"],cost:2,display:{row:16,col:4},properties:{},effects:[{type:"replace_spell",name:"Phantom Ray",cost:40,display_text:"Max Damage",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Arrow",type:"damage",multipliers:[25,0,5,0,0,0]},{name:"Total Damage",type:"total",hits:{"Single Arrow":16}}]}]},{display_name:"Arrow Rain",desc:"When Arrow Shield loses its last charge, unleash 200 arrows raining down on enemies",archetype:"Trapper",archetype_req:0,parents:["Nimble String","Air Mastery"],dependencies:["Arrow Shield"],blockers:[],cost:2,display:{row:15,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Arrow Rain",cost:0,multipliers:[120,0,0,0,0,80]}]},{display_name:"Decimator",desc:"Phantom Ray will increase its damage by 10% everytime you do not miss with it (Max 50%)",archetype:"Sharpshooter",archetype_req:0,parents:["Cheaper Arrow Shield"],dependencies:["Phantom Ray"],blockers:[],cost:1,display:{row:34,col:5},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Phantom Ray hits",output:{type:"stat",name:"PhRayDmg"},scaling:10,max:50}]}],Warrior:[{display_name:"Bash",desc:"Violently bash the ground, dealing high damage in a large area",archetype:"",archetype_req:0,parents:[],dependencies:[],blockers:[],cost:1,display:{row:0,col:4},properties:{aoe:4,range:3},effects:[{type:"replace_spell",name:"Bash",cost:45,display_text:"Total Damage Average",base_spell:1,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"Single Hit",type:"damage",multipliers:[130,20,0,0,0,0]},{name:"Total Damage",type:"total",hits:{"Single Hit":1}}]}]},{display_name:"Spear Proficiency 1",desc:"Improve your Main Attack's damage and range w/ spear",archetype:"",archetype_req:0,parents:["Bash"],dependencies:[],blockers:[],cost:1,display:{row:2,col:4},properties:{melee_range:1},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Bash",desc:"Reduce the Mana cost of Bash",archetype:"",archetype_req:0,parents:["Spear Proficiency 1"],dependencies:[],blockers:[],cost:1,display:{row:2,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-10}]},{display_name:"Double Bash",desc:"Bash will hit a second time at a farther range",archetype:"",archetype_req:0,parents:["Spear Proficiency 1"],dependencies:[],blockers:[],cost:1,display:{row:4,col:4},properties:{range:3},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{name:"Single Hit",value:1}},{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[-50,0,0,0,0,0]}]},{display_name:"Charge",desc:"Charge forward at high speed (hold shift to cancel)",archetype:"",archetype_req:0,parents:["Double Bash"],dependencies:[],blockers:[],cost:1,display:{row:6,col:4},properties:{},effects:[{type:"replace_spell",name:"Charge",cost:25,display_text:"Total Damage Average",base_spell:2,spell_type:"damage",scaling:"spell",display:"Total Damage",parts:[{name:"None",type:"damage",multipliers:[0,0,0,0,0,0]},{name:"Total Damage",type:"total",hits:{None:0}}]}]},{display_name:"Heavy Impact",desc:"After using Charge, violently crash down into the ground and deal damage",archetype:"",archetype_req:0,parents:["Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:9,col:1},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Heavy Impact",cost:0,multipliers:[100,0,0,0,0,0]}]},{display_name:"Vehement",desc:"For every 1% or 1 Raw Main Attack Damage you have from items, gain +2% Walk Speed (Max 20%)",archetype:"Fallen",archetype_req:0,parents:["Charge"],dependencies:[],blockers:["Tougher Skin"],cost:1,display:{row:6,col:2},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"mdPct"},{type:"stat",name:"mdRaw"}],output:{type:"stat",name:"spd"},scaling:[1,1],max:20}]},{display_name:"Tougher Skin",desc:"Harden your skin and become permanently +5% more resistant\nFor every 1% or 1 Raw Heath Regen you have from items, gain +10 Health (Max 100)",archetype:"Paladin",archetype_req:0,parents:["Charge"],dependencies:[],blockers:["Vehement"],cost:1,display:{row:6,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"baseResist",value:"5"}]},{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hprRaw"},{type:"stat",name:"hprPct"}],output:{type:"stat",name:"hpBonus"},scaling:[10,10],max:100}]},{display_name:"Uppercut",desc:"Rocket enemies in the air and deal massive damage",archetype:"",archetype_req:0,parents:["Vehement"],dependencies:[],blockers:[],cost:1,display:{row:8,col:2},properties:{aoe:3,range:5},effects:[{type:"replace_spell",name:"Uppercut",cost:45,display_text:"Total Damage Average",base_spell:3,spell_type:"damage",scaling:"spell",display:"total",parts:[{name:"Uppercut",type:"damage",multipliers:[150,50,50,0,0,0]},{name:"Total Damage",type:"total",hits:{Uppercut:1}}]}]},{display_name:"Cheaper Charge",desc:"Reduce the Mana cost of Charge",archetype:"",archetype_req:0,parents:["Uppercut","War Scream"],dependencies:[],blockers:[],cost:1,display:{row:8,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5}]},{display_name:"War Scream",desc:"Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies",archetype:"",archetype_req:0,parents:["Tougher Skin"],dependencies:[],blockers:[],cost:1,display:{row:8,col:6},properties:{duration:30,aoe:12,defense_bonus:10},effects:[{type:"replace_spell",name:"War Scream",cost:35,display_text:"War Scream",base_spell:4,spell_type:"damage",scaling:"spell",display:"Total Damage Average",parts:[{name:"War Scream",type:"damage",multipliers:[50,0,0,0,50,0]}]}]},{display_name:"Earth Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Fallen",archetype_req:0,parents:["Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:10,col:0},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"eDamPct",value:20},{type:"stat",name:"eDam",value:[2,4]}]}]},{display_name:"Thunder Mastery",desc:"Increases base damage from all Thunder attacks",archetype:"Fallen",archetype_req:0,parents:["Uppercut","Air Mastery"],dependencies:[],blockers:[],cost:1,display:{row:10,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"tDamPct",value:10},{type:"stat",name:"tDam",value:[1,8]}]}]},{display_name:"Water Mastery",desc:"Increases base damage from all Water attacks",archetype:"Battle Monk",archetype_req:0,parents:["Cheaper Charge","Thunder Mastery","Air Mastery"],dependencies:[],blockers:[],cost:1,display:{row:11,col:4},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"wDamPct",value:15},{type:"stat",name:"wDam",value:[2,4]}]}]},{display_name:"Air Mastery",desc:"Increases base damage from all Air attacks",archetype:"Battle Monk",archetype_req:0,parents:["War Scream","Thunder Mastery"],dependencies:[],blockers:[],cost:1,display:{row:10,col:6},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"aDamPct",value:15},{type:"stat",name:"aDam",value:[3,4]}]}]},{display_name:"Fire Mastery",desc:"Increases base damage from all Earth attacks",archetype:"Paladin",archetype_req:0,parents:["War Scream"],dependencies:[],blockers:[],cost:1,display:{row:10,col:8},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"fDamPct",value:15},{type:"stat",name:"fDam",value:[3,5]}]}]},{display_name:"Quadruple Bash",desc:"Bash will hit 4 times at an even larger range",archetype:"Fallen",archetype_req:0,parents:["Earth Mastery","Fireworks"],dependencies:[],blockers:[],cost:2,display:{row:12,col:0},properties:{range:6},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Total Damage",cost:0,hits:{"Single Hit":2}},{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[-20,0,0,0,0,0]}]},{display_name:"Fireworks",desc:"Mobs hit by Uppercut will explode mid-air and receive additional damage",archetype:"Fallen",archetype_req:0,parents:["Thunder Mastery","Quadruple Bash"],dependencies:[],blockers:[],cost:2,display:{row:12,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Fireworks",cost:0,multipliers:[80,0,20,0,0,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{Fireworks:1}}]},{display_name:"Half-Moon Swipe",desc:"Uppercut will deal a footsweep attack at a longer and wider angle. All elemental conversions become Water",archetype:"Battle Monk",archetype_req:1,parents:["Water Mastery"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:13,col:4},properties:{range:4},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:-10,multipliers:[-70,0,0,0,0,0]},{type:"convert_spell_conv",target_part:"all",conversion:"water"}]},{display_name:"Flyby Jab",desc:"Damage enemies in your way when using Charge",archetype:"",archetype_req:0,parents:["Air Mastery","Flaming Uppercut"],dependencies:[],blockers:[],cost:2,display:{row:12,col:6},properties:{aoe:2},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Flyby Jab",cost:0,multipliers:[20,0,0,0,0,40]}]},{display_name:"Flaming Uppercut",desc:"Uppercut will light mobs on fire, dealing damage every 0.6 seconds",archetype:"Paladin",archetype_req:0,parents:["Fire Mastery","Flyby Jab"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:12,col:8},properties:{duration:3,tick:.6},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Flaming Uppercut",cost:0,multipliers:[0,0,0,0,50,0]},{type:"add_spell_prop",base_spell:3,target_part:"Flaming Uppercut Total Damage",cost:0,hits:{"Flaming Uppercut":5}},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{"Flaming Uppercut":5}}]},{display_name:"Iron Lungs",desc:"War Scream deals more damage",archetype:"",archetype_req:0,parents:["Flyby Jab","Flaming Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:13,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"War Scream",cost:0,multipliers:[30,0,0,0,0,30]}]},{display_name:"Generalist",desc:"After casting 3 different spells in a row, your next spell will cost 5 mana",archetype:"Battle Monk",archetype_req:3,parents:["Counter"],dependencies:[],blockers:[],cost:2,display:{row:15,col:2},properties:{},effects:[]},{display_name:"Counter",desc:"When dodging a nearby enemy attack, get 30% chance to instantly attack back",archetype:"Battle Monk",archetype_req:0,parents:["Half-Moon Swipe"],dependencies:[],blockers:[],cost:2,display:{row:15,col:4},properties:{chance:30},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Counter",cost:0,multipliers:[60,0,20,0,0,20]}]},{display_name:"Mantle of the Bovemists",desc:"When casting War Scream, create a holy shield around you that reduces all incoming damage by 70% for 3 hits (20s cooldown)",archetype:"Paladin",archetype_req:3,parents:["Iron Lungs"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:15,col:7},properties:{mantle_charge:3},effects:[]},{display_name:"Bak'al's Grasp",desc:"After casting War Scream, become Corrupted (15s Cooldown). You cannot heal while in that state\n\nWhile Corrupted, every 2% of Health you lose will add +4 Raw Damage to your attacks (Max 120)",archetype:"Fallen",archetype_req:2,parents:["Quadruple Bash","Fireworks"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:16,col:1},properties:{cooldown:15},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"raw"},scaling:[4],slider_step:2,max:120}]},{display_name:"Spear Proficiency 2",desc:"Improve your Main Attack's damage and range w/ spear",archetype:"",archetype_req:0,parents:["Bak'al's Grasp","Cheaper Uppercut"],dependencies:[],blockers:[],cost:1,display:{row:17,col:0},properties:{melee_range:1},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"mdPct",value:5}]}]},{display_name:"Cheaper Uppercut",desc:"Reduce the Mana Cost of Uppercut",archetype:"",archetype_req:0,parents:["Spear Proficiency 2","Aerodynamics","Counter"],dependencies:[],blockers:[],cost:1,display:{row:17,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Aerodynamics",desc:"During Charge, you can steer and change direction",archetype:"Battle Monk",archetype_req:0,parents:["Cheaper Uppercut","Provoke"],dependencies:[],blockers:[],cost:2,display:{row:17,col:5},properties:{},effects:[]},{display_name:"Provoke",desc:"Mobs damaged by War Scream will target only you for at least 5s \n\nReduce the Mana cost of War Scream",archetype:"Paladin",archetype_req:0,parents:["Aerodynamics","Mantle of the Bovemists"],dependencies:[],blockers:[],cost:1,display:{row:17,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Precise Strikes",desc:"+30% Critical Hit Damage",archetype:"",archetype_req:0,parents:["Cheaper Uppercut","Spear Proficiency 2"],dependencies:[],blockers:[],cost:1,display:{row:18,col:2},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"critDmg",value:30}]}]},{display_name:"Air Shout",desc:"War Scream will fire a projectile that can go through walls and deal damage multiple times",archetype:"",archetype_req:0,parents:["Aerodynamics","Provoke"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:18,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Air Shout",cost:0,multipliers:[20,0,0,0,0,5]}]},{display_name:"Enraged Blow",desc:"While Corriupted, every 1% of Health you lose will increase your damage by +2% (Max 200%)",archetype:"Fallen",archetype_req:0,parents:["Spear Proficiency 2"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:20,col:0},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hpBonus"}],output:{type:"stat",name:"dmgPct"},scaling:[2],max:200}]},{display_name:"Flying Kick",desc:"When using Charge, mobs hit will halt your momentum and get knocked back",archetype:"Battle Monk",archetype_req:1,parents:["Cheaper Uppercut","Stronger Mantle"],dependencies:[],blockers:[],cost:2,display:{row:20,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Flying Kick",cost:0,multipliers:[120,0,0,10,0,20]}]},{display_name:"Stronger Mantle",desc:"Add +2 additional charges to Mantle of the Bovemists",archetype:"Paladin",archetype_req:0,parents:["Manachism","Flying Kick"],dependencies:[],blockers:[],cost:1,display:{row:20,col:6},properties:{mantle_charge:2},effects:[]},{display_name:"Manachism",desc:"If you receive a hit that's less than 5% of your max HP, gain 10 Mana (1s Cooldown)",archetype:"Paladin",archetype_req:3,parents:["Stronger Mantle","Provoke"],dependencies:[],blockers:[],cost:2,display:{row:20,col:8},properties:{cooldown:1},effects:[]},{display_name:"Boiling Blood",desc:"Bash leaves a trail of boiling blood behind its first explosion, slowing down and damaging enemies above it every 0.4 seconds",archetype:"",archetype_req:0,parents:["Enraged Blow","Ragnarokkr"],dependencies:[],blockers:[],cost:2,display:{row:22,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Boiling Blood",cost:0,multipliers:[25,0,0,0,5,0]}]},{display_name:"Ragnarokkr",desc:"War Scream become deafening, increasing its range and giving damage bonus to players",archetype:"Fallen",archetype_req:0,parents:["Boiling Blood","Flying Kick"],dependencies:["War Scream"],blockers:[],cost:2,display:{row:22,col:2},properties:{damage_bonus:30,aoe:2},effects:[{type:"add_spell_prop",base_spell:4,cost:10}]},{display_name:"Ambidextrous",desc:"Increase your chance to attack with Counter by +30%",archetype:"",archetype_req:0,parents:["Flying Kick","Stronger Mantle","Burning Heart"],dependencies:["Counter"],blockers:[],cost:1,display:{row:22,col:4},properties:{chance:30},effects:[]},{display_name:"Burning Heart",desc:"For every 100 Health Bonus you have from item IDs, gain +2% Fire Damage (Max 100%)",archetype:"Paladin",archetype_req:0,parents:["Ambidextrous","Stronger Bash"],dependencies:[],blockers:[],cost:1,display:{row:22,col:6},properties:{},effects:[{type:"stat_scaling",slider:!1,inputs:[{type:"stat",name:"hpBonus"}],output:{type:"stat",name:"fDamPct"},scaling:[2],max:100,slider_step:100}]},{display_name:"Stronger Bash",desc:"Increase the damage of Bash",archetype:"",archetype_req:0,parents:["Burning Heart","Manachism"],dependencies:[],blockers:[],cost:1,display:{row:22,col:8},properties:{},effects:[{type:"add_spell_prop",base_spell:1,target_part:"Single Hit",cost:0,multipliers:[30,0,0,0,0,0]}]},{display_name:"Intoxicating Blood",desc:"After leaving Corrupted, gain 2% of the health lost back for each enemy killed while Corrupted",archetype:"Fallen",archetype_req:5,parents:["Ragnarokkr","Boiling Blood"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:23,col:1},properties:{},effects:[]},{display_name:"Comet",desc:"After being hit by Fireworks, enemies will crash into the ground and receive more damage",archetype:"Fallen",archetype_req:0,parents:["Ragnarokkr"],dependencies:["Fireworks"],blockers:[],cost:2,display:{row:24,col:2},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Comet",cost:0,multipliers:[80,20,0,0,0,0]},{type:"add_spell_prop",base_spell:3,target_part:"Total Damage",cost:0,hits:{Comet:1}}]},{display_name:"Collide",desc:"Mobs thrown into walls from Flying Kick will explode and receive additonal damage",archetype:"Battle Monk",archetype_req:4,parents:["Ambidextrous","Burning Heart"],dependencies:["Flying Kick"],blockers:[],cost:2,display:{row:23,col:5},properties:{aoe:4},effects:[{type:"add_spell_prop",base_spell:2,target_part:"Collide",cost:0,multipliers:[100,0,0,0,50,0]}]},{display_name:"Rejuvenating Skin",desc:"Regain back 30% of the damage you take as healing over 30s",archetype:"Paladin",archetype_req:0,parents:["Burning Heart","Stronger Bash"],dependencies:[],blockers:[],cost:2,display:{row:23,col:7},properties:{},effects:[]},{display_name:"Uncontainable Corruption",desc:"Reduce the cooldown of Bak'al's Grasp by -5s, and increase the raw damage gained for every 2% of health lost by +1",archetype:"",archetype_req:0,parents:["Boiling Blood","Radiant Devotee"],dependencies:["Bak'al's Grasp"],blockers:[],cost:1,display:{row:26,col:0},properties:{cooldown:-5},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"raw"},scaling:[1],slider_step:2,max:50}]},{display_name:"Radiant Devotee",desc:"For every 4% Reflection you have from items, gain +1/5s Mana Regen (Max 10/5s)",archetype:"Battle Monk",archetype_req:1,parents:["Whirlwind Strike","Uncontainable Corruption"],dependencies:[],blockers:[],cost:1,display:{row:26,col:2},properties:{},effects:[{type:"stat_scaling",inputs:[{type:"stat",name:"ref"}],output:{type:"stat",name:"mr"},scaling:[1],max:10,slider_step:4}]},{display_name:"Whirlwind Strike",desc:"Uppercut will create a strong gust of air, launching you upward with enemies (Hold shift to stay grounded)",archetype:"Battle Monk",archetype_req:5,parents:["Ambidextrous","Radiant Devotee"],dependencies:["Uppercut"],blockers:[],cost:2,display:{row:26,col:4},properties:{range:2},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:0,multipliers:[0,0,0,0,0,50]}]},{display_name:"Mythril Skin",desc:"Gain +5% Base Resistance and become immune to knockback",archetype:"Paladin",archetype_req:6,parents:["Rejuvenating Skin"],dependencies:[],blockers:[],cost:2,display:{row:26,col:7},properties:{},effects:[{type:"raw_stat",bonuses:[{type:"stat",name:"baseResist",value:5}]}]},{display_name:"Armour Breaker",desc:"While Corrupted, losing 30% Health will make your next Uppercut destroy enemies' defense, rendering them weaker to damage",archetype:"Fallen",archetype_req:0,parents:["Uncontainable Corruption","Radiant Devotee"],dependencies:["Bak'al's Grasp"],blockers:[],cost:2,display:{row:27,col:1},properties:{duration:5},effects:[]},{display_name:"Shield Strike",desc:"When your Mantle of the Bovemist loses all charges, deal damage around you for each Mantle individually lost",archetype:"Paladin",archetype_req:0,parents:["Mythril Skin","Sparkling Hope"],dependencies:[],blockers:[],cost:2,display:{row:27,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Shield Strike",cost:0,multipliers:[60,0,20,0,0,0]}]},{display_name:"Sparkling Hope",desc:"Everytime you heal 5% of your max health, deal damage to all nearby enemies",archetype:"Paladin",archetype_req:0,parents:["Mythril Skin"],dependencies:[],blockers:[],cost:2,display:{row:27,col:8},properties:{aoe:6},effects:[{type:"add_spell_prop",base_spell:5,target_part:"Sparkling Hope",cost:0,multipliers:[10,0,5,0,0,0]}]},{display_name:"Massive Bash",desc:"While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)",archetype:"Fallen",archetype_req:8,parents:["Tempest","Uncontainable Corruption"],dependencies:[],blockers:[],cost:2,display:{row:28,col:0},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Corrupted",output:{type:"stat",name:"bashAoE"},scaling:[1],max:10,slider_step:3}]},{display_name:"Tempest",desc:"War Scream will ripple the ground and deal damage 3 times in a large area",archetype:"Battle Monk",archetype_req:0,parents:["Massive Bash","Spirit of the Rabbit"],dependencies:[],blockers:[],cost:2,display:{row:28,col:2},properties:{aoe:16},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Tempest",cost:"0",multipliers:[30,10,0,0,0,10]},{type:"add_spell_prop",base_spell:4,target_part:"Tempest Total Damage",cost:"0",hits:{Tempest:3}},{type:"add_spell_prop",base_spell:4,target_part:"Total Damage",cost:"0",hits:{Tempest:3}}]},{display_name:"Spirit of the Rabbit",desc:"Reduce the Mana cost of Charge and increase your Walk Speed by +20%",archetype:"Battle Monk",archetype_req:5,parents:["Tempest","Whirlwind Strike"],dependencies:[],blockers:[],cost:1,display:{row:28,col:4},properties:{},effects:[{type:"add_spell_prop",base_spell:2,cost:-5},{type:"raw_stat",bonuses:[{type:"stat",name:"spd",value:20}]}]},{display_name:"Massacre",desc:"While Corrupted, if your effective attack speed is Slow or lower, hitting an enemy with your Main Attack will add +1% to your Corrupted bar",archetype:"Fallen",archetype_req:5,parents:["Tempest","Massive Bash"],dependencies:[],blockers:[],cost:2,display:{row:29,col:1},properties:{},effects:[]},{display_name:"Axe Kick",desc:"Increase the damage of Uppercut, but also increase its mana cost",archetype:"",archetype_req:0,parents:["Tempest","Spirit of the Rabbit"],dependencies:[],blockers:[],cost:1,display:{row:29,col:3},properties:{},effects:[{type:"add_spell_prop",base_spell:3,target_part:"Uppercut",cost:10,multipliers:[100,0,0,0,0,0]}]},{display_name:"Radiance",desc:"Bash will buff your allies' positive IDs. (15s Cooldown)",archetype:"Paladin",archetype_req:2,parents:["Spirit of the Rabbit","Cheaper Bash 2"],dependencies:[],blockers:[],cost:2,display:{row:29,col:5},properties:{cooldown:15},effects:[]},{display_name:"Cheaper Bash 2",desc:"Reduce the Mana cost of Bash",archetype:"",archetype_req:0,parents:["Radiance","Shield Strike","Sparkling Hope"],dependencies:[],blockers:[],cost:1,display:{row:29,col:7},properties:{},effects:[{type:"add_spell_prop",base_spell:1,cost:-5}]},{display_name:"Cheaper War Scream",desc:"Reduce the Mana cost of War Scream",archetype:"",archetype_req:0,parents:["Massive Bash"],dependencies:[],blockers:[],cost:1,display:{row:31,col:0},properties:{},effects:[{type:"add_spell_prop",base_spell:4,cost:-5}]},{display_name:"Discombobulate",desc:"Every time you hit an enemy, briefly increase your elemental damage dealt to them by +2 (Additive, Max +50). This bonus decays -5 every second",archetype:"Battle Monk",archetype_req:12,parents:["Thunderclap"],dependencies:[],blockers:[],cost:2,display:{row:31,col:2},properties:{},effects:[{type:"stat_scaling",slider:!0,slider_name:"Hits dealt",output:{type:"stat",name:"rainrawButDifferent"},scaling:[2],max:50}]},{display_name:"Thunderclap",desc:"Bash will cast at the player's position and gain additional AoE.\n\n All elemental conversions become Thunder",archetype:"Battle Monk",archetype_req:8,parents:["Spirit of the Rabbit"],dependencies:[],blockers:[],cost:2,display:{row:31,col:4},properties:{aoe:2},effects:[{type:"convert_spell_conv",target_part:"all",conversion:"thunder"}]},{display_name:"Cyclone",desc:"After casting War Scream, envelop yourself with a vortex that damages nearby enemies every 0.5s",archetype:"Battle Monk",archetype_req:0,parents:["Thunderclap"],dependencies:[],blockers:[],cost:1,display:{row:32,col:5},properties:{aoe:4,duration:20},effects:[{type:"add_spell_prop",base_spell:4,target_part:"Cyclone",cost:0,multipliers:[10,0,0,0,5,10]},{type:"add_spell_prop",base_spell:4,target_part:"Cyclone Total Damage",cost:0,hits:{Cyclone:40}}]},{display_name:"Second Chance",desc:"When you receive a fatal blow, survive and regain 30% of your Health (10m Cooldown)",archetype:"Paladin",archetype_req:12,parents:["Cheaper Bash 2"],dependencies:[],blockers:[],cost:2,display:{row:32,col:7},properties:{},effects:[]},{display_name:"Blood Pact",desc:"If you do not have enough mana to cast a spell, spend health instead (1% health per mana)",archetype:"",archetype_req:10,parents:["Cheaper War Scream"],dependencies:[],blockers:[],cost:2,display:{row:34,col:1},properties:{},effects:[]},{display_name:"Haemorrhage",desc:"Reduce Blood Pact's health cost. (0.5% health per mana)",archetype:"Fallen",archetype_req:0,parents:["Blood Pact"],dependencies:["Blood Pact"],blockers:[],cost:1,display:{row:35,col:2},properties:{},effects:[]},{display_name:"Brink of Madness",desc:"If your health is 25% full or less, gain +40% Resistance",archetype:"",archetype_req:0,parents:["Blood Pact","Cheaper Uppercut 2"],dependencies:[],blockers:[],cost:2,display:{row:35,col:4},properties:{},effects:[]},{display_name:"Cheaper Uppercut 2",desc:"Reduce the Mana cost of Uppercut",archetype:"",archetype_req:0,parents:["Second Chance","Brink of Madness"],dependencies:[],blockers:[],cost:1,display:{row:35,col:6},properties:{},effects:[{type:"add_spell_prop",base_spell:3,cost:-5}]},{display_name:"Martyr",desc:"When you receive a fatal blow, all nearby allies become invincible",archetype:"Paladin",archetype_req:0,parents:["Second Chance"],dependencies:[],blockers:[],cost:2,display:{row:35,col:8},properties:{duration:3,aoe:12},effects:[]}]},atree_example=[{title:"skill",desc:"desc",image:"../media/atree/node.png",connector:!1,row:5,col:3},{image:"../media/atree/connect_angle.png",connector:!0,rotate:270,row:4,col:3},{title:"skill2",desc:"desc",image:"../media/atree/node.png",connector:!1,row:0,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:1,col:2},{title:"skill3",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:2},{image:"../media/atree/connect_line.png",connector:!0,rotate:90,row:2,col:3},{title:"skill4",desc:"desc",image:"../media/atree/node.png",connector:!1,row:2,col:4},{image:"../media/atree/connect_line.png",connector:!0,rotate:0,row:3,col:2},{title:"skill5",desc:"desc",image:"../media/atree/node.png",connector:!1,row:4,col:2},] From 14ab20c4464fe96cf4ad7e3185974ce79443cb0f Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 20:18:08 -0700 Subject: [PATCH 63/68] Remove extra prints, make response time of nodes much faster hides a bug that we have where two updates are scheduled if you hit enter and tab... no....... --- js/builder_graph.js | 2 +- js/computation_graph.js | 4 ++-- js/display_atree.js | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/js/builder_graph.js b/js/builder_graph.js index a3e0052..899fd6d 100644 --- a/js/builder_graph.js +++ b/js/builder_graph.js @@ -178,6 +178,7 @@ class ItemInputNode extends InputNode { } compute_func(input_map) { + console.log("Item update...." + Date.now()); const powdering = input_map.get('powdering'); // built on the assumption of no one will type in CI/CR letter by letter @@ -337,7 +338,6 @@ class WeaponInputDisplayNode extends ComputeNode { toggle_tab('atree-dropdown'); toggleButton('toggle-atree'); } - console.log(document.getElementById("toggle-atree").classList.contains("toggleOn")); } } diff --git a/js/computation_graph.js b/js/computation_graph.js index 90fe08c..0066186 100644 --- a/js/computation_graph.js +++ b/js/computation_graph.js @@ -139,8 +139,8 @@ class InputNode extends ComputeNode { constructor(name, input_field) { super(name); this.input_field = input_field; - this.input_field.addEventListener("input", () => calcSchedule(this, 5000)); - this.input_field.addEventListener("change", () => calcSchedule(this, 500)); + this.input_field.addEventListener("input", () => calcSchedule(this, 500)); + this.input_field.addEventListener("change", () => calcSchedule(this, 5)); //calcSchedule(this); Manually fire first update for better control } diff --git a/js/display_atree.js b/js/display_atree.js index 0c5254a..888383e 100644 --- a/js/display_atree.js +++ b/js/display_atree.js @@ -36,7 +36,6 @@ function construct_AT(elem, tree) { row.classList.add("row"); row.id = "atree-row-" + j; //was causing atree rows to be 0 height - console.log(elem.scrollWidth / 9); row.style.minHeight = elem.scrollWidth / 9 + "px"; //row.style.minHeight = elem.getBoundingClientRect().width / 9 + "px"; From 623037eda0394e4a04c4d969970ac928c3b65edb Mon Sep 17 00:00:00 2001 From: hppeng Date: Thu, 23 Jun 2022 22:33:17 -0700 Subject: [PATCH 64/68] HOTFIX: display weapon as the last part of copy for sharing --- js/build_encode_decode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/build_encode_decode.js b/js/build_encode_decode.js index 618bb71..c7c42c4 100644 --- a/js/build_encode_decode.js +++ b/js/build_encode_decode.js @@ -210,7 +210,7 @@ function shareBuild(build) { "> "+build.items[5].statMap.get("displayName")+"\n"+ "> "+build.items[6].statMap.get("displayName")+"\n"+ "> "+build.items[7].statMap.get("displayName")+"\n"+ - "> "+build.items[8].statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]"; + "> "+build.items[15].statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]"; copyTextToClipboard(text); document.getElementById("share-button").textContent = "Copied!"; } From 39e6ade1421ef8e3c19e24fddaec270789f515cf Mon Sep 17 00:00:00 2001 From: hppeng Date: Fri, 24 Jun 2022 06:06:24 -0700 Subject: [PATCH 65/68] HOTFIX: Patch dps_vis --- js/dps_vis.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/dps_vis.js b/js/dps_vis.js index 85386b0..2a7828d 100644 --- a/js/dps_vis.js +++ b/js/dps_vis.js @@ -213,14 +213,14 @@ function redraw(data) { let tier_mod = tiers_mod.get(tier); let y_max = baseline_y.map(x => 2.1*x*tier_mod*type_mod); let y_min = baseline_y.map(x => 2.0*x*tier_mod*type_mod); - line_top.datum(zip(baseline_x, y_max)) + line_top.datum(zip2(baseline_x, y_max)) .attr("fill", "none") .attr("stroke", d => colorMap.get(tier)) .attr("d", d3.line() .x(function(d) { return x(d[0]) }) .y(function(d) { return y(d[1]) }) ) - line_bot.datum(zip(baseline_x, y_min)) + line_bot.datum(zip2(baseline_x, y_min)) .attr("fill", "none") .attr("stroke", d => colorMap.get(tier)) .attr("d", d3.line() From 17311ff3b19f07f1a751283ebd25340ff2e4c1ac Mon Sep 17 00:00:00 2001 From: ferricles Date: Fri, 24 Jun 2022 19:19:15 -0700 Subject: [PATCH 66/68] first batch of generic assert tests --- js/utils.js | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/js/utils.js b/js/utils.js index fed7df8..a14afff 100644 --- a/js/utils.js +++ b/js/utils.js @@ -412,4 +412,101 @@ async function hardReload() { function capitalizeFirst(str) { return str[0].toUpperCase() + str.substring(1); -} \ No newline at end of file +} + +/** https://stackoverflow.com/questions/16839698/jquery-getscript-alternative-in-native-javascript + * If we ever want to write something that needs to import other js files + */ + const getScript = url => new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = url + script.async = true + + script.onerror = reject + + script.onload = script.onreadystatechange = function() { + const loadState = this.readyState + + if (loadState && loadState !== 'loaded' && loadState !== 'complete') return + + script.onload = script.onreadystatechange = null + + resolve() + } + + document.head.appendChild(script) + }) + +/* +GENERIC TEST FUNCTIONS +*/ +/** The generic assert function. Fails on all "false-y" values. Useful for non-object equality checks, boolean value checks, and existence checks. + * + * @param {*} arg - argument to assert. + * @param {String} msg - the error message to throw. + */ + function assert(arg, msg) { + if (!arg) { + throw new Error(msg ? msg : "Assert failed."); + } +} + +/** Asserts object equality of the 2 parameters. For loose and strict asserts, use assert(). + * + * @param {*} arg1 - first argument to compare. + * @param {*} arg2 - second argument to compare. + * @param {String} msg - the error message to throw. + */ +function assert_equals(arg1, arg2, msg) { + if (!Object.is(arg1, arg2)) { + throw new Error(msg ? msg : "Assert Equals failed. " + arg1 + " is not " + arg2 + "."); + } +} + +/** Asserts object inequality of the 2 parameters. For loose and strict asserts, use assert(). + * + * @param {*} arg1 - first argument to compare. + * @param {*} arg2 - second argument to compare. + * @param {String} msg - the error message to throw. + */ + function assert_not_equals(arg1, arg2, msg) { + if (Object.is(arg1, arg2)) { + throw new Error(msg ? msg : "Assert Not Equals failed. " + arg1 + " is " + arg2 + "."); + } +} + +/** Asserts proximity between 2 arguments. Should be used for any floating point datatype. + * + * @param {*} arg1 - first argument to compare. + * @param {*} arg2 - second argument to compare. + * @param {Number} epsilon - the margin of error (<= del difference is ok). + * @param {String} msg - the error message to throw. + */ +function assert_near(arg1, arg2, epsilon, msg) { + if (Math.abs(arg1 - arg2) > epsilon) { + throw new Error(msg ? msg : "Assert Near failed. " + arg1 + " is not within " + epsilon + " of " + arg2 + "."); + } +} + +/** Asserts that the input argument is null. + * + * @param {*} arg - the argument to test for null. + * @param {String} msg - the error message to throw. + */ +function assert_null(arg, msg) { + if (arg !== null) { + throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not null."); + } +} + +/** Asserts that the input argument is undefined. + * + * @param {*} arg - the argument to test for undefined. + * @param {String} msg - the error message to throw. + */ + function assert_undefined(arg, msg) { + if (arg !== undefined) { + throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not undefined."); + } +} + From 61dfe2de65f4afa067eb37b2a24b8d73448972a1 Mon Sep 17 00:00:00 2001 From: ferricles Date: Fri, 24 Jun 2022 21:01:54 -0700 Subject: [PATCH 67/68] assert error --- js/utils.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/utils.js b/js/utils.js index a14afff..f3352fd 100644 --- a/js/utils.js +++ b/js/utils.js @@ -510,3 +510,16 @@ function assert_null(arg, msg) { } } +/** Asserts that there is an error when a callback function is run. + * + * @param {Function} func_binding - a function binding to run. Can be passed in with func.bind(null, arg1, ..., argn) + * @param {String} msg - the error message to throw. + */ +function assert_error(func_binding, msg) { + try { + func_binding(); + console.trace(msg ? msg : "Function didn't throw an error."); + } catch (err) { + return; + } +} From 8805521ce257d523a6feb1742c40e50cf8cdd961 Mon Sep 17 00:00:00 2001 From: ferricles Date: Fri, 24 Jun 2022 21:24:22 -0700 Subject: [PATCH 68/68] assert error change for error thrown, formatted getScript, added default epsilon for assert near --- js/utils.js | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/js/utils.js b/js/utils.js index f3352fd..7efdd21 100644 --- a/js/utils.js +++ b/js/utils.js @@ -417,25 +417,25 @@ function capitalizeFirst(str) { /** https://stackoverflow.com/questions/16839698/jquery-getscript-alternative-in-native-javascript * If we ever want to write something that needs to import other js files */ - const getScript = url => new Promise((resolve, reject) => { - const script = document.createElement('script') - script.src = url - script.async = true - - script.onerror = reject - - script.onload = script.onreadystatechange = function() { - const loadState = this.readyState - - if (loadState && loadState !== 'loaded' && loadState !== 'complete') return - - script.onload = script.onreadystatechange = null - - resolve() +const getScript = url => new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + script.async = true; + + script.onerror = reject; + + script.onload = script.onreadystatechange = function () { + const loadState = this.readyState; + + if (loadState && loadState !== 'loaded' && loadState !== 'complete') return + + script.onload = script.onreadystatechange = null; + + resolve(); } - - document.head.appendChild(script) - }) + + document.head.appendChild(script); +}) /* GENERIC TEST FUNCTIONS @@ -479,10 +479,10 @@ function assert_equals(arg1, arg2, msg) { * * @param {*} arg1 - first argument to compare. * @param {*} arg2 - second argument to compare. - * @param {Number} epsilon - the margin of error (<= del difference is ok). + * @param {Number} epsilon - the margin of error (<= del difference is ok). Defaults to -1E5. * @param {String} msg - the error message to throw. */ -function assert_near(arg1, arg2, epsilon, msg) { +function assert_near(arg1, arg2, epsilon = 1E-5, msg) { if (Math.abs(arg1 - arg2) > epsilon) { throw new Error(msg ? msg : "Assert Near failed. " + arg1 + " is not within " + epsilon + " of " + arg2 + "."); } @@ -518,8 +518,8 @@ function assert_null(arg, msg) { function assert_error(func_binding, msg) { try { func_binding(); - console.trace(msg ? msg : "Function didn't throw an error."); } catch (err) { return; } + throw new Error(msg ? msg : "Function didn't throw an error."); }