Merge pull request #60 from hppeng-wynn/atree_aggregator

Atree aggregator
This commit is contained in:
hppeng-wynn 2022-06-29 19:18:19 -07:00 committed by GitHub
commit 6e549e8325
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 687 additions and 333 deletions

View file

@ -1264,21 +1264,23 @@
<div class = "col"> <div class = "col">
<div class = "col spell-display dark-5 rounded dark-shadow py-2 border border-dark" id="build-poison-stats">poison</div> <div class = "col spell-display dark-5 rounded dark-shadow py-2 border border-dark" id="build-poison-stats">poison</div>
</div> </div>
<div class = "col"> <div id="all-spells-display" class="row row-cols-1 gy-3 text-center scaled-font pe-0">
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell0-infoAvg">spell1</div> <div class = "col pe-0">
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell0-info" style="display: none;">Spell 1</div> <div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell0-infoAvg">spell1</div>
</div> <div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell0-info" style="display: none;">Spell 1</div>
<div class = "col"> </div>
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell1-infoAvg">spell2</div> <div class = "col pe-0">
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell1-info" style="display: none;">Spell 2</div> <div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell1-infoAvg">spell2</div>
</div> <div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell1-info" style="display: none;">Spell 2</div>
<div class = "col"> </div>
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell2-infoAvg">spell3</div> <div class = "col pe-0">
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell2-info" style="display: none;">Spell 3</div> <div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell2-infoAvg">spell3</div>
</div> <div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell2-info" style="display: none;">Spell 3</div>
<div class = "col"> </div>
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell3-infoAvg">spell4</div> <div class = "col pe-0">
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell3-info" style="display: none;">Spell 4</div> <div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell3-infoAvg">spell4</div>
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell3-info" style="display: none;">Spell 4</div>
</div>
</div> </div>
<div class = "col"> <div class = "col">
<div class = "spell-display dark-5 rounded dark-shadow py-2 border border-dark" id = "powder-special-stats"></div> <div class = "spell-display dark-5 rounded dark-shadow py-2 border border-dark" id = "powder-special-stats"></div>

View file

