fixed merge conflict
This commit is contained in:
commit
7832c1a1aa
14 changed files with 1729 additions and 1225 deletions
|
@ -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">
|
||||
|
|
|
@ -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
20
credits.txt
Normal 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)
|
271
js/atree.js
271
js/atree.js
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" ]
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
244
js/utils.js
244
js/utils.js
|
@ -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.
|
||||
|
|
|
@ -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"]]
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue