wynnbuilder-forked-for-changes/js/builder_graph.js
2022-07-27 22:18:44 +09:30

1354 lines
40 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 += 0.1;
}
if (key === "vanish") {
def_boost += 0.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
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) {
if (first.length > 0) {
errorederrors.push(first);
} else {
break;
}
} else {
powdering.push(powder);
}
input = input.slice(2);
}
if (this.input_field.getAttribute("placeholder") != null) {
let placeholder = this.input_field.getAttribute("placeholder");
if (
placeholder.includes(" slots") &&
parseInt(placeholder.substring(0, placeholder.indexOf(" "))) <
powdering.length
)
errorederrors.push("Too many powders: " + powdering.length);
}
if (errorederrors.length) {
this.input_field.classList.add("is-invalid");
} else {
this.input_field.classList.remove("is-invalid");
}
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;
}