@ -1,9 +1,130 @@
let abil_points_current; let abil_points_current;
/**
ATreeNode spec:
ATreeNode: {
children: List[ATreeNode] // nodes that this node can link to downstream (or sideways)
parents: List[ATreeNode] // nodes that can link to this one from upstream (or sideways)
ability: atree_node // raw data from atree json
}
atree_node: {
display_name: str
id: int
desc: str
archetype: Optional[str] // not present or empty string = no arch
archetype_req: Optional[int] // default: 0
base_abil: Optional[int] // Modify another abil? poorly defined...
parents: List[int]
dependencies: List[int] // Hard reqs
blockers: List[int] // If any in here are taken, i am invalid
cost: int // cost in AP
display: { // stuff for rendering ATree
row: int
col: int
icon: str
}
properties: Map[str, float] // Dynamic (modifiable) misc. properties; ex. AOE
effects: List[effect]
}
effect: replace_spell | add_spell_prop | convert_spell_conv | raw_stat | stat_scaling
replace_spell: {
type: "replace_spell"
... rest of fields are same as `spell` type (see: damage_calc.js)
}
add_spell_prop: {
type: "add_spell_prop"
base_spell: int // spell identifier
target_part: Optional[str] // Part of the spell to modify. Can be not present/empty for ex. cost modifier.
// If target part does not exist, a new part is created.
cost: Optional[int] // change to spellcost
multipliers: Optional[array[float, 6]] // Additive changes to spellmult (for damage spell)
power: Optional[float] // Additive change to healing power (for heal spell)
hits: Optional[Map[str, float]] // Additive changes to hits (for total entry)
display: Optional[str] // Optional change to the displayed entry. Replaces old
}
convert_spell_conv: {
"type": "convert_spell_conv",
"base_spell": int
"target_part": "all" | str,
"conversion": element_str
}
raw_stat: {
"type": "raw_stat",
"bonuses": list[stat_bonus]
}
stat_bonus: {
"type": "stat" | "prop",
"abil": Optional[int],
"name": str,
"value": float
}
stat_scaling: {
"type": "stat_scaling",
"slider": bool,
"slider_name": Optional[str],
"slider_step": Optional[float],
"inputs": Optional[list[scaling_target]],
"output": scaling_target,
"scaling": list[float],
"max": float
}
scaling_target: {
"type": "stat" | "prop",
"abil": Optional[int],
"name": str
}
*/
// TODO: Range numbers
const default_abils = {
wand: [{
display_name: "Mage Melee",
id: 999,
desc: "Mage basic attack.",
properties: {range: 5000},
effects: [default_spells.wand[0]]
}],
spear: [{
display_name: "Warrior Melee",
id: 999,
desc: "Warrior basic attack.",
properties: {range: 2},
effects: [default_spells.spear[0]]
}],
bow: [{
display_name: "Archer Melee",
id: 999,
desc: "Archer basic attack.",
properties: {range: 20},
effects: [default_spells.bow[0]]
}],
dagger: [{
display_name: "Assassin Melee",
id: 999,
desc: "Assassin basic attack.",
properties: {range: 2},
effects: [default_spells.dagger[0]]
}],
relik: [{
display_name: "Shaman Melee",
id: 999,
desc: "Shaman basic attack.",
properties: {range: 15, speed: 0},
effects: [default_spells.relik[0]]
}],
};
/** /**
* Update ability tree internal representation. (topologically sorted node list) * Update ability tree internal representation. (topologically sorted node list)
* *
* Signature: AbilityTreeUpdateNode(build: Build) => ATree * Signature: AbilityTreeUpdateNode(build: Build) => ATree (List of atree nodes in topological order)
*/ */
const atree_node = new (class extends ComputeNode { const atree_node = new (class extends ComputeNode {
constructor() { super('builder-atree-update'); } constructor() { super('builder-atree-update'); }
@ -18,7 +139,7 @@ const atree_node = new (class extends ComputeNode {
let atree_map = new Map(); let atree_map = new Map();
let atree_head; let atree_head;
for (const i of atree_raw) { for (const i of atree_raw) {
atree_map.set(i.id, {children: [], node: i}); atree_map.set(i.id, {children: [], ability: i});
if (i.parents.length == 0) { if (i.parents.length == 0) {
// Assuming there is only one head. // Assuming there is only one head.
atree_head = atree_map.get(i.id); atree_head = atree_map.get(i.id);
@ -27,7 +148,7 @@ const atree_node = new (class extends ComputeNode {
for (const i of atree_raw) { for (const i of atree_raw) {
let node = atree_map.get(i.id); let node = atree_map.get(i.id);
let parents = []; let parents = [];
for (const parent_id of node.node.parents) { for (const parent_id of node.ability.parents) {
let parent_node = atree_map.get(parent_id); let parent_node = atree_map.get(parent_id);
parent_node.children.push(node); parent_node.children.push(node);
parents.push(parent_node); parents.push(parent_node);
@ -45,7 +166,7 @@ const atree_node = new (class extends ComputeNode {
/** /**
* Display ability tree from topologically sorted list. * Display ability tree from topologically sorted list.
* *
* Signature: AbilityTreeRenderNode(atree: ATree) => null * Signature: AbilityTreeRenderNode(atree: ATree) => RenderedATree ( Map[id, RenderedATNode] )
*/ */
const atree_render = new (class extends ComputeNode { const atree_render = new (class extends ComputeNode {
constructor() { super('builder-atree-render'); this.fail_cb = true; } constructor() { super('builder-atree-render'); this.fail_cb = true; }
@ -55,16 +176,15 @@ const atree_render = new (class extends ComputeNode {
const [atree] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element const [atree] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
//for some reason we have to cast to string //for some reason we have to cast to string
if (atree) { render_AT(document.getElementById("atree-ui"), document.getElementById("atree-active"), atree); } let ret = null;
if (atree) { ret = render_AT(document.getElementById("atree-ui"), document.getElementById("atree-active"), atree); }
if (document.getElementById("toggle-atree").classList.contains("toggleOn")) { //Toggle on, previously was toggled off
toggle_tab('atree-dropdown'); toggle_tab('atree-dropdown'); toggleButton('toggle-atree');
toggleButton('toggle-atree');
} return ret;
} }
})(); })().link_to(atree_node);
atree_render.link_to(atree_node);
/** /**
* Create a reverse topological sort of the tree in the result list. * Create a reverse topological sort of the tree in the result list.
@ -86,15 +206,234 @@ function topological_sort_tree(tree, res, mark_state) {
res.push(tree); res.push(tree);
} }
// these cases are not needed. Case 1 does nothing, case 2 should never happen. // these cases are not needed. Case 1 does nothing, case 2 should never happen.
// else if (state === true) { // else if (state === true) { return; } // permanent mark.
// // permanent mark. // else if (state === false) { throw "not a DAG"; } // temporary mark.
// return;
// }
// else if (state === false) {
// // temporary mark.
// }
} }
/**
* Collect abilities and condense them into a list of "final abils".
* This is just for rendering purposes, and for collecting things that modify spells into one chunk.
* I stg if wynn makes abils that modify multiple spells
* ... well we can extend this by making `base_abil` a list instead but annoy
*
* Signature: AbilityTreeMergeNode(atree: ATree, atree-state: RenderedATree) => Map[id, Ability]
*/
const atree_merge = new (class extends ComputeNode {
constructor() { super('builder-atree-merge'); }
compute_func(input_map) {
const build = input_map.get('build');
const atree_state = input_map.get('atree-state');
const atree_order = input_map.get('atree');
let abils_merged = new Map();
for (const abil of default_abils[build.weapon.statMap.get('type')]) {
let tmp_abil = deepcopy(abil);
if (!Array.isArray(tmp_abil.desc)) {
tmp_abil.desc = [tmp_abil.desc];
}
tmp_abil.subparts = [abil.id];
abils_merged.set(abil.id, tmp_abil);
}
for (const node of atree_order) {
const abil_id = node.ability.id;
if (!atree_state.get(abil_id).active) {
continue;
}
const abil = node.ability;
if (abils_merged.has(abil.base_abil)) {
// Merge abilities.
// TODO: What if there is more than one base abil?
let base_abil = abils_merged.get(abil.base_abil);
if (Array.isArray(abil.desc)) { base_abil.desc = base_abil.desc.concat(abil.desc); }
else { base_abil.desc.push(abil.desc); }
base_abil.subparts.push(abil.id);
base_abil.effects = base_abil.effects.concat(abil.effects);
for (let propname in abil.properties) {
base_abil[propname] = abil[propname];
}
}
else {
let tmp_abil = deepcopy(abil);
if (!Array.isArray(tmp_abil.desc)) {
tmp_abil.desc = [tmp_abil.desc];
}
tmp_abil.subparts = [abil.id];
abils_merged.set(abil_id, tmp_abil);
}
}
return abils_merged;
}
})().link_to(atree_node, 'atree').link_to(atree_render, 'atree-state'); // TODO: THIS IS WRONG!!!!! Need one "collect" node...
/**
* Collect spells from abilities.
*
* Signature: AbilityCollectSpellsNode(atree-merged: Map[id, Ability]) => List[Spell]
*/
const atree_collect_spells = new (class extends ComputeNode {
constructor() { super('atree-spell-collector'); }
compute_func(input_map) {
if (input_map.size !== 1) { throw "AbilityTreeCollectSpellsNode accepts exactly one input (atree-merged)"; }
const [atree_merged] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
let ret_spells = new Map();
for (const [abil_id, abil] of atree_merged.entries()) {
// TODO: Possibly, make a better way for detecting "spell abilities"?
if (abil.effects.length == 0 || abil.effects[0].type !== 'replace_spell') { continue; }
let ret_spell = deepcopy(abil.effects[0]); // NOTE: do not mutate results of previous steps!
const base_spell_id = ret_spell.base_spell;
for (const effect of abil.effects) {
switch (effect.type) {
case 'replace_spell':
// replace_spell just replaces all (defined) aspects.
for (const key in effect) {
ret_spell[key] = effect[key];
}
continue;
case 'add_spell_prop': {
const { base_spell, target_part = null, cost = 0} = effect;
if (base_spell !== base_spell_id) { continue; } // TODO: redundant? if we assume abils only affect one spell
ret_spell.cost += cost;
if (target_part === null) {
continue;
}
let found_part = false;
for (let part of ret_spell.parts) { // TODO: replace with Map? to avoid this linear search... idk prolly good since its not more verbose to type in json
if (part.name === target_part) {
if ('display' in effect) {
part.display = effect.display;
}
if ('multipliers' in effect) {
for (const [idx, v] of effect.multipliers.entries()) { // python: enumerate()
part.multipliers[idx] += v;
}
}
else if ('power' in effect) {
part.power += effect.power;
}
else if ('hits' in effect) {
for (const [idx, v] of Object.entries(effect.hits)) { // looks kinda similar to multipliers case... hmm... can we unify all of these three? (make healpower a list)
part.hits[idx] += v;
}
}
else {
throw "uhh invalid spell add effect";
}
found_part = true;
break;
}
}
if (!found_part) { // add part.
let spell_part = deepcopy(effect);
spell_part.name = target_part; // has some extra fields but whatever
ret_spell.parts.push(spell_part);
}
continue;
}
case 'convert_spell_conv':
const { base_spell, target_part, conversion } = effect;
if (base_spell !== base_spell_id) { continue; } // TODO: redundant? if we assume abils only affect one spell
const elem_idx = damageClasses.indexOf(conversion);
let filter = target_part === 'all';
for (let part of ret_spell.parts) { // TODO: replace with Map? to avoid this linear search... idk prolly good since its not more verbose to type in json
if (filter || part.name === target_part) {
if ('multipliers' in part) {
let total_conv = 0;
for (let i = 1; i < 6; ++i) { // skip neutral
total_conv += part.multipliers[i];
}
let new_conv = [part.multipliers[0], 0, 0, 0, 0, 0];
new_conv[elem_idx] = total_conv;
part.multipliers = new_conv;
}
}
}
continue;
}
}
ret_spells.set(base_spell_id, ret_spell);
}
return ret_spells;
}
})().link_to(atree_merge, 'atree-merged');
/**
* Construct compute nodes to link builder items and edit IDs to the appropriate display outputs.
* To make things a bit cleaner, the compute graph structure goes like
* [builder, build stats] -> [one agg node that is just a passthrough] -> all the spell calc nodes
* This way, when things have to be deleted i can just delete one node from the dependencies of builder/build stats...
* thats the idea anyway.
*
* Whenever this is updated, it forces an update of all the newly created spell nodes (if the build is clean).
*
* Signature: AbilityEnsureSpellsNodes(spells: Map[id, Spell]) => null
*/
class AbilityTreeEnsureNodesNode extends ComputeNode {
/**
* Kinda "hyper-node": Constructor takes nodes that should be linked to (build node and stat agg node)
*/
constructor(build_node, stat_agg_node) {
super('atree-make-nodes');
this.build_node = build_node;
this.stat_agg_node = stat_agg_node;
// Slight amount of wasted compute to keep internal state non-changing.
this.passthrough = new PassThroughNode('atree-make-nodes_internal').link_to(this.build_node, 'build').link_to(this.stat_agg_node, 'stats');
this.spelldmg_nodes = []; // debugging use
this.spell_display_elem = document.getElementById("all-spells-display");
}
compute_func(input_map) {
console.log('atree make nodes');
this.passthrough.remove_link(this.build_node);
this.passthrough.remove_link(this.stat_agg_node);
this.passthrough = new PassThroughNode('atree-make-nodes_internal').link_to(this.build_node, 'build').link_to(this.stat_agg_node, 'stats');
this.spell_display_elem.textContent = "";
const build_node = this.passthrough.get_node('build'); // aaaaaaaaa performance... savings... help....
const stat_agg_node = this.passthrough.get_node('stats');
const spell_map = input_map.get('spells'); // TODO: is this gonna need more? idk...
// TODO shortcut update path for sliders
for (const [spell_id, spell] of spell_map.entries()) {
let spell_node = new SpellSelectNode(spell);
spell_node.link_to(build_node, 'build');
let calc_node = new SpellDamageCalcNode(spell.base_spell);
calc_node.link_to(build_node, 'build').link_to(stat_agg_node, 'stats')
.link_to(spell_node, 'spell-info');
this.spelldmg_nodes.push(calc_node);
let display_elem = document.createElement('div');
display_elem.classList.add("col", "pe-0");
// TODO: just pass these elements into the display node instead of juggling the raw IDs...
let spell_summary = document.createElement('div'); spell_summary.setAttribute('id', "spell"+spell.base_spell+"-infoAvg");
spell_summary.classList.add("col", "spell-display", "spell-expand", "dark-5", "rounded", "dark-shadow", "pt-2", "border", "border-dark");
let spell_detail = document.createElement('div'); spell_detail.setAttribute('id', "spell"+spell.base_spell+"-info");
spell_detail.classList.add("col", "spell-display", "dark-5", "rounded", "dark-shadow", "py-2");
spell_detail.style.display = "none";
display_elem.appendChild(spell_summary); display_elem.appendChild(spell_detail);
let display_node = new SpellDisplayNode(spell.base_spell);
display_node.link_to(stat_agg_node, 'stats');
display_node.link_to(spell_node, 'spell-info');
display_node.link_to(calc_node, 'spell-damage');
this.spell_display_elem.appendChild(display_elem);
}
this.passthrough.mark_dirty().update(); // Force update once.
}
}
/** The main function for rendering an ability tree. /** The main function for rendering an ability tree.
* *
@ -151,21 +490,21 @@ function render_AT(UI_elem, list_elem, tree) {
let atree_connectors_map = new Map() let atree_connectors_map = new Map()
let max_row = 0; let max_row = 0;
for (const i of tree) { for (const i of tree) {
atree_map.set(i.node.id, {node: i.node, connectors: new Map(), active: false}); atree_map.set(i.ability.id, {ability: i.ability, connectors: new Map(), active: false});
if (i.node.display.row > max_row) { if (i.ability.display.row > max_row) {
max_row = i.node.display.row; max_row = i.ability.display.row;
} }
} }
// Copy graph structure. // Copy graph structure.
for (const i of tree) { for (const i of tree) {
let node_wrapper = atree_map.get(i.node.id); let node_wrapper = atree_map.get(i.ability.id);
node_wrapper.parents = []; node_wrapper.parents = [];
node_wrapper.children = []; node_wrapper.children = [];
for (const parent of i.parents) { for (const parent of i.parents) {
node_wrapper.parents.push(atree_map.get(parent.node.id)); node_wrapper.parents.push(atree_map.get(parent.ability.id));
} }
for (const child of i.children) { for (const child of i.children) {
node_wrapper.children.push(atree_map.get(child.node.id)); node_wrapper.children.push(atree_map.get(child.ability.id));
} }
} }
@ -184,51 +523,54 @@ function render_AT(UI_elem, list_elem, tree) {
} }
for (const _node of tree) { for (const _node of tree) {
let node_wrap = atree_map.get(_node.node.id); let node_wrap = atree_map.get(_node.ability.id);
let node = _node.node; let ability = _node.ability;
// create connectors based on parent location // create connectors based on parent location
for (let parent of node_wrap.parents) { for (let parent of node_wrap.parents) {
node_wrap.connectors.set(parent, []); node_wrap.connectors.set(parent, []);
let parent_node = parent.node; let parent_abil = parent.ability;
const parent_id = parent_node.id; const parent_id = parent_abil.id;
let connect_elem = document.createElement("div"); let connect_elem = document.createElement("div");
connect_elem.style = "background-size: cover; width: 100%; height: 100%;"; connect_elem.style = "background-size: cover; width: 100%; height: 100%;";
// connect up // connect up
for (let i = node.display.row - 1; i > parent_node.display.row; i--) { for (let i = ability.display.row - 1; i > parent_abil.display.row; i--) {
const coord = i + "," + ability.display.col;
let connector = connect_elem.cloneNode(); let connector = connect_elem.cloneNode();
node_wrap.connectors.get(parent).push(i + "," + node.display.col); node_wrap.connectors.get(parent).push(coord);
resolve_connector(atree_connectors_map, i + "," + node.display.col, {connector: connector, connections: [0, 0, 1, 1]}); resolve_connector(atree_connectors_map, coord, {connector: connector, connections: [0, 0, 1, 1]});
} }
// connect horizontally // connect horizontally
let min = Math.min(parent_node.display.col, node.display.col); let min = Math.min(parent_abil.display.col, ability.display.col);
let max = Math.max(parent_node.display.col, node.display.col); let max = Math.max(parent_abil.display.col, ability.display.col);
for (let i = min + 1; i < max; i++) { for (let i = min + 1; i < max; i++) {
const coord = parent_abil.display.row + "," + i;
let connector = connect_elem.cloneNode(); let connector = connect_elem.cloneNode();
node_wrap.connectors.get(parent).push(parent_node.display.row + "," + i); node_wrap.connectors.get(parent).push(coord);
resolve_connector(atree_connectors_map, parent_node.display.row + "," + i, {connector: connector, connections: [1, 1, 0, 0]}); resolve_connector(atree_connectors_map, coord, {connector: connector, connections: [1, 1, 0, 0]});
} }
// connect corners // connect corners
if (parent_node.display.row != node.display.row && parent_node.display.col != node.display.col) { if (parent_abil.display.row != ability.display.row && parent_abil.display.col != ability.display.col) {
const coord = parent_abil.display.row + "," + ability.display.col;
let connector = connect_elem.cloneNode(); let connector = connect_elem.cloneNode();
node_wrap.connectors.get(parent).push(parent_node.display.row + "," + node.display.col); node_wrap.connectors.get(parent).push(coord);
let connections = [0, 0, 0, 1]; let connections = [0, 0, 0, 1];
if (parent_node.display.col > node.display.col) { if (parent_abil.display.col > ability.display.col) {
connections[1] = 1; connections[1] = 1;
} }
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)) {
connections[0] = 1; connections[0] = 1;
} }
resolve_connector(atree_connectors_map, parent_node.display.row + "," + node.display.col, {connector: connector, connections: connections}); resolve_connector(atree_connectors_map, coord, {connector: connector, connections: connections});
} }
} }
// create node // create node
let node_elem = document.createElement('div'); let node_elem = document.createElement('div');
let icon = node.display.icon; let icon = ability.display.icon;
if (icon === undefined) { if (icon === undefined) {
icon = "node"; icon = "node";
} }
@ -263,15 +605,15 @@ function render_AT(UI_elem, list_elem, tree) {
let active_tooltip_title = document.createElement('b'); let active_tooltip_title = document.createElement('b');
active_tooltip_title.classList.add("scaled-font"); active_tooltip_title.classList.add("scaled-font");
active_tooltip_title.innerHTML = node.display_name; active_tooltip_title.innerHTML = ability.display_name;
let active_tooltip_desc = document.createElement('p'); let active_tooltip_desc = document.createElement('p');
active_tooltip_desc.classList.add("scaled-font-sm", "my-0", "mx-1", "text-wrap"); active_tooltip_desc.classList.add("scaled-font-sm", "my-0", "mx-1", "text-wrap");
active_tooltip_desc.textContent = node.desc; active_tooltip_desc.textContent = ability.desc;
let active_tooltip_cost = document.createElement('p'); let active_tooltip_cost = document.createElement('p');
active_tooltip_cost.classList.add("scaled-font-sm", "my-0", "mx-1", "text-start"); active_tooltip_cost.classList.add("scaled-font-sm", "my-0", "mx-1", "text-start");
active_tooltip_cost.textContent = "Cost: " + node.cost + " AP"; active_tooltip_cost.textContent = "Cost: " + ability.cost + " AP";
active_tooltip.appendChild(active_tooltip_title); active_tooltip.appendChild(active_tooltip_title);
active_tooltip.appendChild(active_tooltip_desc); active_tooltip.appendChild(active_tooltip_desc);
@ -279,7 +621,7 @@ function render_AT(UI_elem, list_elem, tree) {
node_tooltip = active_tooltip.cloneNode(true); node_tooltip = active_tooltip.cloneNode(true);
active_tooltip.id = "atree-ab-" + node.id; active_tooltip.id = "atree-ab-" + ability.id;
node_tooltip.style.position = "absolute"; node_tooltip.style.position = "absolute";
node_tooltip.style.zIndex = "100"; node_tooltip.style.zIndex = "100";
@ -289,19 +631,21 @@ function render_AT(UI_elem, list_elem, tree) {
node_elem.addEventListener('click', function(e) { node_elem.addEventListener('click', function(e) {
if (e.target !== this && e.target!== this.children[0]) {return;} if (e.target !== this && e.target!== this.children[0]) {return;}
let tooltip = document.getElementById("atree-ab-" + node.id); let tooltip = document.getElementById("atree-ab-" + ability.id);
if (tooltip.style.display == "block") { if (tooltip.style.display === "block") {
tooltip.style.display = "none"; tooltip.style.display = "none";
this.classList.remove("atree-selected"); this.classList.remove("atree-selected");
abil_points_current -= node.cost; abil_points_current -= ability.cost;
} }
else { else {
tooltip.style.display = "block"; tooltip.style.display = "block";
this.classList.add("atree-selected"); this.classList.add("atree-selected");
abil_points_current += node.cost; abil_points_current += ability.cost;
}; };
document.getElementById("active_AP_cost").textContent = abil_points_current; document.getElementById("active_AP_cost").textContent = abil_points_current;
atree_toggle_state(atree_connectors_map, node_wrap); atree_toggle_state(atree_connectors_map, node_wrap);
atree_merge.mark_dirty();
atree_merge.update();
}); });
// add tooltip // add tooltip
@ -321,10 +665,12 @@ function render_AT(UI_elem, list_elem, tree) {
tooltip.style.display = "none"; tooltip.style.display = "none";
}); });
document.getElementById("atree-row-" + node.display.row).children[node.display.col].appendChild(node_elem); document.getElementById("atree-row-" + ability.display.row).children[ability.display.col].appendChild(node_elem);
}; };
console.log(atree_connectors_map); console.log(atree_connectors_map);
atree_render_connection(atree_connectors_map); atree_render_connection(atree_connectors_map);
return atree_map;
}; };
// resolve connector conflict, when they occupy the same cell. // resolve connector conflict, when they occupy the same cell.
@ -379,7 +725,6 @@ function atree_render_connection(atree_connectors_map) {
connector_img.style = "width: 100%; height: 100%;" connector_img.style = "width: 100%; height: 100%;"
connector_elem.replaceChildren(connector_img); connector_elem.replaceChildren(connector_img);
connector_info.highlight = [0, 0, 0, 0]; connector_info.highlight = [0, 0, 0, 0];
console.log(i + ", " + connector_info.type);
let target_elem = document.getElementById("atree-row-" + i.split(",")[0]).children[i.split(",")[1]]; let target_elem = document.getElementById("atree-row-" + i.split(",")[0]).children[i.split(",")[1]];
if (target_elem.children.length != 0) { if (target_elem.children.length != 0) {
// janky special case... // janky special case...
@ -391,7 +736,6 @@ function atree_render_connection(atree_connectors_map) {
// toggle the state of a node. // toggle the state of a node.
function atree_toggle_state(atree_connectors_map, node_wrapper) { function atree_toggle_state(atree_connectors_map, node_wrapper) {
let node = node_wrapper.node;
const new_state = !node_wrapper.active; const new_state = !node_wrapper.active;
node_wrapper.active = new_state node_wrapper.active = new_state
for (const parent of node_wrapper.parents) { for (const parent of node_wrapper.parents) {
@ -423,10 +767,10 @@ function atree_update_connector() {
function atree_set_edge(atree_connectors_map, parent, child, state) { function atree_set_edge(atree_connectors_map, parent, child, state) {
const connectors = child.connectors.get(parent); const connectors = child.connectors.get(parent);
const parent_row = parent.node.display.row; const parent_row = parent.ability.display.row;
const parent_col = parent.node.display.col; const parent_col = parent.ability.display.col;
const child_row = child.node.display.row; const child_row = child.ability.display.row;
const child_col = child.node.display.col; const child_col = child.ability.display.col;
let state_delta = (state ? 1 : -1); let state_delta = (state ? 1 : -1);
let child_side_idx = (parent_col > child_col ? 0 : 1); let child_side_idx = (parent_col > child_col ? 0 : 1);

View file

@ -3,8 +3,6 @@ const atrees = {
{ {
"display_name": "Arrow Shield", "display_name": "Arrow Shield",
"desc": "Create a shield around you that deal damage and knockback mobs when triggered. (2 Charges)", "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"], "parents": ["Power Shots", "Cheaper Escape"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -58,11 +56,10 @@ const atrees = {
"col": 4 "col": 4
}, },
"properties": { "properties": {
"aoe": 0, "aoe": 0,
"range": 0 "range": 0
}, },
"effects": [ "effects": [{
{
"type": "replace_spell", "type": "replace_spell",
"name": "Escape", "name": "Escape",
"cost": 25, "cost": 25,
@ -72,41 +69,30 @@ const atrees = {
"scaling": "spell", "scaling": "spell",
"display": "Total Damage", "display": "Total Damage",
"parts": [ "parts": [
{ {
"name": "None", "name": "Total Damage",
"type": "damage", "type": "total",
"multipliers": [0, 0, 0, 0, 0, 0] "hits": {}
},
{
"name": "Total Damage",
"type": "total",
"hits": {
"None": 0
} }
}
] ]
} }]
]
}, },
{ {
"display_name": "Arrow Bomb", "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)", "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": [], "parents": [],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
"cost": 1, "cost": 1,
"display": { "display": {
"row": 0, "row": 0,
"col": 4 "col": 4
}, },
"properties": { "properties": {
"aoe": 4.5, "aoe": 4.5,
"range": 26 "range": 26
}, },
"effects": [ "effects": [{
{
"type": "replace_spell", "type": "replace_spell",
"name": "Arrow Bomb", "name": "Arrow Bomb",
"cost": 50, "cost": 50,
@ -116,48 +102,41 @@ const atrees = {
"scaling": "spell", "scaling": "spell",
"display": "Total Damage", "display": "Total Damage",
"parts": [ "parts": [
{ {
"name": "Arrow Bomb", "name": "Arrow Bomb",
"type": "damage", "type": "damage",
"multipliers": [160, 0, 0, 0, 20, 0] "multipliers": [160, 0, 0, 0, 20, 0]
}, },
{ {
"name": "Total Damage", "name": "Total Damage",
"type": "total", "type": "total",
"hits": { "hits": {
"Arrow Bomb": 1 "Arrow Bomb": 1
}
} }
}
] ]
} }]
]
}, },
{ {
"display_name": "Heart Shatter", "display_name": "Heart Shatter",
"desc": "If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage.", "desc": "If you hit a mob directly with Arrow Bomb, shatter its heart and deal bonus damage.",
"archetype": "", "base_abil": "Arrow Bomb",
"archetype_req": 0,
"parents": ["Bow Proficiency I"], "parents": ["Bow Proficiency I"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
"cost": 1, "cost": 1,
"display": { "display": {
"row": 4, "row": 4,
"col": 4 "col": 4
}, },
"properties": {}, "properties": {},
"effects": [ "effects": [{
{
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 3, "base_spell": 3,
"target_part": "Arrow Bomb", "target_part": "Arrow Bomb",
"cost": 0, "cost": 0,
"multipliers": [100, 0, 0, 0, 0, 0] "multipliers": [100, 0, 0, 0, 0, 0]
}, }]
{
}
]
}, },
{ {
"display_name": "Fire Creep", "display_name": "Fire Creep",
@ -377,17 +356,19 @@ const atrees = {
"col": 1 "col": 1
}, },
"properties": { "properties": {
"aoe": 8, "aoe": 8,
"duration": 120 "duration": 120
}, },
"type": "stat_bonus", "effects": [{
"bonuses": [ "type": "stat_bonus",
{ "bonuses": [
"type": "stat", {
"name": "spd", "type": "stat",
"value": 20 "name": "spd",
} "value": 20
] }
]
}]
}, },
{ {
"display_name": "Basaltic Trap", "display_name": "Basaltic Trap",
@ -642,7 +623,7 @@ const atrees = {
{ {
"type": "convert_spell_conv", "type": "convert_spell_conv",
"target_part": "all", "target_part": "all",
"conversion": "thunder" "conversion": "Thunder"
} }
] ]
}, },
@ -714,15 +695,17 @@ const atrees = {
"properties": { "properties": {
"focus": 1, "focus": 1,
"timer": 5 "timer": 5
}, },
"type": "stat_bonus", "effects": [{
"bonuses": [ "type": "stat_bonus",
{ "bonuses": [
"type": "stat", {
"name": "damPct", "type": "stat",
"value": 50 "name": "damPct",
} "value": 50
] }
]
}]
}, },
{ {
"display_name": "Call of the Hound", "display_name": "Call of the Hound",
@ -825,7 +808,7 @@ const atrees = {
"base_spell": 5, "base_spell": 5,
"spell_type": "damage", "spell_type": "damage",
"scaling": "spell", "scaling": "spell",
"display": "One Focus", "display": "DPS",
"cost": 0, "cost": 0,
"parts": [ "parts": [
@ -835,7 +818,7 @@ const atrees = {
"multipliers": [10, 0, 0, 5, 0, 0] "multipliers": [10, 0, 0, 5, 0, 0]
}, },
{ {
"name": "One Focus", "name": "DPS",
"type": "total", "type": "total",
"hits": { "hits": {
"Single Arrow": 20 "Single Arrow": 20
@ -845,7 +828,7 @@ const atrees = {
"name": "Total Damage", "name": "Total Damage",
"type": "total", "type": "total",
"hits": { "hits": {
"One Focus": 7 "DPS": 7
} }
} }
] ]
@ -915,13 +898,14 @@ const atrees = {
"blockers": [], "blockers": [],
"cost": 2, "cost": 2,
"display": { "display": {
"row": 39, "row": 39,
"col": 2 "col": 2
}, },
"properties": { "properties": {
"range": 2.5, "range": 2.5,
"slowness": 0.3 "slowness": 0.3
} },
"effects": []
}, },
{ {
"display_name": "All-Seeing Panoptes", "display_name": "All-Seeing Panoptes",
@ -1263,10 +1247,11 @@ const atrees = {
"display": { "display": {
"row": 21, "row": 21,
"col": 7 "col": 7
}, },
"properties": { "properties": {
"shieldCharges": 2 "shieldCharges": 2
} },
"effects": []
}, },
{ {
"display_name": "Stormy Feet", "display_name": "Stormy Feet",
@ -1284,18 +1269,16 @@ const atrees = {
"properties": { "properties": {
"duration": 60 "duration": 60
}, },
"effects": [ "effects": [{
{ "type": "stat_bonus",
"type": "stat_bonus", "bonuses": [
"bonuses": [ {
{ "type": "stat",
"type": "stat", "name": "spdPct",
"name": "spdPct", "value": 20
"value": 20 }
}
] ]
} }]
]
}, },
{ {
"display_name": "Refined Gunpowder", "display_name": "Refined Gunpowder",
@ -2028,8 +2011,6 @@ const atrees = {
{ {
"display_name": "Bash", "display_name": "Bash",
"desc": "Violently bash the ground, dealing high damage in a large area", "desc": "Violently bash the ground, dealing high damage in a large area",
"archetype": "",
"archetype_req": 0,
"parents": [], "parents": [],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -2073,8 +2054,7 @@ const atrees = {
{ {
"display_name": "Spear Proficiency 1", "display_name": "Spear Proficiency 1",
"desc": "Improve your Main Attack's damage and range w/ spear", "desc": "Improve your Main Attack's damage and range w/ spear",
"archetype": "", "base_abil": 999,
"archetype_req": 0,
"parents": ["Bash"], "parents": ["Bash"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -2104,8 +2084,7 @@ const atrees = {
{ {
"display_name": "Cheaper Bash", "display_name": "Cheaper Bash",
"desc": "Reduce the Mana cost of Bash", "desc": "Reduce the Mana cost of Bash",
"archetype": "", "base_abil": "Bash",
"archetype_req": 0,
"parents": ["Spear Proficiency 1"], "parents": ["Spear Proficiency 1"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -2129,9 +2108,8 @@ const atrees = {
{ {
"display_name": "Double Bash", "display_name": "Double Bash",
"desc": "Bash will hit a second time at a farther range", "desc": "Bash will hit a second time at a farther range",
"archetype": "",
"archetype_req": 0,
"parents": ["Spear Proficiency 1"], "parents": ["Spear Proficiency 1"],
"base_abil": "Bash",
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
"cost": 1, "cost": 1,
@ -2150,8 +2128,7 @@ const atrees = {
"target_part": "Total Damage", "target_part": "Total Damage",
"cost": 0, "cost": 0,
"hits": { "hits": {
"name": "Single Hit", "Single Hit": 1
"value": 1
} }
}, },
{ {
@ -2167,8 +2144,6 @@ const atrees = {
{ {
"display_name": "Charge", "display_name": "Charge",
"desc": "Charge forward at high speed (hold shift to cancel)", "desc": "Charge forward at high speed (hold shift to cancel)",
"archetype": "",
"archetype_req": 0,
"parents": ["Double Bash"], "parents": ["Double Bash"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -2178,34 +2153,23 @@ const atrees = {
"col": 4, "col": 4,
"icon": "node_4" "icon": "node_4"
}, },
"properties": { "properties": {},
}, "effects": [{
"effects": [ "type": "replace_spell",
{ "name": "Charge",
"type": "replace_spell", "cost": 25,
"name": "Charge", "display_text": "Total Damage Average",
"cost": 25, "base_spell": 2,
"display_text": "Total Damage Average", "spell_type": "damage",
"base_spell": 2, "scaling": "spell",
"spell_type": "damage", "display": "Total Damage",
"scaling": "spell", "parts": [
"display": "Total Damage", {
"parts": [ "name": "Total Damage",
{ "hits": {}
"name": "None", }
"type": "damage", ]
"multipliers": [0, 0, 0, 0, 0, 0] }]
},
{
"name": "Total Damage",
"type": "total",
"hits": {
"None": 0
}
}
]
}
]
}, },
{ {
@ -2329,9 +2293,7 @@ const atrees = {
{ {
"display_name": "Uppercut", "display_name": "Uppercut",
"desc": "Rocket enemies in the air and deal massive damage", "desc": "Rocket enemies in the air and deal massive damage",
"archetype": "", "parents": ["Vehement", "Cheaper Charge"],
"archetype_req": 0,
"parents": ["Vehement"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
"cost": 1, "cost": 1,
@ -2353,19 +2315,15 @@ const atrees = {
"base_spell": 3, "base_spell": 3,
"spell_type": "damage", "spell_type": "damage",
"scaling": "spell", "scaling": "spell",
"display": "total", "display": "Total Damage",
"parts": [ "parts": [
{ {
"name": "Uppercut", "name": "Uppercut",
"type": "damage",
"multipliers": [150, 50, 50, 0, 0, 0] "multipliers": [150, 50, 50, 0, 0, 0]
}, },
{ {
"name": "Total Damage", "name": "Total Damage",
"type": "total", "hits": { "Uppercut": 1 }
"hits": {
"Uppercut": 1
}
} }
] ]
} }
@ -2375,8 +2333,7 @@ const atrees = {
{ {
"display_name": "Cheaper Charge", "display_name": "Cheaper Charge",
"desc": "Reduce the Mana cost of Charge", "desc": "Reduce the Mana cost of Charge",
"archetype": "", "base_abil": "Charge",
"archetype_req": 0,
"parents": ["Uppercut", "War Scream"], "parents": ["Uppercut", "War Scream"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -2400,9 +2357,7 @@ const atrees = {
{ {
"display_name": "War Scream", "display_name": "War Scream",
"desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies", "desc": "Emit a terrorizing roar that deals damage, pull nearby enemies, and add damage resistance to yourself and allies",
"archetype": "", "parents": ["Tougher Skin", "Cheaper Charge"],
"archetype_req": 0,
"parents": ["Tougher Skin"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
"cost": 1, "cost": 1,
@ -2429,8 +2384,11 @@ const atrees = {
"parts": [ "parts": [
{ {
"name": "War Scream", "name": "War Scream",
"type": "damage",
"multipliers": [50, 0, 0, 0, 50, 0] "multipliers": [50, 0, 0, 0, 50, 0]
},
{
"name": "Total Damage Average",
"hits": { "War Scream": 1 }
} }
] ]
} }
@ -2634,7 +2592,6 @@ const atrees = {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 1, "base_spell": 1,
"target_part": "Total Damage", "target_part": "Total Damage",
"cost": 0,
"hits": { "hits": {
"Single Hit": 2 "Single Hit": 2
} }
@ -2643,7 +2600,6 @@ const atrees = {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 1, "base_spell": 1,
"target_part": "Single Hit", "target_part": "Single Hit",
"cost": 0,
"multipliers": [-20, 0, 0, 0, 0, 0] "multipliers": [-20, 0, 0, 0, 0, 0]
} }
] ]
@ -2670,14 +2626,12 @@ const atrees = {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 3, "base_spell": 3,
"target_part": "Fireworks", "target_part": "Fireworks",
"cost": 0,
"multipliers": [80, 0, 20, 0, 0, 0] "multipliers": [80, 0, 20, 0, 0, 0]
}, },
{ {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 3, "base_spell": 3,
"target_part": "Total Damage", "target_part": "Total Damage",
"cost": 0,
"hits": { "hits": {
"Fireworks": 1 "Fireworks": 1
} }
@ -2713,7 +2667,7 @@ const atrees = {
{ {
"type": "convert_spell_conv", "type": "convert_spell_conv",
"target_part": "all", "target_part": "all",
"conversion": "water" "conversion": "Water"
} }
] ]
}, },
@ -2740,7 +2694,6 @@ const atrees = {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 2, "base_spell": 2,
"target_part": "Flyby Jab", "target_part": "Flyby Jab",
"cost": 0,
"multipliers": [20, 0, 0, 0, 0, 40] "multipliers": [20, 0, 0, 0, 0, 40]
} }
] ]
@ -2769,14 +2722,12 @@ const atrees = {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 3, "base_spell": 3,
"target_part": "Flaming Uppercut", "target_part": "Flaming Uppercut",
"cost": 0,
"multipliers": [0, 0, 0, 0, 50, 0] "multipliers": [0, 0, 0, 0, 50, 0]
}, },
{ {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 3, "base_spell": 3,
"target_part": "Flaming Uppercut Total Damage", "target_part": "Flaming Uppercut Total Damage",
"cost": 0,
"hits": { "hits": {
"Flaming Uppercut": 5 "Flaming Uppercut": 5
} }
@ -2785,7 +2736,6 @@ const atrees = {
"type": "add_spell_prop", "type": "add_spell_prop",
"base_spell": 3, "base_spell": 3,
"target_part": "Total Damage", "target_part": "Total Damage",
"cost": 0,
"hits": { "hits": {
"Flaming Uppercut": 5 "Flaming Uppercut": 5
} }
@ -2807,8 +2757,7 @@ const atrees = {
"col": 7, "col": 7,
"icon": "node_0" "icon": "node_0"
}, },
"properties": { "properties": {},
},
"effects": [ "effects": [
{ {
"type": "add_spell_prop", "type": "add_spell_prop",
@ -2834,11 +2783,8 @@ const atrees = {
"col": 2, "col": 2,
"icon": "node_3" "icon": "node_3"
}, },
"properties": { "properties": {},
}, "effects": []
"effects": [
]
}, },
{ {
@ -2860,11 +2806,17 @@ const atrees = {
}, },
"effects": [ "effects": [
{ {
"type": "add_spell_prop", "type": "replace_spell",
"name": "Counter",
"display_text": "Counter",
"base_spell": 5, "base_spell": 5,
"target_part": "Counter", "display": "Counter Damage",
"cost": 0, "parts": [
"multipliers": [60, 0, 20, 0, 0, 20] {
"name": "Counter Damage",
"multipliers": [60, 0, 20, 0, 0, 20]
}
]
} }
] ]
}, },
@ -3646,6 +3598,7 @@ const atrees = {
"desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)", "desc": "While Corrupted, every 3% Health you lose will add +1 AoE to Bash (Max 10)",
"archetype": "Fallen", "archetype": "Fallen",
"archetype_req": 8, "archetype_req": 8,
"base_abil": "Bak'al's Grasp",
"parents": ["Tempest", "Uncontainable Corruption"], "parents": ["Tempest", "Uncontainable Corruption"],
"dependencies": [], "dependencies": [],
"blockers": [], "blockers": [],
@ -3663,8 +3616,9 @@ const atrees = {
"slider": true, "slider": true,
"slider_name": "Corrupted", "slider_name": "Corrupted",
"output": { "output": {
"type": "stat", "type": "prop",
"name": "bashAoE" "abil": "Bash",
"name": "aoe"
}, },
"scaling": [1], "scaling": [1],
"max": 10, "max": 10,
@ -3924,7 +3878,7 @@ const atrees = {
{ {
"type": "convert_spell_conv", "type": "convert_spell_conv",
"target_part": "all", "target_part": "all",
"conversion": "thunder" "conversion": "Thunder"
}, },
{ {
"type": "raw_stat", "type": "raw_stat",

File diff suppressed because one or more lines are too long

View file

@ -149,17 +149,6 @@ function toggle_tab(tab) {
} }
} }
// toggle spell arrow
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) { function toggle_boost_tab(tab) {
for (const i of skp_order) { for (const i of skp_order) {
@ -396,16 +385,12 @@ function collapse_element(elmnt) {
document.querySelector(elmnt).style.removeProperty('display'); document.querySelector(elmnt).style.removeProperty('display');
} }
// TODO: Learn and use await
function init() { function init() {
console.log("builder.js init"); console.log("builder.js init");
init_autocomplete(); init_autocomplete();
// Other "main" stuff // Other "main" stuff
// Spell dropdowns // Spell dropdowns
for (const i of spell_disp) {
document.querySelector("#"+i+"Avg").addEventListener("click", () => toggle_spell_tab(i));
}
for (const eq of equipment_keys) { for (const eq of equipment_keys) {
document.querySelector("#"+eq+"-tooltip").addEventListener("click", () => collapse_element('#'+eq+'-tooltip')); document.querySelector("#"+eq+"-tooltip").addEventListener("click", () => collapse_element('#'+eq+'-tooltip'));
} }

View file

@ -468,41 +468,17 @@ class PowderInputNode extends InputNode {
* Signature: SpellSelectNode<int>(build: Build) => [Spell, SpellParts] * Signature: SpellSelectNode<int>(build: Build) => [Spell, SpellParts]
*/ */
class SpellSelectNode extends ComputeNode { class SpellSelectNode extends ComputeNode {
// TODO: rewrite me entirely... constructor(spell) {
constructor(spell_num) { super("builder-spell"+spell.base_spell+"-select");
super("builder-spell"+spell_num+"-select"); this.spell = spell;
this.spell_idx = spell_num;
} }
compute_func(input_map) { compute_func(input_map) {
const build = input_map.get('build'); const build = input_map.get('build');
let stats = build.statMap; let stats = build.statMap;
// TODO: apply major ids... DOOM.....
const i = this.spell_idx; return [this.spell, this.spell.parts];
const spells = default_spells[build.weapon.statMap.get("type")];
let spell;
for (const _spell of spells) {
if (_spell.base_spell === i) {
spell = _spell;
break;
}
}
if (spell === undefined) { return null; }
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];
} }
} }
@ -995,15 +971,15 @@ class SumNumberInputNode extends InputNode {
let item_nodes = []; let item_nodes = [];
let powder_nodes = []; let powder_nodes = [];
let spelldmg_nodes = [];
let edit_input_nodes = []; let edit_input_nodes = [];
let skp_inputs = []; let skp_inputs = [];
let build_node; let build_node;
let stat_agg_node; let stat_agg_node;
let edit_agg_node; let edit_agg_node;
let atree_graph_creator;
function builder_graph_init() { function builder_graph_init() {
// Phase 1/2: Set up item input, propagate updates, etc. // Phase 1/3: Set up item input, propagate updates, etc.
// Bind item input fields to input nodes, and some display stuff (for auto colorizing stuff). // 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)) { for (const [eq, display_elem, none_item] of zip3(equipment_fields, build_fields, none_items)) {
@ -1060,7 +1036,7 @@ function builder_graph_init() {
item_nodes[3].link_to(powder_nodes[3], 'powdering'); item_nodes[3].link_to(powder_nodes[3], 'powdering');
item_nodes[8].link_to(powder_nodes[4], 'powdering'); item_nodes[8].link_to(powder_nodes[4], 'powdering');
// Phase 2/2: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage // Phase 2/3: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage
let build_disp_node = new BuildDisplayNode() let build_disp_node = new BuildDisplayNode()
build_disp_node.link_to(build_node, 'build'); build_disp_node.link_to(build_node, 'build');
@ -1092,8 +1068,18 @@ function builder_graph_init() {
} }
stat_agg_node.link_to(edit_agg_node); stat_agg_node.link_to(edit_agg_node);
build_disp_node.link_to(stat_agg_node, 'stats'); build_disp_node.link_to(stat_agg_node, 'stats');
atree_node.link_to(build_node, 'build');
// Phase 3/3: Set up atree stuff.
// These two are defined in `atree.js`
atree_node.link_to(build_node, 'build');
atree_merge.link_to(build_node, 'build');
atree_graph_creator = new AbilityTreeEnsureNodesNode(build_node, stat_agg_node)
.link_to(atree_collect_spells, 'spells');
// ---------------------------------------------------------------
// Trigger the update cascade for build!
// ---------------------------------------------------------------
for (const input_node of item_nodes.concat(powder_nodes)) { for (const input_node of item_nodes.concat(powder_nodes)) {
input_node.update(); input_node.update();
} }
@ -1112,23 +1098,6 @@ function builder_graph_init() {
// Also do something similar for skill points // Also do something similar for skill points
//for (let i = 0; i < 4; ++i) { TODO: testing code
for (let i = 0; i < 1; ++i) {
let spell_node = new SpellSelectNode(i);
spell_node.link_to(build_node, 'build');
// 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(build_node, 'build').link_to(stat_agg_node, 'stats')
.link_to(spell_node, 'spell-info');
spelldmg_nodes.push(calc_node);
let display_node = new SpellDisplayNode(i);
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');
}
for (const node of edit_input_nodes) { for (const node of edit_input_nodes) {
node.update(); node.update();
} }

View file

@ -112,7 +112,7 @@ class ComputeNode {
const idx = this.inputs.indexOf(parent_node); // Get idx const idx = this.inputs.indexOf(parent_node); // Get idx
this.inputs.splice(idx, 1); // remove element this.inputs.splice(idx, 1); // remove element
this.input_translations.delete(parent_node.name); this.input_translation.delete(parent_node.name);
const was_dirty = this.inputs_dirty.get(parent_node.name); const was_dirty = this.inputs_dirty.get(parent_node.name);
this.inputs_dirty.delete(parent_node.name); this.inputs_dirty.delete(parent_node.name);
if (was_dirty) { if (was_dirty) {
@ -173,3 +173,42 @@ class InputNode extends ComputeNode {
return this.input_field.value; return this.input_field.value;
} }
} }
/**
* Passthrough node for simple aggregation.
* Unfortunately if you use this too much you get layers and layers of maps...
*
* Signature: PassThroughNode(**kwargs) => Map[...]
*/
class PassThroughNode extends ComputeNode {
constructor(name) {
super(name);
this.breakout_nodes = new Map();
}
compute_func(input_map) {
return input_map;
}
/**
* Get a ComputeNode that will "break out" one part of this aggregation input.
* There is some overhead to this operation because ComputeNode is not exactly a free abstraction... oof
* Also you will recv updates whenever any input that is part of the aggregation changes even
* if the specific sub-input didn't change.
*
* Parameters:
* sub-input: The key to listen to
*/
get_node(sub_input) {
if (this.breakout_nodes.has(sub_input)) {
return this.breakout_nodes.get(sub_input);
}
const _name = this.name;
const ret = new (class extends ComputeNode {
constructor() { super('passthrough-'+_name+'-'+sub_input); }
compute_func(input_map) { return input_map.get(_name).get(sub_input); }
})().link_to(this);
this.breakout_nodes.set(sub_input, ret);
return ret;
}
}

View file

@ -61,7 +61,7 @@ function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, igno
if (conversions[i] > 0) { if (conversions[i] > 0) {
const conv_frac = conversions[i]/100; const conv_frac = conversions[i]/100;
damages[i][0] += conv_frac * weapon_min; damages[i][0] += conv_frac * weapon_min;
damages[i][1] += conf_frac * weapon_max; damages[i][1] += conv_frac * weapon_max;
present[i] = true; present[i] = true;
total_convert += conv_frac total_convert += conv_frac
} }
@ -240,6 +240,7 @@ spell_heal: {
const default_spells = { const default_spells = {
wand: [{ wand: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Wand Melee", // TODO: name for melee attacks? name: "Wand Melee", // TODO: name for melee attacks?
display_text: "Mage basic attack", display_text: "Mage basic attack",
base_spell: 0, base_spell: 0,
@ -247,7 +248,7 @@ const default_spells = {
display: "Melee", display: "Melee",
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}, { }, {
name: "Heal", // TODO: name for melee attacks? name: "Heal", // TODO: name for melee attacks? // JUST FOR TESTING...
display_text: "Heal spell!", display_text: "Heal spell!",
base_spell: 1, base_spell: 1,
display: "Total Heal", display: "Total Heal",
@ -258,6 +259,7 @@ const default_spells = {
] ]
}], }],
spear: [{ spear: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Melee", // TODO: name for melee attacks? name: "Melee", // TODO: name for melee attacks?
display_text: "Warrior basic attack", display_text: "Warrior basic attack",
base_spell: 0, base_spell: 0,
@ -266,6 +268,7 @@ const default_spells = {
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}], }],
bow: [{ bow: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Bow Shot", // TODO: name for melee attacks? name: "Bow Shot", // TODO: name for melee attacks?
display_text: "Archer basic attack", display_text: "Archer basic attack",
base_spell: 0, base_spell: 0,
@ -274,6 +277,7 @@ const default_spells = {
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}], }],
dagger: [{ dagger: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Melee", // TODO: name for melee attacks? name: "Melee", // TODO: name for melee attacks?
display_text: "Assassin basic attack", display_text: "Assassin basic attack",
base_spell: 0, base_spell: 0,
@ -282,6 +286,7 @@ const default_spells = {
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }] parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}], }],
relik: [{ relik: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Relik Melee", // TODO: name for melee attacks? name: "Relik Melee", // TODO: name for melee attacks?
display_text: "Shaman basic attack", display_text: "Shaman basic attack",
base_spell: 0, base_spell: 0,

View file

@ -33,9 +33,8 @@ function displaySetBonuses(parent_id,build) {
set_summary_elem.append(set_elem); set_summary_elem.append(set_elem);
const bonus = active_set.bonuses[count-1]; const bonus = active_set.bonuses[count-1];
let mock_item = new Map(); let mock_item = new Map([["fixID", true],
mock_item.set("fixID", true); ["displayName", setName+" Set: "+count+"/"+sets.get(setName).items.length]]);
mock_item.set("displayName", setName+" Set: "+count+"/"+sets.get(setName).items.length);
let mock_minRolls = new Map(); let mock_minRolls = new Map();
let mock_maxRolls = new Map(); let mock_maxRolls = new Map();
mock_item.set("minRolls", mock_minRolls); mock_item.set("minRolls", mock_minRolls);
@ -1216,7 +1215,7 @@ function displayMeleeDamage(parent_elem, overallparent_elem, meleeStats) {
critStats.append(critChance); critStats.append(critChance);
parent_elem.append(critStats); parent_elem.append(critStats);
addClickableArrow(overallparent_elem); addClickableArrow(overallparent_elem, parent_elem);
} }
function displayDefenseStats(parent_elem, statMap, insertSummary){ function displayDefenseStats(parent_elem, statMap, insertSummary){
@ -1567,15 +1566,15 @@ function displayPowderSpecials(parent_elem, powderSpecials, stats, weapon, overa
} }
} }
function getSpellCost(stats, spellIdx, cost) { function getSpellCost(stats, spell) {
return Math.max(1, getBaseSpellCost(stats, spellIdx, cost)); return Math.max(1, getBaseSpellCost(stats, spell));
} }
function getBaseSpellCost(stats, spellIdx, cost) { function getBaseSpellCost(stats, spell) {
// old intelligence: // old intelligence:
cost = Math.ceil(cost * (1 - skillPointsToPercentage(stats.get('int')))); let cost = spell.cost; //Math.ceil(spell.cost * (1 - skillPointsToPercentage(stats.get('int'))));
cost += stats.get("spRaw"+spellIdx); cost += stats.get("spRaw"+spell.base_spell);
return Math.floor(cost * (1 + stats.get("spPct"+spellIdx) / 100)); return Math.floor(cost * (1 + stats.get("spPct"+spell.base_spell) / 100));
} }
@ -1591,12 +1590,12 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell
if ('cost' in spell) { if ('cost' in spell) {
let first = document.createElement("span"); let first = document.createElement("span");
first.textContent = spell.title + " ("; first.textContent = spell.name + " (";
title_elem.appendChild(first.cloneNode(true)); //cloneNode is needed here. title_elem.appendChild(first.cloneNode(true)); //cloneNode is needed here.
title_elemavg.appendChild(first); title_elemavg.appendChild(first);
let second = document.createElement("span"); let second = document.createElement("span");
second.textContent = getSpellCost(stats, spellIdx, spell.cost); second.textContent = getSpellCost(stats, spell);
second.classList.add("Mana"); second.classList.add("Mana");
title_elem.appendChild(second.cloneNode(true)); title_elem.appendChild(second.cloneNode(true));
@ -1618,9 +1617,10 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell
parent_elem.append(title_elem); parent_elem.append(title_elem);
overallparent_elem.append(title_elemavg); overallparent_elem.append(title_elemavg);
if ('cost' in spell) { // if ('cost' in spell) {
overallparent_elem.append(displayNextCosts(stats, spell, spellIdx)); // :( ...... ?
} // overallparent_elem.append(displayNextCosts(stats, spell, spellIdx));
// }
let critChance = skillPointsToPercentage(stats.get('dex')); let critChance = skillPointsToPercentage(stats.get('dex'));
@ -1695,7 +1695,7 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell
} }
} }
addClickableArrow(overallparent_elem); addClickableArrow(overallparent_elem, parent_elem);
} }
/** Displays the ID costs of an item /** Displays the ID costs of an item
@ -2108,11 +2108,23 @@ function stringCDF(id,val,base,amp) {
document.getElementById(id + "-cdf").appendChild(b3); document.getElementById(id + "-cdf").appendChild(b3);
} }
function addClickableArrow(elem) { function addClickableArrow(elem, target) {
//up and down arrow - done ugly //up and down arrow - done ugly
let arrow = document.createElement("img"); let arrow = document.createElement("img");
arrow.id = "arrow_" + elem.id; arrow.id = "arrow_" + elem.id;
arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem"; arrow.style.maxWidth = document.body.clientWidth > 900 ? "3rem" : "10rem";
arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png"; arrow.src = "../media/icons/" + (newIcons ? "new" : "old") + "/toggle_down.png";
elem.appendChild(arrow); elem.appendChild(arrow);
arrow.addEventListener("click", () => toggle_spell_tab(arrow, target));
}
// toggle arrow thinger
function toggle_spell_tab(arrow_img, target) {
if (target.style.display == "none") {
target.style.display = "";
arrow_img.src = arrow_img.src.replace("down", "up");
} else {
target.style.display = "none";
arrow_img.src = arrow_img.src.replace("up", "down");
}
} }

View file

@ -499,3 +499,17 @@ function assert_error(func_binding, msg) {
} }
throw new Error(msg ? msg : "Function didn't throw an error."); throw new Error(msg ? msg : "Function didn't throw an error.");
} }
/**
* Deep copy object/array of basic types.
*/
function deepcopy(obj) {
if (typeof(obj) !== 'object' || obj === null) { // null or value type
return obj;
}
let ret = Array.isArray(obj) ? [] : {};
for (let key in obj) {
ret[key] = deepcopy(obj[key]);
}
return ret;
}

View file

@ -14,16 +14,46 @@ with open("atree_constants.json") as f:
atree_data = json.loads(f.read()) atree_data = json.loads(f.read())
for _class, info in atree_data.items(): for _class, info in atree_data.items():
def translate(path, ref):
ref_dict = info
for x in path:
ref_dict = ref_dict[x]
ref_dict[ref] = id_data[_class][ref_dict[ref]]
for abil in range(len(info)): for abil in range(len(info)):
info[abil]["id"] = id_data[_class][info[abil]["display_name"]] info[abil]["id"] = id_data[_class][info[abil]["display_name"]]
for ref in range(len(info[abil]["parents"])): for ref in range(len(info[abil]["parents"])):
info[abil]["parents"][ref] = id_data[_class][info[abil]["parents"][ref]] translate([abil, "parents"], ref)
for ref in range(len(info[abil]["dependencies"])): for ref in range(len(info[abil]["dependencies"])):
info[abil]["dependencies"][ref] = id_data[_class][info[abil]["dependencies"][ref]] translate([abil, "dependencies"], ref)
for ref in range(len(info[abil]["blockers"])): for ref in range(len(info[abil]["blockers"])):
info[abil]["blockers"][ref] = id_data[_class][info[abil]["blockers"][ref]] translate([abil, "blockers"], ref)
if "base_abil" in info[abil]:
base_abil_name = info[abil]["base_abil"]
if base_abil_name in id_data[_class]:
translate([abil], "base_abil")
if "effects" not in info[abil]:
print(info[abil])
info[abil]["effects"] = []
for effect in info[abil]["effects"]:
if effect["type"] == "raw_stat":
for bonus in effect["bonuses"]:
if "abil" in bonus:
bonus["abil"] = id_data[_class][bonus["abil"]]
elif effect["type"] == "stat_scaling":
if "inputs" in effect: # Might not exist for sliders
for _input in effect["inputs"]:
if "abil" in _input:
_input["abil"] = id_data[_class][_input["abil"]]
if "abil" in effect["output"]:
effect["output"]["abil"] = id_data[_class][effect["output"]["abil"]]
with open('atree_constants_idfied.json', 'w', encoding='utf-8') as abil_dest: with open('atree_constants_idfied.json', 'w', encoding='utf-8') as abil_dest:
json.dump(atree_data, abil_dest, ensure_ascii=False, indent=4) json.dump(atree_data, abil_dest, ensure_ascii=False, indent=4)