fixed merge conflict

This commit is contained in:
ferricles 2022-07-01 17:19:01 -07:00
commit 7832c1a1aa
14 changed files with 1729 additions and 1225 deletions

View file

@ -429,7 +429,7 @@
</div>
<div class = "col-3 py-2">
<button class = "col-auto button rounded scaled-font fw-bold text-light dark-5" id = "toggle-atree" onclick = "toggle_tab('atree-dropdown'); toggleButton('toggle-atree')">
Show Ability Tree
Edit Abilities
</button>
</div>
<div class = "col-3 py-2">
@ -618,10 +618,14 @@
</div>
<div class = "col dark-6 rounded-bottom my-3 my-xl-1" id = "atree-dropdown" style = "display:none;">
<div class="row row-cols-1 row-cols-xl-2">
<div class="col border border-semi-light rounded dark-9 hide-scroll" id="atree-ui" style="height: 50vh; overflow-y: auto;">
<div class="col border border-semi-light rounded dark-9 hide-scroll" id="atree-ui" style="height: 90vh; overflow-y: auto;">
</div>
<div class="col mx-auto" style="height: 50vh; overflow-y: auto;" id="atree-active">
<div class="col mx-auto" style="height: 90vh; overflow-y: auto;" id="atree-rhs">
<div class="col mx-auto" style="height: 2em; overflow-y: auto;" id="atree-header">
</div>
<div class="col mx-auto" style="overflow-y: auto;" id="atree-active">
</div>
</div>
</div>
</div>
@ -1332,7 +1336,7 @@
<div class="col-12 dark-5 scaled-font">
<footer class="text-center">
<div id="header2">
<p>Made by <b class = "hppeng">hppeng</b> and <b class = "ferricles">ferricles</b> with <a href = "../atlas" target = "_blank" class = "atlas link">Atlas Inc</a> (JavaScript required to function, nothing works without js)</p>
<p>Made by <b class = "hppeng">hppeng</b>, <b class = "ferricles">ferricles</b>, and <b>reschan</b> with <a href = "../atlas" target = "_blank" class = "atlas link">Atlas Inc</a> (JavaScript required to function, nothing works without js)</p>
<p>Hard refresh the page (Ctrl+Shift+R on windows/chrome) if it isn't updating correctly.</p>
</div>
<div id="credits">

View file

@ -282,7 +282,7 @@
<div class="col dark-5 scaled-font">
<footer class="text-center">
<div id="header2">
<p>Made by <b class = "hppeng">hppeng</b> and <b class = "ferricles">ferricles</b> with <a href = "../atlas" target = "_blank" class = "atlas link">Atlas Inc</a> (JavaScript required to function, nothing works without js)</p>
<p>Made by <b class = "hppeng">hppeng</b>, <b class = "ferricles">ferricles</b>, and <b>reschan</b> with <a href = "../atlas" target = "_blank" class = "atlas link">Atlas Inc</a> (JavaScript required to function, nothing works without js)</p>
<p>Hard refresh the page (Ctrl+Shift+R on windows/chrome) if it isn't updating correctly.</p>
</div>
<div id="credits">

20
credits.txt Normal file
View file

@ -0,0 +1,20 @@
Theme, formatting, and overall inspiration: Wynndata (Dukio)
- https://wynndata.tk
The game, of course
- wynncraft.com
Additional Contributors, in no particular order:
- Kiocifer (Icons!)
- IncinerateMe (helping transition to 1.20.3 / CI helper)
- puppy (wynn2 ability tree help)
- SockMower (ability tree encode/decode optimization)
- ITechnically (coding emotional support / misc)
- touhoku (best IM)
- HeyZeer0 (huge help in getting our damage formulas right)
- Lennon (Skill point formula reversing)
- Phanta (WynnAtlas custom expression parser / item search)
- nbcss (Crafted Item mechanics reverse engineering)
- dr_carlos (Hiding UI elements properly, fade animations, proper error handling)
- Atlas Inc discord (feedback, ideas, damage calc, etc)

View file

