538202e7ac
remove poison stats box for now now melee stats box is correct with melee modifiers, and also neater in code
1100 lines
42 KiB
JavaScript
1100 lines
42 KiB
JavaScript
let armor_powder_node = new (class extends ComputeNode {
|
|
constructor() { super('builder-armor-powder-input'); }
|
|
|
|
compute_func(input_map) {
|
|
let damage_boost = 0;
|
|
let def_boost = 0;
|
|
let statMap = new Map();
|
|
for (const [e, elem] of zip2(skp_elements, skp_order)) {
|
|
let val = parseInt(document.getElementById(elem+"_boost_armor").value);
|
|
statMap.set(e+'DamPct', val);
|
|
}
|
|
return statMap;
|
|
}
|
|
})();
|
|
|
|
let boosts_node = new (class extends ComputeNode {
|
|
constructor() { super('builder-boost-input'); }
|
|
|
|
compute_func(input_map) {
|
|
let damage_boost = 0;
|
|
let def_boost = 0;
|
|
for (const [key, value] of damageMultipliers) {
|
|
let elem = document.getElementById(key + "-boost")
|
|
if (elem.classList.contains("toggleOn")) {
|
|
damage_boost += value;
|
|
if (key === "warscream") { def_boost += .10 }
|
|
if (key === "vanish") { def_boost += .15 }
|
|
}
|
|
}
|
|
let res = new Map();
|
|
res.set('damMult.Potion', 100*damage_boost);
|
|
res.set('defMult.Potion', 100*def_boost);
|
|
return res;
|
|
}
|
|
})().update();
|
|
|
|
/* Updates all spell boosts
|
|
*/
|
|
function update_boosts(buttonId) {
|
|
let elem = document.getElementById(buttonId);
|
|
if (elem.classList.contains("toggleOn")) {
|
|
elem.classList.remove("toggleOn");
|
|
} else {
|
|
elem.classList.add("toggleOn");
|
|
}
|
|
boosts_node.mark_dirty().update();
|
|
}
|
|
|
|
let specialNames = ["Quake", "Chain Lightning", "Curse", "Courage", "Wind Prison"];
|
|
let powder_special_input = new (class extends ComputeNode {
|
|
constructor() { super('builder-powder-special-input'); }
|
|
|
|
compute_func(input_map) {
|
|
let powder_specials = []; // [ [special, power], [special, power]]
|
|
for (const sName of specialNames) {
|
|
for (let i = 1;i < 6; i++) {
|
|
if (document.getElementById(sName.replace(" ","_") + "-" + i).classList.contains("toggleOn")) {
|
|
let powder_special = powderSpecialStats[specialNames.indexOf(sName.replace("_"," "))];
|
|
powder_specials.push([powder_special, i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return powder_specials;
|
|
}
|
|
})();
|
|
|
|
function updatePowderSpecials(buttonId) {
|
|
let prefix = (buttonId).split("-")[0].replace(' ', '_') + '-';
|
|
let elem = document.getElementById(buttonId);
|
|
if (elem.classList.contains("toggleOn")) { elem.classList.remove("toggleOn"); }
|
|
else {
|
|
for (let i = 1;i < 6; i++) { //toggle all pressed buttons of the same powder special off
|
|
//name is same, power is i
|
|
const elem2 = document.getElementById(prefix + i);
|
|
if(elem2.classList.contains("toggleOn")) { elem2.classList.remove("toggleOn"); }
|
|
}
|
|
//toggle the pressed button on
|
|
elem.classList.add("toggleOn");
|
|
}
|
|
powder_special_input.mark_dirty().update();
|
|
}
|
|
|
|
class PowderSpecialCalcNode extends ComputeNode {
|
|
constructor() { super('builder-powder-special-apply'); }
|
|
|
|
compute_func(input_map) {
|
|
const powder_specials = input_map.get('powder-specials');
|
|
let stats = new Map();
|
|
for (const [special, power] of powder_specials) {
|
|
if (special["weaponSpecialEffects"].has("Damage Boost")) {
|
|
let name = special["weaponSpecialName"];
|
|
if (name === "Courage" || name === "Curse") { //courage and curse are is universal damage boost
|
|
stats.set("sdPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]);
|
|
stats.set("mdPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]);
|
|
stats.set("poisonPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]);
|
|
} else if (name === "Wind Prison") {
|
|
stats.set("aDamPct", special.weaponSpecialEffects.get("Damage Boost")[power-1]);
|
|
}
|
|
}
|
|
}
|
|
return stats;
|
|
}
|
|
}
|
|
|
|
class PowderSpecialDisplayNode extends ComputeNode {
|
|
// TODO: Refactor this entirely to be adding more spells to the spell list
|
|
constructor() {
|
|
super('builder-powder-special-display');
|
|
this.fail_cb = true;
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
const powder_specials = input_map.get('powder-specials');
|
|
const stats = input_map.get('stats');
|
|
const weapon = input_map.get('build').weapon;
|
|
displayPowderSpecials(document.getElementById("powder-special-stats"), powder_specials, stats, weapon.statMap, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Node for getting an item's stats from an item input field.
|
|
*
|
|
* Signature: ItemInputNode(powdering: Optional[list[powder]]) => Item | null
|
|
*/
|
|
class ItemInputNode extends InputNode {
|
|
/**
|
|
* Make an item stat pulling compute node.
|
|
*
|
|
* @param name: Name of this node.
|
|
* @param item_input_field: Input field (html element) to listen for item names from.
|
|
* @param none_item: Item object to use as the "none" for this field.
|
|
*/
|
|
constructor(name, item_input_field, none_item) {
|
|
super(name, item_input_field);
|
|
this.none_item = new Item(none_item);
|
|
this.category = this.none_item.statMap.get('category');
|
|
if (this.category == 'armor' || this.category == 'weapon') {
|
|
this.none_item.statMap.set('powders', []);
|
|
apply_weapon_powders(this.none_item.statMap); // Needed to put in damagecalc zeros
|
|
}
|
|
this.none_item.statMap.set('NONE', true);
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
const powdering = input_map.get('powdering');
|
|
|
|
// built on the assumption of no one will type in CI/CR letter by letter
|
|
let item_text = this.input_field.value;
|
|
if (!item_text) {
|
|
return this.none_item;
|
|
}
|
|
|
|
let item;
|
|
if (item_text.slice(0, 3) == "CI-") { item = getCustomFromHash(item_text); }
|
|
else if (item_text.slice(0, 3) == "CR-") { item = getCraftFromHash(item_text); }
|
|
else if (itemMap.has(item_text)) { item = new Item(itemMap.get(item_text)); }
|
|
else if (tomeMap.has(item_text)) { item = new Item(tomeMap.get(item_text)); }
|
|
|
|
if (item) {
|
|
if (powdering !== undefined) {
|
|
const max_slots = item.statMap.get('slots');
|
|
item.statMap.set('powders', powdering.slice(0, max_slots));
|
|
}
|
|
let type_match;
|
|
if (this.category == 'weapon') {
|
|
type_match = item.statMap.get('category') == 'weapon';
|
|
} else {
|
|
type_match = item.statMap.get('type') == this.none_item.statMap.get('type');
|
|
}
|
|
if (type_match) {
|
|
if (item.statMap.get('category') == 'armor') {
|
|
applyArmorPowders(item.statMap);
|
|
}
|
|
else if (item.statMap.get('category') == 'weapon') {
|
|
apply_weapon_powders(item.statMap);
|
|
}
|
|
return item;
|
|
}
|
|
}
|
|
else if (this.none_item.statMap.get('category') === 'weapon' && item_text.startsWith("Morph-")) {
|
|
let replace_items = [ "Morph-Stardust",
|
|
"Morph-Steel",
|
|
"Morph-Iron",
|
|
"Morph-Gold",
|
|
"Morph-Topaz",
|
|
"Morph-Emerald",
|
|
"Morph-Amethyst",
|
|
"Morph-Ruby",
|
|
item_text.substring(6)
|
|
]
|
|
|
|
for (const [i, x] of zip2(equipment_inputs, replace_items)) { setValue(i, x); }
|
|
|
|
for (const node of item_nodes) {
|
|
if (node !== this) {
|
|
// save a tiny bit of compute
|
|
calcSchedule(node, 10);
|
|
}
|
|
}
|
|
// Needed to push the weapon node's updates forward
|
|
return this.compute_func(input_map);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Node for updating item input fields from parsed items.
|
|
*
|
|
* Signature: ItemInputDisplayNode(item: Item) => null
|
|
*/
|
|
class ItemInputDisplayNode extends ComputeNode {
|
|
|
|
constructor(name, eq, item_image) {
|
|
super(name);
|
|
this.input_field = document.getElementById(eq+"-choice");
|
|
this.health_field = document.getElementById(eq+"-health");
|
|
this.level_field = document.getElementById(eq+"-lv");
|
|
this.powder_field = document.getElementById(eq+"-powder"); // possibly None
|
|
this.image = item_image;
|
|
this.fail_cb = true;
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; }
|
|
const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
|
|
|
|
this.input_field.classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom');
|
|
this.input_field.classList.add("text-light");
|
|
this.image.classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow');
|
|
|
|
if (this.health_field) {
|
|
// Doesn't exist for weapons.
|
|
this.health_field.textContent = "0";
|
|
}
|
|
if (this.level_field) {
|
|
// Doesn't exist for tomes.
|
|
this.level_field.textContent = "0";
|
|
}
|
|
if (!item) {
|
|
this.input_field.classList.add("is-invalid");
|
|
return null;
|
|
}
|
|
if (this.powder_field && item.statMap.has('powders')) {
|
|
this.powder_field.placeholder = "powders";
|
|
}
|
|
|
|
if (item.statMap.has('NONE')) {
|
|
return null;
|
|
}
|
|
|
|
if (this.powder_field && item.statMap.has('powders')) {
|
|
this.powder_field.placeholder = item.statMap.get('slots') + ' slots';
|
|
}
|
|
|
|
const tier = item.statMap.get('tier');
|
|
this.input_field.classList.add(tier);
|
|
if (this.health_field) {
|
|
// Doesn't exist for weapons.
|
|
this.health_field.textContent = item.statMap.get('hp');
|
|
}
|
|
if (this.level_field) {
|
|
// Doesn't exist for tomes.
|
|
this.level_field.textContent = item.statMap.get('lvl');
|
|
}
|
|
this.image.classList.add(tier + "-shadow");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Node for rendering an item.
|
|
*
|
|
* Signature: ItemDisplayNode(item: Item) => null
|
|
*/
|
|
class ItemDisplayNode extends ComputeNode {
|
|
constructor(name, target_elem) {
|
|
super(name);
|
|
this.target_elem = target_elem;
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
if (input_map.size !== 1) { throw "ItemInputDisplayNode accepts exactly one input (item)"; }
|
|
const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
|
|
|
|
displayExpandedItem(item.statMap, this.target_elem);
|
|
collapse_element("#"+this.target_elem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the weapon to match correct type.
|
|
*
|
|
* Signature: WeaponInputDisplayNode(item: Item) => null
|
|
*/
|
|
class WeaponInputDisplayNode extends ComputeNode {
|
|
|
|
constructor(name, image_field, dps_field) {
|
|
super(name);
|
|
this.image = image_field;
|
|
this.dps_field = dps_field;
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
if (input_map.size !== 1) { throw "WeaponDisplayNode accepts exactly one input (item)"; }
|
|
const [item] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
|
|
|
|
const type = item.statMap.get('type');
|
|
this.image.setAttribute('src', '../media/items/new/generic-'+type+'.png');
|
|
let dps = get_base_dps(item.statMap);
|
|
if (isNaN(dps)) {
|
|
dps = dps[1];
|
|
if (isNaN(dps)) dps = 0;
|
|
}
|
|
this.dps_field.textContent = Math.round(dps);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode the build into a url-able string.
|
|
*
|
|
* Signature: BuildEncodeNode(build: Build,
|
|
* helmet-powder: List[powder],
|
|
* chestplate-powder: List[powder],
|
|
* leggings-powder: List[powder],
|
|
* boots-powder: List[powder],
|
|
* weapon-powder: List[powder]) => str
|
|
*/
|
|
class BuildEncodeNode extends ComputeNode {
|
|
constructor() { super("builder-encode"); }
|
|
|
|
compute_func(input_map) {
|
|
const build = input_map.get('build');
|
|
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'),
|
|
input_map.get('leggings-powder'),
|
|
input_map.get('boots-powder'),
|
|
input_map.get('weapon-powder')
|
|
];
|
|
const skillpoints = [
|
|
input_map.get('str'),
|
|
input_map.get('dex'),
|
|
input_map.get('int'),
|
|
input_map.get('def'),
|
|
input_map.get('agi')
|
|
];
|
|
// TODO: grr global state for copy button..
|
|
player_build = build;
|
|
build_powders = powders;
|
|
return encodeBuild(build, powders, skillpoints, atree, atree_state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the window's URL.
|
|
*
|
|
* Signature: URLUpdateNode(build_str: str) => null
|
|
*/
|
|
class URLUpdateNode extends ComputeNode {
|
|
constructor() { super("builder-url-update"); }
|
|
|
|
compute_func(input_map) {
|
|
if (input_map.size !== 1) { throw "URLUpdateNode accepts exactly one input (build_str)"; }
|
|
const [build_str] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
|
|
location.hash = build_str;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a "build" object from a set of equipments.
|
|
* Returns a new Build object, or null if all items are NONE items.
|
|
*
|
|
* Signature: BuildAssembleNode(helmet-input: Item,
|
|
* chestplate-input: Item,
|
|
* leggings-input: Item,
|
|
* boots-input: Item,
|
|
* ring1-input: Item,
|
|
* ring2-input: Item,
|
|
* bracelet-input: Item,
|
|
* necklace-input: Item,
|
|
* weapon-input: Item,
|
|
* level-input: int) => Build | null
|
|
*/
|
|
class BuildAssembleNode extends ComputeNode {
|
|
constructor() { super("builder-make-build"); }
|
|
|
|
compute_func(input_map) {
|
|
let equipments = [
|
|
input_map.get('helmet-input'),
|
|
input_map.get('chestplate-input'),
|
|
input_map.get('leggings-input'),
|
|
input_map.get('boots-input'),
|
|
input_map.get('ring1-input'),
|
|
input_map.get('ring2-input'),
|
|
input_map.get('bracelet-input'),
|
|
input_map.get('necklace-input'),
|
|
input_map.get('weaponTome1-input'),
|
|
input_map.get('weaponTome2-input'),
|
|
input_map.get('armorTome1-input'),
|
|
input_map.get('armorTome2-input'),
|
|
input_map.get('armorTome3-input'),
|
|
input_map.get('armorTome4-input'),
|
|
input_map.get('guildTome1-input')
|
|
];
|
|
let weapon = input_map.get('weapon-input');
|
|
let level = parseInt(input_map.get('level-input'));
|
|
if (isNaN(level)) {
|
|
level = 106;
|
|
}
|
|
|
|
let all_none = weapon.statMap.has('NONE');
|
|
for (const item of equipments) {
|
|
all_none = all_none && item.statMap.has('NONE');
|
|
}
|
|
if (all_none && !location.hash) {
|
|
return null;
|
|
}
|
|
return new Build(level, equipments, weapon);
|
|
}
|
|
}
|
|
|
|
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.
|
|
*
|
|
* Signature: PowderInputNode() => List[powder] | null
|
|
*/
|
|
class PowderInputNode extends InputNode {
|
|
|
|
constructor(name, input_field) { super(name, input_field); }
|
|
|
|
compute_func(input_map) {
|
|
// TODO: haha improve efficiency to O(n) dumb
|
|
// also, error handling is missing
|
|
let input = this.input_field.value.trim();
|
|
let powdering = [];
|
|
let errorederrors = [];
|
|
while (input) {
|
|
let first = input.slice(0, 2);
|
|
let powder = powderIDs.get(first);
|
|
if (powder === undefined) {
|
|
return null;
|
|
} else {
|
|
powdering.push(powder);
|
|
}
|
|
input = input.slice(2);
|
|
}
|
|
return powdering;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select a spell+spell "variation" based on a build / spell idx.
|
|
* Right now this isn't much logic and is only used to abstract away major id interactions
|
|
* but will become significantly more complex in wynn2.
|
|
*
|
|
* Signature: SpellSelectNode<int>(build: Build) => [Spell, SpellParts]
|
|
*/
|
|
class SpellSelectNode extends ComputeNode {
|
|
constructor(spell) {
|
|
super("builder-spell"+spell.base_spell+"-select");
|
|
this.spell = spell;
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
const build = input_map.get('build');
|
|
let stats = build.statMap;
|
|
// TODO: apply major ids... DOOM.....
|
|
|
|
return [this.spell, this.spell.parts];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Get all defensive stats for this build.
|
|
*/
|
|
function getDefenseStats(stats) {
|
|
let defenseStats = [];
|
|
let def_pct = skillPointsToPercentage(stats.get('def')) * skillpoint_final_mult[3];
|
|
let agi_pct = skillPointsToPercentage(stats.get('agi')) * skillpoint_final_mult[4];
|
|
//total hp
|
|
let totalHp = stats.get("hp") + stats.get("hpBonus");
|
|
if (totalHp < 5) totalHp = 5;
|
|
defenseStats.push(totalHp);
|
|
//EHP
|
|
let ehp = [totalHp, totalHp];
|
|
let defMult = (2 - stats.get("classDef"));
|
|
for (const [k, v] of stats.get("defMult").entries()) {
|
|
defMult *= (1 - v/100);
|
|
}
|
|
// newehp = oldehp / [0.1 * A(x) + (1 - A(x)) * (1 - D(x))]
|
|
ehp[0] = ehp[0] / (0.1*agi_pct + (1-agi_pct) * (1-def_pct));
|
|
ehp[0] /= defMult;
|
|
// ehp[0] /= (1-def_pct)*(1-agi_pct)*defMult;
|
|
ehp[1] /= (1-def_pct)*defMult;
|
|
defenseStats.push(ehp);
|
|
//HPR
|
|
let totalHpr = rawToPct(stats.get("hprRaw"), stats.get("hprPct")/100.);
|
|
defenseStats.push(totalHpr);
|
|
//EHPR
|
|
let ehpr = [totalHpr, totalHpr];
|
|
ehpr[0] /= (1-def_pct)*(1-agi_pct)*defMult;
|
|
ehpr[1] /= (1-def_pct)*defMult;
|
|
defenseStats.push(ehpr);
|
|
//skp stats
|
|
defenseStats.push([ def_pct*100, agi_pct*100]);
|
|
//eledefs - TODO POWDERS
|
|
let eledefs = [0, 0, 0, 0, 0];
|
|
for(const i in skp_elements){ //kinda jank but ok
|
|
eledefs[i] = rawToPct(stats.get(skp_elements[i] + "Def"), stats.get(skp_elements[i] + "DefPct")/100.);
|
|
}
|
|
defenseStats.push(eledefs);
|
|
|
|
//[total hp, [ehp w/ agi, ehp w/o agi], total hpr, [ehpr w/ agi, ehpr w/o agi], [def%, agi%], [edef,tdef,wdef,fdef,adef]]
|
|
return defenseStats;
|
|
}
|
|
|
|
/**
|
|
* Compute spell damage of spell parts.
|
|
* Currently kinda janky / TODO while we rework the internal rep. of spells.
|
|
*
|
|
* Signature: SpellDamageCalcNode(weapon-input: Item,
|
|
* stats: StatMap,
|
|
* spell-info: [Spell, SpellParts]) => List[SpellDamage]
|
|
*/
|
|
class SpellDamageCalcNode extends ComputeNode {
|
|
constructor(spell_num) {
|
|
super("builder-spell"+spell_num+"-calc");
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
const weapon = input_map.get('build').weapon.statMap;
|
|
const spell_info = input_map.get('spell-info');
|
|
const spell = spell_info[0];
|
|
const spell_parts = spell_info[1];
|
|
const stats = input_map.get('stats');
|
|
const skillpoints = [
|
|
stats.get('str'),
|
|
stats.get('dex'),
|
|
stats.get('int'),
|
|
stats.get('def'),
|
|
stats.get('agi')
|
|
];
|
|
let spell_results = []
|
|
let spell_result_map = new Map();
|
|
const use_speed = (('use_atkspd' in spell) ? spell.use_atkspd : true);
|
|
const use_spell = (('scaling' in spell) ? spell.scaling === 'spell' : true);
|
|
|
|
// TODO: move preprocessing to separate node/node chain
|
|
for (const part of spell_parts) {
|
|
let spell_result;
|
|
if ('multipliers' in part) { // damage type spell
|
|
let results = calculateSpellDamage(stats, weapon, part.multipliers, use_spell, !use_speed, spell.base_spell + '.' + part.name);
|
|
spell_result = {
|
|
type: "damage",
|
|
normal_min: results[2].map(x => x[0]),
|
|
normal_max: results[2].map(x => x[1]),
|
|
normal_total: results[0],
|
|
crit_min: results[2].map(x => x[2]),
|
|
crit_max: results[2].map(x => x[3]),
|
|
crit_total: results[1],
|
|
}
|
|
} else if ('power' in part) {
|
|
// TODO: wynn2 formula
|
|
let _heal_amount = (part.power * getDefenseStats(stats)[0] * (stats.get('healPct')/100));
|
|
spell_result = {
|
|
type: "heal",
|
|
heal_amount: _heal_amount
|
|
}
|
|
}
|
|
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],
|
|
crit_min: [0, 0, 0, 0, 0, 0],
|
|
crit_max: [0, 0, 0, 0, 0, 0],
|
|
crit_total: [0, 0],
|
|
heal_amount: 0
|
|
}
|
|
const dam_res_keys = ['normal_min', 'normal_max', 'normal_total', 'crit_min', 'crit_max', 'crit_total'];
|
|
for (const [subpart_name, hits] of Object.entries(part.hits)) {
|
|
const subpart = spell_result_map.get(subpart_name);
|
|
if (!subpart) { continue; }
|
|
if (spell_result.type) {
|
|
if (subpart.type !== spell_result.type) {
|
|
throw "SpellCalc total subpart type mismatch";
|
|
}
|
|
}
|
|
else {
|
|
spell_result.type = subpart.type;
|
|
}
|
|
if (spell_result.type === 'damage') {
|
|
for (const key of dam_res_keys) {
|
|
for (let i in spell_result.normal_min) {
|
|
spell_result[key][i] += subpart[key][i] * hits;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Display spell damage from spell parts.
|
|
* Currently kinda janky / TODO while we rework the internal rep. of spells.
|
|
*
|
|
* Signature: SpellDisplayNode(stats: StatMap,
|
|
* spell-info: [Spell, SpellParts],
|
|
* spell-damage: List[SpellDamage]) => null
|
|
*/
|
|
class SpellDisplayNode extends ComputeNode {
|
|
constructor(spell_num) {
|
|
super("builder-spell"+spell_num+"-display");
|
|
this.spell_idx = spell_num;
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
const stats = input_map.get('stats');
|
|
const spell_info = input_map.get('spell-info');
|
|
const damages = input_map.get('spell-damage');
|
|
const spell = spell_info[0];
|
|
|
|
const i = this.spell_idx;
|
|
let parent_elem = document.getElementById("spell"+i+"-info");
|
|
let overallparent_elem = document.getElementById("spell"+i+"-infoAvg");
|
|
displaySpellDamage(parent_elem, overallparent_elem, stats, spell, i, damages);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display build stats.
|
|
*
|
|
* Signature: BuildDisplayNode(build: Build) => null
|
|
*/
|
|
class BuildDisplayNode extends ComputeNode {
|
|
constructor() { super("builder-stats-display"); }
|
|
|
|
compute_func(input_map) {
|
|
const build = input_map.get('build');
|
|
const stats = input_map.get('stats');
|
|
displayBuildStats('overall-stats', build, build_all_display_commands, stats);
|
|
displayBuildStats("offensive-stats", build, build_offensive_display_commands, stats);
|
|
displaySetBonuses("set-info", build);
|
|
// TODO: move weapon out?
|
|
displayDefenseStats(document.getElementById("defensive-stats"), stats);
|
|
|
|
//displayPoisonDamage(document.getElementById("build-poison-stats"), build);
|
|
displayEquipOrder(document.getElementById("build-order"), build.equip_order);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show warnings for skillpoints, level, set bonus for a build
|
|
* Also shosw skill point remaining and other misc. info
|
|
*
|
|
* Signature: DisplayBuildWarningNode(build: Build, str: int, dex: int, int: int, def: int, agi: int) => null
|
|
*/
|
|
class DisplayBuildWarningsNode extends ComputeNode {
|
|
constructor() { super("builder-show-warnings"); }
|
|
|
|
compute_func(input_map) {
|
|
const build = input_map.get('build');
|
|
const min_assigned = build.base_skillpoints;
|
|
const base_totals = build.total_skillpoints;
|
|
const skillpoints = [
|
|
input_map.get('str'),
|
|
input_map.get('dex'),
|
|
input_map.get('int'),
|
|
input_map.get('def'),
|
|
input_map.get('agi')
|
|
];
|
|
let skp_effects = ["% more damage dealt.","% chance to crit.","% spell cost reduction.","% less damage taken.","% chance to dodge."];
|
|
let total_assigned = 0;
|
|
for (let i in skp_order){ //big bren
|
|
const assigned = skillpoints[i] - base_totals[i] + min_assigned[i]
|
|
setText(skp_order[i] + "-skp-base", "Original: " + base_totals[i]);
|
|
setText(skp_order[i] + "-skp-assign", "Assign: " + assigned);
|
|
setValue(skp_order[i] + "-skp", skillpoints[i]);
|
|
let linebreak = document.createElement("br");
|
|
linebreak.classList.add("itemp");
|
|
setText(skp_order[i] + "-skp-pct", (skillPointsToPercentage(skillpoints[i])*100*skillpoint_final_mult[i]).toFixed(1).concat(skp_effects[i]));
|
|
document.getElementById(skp_order[i]+"-warnings").textContent = ''
|
|
if (assigned > 100) {
|
|
let skp_warning = document.createElement("p");
|
|
skp_warning.classList.add("warning", "small-text");
|
|
skp_warning.textContent += "Cannot assign " + assigned + " skillpoints in " + ["Strength","Dexterity","Intelligence","Defense","Agility"][i] + " manually.";
|
|
document.getElementById(skp_order[i]+"-warnings").appendChild(skp_warning);
|
|
}
|
|
total_assigned += assigned;
|
|
}
|
|
|
|
let summarybox = document.getElementById("summary-box");
|
|
summarybox.textContent = "";
|
|
let skpRow = document.createElement("p");
|
|
|
|
let remainingSkp = document.createElement("p");
|
|
remainingSkp.classList.add("scaled-font");
|
|
let remainingSkpTitle = document.createElement("b");
|
|
remainingSkpTitle.textContent = "Assigned " + total_assigned + " skillpoints. Remaining skillpoints: ";
|
|
let remainingSkpContent = document.createElement("b");
|
|
remainingSkpContent.textContent = "" + (levelToSkillPoints(build.level) - total_assigned);
|
|
remainingSkpContent.classList.add(levelToSkillPoints(build.level) - total_assigned < 0 ? "negative" : "positive");
|
|
|
|
remainingSkp.appendChild(remainingSkpTitle);
|
|
remainingSkp.appendChild(remainingSkpContent);
|
|
|
|
summarybox.append(skpRow);
|
|
summarybox.append(remainingSkp);
|
|
if(total_assigned > levelToSkillPoints(build.level)){
|
|
let skpWarning = document.createElement("span");
|
|
//skpWarning.classList.add("itemp");
|
|
skpWarning.classList.add("warning");
|
|
skpWarning.textContent = "WARNING: Too many skillpoints need to be assigned!";
|
|
let skpCount = document.createElement("p");
|
|
skpCount.classList.add("warning");
|
|
skpCount.textContent = "For level " + (build.level>101 ? "101+" : build.level) + ", there are only " + levelToSkillPoints(build.level) + " skill points available.";
|
|
summarybox.append(skpWarning);
|
|
summarybox.append(skpCount);
|
|
}
|
|
let lvlWarning;
|
|
for (const item of build.items) {
|
|
let item_lvl;
|
|
if (item.statMap.get("crafted")) {
|
|
//item_lvl = item.get("lvlLow") + "-" + item.get("lvl");
|
|
item_lvl = item.statMap.get("lvlLow");
|
|
}
|
|
else {
|
|
item_lvl = item.statMap.get("lvl");
|
|
}
|
|
|
|
if (build.level < item_lvl) {
|
|
if (!lvlWarning) {
|
|
lvlWarning = document.createElement("p");
|
|
lvlWarning.classList.add("itemp"); lvlWarning.classList.add("warning");
|
|
lvlWarning.textContent = "WARNING: A level " + build.level + " player cannot use some piece(s) of this build."
|
|
}
|
|
let baditem = document.createElement("p");
|
|
baditem.classList.add("nocolor"); baditem.classList.add("itemp");
|
|
baditem.textContent = item.statMap.get("displayName") + " requires level " + item_lvl + " to use.";
|
|
lvlWarning.appendChild(baditem);
|
|
}
|
|
}
|
|
if(lvlWarning){
|
|
summarybox.append(lvlWarning);
|
|
}
|
|
for (const [setName, count] of build.activeSetCounts) {
|
|
const bonus = sets.get(setName).bonuses[count-1];
|
|
if (bonus["illegal"]) {
|
|
let setWarning = document.createElement("p");
|
|
setWarning.classList.add("itemp"); setWarning.classList.add("warning");
|
|
setWarning.textContent = "WARNING: illegal item combination: " + setName
|
|
summarybox.append(setWarning);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aggregate stats from all inputs (merges statmaps).
|
|
*
|
|
* Signature: AggregateStatsNode(*args) => StatMap
|
|
*/
|
|
class AggregateStatsNode extends ComputeNode {
|
|
constructor() { super("builder-aggregate-stats"); }
|
|
|
|
compute_func(input_map) {
|
|
const output_stats = new Map();
|
|
for (const [k, v] of input_map.entries()) {
|
|
for (const [k2, v2] of v.entries()) {
|
|
merge_stat(output_stats, k2, v2);
|
|
}
|
|
}
|
|
return output_stats;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Aggregate editable ID stats with build and weapon type.
|
|
*
|
|
* Signature: AggregateEditableIDNode(build: Build, weapon: Item, *args) => StatMap
|
|
*/
|
|
class AggregateEditableIDNode extends ComputeNode {
|
|
constructor() { super("builder-aggregate-inputs"); }
|
|
|
|
compute_func(input_map) {
|
|
const build = input_map.get('build'); input_map.delete('build');
|
|
|
|
const output_stats = new Map(build.statMap);
|
|
for (const [k, v] of input_map.entries()) {
|
|
output_stats.set(k, v);
|
|
}
|
|
|
|
output_stats.set('classDef', classDefenseMultipliers.get(build.weapon.statMap.get("type")));
|
|
return output_stats;
|
|
}
|
|
}
|
|
|
|
let edit_id_output;
|
|
function resetEditableIDs() {
|
|
edit_id_output.notify();
|
|
}
|
|
/**
|
|
* Set the editble id fields.
|
|
*
|
|
* Signature: EditableIDSetterNode(build: Build) => null
|
|
*/
|
|
class EditableIDSetterNode extends ComputeNode {
|
|
constructor(notify_nodes) {
|
|
super("builder-id-setter");
|
|
this.notify_nodes = notify_nodes.slice();
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
if (input_map.size !== 1) { throw "EditableIDSetterNode accepts exactly one input (build)"; }
|
|
const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
|
|
for (const id of editable_item_fields) {
|
|
const val = build.statMap.get(id);
|
|
document.getElementById(id).value = val;
|
|
document.getElementById(id+'-base').textContent = 'Original Value: ' + val;
|
|
}
|
|
}
|
|
|
|
notify() {
|
|
this.mark_dirty();
|
|
this.update();
|
|
// NOTE: DO NOT merge these loops for performance reasons!!!
|
|
for (const node of this.notify_nodes) {
|
|
node.mark_dirty();
|
|
}
|
|
for (const node of this.notify_nodes) {
|
|
node.update();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set skillpoint fields from build.
|
|
* This is separate because..... because of the way we work with edit ids vs skill points during the load sequence....
|
|
*
|
|
* Signature: SkillPointSetterNode(build: Build) => null
|
|
*/
|
|
class SkillPointSetterNode extends ComputeNode {
|
|
constructor(notify_nodes) {
|
|
super("builder-skillpoint-setter");
|
|
this.notify_nodes = notify_nodes.slice();
|
|
}
|
|
|
|
compute_func(input_map) {
|
|
if (input_map.size !== 1) { throw "SkillPointSetterNode accepts exactly one input (build)"; }
|
|
const [build] = input_map.values(); // Extract values, pattern match it into size one list and bind to first element
|
|
for (const [idx, elem] of skp_order.entries()) {
|
|
document.getElementById(elem+'-skp').value = build.total_skillpoints[idx];
|
|
}
|
|
// NOTE: DO NOT merge these loops for performance reasons!!!
|
|
for (const node of this.notify_nodes) {
|
|
node.mark_dirty();
|
|
}
|
|
for (const node of this.notify_nodes) {
|
|
node.update();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get number (possibly summed) from a text input.
|
|
*
|
|
* Signature: SumNumberInputNode() => int
|
|
*/
|
|
class SumNumberInputNode extends InputNode {
|
|
compute_func(input_map) {
|
|
let value = this.input_field.value;
|
|
if (value === "") { value = "0"; }
|
|
|
|
let input_num = 0;
|
|
if (value.includes("+")) {
|
|
let skp = value.split("+");
|
|
for (const s of skp) {
|
|
const val = parseInt(s,10);
|
|
if (isNaN(val)) {
|
|
return null;
|
|
}
|
|
input_num += val;
|
|
}
|
|
} else {
|
|
input_num = parseInt(value,10);
|
|
if (isNaN(input_num)) {
|
|
return null;
|
|
}
|
|
}
|
|
return input_num;
|
|
}
|
|
}
|
|
|
|
let item_nodes = [];
|
|
let powder_nodes = [];
|
|
let edit_input_nodes = [];
|
|
let skp_inputs = [];
|
|
let build_node;
|
|
let stat_agg_node;
|
|
let edit_agg_node;
|
|
let atree_graph_creator;
|
|
|
|
function builder_graph_init() {
|
|
// 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).
|
|
for (const [eq, display_elem, none_item] of zip3(equipment_fields, build_fields, none_items)) {
|
|
let input_field = document.getElementById(eq+"-choice");
|
|
let item_image = document.getElementById(eq+"-img");
|
|
|
|
let item_input = new ItemInputNode(eq+'-input', input_field, none_item);
|
|
item_nodes.push(item_input);
|
|
new ItemInputDisplayNode(eq+'-input-display', eq, item_image).link_to(item_input);
|
|
new ItemDisplayNode(eq+'-item-display', display_elem).link_to(item_input);
|
|
//new PrintNode(eq+'-debug').link_to(item_input);
|
|
//document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm');
|
|
}
|
|
for (const [eq, none_item] of zip2(tome_fields, [none_tomes[0], none_tomes[0], none_tomes[1], none_tomes[1], none_tomes[1], none_tomes[1], none_tomes[2]])) {
|
|
let input_field = document.getElementById(eq+"-choice");
|
|
let item_image = document.getElementById(eq+"-img");
|
|
|
|
let item_input = new ItemInputNode(eq+'-input', input_field, none_item);
|
|
item_nodes.push(item_input);
|
|
new ItemInputDisplayNode(eq+'-input-display', eq, item_image).link_to(item_input);
|
|
}
|
|
|
|
// weapon image changer node.
|
|
let weapon_image = document.getElementById("weapon-img");
|
|
let weapon_dps = document.getElementById("weapon-dps");
|
|
new WeaponInputDisplayNode('weapon-type', weapon_image, weapon_dps).link_to(item_nodes[8]);
|
|
|
|
// Level input node.
|
|
let level_input = new InputNode('level-input', document.getElementById('level-choice'));
|
|
|
|
// linking to atree verification
|
|
atree_validate.link_to(level_input, 'level');
|
|
|
|
// "Build" now only refers to equipment and level (no powders). Powders are injected before damage calculation / stat display.
|
|
build_node = new BuildAssembleNode();
|
|
for (const input of item_nodes) {
|
|
build_node.link_to(input);
|
|
}
|
|
build_node.link_to(level_input);
|
|
|
|
let build_encode_node = new BuildEncodeNode();
|
|
build_encode_node.link_to(build_node, 'build');
|
|
|
|
let url_update_node = new URLUpdateNode();
|
|
url_update_node.link_to(build_encode_node, 'build-str');
|
|
|
|
|
|
for (const input of powder_inputs) {
|
|
let powder_node = new PowderInputNode(input, document.getElementById(input));
|
|
powder_nodes.push(powder_node);
|
|
build_encode_node.link_to(powder_node, input);
|
|
}
|
|
|
|
item_nodes[0].link_to(powder_nodes[0], 'powdering');
|
|
item_nodes[1].link_to(powder_nodes[1], 'powdering');
|
|
item_nodes[2].link_to(powder_nodes[2], 'powdering');
|
|
item_nodes[3].link_to(powder_nodes[3], 'powdering');
|
|
item_nodes[8].link_to(powder_nodes[4], 'powdering');
|
|
|
|
// Phase 2/3: Set up editable IDs, skill points; use decodeBuild() skill points, calculate damage
|
|
|
|
// Create one node that will be the "aggregator node" (listen to all the editable id nodes, as well as the build_node (for non editable stats) and collect them into one statmap)
|
|
stat_agg_node = new AggregateStatsNode();
|
|
edit_agg_node = new AggregateEditableIDNode();
|
|
edit_agg_node.link_to(build_node, 'build');
|
|
for (const field of editable_item_fields) {
|
|
// Create nodes that listens to each editable id input, the node name should match the "id"
|
|
const elem = document.getElementById(field);
|
|
const node = new SumNumberInputNode('builder-'+field+'-input', elem);
|
|
|
|
edit_agg_node.link_to(node, field);
|
|
edit_input_nodes.push(node);
|
|
}
|
|
// Edit IDs setter declared up here to set ids so they will be populated by default.
|
|
edit_id_output = new EditableIDSetterNode(edit_input_nodes); // Makes shallow copy of list.
|
|
edit_id_output.link_to(build_node);
|
|
|
|
for (const skp of skp_order) {
|
|
const elem = document.getElementById(skp+'-skp');
|
|
const node = new SumNumberInputNode('builder-'+skp+'-input', elem);
|
|
|
|
edit_agg_node.link_to(node, skp);
|
|
build_encode_node.link_to(node, skp);
|
|
edit_input_nodes.push(node);
|
|
skp_inputs.push(node);
|
|
}
|
|
stat_agg_node.link_to(edit_agg_node);
|
|
|
|
// 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(class_node, 'player-class');
|
|
atree_merge.link_to(class_node, 'player-class');
|
|
atree_stats.link_to(build_node, 'build');
|
|
stat_agg_node.link_to(atree_stats, 'atree-stats');
|
|
|
|
build_encode_node.link_to(atree_node, 'atree').link_to(atree_state_node, 'atree-state');
|
|
|
|
// ---------------------------------------------------------------
|
|
// Trigger the update cascade for build!
|
|
// ---------------------------------------------------------------
|
|
for (const input_node of item_nodes.concat(powder_nodes)) {
|
|
input_node.update();
|
|
}
|
|
armor_powder_node.update();
|
|
level_input.update();
|
|
|
|
atree_graph_creator = new AbilityTreeEnsureNodesNode(build_node, stat_agg_node)
|
|
.link_to(atree_collect_spells, 'spells');
|
|
|
|
// 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')
|
|
.link_to(stat_agg_node, 'stats').link_to(build_node, 'build');
|
|
stat_agg_node.link_to(powder_special_calc, 'powder-boost');
|
|
stat_agg_node.link_to(armor_powder_node, 'armor-powder');
|
|
powder_special_input.update();
|
|
|
|
// Potion boost.
|
|
stat_agg_node.link_to(boosts_node, 'potion-boost');
|
|
|
|
// Also do something similar for skill points
|
|
|
|
let build_disp_node = new BuildDisplayNode()
|
|
build_disp_node.link_to(build_node, 'build');
|
|
build_disp_node.link_to(stat_agg_node, 'stats');
|
|
|
|
for (const node of edit_input_nodes) {
|
|
node.update();
|
|
}
|
|
|
|
let skp_output = new SkillPointSetterNode(edit_input_nodes);
|
|
skp_output.link_to(build_node);
|
|
|
|
let build_warnings_node = new DisplayBuildWarningsNode();
|
|
build_warnings_node.link_to(build_node, 'build');
|
|
for (const [skp_input, skp] of zip2(skp_inputs, skp_order)) {
|
|
build_warnings_node.link_to(skp_input, skp);
|
|
}
|
|
build_warnings_node.update();
|
|
|
|
// call node.update() for each skillpoint node and stat edit listener node manually
|
|
// NOTE: the text boxes for skill points are already filled out by decodeBuild() so this will fix them
|
|
// this will propagate the update to the `stat_agg_node`, and then to damage calc
|
|
|
|
console.log("Set up graph");
|
|
graph_live_update = true;
|
|
}
|
|
|