@ -1,5 +1,3 @@
let abil_points_current;
/**
ATreeNode spec:
@ -49,14 +47,17 @@ add_spell_prop: {
}
convert_spell_conv: {
"type": "convert_spell_conv",
"base_spell": int
"target_part": "all" | str,
"conversion": element_str
type: "convert_spell_conv"
base_spell: int // spell identifier
target_part: "all" | str // Part of the spell to modify. Can be not present/empty for ex. cost modifier.
// "all" means modify all parts.
conversion: element_str
}
raw_stat: {
"type": "raw_stat",
"bonuses": list[stat_bonus]
type: "raw_stat"
toggle: Optional[bool | str] // default: false; true means create anon. toggle,
// string value means bind to (or create) named button
bonuses: List[stat_bonus]
}
stat_bonus: {
"type": "stat" | "prop",
@ -70,7 +71,7 @@ stat_scaling: {
"slider_name": Optional[str],
"slider_step": Optional[float],
"inputs": Optional[list[scaling_target]],
"output": scaling_target,
"output": scaling_target | List[scaling_target],
"scaling": list[float],
"max": float
}
@ -124,16 +125,16 @@ const default_abils = {
/**
* Update ability tree internal representation. (topologically sorted node list)
*
* Signature: AbilityTreeUpdateNode(build: Build) => ATree (List of atree nodes in topological order)
* Signature: AbilityTreeUpdateNode(player-class: str) => ATree (List of atree nodes in topological order)
*/
const atree_node = new (class extends ComputeNode {
constructor() { super('builder-atree-update'); }
compute_func(input_map) {
if (input_map.size !== 1) { throw "AbilityTreeUpdateNode accepts exactly one input (build)"; }
const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
if (input_map.size !== 1) { throw "AbilityTreeUpdateNode accepts exactly one input (player-class)"; }
const [player_class] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
const atree_raw = atrees[wep_to_class.get(build.weapon.statMap.get('type'))];
const atree_raw = atrees[player_class];
if (!atree_raw) return null;
let atree_map = new Map();
@ -169,15 +170,23 @@ const atree_node = new (class extends ComputeNode {
* Signature: AbilityTreeRenderNode(atree: ATree) => RenderedATree ( Map[id, RenderedATNode] )
*/
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;
this.UI_elem = document.getElementById("atree-ui");
this.list_elem = document.getElementById("atree-header");
}
compute_func(input_map) {
if (input_map.size !== 1) { throw "AbilityTreeRenderNode accepts exactly one input (atree)"; }
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
this.list_elem.innerHTML = ""; //reset all atree actives - should be done in a more general way later
this.UI_elem.innerHTML = ""; //reset the atree in the DOM
let ret = null;
if (atree) { ret = render_AT(document.getElementById("atree-ui"), document.getElementById("atree-active"), atree); }
if (atree) { ret = render_AT(this.UI_elem, this.list_elem, atree); }
//Toggle on, previously was toggled off
toggle_tab('atree-dropdown'); toggleButton('toggle-atree');
@ -186,6 +195,17 @@ const atree_render = new (class extends ComputeNode {
}
})().link_to(atree_node);
// This exists so i don't have to re-render the UI to push atree updates.
const atree_state_node = new (class extends ComputeNode {
constructor() { super('builder-atree-state'); }
compute_func(input_map) {
if (input_map.size !== 1) { throw "AbilityTreeStateNode accepts exactly one input (atree-rendered)"; }
const [rendered_atree] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
return rendered_atree;
}
})().link_to(atree_render, 'atree-render');
/**
* Create a reverse topological sort of the tree in the result list.
*
@ -216,7 +236,7 @@ function topological_sort_tree(tree, res, mark_state) {
* 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]
* Signature: AbilityTreeMergeNode(build: Build, atree: ATree, atree-state: RenderedATree) => Map[id, Ability]
*/
const atree_merge = new (class extends ComputeNode {
constructor() { super('builder-atree-merge'); }
@ -267,7 +287,146 @@ const atree_merge = new (class extends ComputeNode {
}
return abils_merged;
}
})().link_to(atree_node, 'atree').link_to(atree_render, 'atree-state'); // TODO: THIS IS WRONG!!!!! Need one "collect" node...
})().link_to(atree_node, 'atree').link_to(atree_state_node, 'atree-state'); // TODO: THIS IS WRONG!!!!! Need one "collect" node...
/**
* Validate ability tree.
* Return list of errors for rendering.
*
* Signature: AbilityTreeMergeNode(atree: ATree, atree-state: RenderedATree) => List[str]
*/
const atree_validate = new (class extends ComputeNode {
constructor() { super('atree-validator'); }
compute_func(input_map) {
const atree_state = input_map.get('atree-state');
const atree_order = input_map.get('atree');
let errors = [];
let reachable = new Map();
atree_dfs_mark(atree_order[0], atree_state, reachable);
let abil_points_total = 0;
let archetype_count = new Map();
for (const node of atree_order) {
const abil = node.ability;
if (!atree_state.get(abil.id).active) { continue; }
abil_points_total += abil.cost;
if (!reachable.get(abil.id)) { errors.push(abil.display_name + ' is not reachable!'); }
let failed_deps = [];
for (const dep_id of abil.dependencies) {
if (!atree_state.get(dep_id).active) { failed_deps.push(dep_id) }
}
if (failed_deps.length > 0) {
const dep_string = failed_deps.map(i => '"' + atree_state.get(i).ability.display_name + '"');
errors.push(abil.display_name + ' dependencies not satisfied: ' + dep_string.join(", "));
}
let blocking_ids = [];
for (const blocker_id of abil.blockers) {
if (atree_state.get(blocker_id).active) { blocking_ids.push(blocker_id); }
}
if (blocking_ids.length > 0) {
const blockers_string = blocking_ids.map(i => '"' + atree_state.get(i).ability.display_name + '"');
errors.push(abil.display_name+' is blocked by: '+blockers_string.join(", "));
}
if ('archetype' in abil && abil.archetype !== "") {
let val = 1;
if (archetype_count.has(abil.archetype)) {
val = archetype_count.get(abil.archetype) + 1;
}
archetype_count.set(abil.archetype, val);
}
}
// TODO: FIX THIS! ARCHETYPE REQ IS A PAIN IN THE ASS
// it doesn't follow topological order and theres some cases where "equip order" matters.
for (const node of atree_order) {
const abil = node.ability;
if (!atree_state.get(abil.id).active) { continue; }
if ('archetype_req' in abil && abil.archetype_req !== 0) {
const others = archetype_count.get(abil.archetype) - 1;
if (others < abil.archetype_req) {
errors.push(abil.display_name+' fails archetype: '+abil.archetype+': '+others+' < '+abil.archetype_req)
}
}
}
if (abil_points_total > 45) {
errors.push('too many ability points assigned! ('+abil_points_total+' > 45)');
}
return [abil_points_total, errors];
}
})().link_to(atree_node, 'atree').link_to(atree_state_node, 'atree-state');
function atree_dfs_mark(start, atree_state, mark) {
if (mark.get(start.ability.id)) { return; }
mark.set(start.ability.id, true);
for (const child of start.children) {
if (atree_state.get(child.ability.id).active) {
atree_dfs_mark(child, atree_state, mark);
}
}
}
const atree_render_active = new (class extends ComputeNode {
constructor() {
super('atree-render-active');
this.list_elem = document.getElementById("atree-active");
}
compute_func(input_map) {
const merged_abils = input_map.get('atree-merged');
const atree_order = input_map.get('atree-order');
const [abil_points_total, errors] = input_map.get('atree-errors');
this.list_elem.innerHTML = ""; //reset all atree actives - should be done in a more general way later
// TODO: move to display?
document.getElementById("active_AP_cost").textContent = abil_points_total;
if (errors.length > 0) {
let errorbox = document.createElement('div');
errorbox.classList.add("rounded-bottom", "dark-4", "border", "p-0", "mx-2", "my-4", "dark-shadow");
this.list_elem.appendChild(errorbox);
let error_title = document.createElement('b');
error_title.classList.add("warning", "scaled-font");
error_title.innerHTML = "ATree Error!";
errorbox.appendChild(error_title);
for (const error of errors) {
let atree_warning = document.createElement("p");
atree_warning.classList.add("warning", "small-text");
atree_warning.textContent = error;
errorbox.appendChild(atree_warning);
}
}
for (const node of atree_order) {
if (!merged_abils.has(node.ability.id)) {
continue;
}
const abil = merged_abils.get(node.ability.id);
let active_tooltip = document.createElement('div');
active_tooltip.classList.add("rounded-bottom", "dark-4", "border", "p-0", "mx-2", "my-4", "dark-shadow");
let active_tooltip_title = document.createElement('b');
active_tooltip_title.classList.add("scaled-font");
active_tooltip_title.innerHTML = abil.display_name;
active_tooltip.appendChild(active_tooltip_title);
for (const desc of abil.desc) {
let active_tooltip_desc = document.createElement('p');
active_tooltip_desc.classList.add("scaled-font-sm", "my-0", "mx-1", "text-wrap");
active_tooltip_desc.textContent = desc;
active_tooltip.appendChild(active_tooltip_desc);
}
this.list_elem.appendChild(active_tooltip);
}
}
})().link_to(atree_node, 'atree-order').link_to(atree_merge, 'atree-merged').link_to(atree_validate, 'atree-errors');
/**
* Collect spells from abilities.
@ -284,17 +443,26 @@ const atree_collect_spells = new (class extends ComputeNode {
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; }
if (abil.effects.length == 0) { continue; }
let ret_spell = deepcopy(abil.effects[0]); // NOTE: do not mutate results of previous steps!
const base_spell_id = ret_spell.base_spell;
let has_spell_def = false;
for (const effect of abil.effects) {
switch (effect.type) {
case 'replace_spell':
if (effect.type === 'replace_spell') {
has_spell_def = true;
// replace_spell just replaces all (defined) aspects.
for (const key in effect) {
ret_spell[key] = effect[key];
}
}
}
if (!has_spell_def) { continue; }
const base_spell_id = ret_spell.base_spell;
for (const effect of abil.effects) {
switch (effect.type) {
case 'replace_spell':
// Already handled above.
continue;
case 'add_spell_prop': {
const { base_spell, target_part = null, cost = 0} = effect;
@ -308,9 +476,6 @@ const atree_collect_spells = new (class extends ComputeNode {
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;
@ -321,7 +486,8 @@ const atree_collect_spells = new (class extends ComputeNode {
}
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;
if (idx in part.hits) { part.hits[idx] += v; }
else { part.hits[idx] = v; }
}
}
else {
@ -336,6 +502,9 @@ const atree_collect_spells = new (class extends ComputeNode {
spell_part.name = target_part; // has some extra fields but whatever
ret_spell.parts.push(spell_part);
}
if ('display' in effect) {
ret_spell.display = effect.display;
}
continue;
}
case 'convert_spell_conv':
@ -404,7 +573,7 @@ class AbilityTreeEnsureNodesNode extends ComputeNode {
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()) {
for (const [spell_id, spell] of new Map([...spell_map].sort((a, b) => a[0] - b[0])).entries()) {
let spell_node = new SpellSelectNode(spell);
spell_node.link_to(build_node, 'build');
@ -443,13 +612,9 @@ class AbilityTreeEnsureNodesNode extends ComputeNode {
*/
function render_AT(UI_elem, list_elem, tree) {
console.log("constructing ability tree UI");
list_elem.innerHTML = ""; //reset all atree actives - should be done in a more general way later
UI_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");
abil_points_current = 0;
let active_word = document.createElement("div");
active_word.classList.add("col-auto");
active_word.textContent = "Active Abilities:";
@ -596,6 +761,13 @@ function render_AT(UI_elem, list_elem, tree) {
tooltip_title.classList.add("row", "justify-content-center", "fw-bold");
tooltip_title.textContent = ability.display_name;
let tooltip_archetype;
if ('archetype' in ability && ability.archetype !== "") {
tooltip_archetype = document.createElement('div');
tooltip_archetype.classList.add("row", "mx-1", "text-start");
tooltip_archetype.textContent = "(Archetype: " + ability.archetype+")";
}
let tooltip_desc = document.createElement('div');
tooltip_desc.classList.add("row", "mx-1", "text-wrap");
tooltip_desc.textContent = ability.desc;
@ -608,8 +780,8 @@ function render_AT(UI_elem, list_elem, tree) {
let node_tooltip = document.createElement('div');
node_tooltip.classList.add("rounded-bottom", "dark-4", "border", "p-0", "my-1", "dark-shadow", "scaled-font", "container");
node_tooltip.style.display = "none";
node_tooltip.appendChild(tooltip_title);
node_tooltip.appendChild(tooltip_desc);
node_tooltip.append(tooltip_title, tooltip_archetype ? tooltip_archetype : "", tooltip_desc);
//active = copy of node
let active_tooltip = node_tooltip.cloneNode(true);
@ -641,25 +813,15 @@ function render_AT(UI_elem, list_elem, tree) {
//append node and active tooltips to corresponding parent elems
node_elem.appendChild(node_tooltip);
list_elem.appendChild(active_tooltip);
//list_elem.appendChild(active_tooltip); NOTE: moved to `atree_render_active`
node_wrap.elem = node_elem;
node_wrap.all_connectors_ref = atree_connectors_map;
node_elem.addEventListener('click', function(e) {
if (e.target !== this && e.target!== this.children[0]) {return;}
let tooltip = document.getElementById("atree-ab-" + ability.id);
if (tooltip.style.display === "block") {
tooltip.style.display = "none";
this.classList.remove("atree-selected");
abil_points_current -= ability.cost;
}
else {
tooltip.style.display = "block";
this.classList.add("atree-selected");
abil_points_current += ability.cost;
};
document.getElementById("active_AP_cost").textContent = abil_points_current;
atree_toggle_state(atree_connectors_map, node_wrap);
atree_merge.mark_dirty();
atree_merge.update();
atree_set_state(node_wrap, !node_wrap.active);
atree_state_node.mark_dirty().update();
});
// add tooltip
@ -748,9 +910,16 @@ function atree_render_connection(atree_connectors_map) {
};
// toggle the state of a node.
function atree_toggle_state(atree_connectors_map, node_wrapper) {
const new_state = !node_wrapper.active;
node_wrapper.active = new_state
function atree_set_state(node_wrapper, new_state) {
if (new_state) {
node_wrapper.active = true;
node_wrapper.elem.classList.add("atree-selected");
}
else {
node_wrapper.active = false;
node_wrapper.elem.classList.remove("atree-selected");
}
let atree_connectors_map = node_wrapper.all_connectors_ref;
for (const parent of node_wrapper.parents) {
if (parent.active) {
atree_set_edge(atree_connectors_map, parent, node_wrapper, new_state); // self->parent state only changes if parent is on

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -20,6 +20,8 @@ function parsePowdering(powder_info) {
return [powdering, powder_info];
}
let atree_data = null;
/*
* Populate fields based on url, and calculate build.
*/
@ -62,7 +64,7 @@ function decodeBuild(url_tag) {
}
info[1] = info_str.slice(start_idx);
}
else if (version_number <= 6) {
else if (version_number <= 7) {
let info_str = info[1];
let start_idx = 0;
for (let i = 0; i < 9; ++i ) {
@ -101,7 +103,7 @@ function decodeBuild(url_tag) {
let powder_info = info[1].slice(10);
let res = parsePowdering(powder_info);
powdering = res[0];
} else if (version_number <= 6){
} else if (version_number <= 7){
level = Base64.toInt(info[1].slice(10,12));
setValue("level-choice",level);
save_skp = true;
@ -117,7 +119,7 @@ function decodeBuild(url_tag) {
info[1] = res[1];
}
// Tomes.
if (version == 6) {
if (version >= 6) {
//tome values do not appear in anything before v6.
for (let i in tomes) {
let tome_str = info[1].charAt(i);
@ -128,6 +130,14 @@ function decodeBuild(url_tag) {
info[1] = info[1].slice(7);
}
if (version >= 7) {
// ugly af. only works since its the last thing. will be fixed with binary decode
atree_data = new BitVector(info[1]);
}
else {
atree_data = null;
}
for (let i in powder_inputs) {
setValue(powder_inputs[i], powdering[i]);
}
@ -139,12 +149,13 @@ function decodeBuild(url_tag) {
/* Stores the entire build in a string using B64 encoding and adds it to the URL.
*/
function encodeBuild(build, powders, skillpoints) {
function encodeBuild(build, powders, skillpoints, atree, atree_state) {
if (build) {
let build_string;
//V6 encoding - Tomes
//V7 encoding - ATree
build_version = 5;
build_string = "";
tome_string = "";
@ -189,6 +200,12 @@ function encodeBuild(build, powders, skillpoints) {
}
build_string += tome_string;
if (atree_state.get(atree[0].ability.id).active) {
build_version = Math.max(build_version, 7);
const bitvec = encode_atree(atree, atree_state);
build_string += bitvec.toB64();
}
return build_version.toString() + "_" + build_string;
}
}
@ -216,3 +233,58 @@ function shareBuild(build) {
}
}
/**
* Ability tree encode and decode functions
*
* Based on a traversal, basically only uses bits to represent the nodes that are on (and "dark" outgoing edges).
* credit: SockMower
*/
/**
* Return: BitVector
*/
function encode_atree(atree, atree_state) {
let ret_vec = new BitVector(0, 0);
function traverse(head, atree_state, visited, ret) {
for (const child of head.children) {
if (visited.has(child.ability.id)) { continue; }
visited.set(child.ability.id, true);
if (atree_state.get(child.ability.id).active) {
ret.append(1, 1);
traverse(child, atree_state, visited, ret);
}
else {
ret.append(0, 1);
}
}
}
traverse(atree[0], atree_state, new Map(), ret_vec);
return ret_vec;
}
/**
* Return: List of active nodes
*/
function decode_atree(atree, bits) {
let i = 0;
let ret = [];
ret.push(atree[0]);
function traverse(head, visited, ret) {
for (const child of head.children) {
if (visited.has(child.ability.id)) { continue; }
visited.set(child.ability.id, true);
if (bits.read_bit(i)) {
i += 1;
ret.push(child);
traverse(child, visited, ret);
}
else {
i += 1;
}
}
}
traverse(atree[0], new Map(), ret);
return ret;
}

View file

@ -76,7 +76,8 @@ let item_fields = [ "name", "displayName", "lore", "color", "tier", "set", "slot
"aMdPct","aMdRaw","aSdPct","aSdRaw",/*"aDamPct"*/,"aDamRaw","aDamAddMin","aDamAddMax",
"nMdPct","nMdRaw","nSdPct","nSdRaw","nDamPct","nDamRaw","nDamAddMin","nDamAddMax", // neutral which is now an element
/*"mdPct","mdRaw","sdPct","sdRaw",*/"damPct","damRaw","damAddMin","damAddMax", // These are the old ids. Become proportional.
"rMdPct","rMdRaw","rSdPct",/*"rSdRaw",*/"rDamPct","rDamRaw","rDamAddMin","rDamAddMax" // rainbow (the "element" of all minus neutral). rSdRaw is rainraw
"rMdPct","rMdRaw","rSdPct",/*"rSdRaw",*/"rDamPct","rDamRaw","rDamAddMin","rDamAddMax", // rainbow (the "element" of all minus neutral). rSdRaw is rainraw
"critDamPct"
];
// Extra fake IDs (reserved for use in spell damage calculation) : damageMultiplier, defMultiplier, poisonPct, activeMajorIDs
let str_item_fields = [ "name", "displayName", "lore", "color", "tier", "set", "type", "material", "drop", "quest", "restrict", "category", "atkSpd" ]

View file

@ -49,7 +49,10 @@ let boosts_node = new (class extends ComputeNode {
if (key === "vanish") { def_boost += .15 }
}
}
return [damage_boost, def_boost];
let res = new Map();
res.set('damageMultiplier', 1+damage_boost);
res.set('defMultiplier', 1+def_boost);
return res;
}
})().update();
@ -344,6 +347,8 @@ class BuildEncodeNode extends ComputeNode {
compute_func(input_map) {
const build = input_map.get('build');
const atree = input_map.get('atree');
const atree_state = input_map.get('atree-state');
let powders = [
input_map.get('helmet-powder'),
input_map.get('chestplate-powder'),
@ -361,7 +366,7 @@ class BuildEncodeNode extends ComputeNode {
// TODO: grr global state for copy button..
player_build = build;
build_powders = powders;
return encodeBuild(build, powders, skillpoints);
return encodeBuild(build, powders, skillpoints, atree, atree_state);
}
}
@ -430,6 +435,16 @@ class BuildAssembleNode extends ComputeNode {
}
}
class PlayerClassNode extends ValueCheckComputeNode {
constructor(name) { super(name); }
compute_func(input_map) {
if (input_map.size !== 1) { throw "PlayerClassNode 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 wep_to_class.get(build.weapon.statMap.get('type'));
}
}
/**
* Read an input field and parse into a list of powderings.
* Every two characters makes one powder. If parsing fails, NULL is returned.
@ -573,8 +588,17 @@ class SpellDamageCalcNode extends ComputeNode {
type: "heal",
heal_amount: _heal_amount
}
} else if ('hits' in part) {
spell_result = {
}
else {
continue;
}
spell_result.name = part.name;
spell_results.push(spell_result);
spell_result_map.set(part.name, spell_result);
}
for (const part of spell_parts) {
if ('hits' in part) {
let spell_result = {
normal_min: [0, 0, 0, 0, 0, 0],
normal_max: [0, 0, 0, 0, 0, 0],
normal_total: [0, 0],
@ -605,11 +629,11 @@ class SpellDamageCalcNode extends ComputeNode {
spell_result.heal_amount += subpart.heal_amount;
}
}
}
spell_result.name = part.name;
spell_results.push(spell_result);
spell_result_map.set(part.name, spell_result);
}
}
return spell_results;
}
}
@ -749,7 +773,7 @@ class DisplayBuildWarningsNode extends ComputeNode {
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.classList.add("warning", "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);
}
@ -1071,12 +1095,15 @@ function builder_graph_init() {
// Phase 3/3: Set up atree stuff.
let class_node = new PlayerClassNode('builder-class').link_to(build_node);
// These two are defined in `atree.js`
atree_node.link_to(build_node, 'build');
atree_node.link_to(class_node, 'player-class');
atree_merge.link_to(build_node, 'build');
atree_graph_creator = new AbilityTreeEnsureNodesNode(build_node, stat_agg_node)
.link_to(atree_collect_spells, 'spells');
build_encode_node.link_to(atree_node, 'atree').link_to(atree_state_node, 'atree-state');
// ---------------------------------------------------------------
// Trigger the update cascade for build!
// ---------------------------------------------------------------
@ -1085,6 +1112,18 @@ function builder_graph_init() {
}
level_input.update();
// kinda janky, manually set atree and update. Some wasted compute here
if (atree_data !== null && atree_node.value !== null) { // janky check if atree is valid
const atree_state = atree_state_node.value;
if (atree_data.length > 0) {
const active_nodes = decode_atree(atree_node.value, atree_data);
for (const node of active_nodes) {
atree_set_state(atree_state.get(node.ability.id), true);
}
atree_state_node.mark_dirty().update();
}
}
// Powder specials.
let powder_special_calc = new PowderSpecialCalcNode().link_to(powder_special_input, 'powder-specials');
new PowderSpecialDisplayNode().link_to(powder_special_input, 'powder-specials')

View file

@ -14,7 +14,10 @@ class ComputeNode {
this.name = name;
this.update_task = null;
this.fail_cb = false; // Set to true to force updates even if parent failed.
this.dirty = true;
this.dirty = 2; // 3 states:
// 2: dirty
// 1: possibly dirty
// 0: clean
this.inputs_dirty = new Map();
this.inputs_dirty_count = 0;
all_nodes.push(this);
@ -27,15 +30,17 @@ class ComputeNode {
if (this.inputs_dirty_count != 0) {
return;
}
if (!this.dirty) {
if (this.dirty === 0) {
return;
}
if (this.dirty == 2) {
let calc_inputs = new Map();
for (const input of this.inputs) {
calc_inputs.set(this.input_translation.get(input.name), input.value);
}
this.value = this.compute_func(calc_inputs);
this.dirty = false;
}
this.dirty = 0;
for (const child of this.children) {
child.mark_input_clean(this.name, this.value);
}
@ -64,12 +69,12 @@ class ComputeNode {
}
}
mark_dirty() {
if (!this.dirty) {
this.dirty = true;
mark_dirty(dirty_state=2) {
if (this.dirty < dirty_state) {
this.dirty = dirty_state;
for (const child of this.children) {
child.mark_input_dirty(this.name);
child.mark_dirty();
child.mark_dirty(dirty_state);
}
}
return this;
@ -125,6 +130,50 @@ class ComputeNode {
}
}
class ValueCheckComputeNode extends ComputeNode {
constructor(name) { super(name); }
/**
* Request update of this compute node. Pushes updates to children,
* but only if this node's value changed.
*/
update() {
if (this.inputs_dirty_count != 0) {
return;
}
if (this.dirty === 0) {
return;
}
let calc_inputs = new Map();
for (const input of this.inputs) {
calc_inputs.set(this.input_translation.get(input.name), input.value);
}
let val = this.compute_func(calc_inputs);
if (val !== this.value) {
super.mark_dirty(2);
}
else {
console.log("soft update");
}
this.value = val;
this.dirty = 0;
for (const child of this.children) {
child.mark_input_clean(this.name, this.value);
}
return this;
}
/**
* Defaulting to "dusty" state.
*/
mark_dirty(dirty_state="unused") {
return super.mark_dirty(1);
}
}
/**
* Schedule a ComputeNode to be updated.
*

View file

@ -180,9 +180,8 @@ function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, igno
Spell schema:
spell: {
name: str internal string name for the spell. Unique identifier
name: str internal string name for the spell. Unique identifier, also display
cost: Optional[int] ignored for spells that are not id 1-4
display_text: str short description of the spell, ex. Bash, Meteor, Arrow Shield
base_spell: int spell index. 0-4 are reserved (0 is melee, 1-4 is common 4 spells)
spell_type: str [TODO: DEPRECATED/REMOVE] "healing" or "damage"
scaling: Optional[str] [DEFAULT: "spell"] "melee" or "spell"
@ -242,14 +241,12 @@ const default_spells = {
wand: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Wand Melee", // TODO: name for melee attacks?
display_text: "Mage basic attack",
base_spell: 0,
scaling: "melee", use_atkspd: false,
display: "Melee",
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}, {
name: "Heal", // TODO: name for melee attacks? // JUST FOR TESTING...
display_text: "Heal spell!",
base_spell: 1,
display: "Total Heal",
parts: [
@ -261,7 +258,6 @@ const default_spells = {
spear: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Melee", // TODO: name for melee attacks?
display_text: "Warrior basic attack",
base_spell: 0,
scaling: "melee", use_atkspd: false,
display: "Melee",
@ -270,16 +266,14 @@ const default_spells = {
bow: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Bow Shot", // TODO: name for melee attacks?
display_text: "Archer basic attack",
base_spell: 0,
scaling: "melee", use_atkspd: false,
display: "Melee",
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
display: "Single Shot",
parts: [{ name: "Single Shot", multipliers: [100, 0, 0, 0, 0, 0] }]
}],
dagger: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Melee", // TODO: name for melee attacks?
display_text: "Assassin basic attack",
base_spell: 0,
scaling: "melee", use_atkspd: false,
display: "Melee",
@ -288,7 +282,6 @@ const default_spells = {
relik: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Relik Melee", // TODO: name for melee attacks?
display_text: "Shaman basic attack",
base_spell: 0,
spell_type: "damage",
scaling: "melee", use_atkspd: false,

View file

@ -1603,7 +1603,7 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell
let third = document.createElement("span");
third.textContent = ") [Base: " + getBaseSpellCost(stats, spellIdx, spell.cost) + " ]";
third.textContent = ")";// [Base: " + getBaseSpellCost(stats, spellIdx, spell.cost) + " ]";
title_elem.appendChild(third);
let third_summary = document.createElement("span");
third_summary.textContent = ")";
@ -1664,7 +1664,7 @@ function displaySpellDamage(parent_elem, overallparent_elem, stats, spell, spell
if (spell_info.name === spell.display) {
_summary(spell_info.name+ " Average: ", averageDamage, "Damage");
_summary(spell_info.name+ ": ", averageDamage, "Damage");
}
function _damage_display(label_text, average, dmg_min, dmg_max) {

View file

@ -74,6 +74,8 @@ function log(b, n) {
// https://stackoverflow.com/a/27696695
// Modified for fixed precision
// Base64.fromInt(-2147483648); // gives "200000"
// Base64.toInt("200000"); // gives -2147483648
Base64 = (function () {
var digitsStr =
// 0 8 16 24 32 40 48 56 63
@ -125,8 +127,246 @@ Base64 = (function () {
};
})();
// Base64.fromInt(-2147483648); // gives "200000"
// Base64.toInt("200000"); // gives -2147483648
/** A class used to represent an arbitrary length bit vector. Very useful for encoding and decoding.
*
*/
class BitVector {
/** Constructs an arbitrary-length bit vector.
* @class
* @param {String | Number} data - The data to append.
* @param {Number} length - A set length for the data. Ignored if data is a string.
*
* The structure of the Uint32Array should be [[last, ..., first], ..., [last, ..., first], [empty space, last, ..., first]]
*/
constructor(data, length) {
let bit_vec = [];
if (typeof data === "string") {
let int = 0;
let bv_idx = 0;
length = data.length * 6;
for (let i = 0; i < data.length; i++) {
let char = Base64.toInt(data[i]);
let pre_pos = bv_idx % 32;
int |= (char << bv_idx);
bv_idx += 6;
let post_pos = bv_idx % 32;
if (post_pos < pre_pos) { //we have to have filled up the integer
bit_vec.push(int);
int = (char >>> (6 - post_pos));
}
if (i == data.length - 1 && post_pos != 0) {
bit_vec.push(int);
}
}
} else if (typeof data === "number") {
if (typeof length === "undefined")
if (length < 0) {
throw new RangeError("BitVector must have nonnegative length.");
}
//convert to int just in case
data = Math.round(data);
//range of numbers that won't fit in a uint32
if (data > 2**32 - 1 || data < -(2 ** 32 - 1)) {
throw new RangeError("Numerical data has to fit within a 32-bit integer range to instantiate a BitVector.");
}
bit_vec.push(data);
} else {
throw new TypeError("BitVector must be instantiated with a Number or a B64 String");
}
this.length = length;
this.bits = new Uint32Array(bit_vec);
}
/** Return value of bit at index idx.
*
* @param {Number} idx - The index to read
*
* @returns The bit value at position idx
*/
read_bit(idx) {
if (idx < 0 || idx >= this.length) {
throw new RangeError("Cannot read bit outside the range of the BitVector. ("+idx+" > "+this.length+")");
}
return ((this.bits[Math.floor(idx / 32)] & (1 << idx)) == 0 ? 0 : 1);
}
/** Returns an integer value (if possible) made from the range of bits [start, end). Undefined behavior if the range to read is too big.
*
* @param {Number} start - The index to start slicing from. Inclusive.
* @param {Number} end - The index to end slicing at. Exclusive.
*
* @returns An integer representation of the sliced bits.
*/
slice(start, end) {
//TO NOTE: JS shifting is ALWAYS in mod 32. a << b will do a << (b mod 32) implicitly.
if (end < start) {
throw new RangeError("Cannot slice a range where the end is before the start.");
} else if (end == start) {
return 0;
} else if (end - start > 32) {
//requesting a slice of longer than 32 bits (safe integer "length")
throw new RangeError("Cannot slice a range of longer than 32 bits (unsafe to store in an integer).");
}
let res = 0;
if (Math.floor((end - 1) / 32) == Math.floor(start / 32)) {
//the range is within 1 uint32 section - do some relatively fast bit twiddling
res = (this.bits[Math.floor(start / 32)] & ~((((~0) << ((end - 1))) << 1) | ~((~0) << (start)))) >>> (start % 32);
} else {
//the number of bits in the uint32s
let start_pos = (start % 32);
let int_idx = Math.floor(start/32);
res = (this.bits[int_idx] & ((~0) << (start))) >>> (start_pos);
res |= (this.bits[int_idx + 1] & ~((~0) << (end))) << (32 - start_pos);
}
return res;
// General code - slow
// for (let i = start; i < end; i++) {
// res |= (get_bit(i) << (i - start));
// }
}
/** Assign bit at index idx to 1.
*
* @param {Number} idx - The index to set.
*/
set_bit(idx) {
if (idx < 0 || idx >= this.length) {
throw new RangeError("Cannot set bit outside the range of the BitVector.");
}
this.bits[Math.floor(idx / 32)] |= (1 << idx % 32);
}
/** Assign bit at index idx to 0.
*
* @param {Number} idx - The index to clear.
*/
clear_bit(idx) {
if (idx < 0 || idx >= this.length) {
throw new RangeError("Cannot clear bit outside the range of the BitVector.");
}
this.bits[Math.floor(idx / 32)] &= ~(1 << idx % 32);
}
/** Creates a string version of the bit vector in B64. Does not keep the order of elements a sensible human readable format.
*
* @returns A b64 string representation of the BitVector.
*/
toB64() {
if (this.length == 0) {
return "";
}
let b64_str = "";
let i = 0;
while (i < this.length) {
b64_str += Base64.fromIntV(this.slice(i, i + 6), 1);
i += 6;
}
return b64_str;
}
/** Returns a BitVector in bitstring format. Probably only useful for dev debugging.
*
* @returns A bit string representation of the BitVector. Goes from higher-indexed bits to lower-indexed bits. (n ... 0)
*/
toString() {
let ret_str = "";
for (let i = 0; i < this.length; i++) {
ret_str = (this.read_bit(i) == 0 ? "0": "1") + ret_str;
}
return ret_str;
}
/** Returns a BitVector in bitstring format. Probably only useful for dev debugging.
*
* @returns A bit string representation of the BitVector. Goes from lower-indexed bits to higher-indexed bits. (0 ... n)
*/
toStringR() {
let ret_str = "";
for (let i = 0; i < this.length; i++) {
ret_str += (this.read_bit(i) == 0 ? "0": "1");
}
return ret_str;
}
/** Appends data to the BitVector.
*
* @param {Number | String} data - The data to append.
* @param {Number} length - The length, in bits, of the new data. This is ignored if data is a string.
*/
append(data, length) {
if (length < 0) {
throw new RangeError("BitVector length must increase by a nonnegative number.");
}
let bit_vec = [];
for (const uint of this.bits) {
bit_vec.push(uint);
}
if (typeof data === "string") {
let int = bit_vec[bit_vec.length - 1];
let bv_idx = this.length;
length = data.length * 6;
let updated_curr = false;
for (let i = 0; i < data.length; i++) {
let char = Base64.toInt(data[i]);
let pre_pos = bv_idx % 32;
int |= (char << bv_idx);
bv_idx += 6;
let post_pos = bv_idx % 32;
if (post_pos < pre_pos) { //we have to have filled up the integer
if (bit_vec.length == this.bits.length && !updated_curr) {
bit_vec[bit_vec.length - 1] = int;
updated_curr = true;
} else {
bit_vec.push(int);
}
int = (char >>> (6 - post_pos));
}
if (i == data.length - 1) {
if (bit_vec.length == this.bits.length && !updated_curr) {
bit_vec[bit_vec.length - 1] = int;
} else if (post_pos != 0) {
bit_vec.push(int);
}
}
}
} else if (typeof data === "number") {
//convert to int just in case
let int = Math.round(data);
//range of numbers that "could" fit in a uint32 -> [0, 2^32) U [-2^31, 2^31)
if (data > 2**32 - 1 || data < -(2 ** 31)) {
throw new RangeError("Numerical data has to fit within a 32-bit integer range to instantiate a BitVector.");
}
//could be split between multiple new ints
//reminder that shifts implicitly mod 32
bit_vec[bit_vec.length - 1] |= ((int & ~((~0) << length)) << (this.length));
if (((this.length - 1) % 32 + 1) + length > 32) {
bit_vec.push(int >>> (32 - this.length));
}
} else {
throw new TypeError("BitVector must be appended with a Number or a B64 String");
}
this.bits = new Uint32Array(bit_vec);
this.length += length;
}
};
/*
Turns a raw stat and a % stat into a final stat on the basis that - raw and >= 100% becomes 0 and + raw and <=-100% becomes negative.

View file

@ -37,21 +37,26 @@ for _class, info in atree_data.items():
translate([abil], "base_abil")
if "effects" not in info[abil]:
print("WARNING: abil missing 'effects' tag")
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:
if "abil" in bonus and bonus["abil"] in id_data[_class]:
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:
if "abil" in _input and _input["abil"] in id_data[_class]:
_input["abil"] = id_data[_class][_input["abil"]]
if "abil" in effect["output"]:
if isinstance(effect["output"], list):
for output in effect["output"]:
if "abil" in output and output["abil"] in id_data[_class]:
output["abil"] = id_data[_class][output["abil"]]
else:
if "abil" in effect["output"] and effect["output"]["abil"] in id_data[_class]:
effect["output"]["abil"] = id_data[_class][effect["output"]["abil"]]