fixed merge conflict

This commit is contained in:
ferricles 2022-07-04 00:21:38 -07:00
commit e956d28c56
292 changed files with 16735 additions and 11026 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.swp
*.bat
*.json
sets/
.idea/

View file

@ -50,7 +50,7 @@
</div>
<div class = "col navbar navbar-fixed-bottom vh-75 min-vh-50 text-break ml-5" id = "bodydiv" style = "min-height: 75vh; display: flex; flex-direction: column;" >
</div>
<audio id="bruh_sound_effect" src="../media/audio/bruh_sound_effect.mp3" preload="auto"></audio>
</div>
<!-- sidebar -->

1438
builder/doc.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -272,7 +272,7 @@
<div class="col-auto order-xl-0 order-1">
<div class="row h-100 dark-shadow dark-6 rounded" id='weapon-dropdown'>
<div class="col-auto px-lg-1 g-0 dark-7 rounded-end my-auto text-center scaled-item-icon" id="weapon-img-loc">
<img id="weapon-img" class="img-fluid rounded" src="../media/items/new/generic-wand.png">
<img id="weapon-img" class="img-fluid rounded" src="../media/items/new/generic-dagger.png">
</div>
<div class="col-3">
<div class="row row-cols-1 h-100 align-items-center">
@ -307,15 +307,17 @@
<input class="equipment-input text-light form-control form-control-sm" id="level-choice" name="level-choice" value="106" placeholder="Build level" value="" tabindex="2"/>
</div>
<div class="col-auto px-1 text-nowrap scaled-font">
<button class="button fw-bold text-light dark-5 scaled-font rounded" id="reset-button" onclick="sq2ResetFields()">Reset</button>
<button class="button fw-bold text-light dark-5 scaled-font rounded" id="reset-button" onclick="resetFields()">Reset</button>
</div>
</div>
<div class="row align-items-center justify-content-center my-1">
<div class="row align-items-center">
<div class="col-auto px-1 text-nowrap scaled-font">
<button class="button fw-bold text-light dark-5 scaled-font rounded" id="copy-button" onclick="copyBuild()">Copy short</button>
<button class="border-dark text-light dark-5 scaled-font rounded" id=copy-button onclick="copyBuild()">Copy short</button>
</div>
<div class="col-auto px-1 text-nowrap scaled-font">
<button class="button fw-bold text-light dark-5 scaled-font rounded" id="share-button" onclick="shareBuild()">Copy for sharing</button>
<button class="border-dark text-light dark-5 scaled-font rounded" id=share-button onclick="shareBuild(player_build)">Copy for sharing</button>
</div>
</div>
</div>
</div>
@ -404,6 +406,8 @@
</div>
<div class="col text-center">
<div id="summary-box"></div>
<div id="err-box"></div>
<div id="stack-box"></div>
<div id="str-warnings"></div>
<div id="dex-warnings"></div>
<div id="int-warnings"></div>
@ -425,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">
@ -434,7 +438,7 @@
</button>
</div>
<div class = "col-3 py-2">
<button class = "button rounded scaled-font fw-bold text-light dark-5" id = "edit-ID-button" onclick = "resetEditableIDs(); updateStats();">
<button class = "button rounded scaled-font fw-bold text-light dark-5" id = "edit-ID-button" onclick = "resetEditableIDs();">
Reset Edited IDs
</button>
</div>
@ -612,13 +616,16 @@
</div>
</div>
</div>
<div class = "col dark-6 rounded-bottom my-3 my-xl-1" id = "atree-dropdown"">
<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 rounded dark-9 hide-scroll" id="atree-ui" style="height: 500px; 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" id="atree-active">
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>
@ -632,7 +639,7 @@
Spell Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="sdPct" name="sdPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="sdPct" name="sdPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "sdPct-base">
Original Value: 0
@ -643,7 +650,7 @@
Spell Damage Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="sdRaw" name="sdRaw" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="sdRaw" name="sdRaw" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "sdRaw-base">
Original Value: 0
@ -654,7 +661,7 @@
Melee Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="mdPct" name="mdPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="mdPct" name="mdPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "mdPct-base">
Original Value: 0
@ -665,7 +672,7 @@
Melee Damage Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="mdRaw" name="mdRaw" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="mdRaw" name="mdRaw" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "mdRaw-base">
Original Value: 0
@ -678,7 +685,7 @@
Poison:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="poison" name="poison" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="poison" name="poison" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "poison-base">
Original Value: 0
@ -689,7 +696,7 @@
Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="eDamPct" name="eDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="eDamPct" name="eDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "eDamPct-base">
Original Value: 0
@ -700,7 +707,7 @@
Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="tDamPct" name="tDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="tDamPct" name="tDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "tDamPct-base">
Original Value: 0
@ -711,7 +718,7 @@
Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="wDamPct" name="wDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="wDamPct" name="wDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "wDamPct-base">
Original Value: 0
@ -724,7 +731,7 @@
Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="fDamPct" name="fDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="fDamPct" name="fDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "fDamPct-base">
Original Value: 0
@ -735,7 +742,7 @@
Damage %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="aDamPct" name="aDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="aDamPct" name="aDamPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "aDamPct-base">
Original Value: 0
@ -746,7 +753,7 @@
+ Tier:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="atkTier" name="atkTier" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="atkTier" name="atkTier" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "atkTier-base">
Original Value: 0
@ -765,7 +772,7 @@
Defense %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="eDefPct" name="eDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="eDefPct" name="eDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "eDefPct-base">
Original Value: 0
@ -776,7 +783,7 @@
Defense %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="tDefPct" name="tDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="tDefPct" name="tDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "tDefPct-base">
Original Value: 0
@ -787,7 +794,7 @@
Defense %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="wDefPct" name="wDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="wDefPct" name="wDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "wDefPct-base">
Original Value: 0
@ -798,7 +805,7 @@
Defense %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="fDefPct" name="fDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="fDefPct" name="fDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "fDefPct-base">
Original Value: 0
@ -811,7 +818,7 @@
Defense %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="aDefPct" name="aDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="aDefPct" name="aDefPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "aDefPct-base">
Original Value: 0
@ -822,7 +829,7 @@
Health Regen Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="hprRaw" name="hprRaw" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="hprRaw" name="hprRaw" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "hprRaw-base">
Original Value: 0
@ -833,7 +840,7 @@
Health Regen %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="hprPct" name="hprPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="hprPct" name="hprPct" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "hprPct-base">
Original Value: 0
@ -844,7 +851,7 @@
Health Bonus:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="hpBonus" name="hpBonus" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="hpBonus" name="hpBonus" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "hpBonus-base">
Original Value: 0
@ -860,7 +867,7 @@
1st Spell Cost %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spPct1" name="spPct1" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spPct1" name="spPct1" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spPct1-base">
Original Value: 0
@ -871,7 +878,7 @@
2nd Spell Cost %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spPct2" name="spPct2" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spPct2" name="spPct2" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spPct2-base">
Original Value: 0
@ -882,7 +889,7 @@
3rd Spell Cost %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spPct3" name="spPct3" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spPct3" name="spPct3" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spPct3-base">
Original Value: 0
@ -893,7 +900,7 @@
4th Spell Cost %:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spPct4" name="spPct4" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spPct4" name="spPct4" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spPct4-base">
Original Value: 0
@ -906,7 +913,7 @@
1st Spell Cost Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spRaw1" name="spRaw1" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spRaw1" name="spRaw1" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spRaw1-base">
Original Value: 0
@ -917,7 +924,7 @@
2nd Spell Cost Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spRaw2" name="spRaw2" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spRaw2" name="spRaw2" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spRaw2-base">
Original Value: 0
@ -928,7 +935,7 @@
3rd Spell Cost Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spRaw3" name="spRaw3" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spRaw3" name="spRaw3" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spRaw3-base">
Original Value: 0
@ -939,7 +946,7 @@
4th Spell Cost Raw:
</div>
<div class = "row">
<input type = "number" placeholder = "0" id="spRaw4" name="spRaw4" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm" oninput = "updateStats()"/>
<input type = "number" placeholder = "0" id="spRaw4" name="spRaw4" value="0" class="border-dark text-light dark-10 rounded scaled-font form-control form-control-sm"/>
</div>
<div class = "row" id = "spRaw4-base">
Original Value: 0
@ -957,27 +964,27 @@
<div class="col skp-tooltip dark-6 rounded-bottom my-3 my-xl-1">
<div class="row row-cols-2 row-cols-xl-0 text-nowrap justify-content-center">
<div class="col-auto p-1">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="vanish-boost" onclick="updateBoosts('vanish-boost', true)">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="vanish-boost" onclick="update_boosts('vanish-boost')">
Vanish (+80%)
</button>
</div>
<div class="col-auto p-1">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="warscream-boost" onclick="updateBoosts('warscream-boost', true)">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="warscream-boost" onclick="update_boosts('warscream-boost')">
War Scream (+10%)
</button>
</div>
<div class="col-auto p-1">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="yourtotem-boost" onclick="updateBoosts('yourtotem-boost', true)">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="yourtotem-boost" onclick="update_boosts('yourtotem-boost')">
Your Totem (+35%)
</button>
</div>
<div class="col-auto p-1">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="allytotem-boost" onclick="updateBoosts('allytotem-boost', true)">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="allytotem-boost" onclick="update_boosts('allytotem-boost')">
Ally Totem (+15%)
</button>
</div>
<div class="col-auto p-1">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="bash-boost" onclick="updateBoosts('bash-boost', true)">
<button class="button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id="bash-boost" onclick="update_boosts('bash-boost')">
Bash (+50%)
</button>
</div>
@ -1014,27 +1021,27 @@
<div class="col skp-tooltip dark-6 rounded-bottom my-3 my-xl-1">
<div class="row row-cols-2 row-cols-xl-0 text-nowrap justify-content-center">
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-1" onclick = "updatePowderSpecials('Quake-1', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-1" onclick = "updatePowderSpecials('Quake-1')">
Lv.4 [e4e4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-2" onclick = "updatePowderSpecials('Quake-2', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-2" onclick = "updatePowderSpecials('Quake-2')">
Lv.4.5 [e5e4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-3" onclick = "updatePowderSpecials('Quake-3', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-3" onclick = "updatePowderSpecials('Quake-3')">
Lv.5 [e5e5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-4" onclick = "updatePowderSpecials('Quake-4', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-4" onclick = "updatePowderSpecials('Quake-4')">
Lv.5.5 [e6e5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-5" onclick = "updatePowderSpecials('Quake-5', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Quake-5" onclick = "updatePowderSpecials('Quake-5')">
Lv.6 [e6e6]
</button>
</div>
@ -1044,7 +1051,7 @@
Rage (Passive)
</div>
<div class="col">
<input type = "range" class = "e_slider" id = "str_boost_armor" name = "str-boost-armor" autocomplete = "off" min = '0' max = '400' value = '0' step = '1' onchange = "updateArmorPowderSpecials('str_boost_armor', true)">
<input type = "range" class = "e_slider" id = "str_boost_armor" name = "str-boost-armor" autocomplete = "off" min = '0' max = '400' value = '0' step = '1' onchange = "update_armor_powder_specials('str_boost_armor')">
<input type="text" id="str_boost_armor_prev" autocomplete = "off" value="0" style = "display:none;">
<label id = "str_boost_armor_label" for="str-boost-armor">% Earth Dmg Boost: 0</label>
</div>
@ -1058,27 +1065,27 @@
<div class="col skp-tooltip dark-6 rounded-bottom my-3 my-xl-1">
<div class="row row-cols-2 row-cols-xl-0 text-nowrap justify-content-center">
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-1" onclick = "updatePowderSpecials('Chain_Lightning-1', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-1" onclick = "updatePowderSpecials('Chain_Lightning-1')">
Lv.4 [t4t4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-2" onclick = "updatePowderSpecials('Chain_Lightning-2', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-2" onclick = "updatePowderSpecials('Chain_Lightning-2')">
Lv.4.5 [t5t4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-3" onclick = "updatePowderSpecials('Chain_Lightning-3', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-3" onclick = "updatePowderSpecials('Chain_Lightning-3')">
Lv.5 [t5t5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-4" onclick = "updatePowderSpecials('Chain_Lightning-4', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-4" onclick = "updatePowderSpecials('Chain_Lightning-4')">
Lv.5.5 [t6t5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-5" onclick = "updatePowderSpecials('Chain_Lightning-5', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Chain_Lightning-5" onclick = "updatePowderSpecials('Chain_Lightning-5')">
Lv.6 [t6t6]
</button>
</div>
@ -1088,7 +1095,7 @@
Kill Streak (Passive)
</div>
<div class="col">
<input type = "range" class = "t_slider" id = "dex_boost_armor" name = "dex-boost-armor" autocomplete = "off" min = '0' max = '200' value = '0' step = '1' onchange = "updateArmorPowderSpecials('dex_boost_armor', true)">
<input type = "range" class = "t_slider" id = "dex_boost_armor" name = "dex-boost-armor" autocomplete = "off" min = '0' max = '200' value = '0' step = '1' onchange = "update_armor_powder_specials('dex_boost_armor')">
<input type="text" id="dex_boost_armor_prev" autocomplete = "off" value="0" style = "display:none;">
<label id = "dex_boost_armor_label" for="dex-boost-armor">% Thunder Dmg Boost: 0</label>
</div>
@ -1102,27 +1109,27 @@
<div class="col skp-tooltip dark-6 rounded-bottom my-3 my-xl-1">
<div class="row row-cols-2 row-cols-xl-0 text-nowrap justify-content-center">
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-1" onclick = "updatePowderSpecials('Curse-1', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-1" onclick = "updatePowderSpecials('Curse-1')">
Lv.4 [w4w4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-2" onclick = "updatePowderSpecials('Curse-2', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-2" onclick = "updatePowderSpecials('Curse-2')">
Lv.4.5 [w5w4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-3" onclick = "updatePowderSpecials('Curse-3', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-3" onclick = "updatePowderSpecials('Curse-3')">
Lv.5 [w5w5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-4" onclick = "updatePowderSpecials('Curse-4', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-4" onclick = "updatePowderSpecials('Curse-4')">
Lv.5.5 [w6w5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-5" onclick = "updatePowderSpecials('Curse-5', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Curse-5" onclick = "updatePowderSpecials('Curse-5')">
Lv.6 [w6w6]
</button>
</div>
@ -1132,7 +1139,7 @@
Concentration (Passive)
</div>
<div class="col">
<input type = "range" class = "w_slider" id = "int_boost_armor" name = "dex-boost-armor" autocomplete = "off" min = '0' max = '150' value = '0' step = '1' onchange = "updateArmorPowderSpecials('int_boost_armor', true)">
<input type = "range" class = "w_slider" id = "int_boost_armor" name = "dex-boost-armor" autocomplete = "off" min = '0' max = '150' value = '0' step = '1' onchange = "update_armor_powder_specials('int_boost_armor')">
<input type="text" id="int_boost_armor_prev" autocomplete = "off" value="0" style = "display:none;">
<label id = "int_boost_armor_label" for="dex-boost-armor">% Water Dmg Boost: 0</label>
</div>
@ -1146,27 +1153,27 @@
<div class="col skp-tooltip dark-6 rounded-bottom my-3 my-xl-1">
<div class="row row-cols-2 row-cols-xl-0 text-nowrap justify-content-center">
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-1" onclick = "updatePowderSpecials('Courage-1', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-1" onclick = "updatePowderSpecials('Courage-1')">
Lv.4 [f4f4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-2" onclick = "updatePowderSpecials('Courage-2', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-2" onclick = "updatePowderSpecials('Courage-2')">
Lv.4.5 [f5f4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-3" onclick = "updatePowderSpecials('Courage-3', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-3" onclick = "updatePowderSpecials('Courage-3')">
Lv.5 [f5f5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-4" onclick = "updatePowderSpecials('Courage-4', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-4" onclick = "updatePowderSpecials('Courage-4')">
Lv.5.5 [f6f5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-5" onclick = "updatePowderSpecials('Courage-5', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Courage-5" onclick = "updatePowderSpecials('Courage-5')">
Lv.6 [f6f6]
</button>
</div>
@ -1176,7 +1183,7 @@
Endurance (Passive)
</div>
<div class="col">
<input type = "range" class = "f_slider" id = "def_boost_armor" name = "def-boost-armor" autocomplete = "off" min = '0' max = '200' value = '0' step = '1' onchange = "updateArmorPowderSpecials('def_boost_armor', true)">
<input type = "range" class = "f_slider" id = "def_boost_armor" name = "def-boost-armor" autocomplete = "off" min = '0' max = '200' value = '0' step = '1' onchange = "update_armor_powder_specials('def_boost_armor')">
<input type="text" id="def_boost_armor_prev" autocomplete = "off" value="0" style = "display:none;">
<label id = "def_boost_armor_label" for="def-boost-armor">% Fire Dmg Boost: 0</label>
</div>
@ -1190,27 +1197,27 @@
<div class="col skp-tooltip dark-6 rounded-bottom my-3 my-xl-1">
<div class="row row-cols-2 row-cols-xl-0 text-nowrap justify-content-center">
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-1" onclick = "updatePowderSpecials('Wind_Prison-1', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-1" onclick = "updatePowderSpecials('Wind_Prison-1')">
Lv.4 [a4a4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-2" onclick = "updatePowderSpecials('Wind_Prison-2', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-2" onclick = "updatePowderSpecials('Wind_Prison-2')">
Lv.4.5 [a5a4]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-3" onclick = "updatePowderSpecials('Wind_Prison-3', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-3" onclick = "updatePowderSpecials('Wind_Prison-3')">
Lv.5 [a5a5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-4" onclick = "updatePowderSpecials('Wind_Prison-4', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-4" onclick = "updatePowderSpecials('Wind_Prison-4')">
Lv.5.5 [a6a5]
</button>
</div>
<div class="col-auto p-1">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-5" onclick = "updatePowderSpecials('Wind_Prison-5', true)">
<button class = "button-boost w-100 border-0 text-white dark-8u dark-shadow-sm" id = "Wind_Prison-5" onclick = "updatePowderSpecials('Wind_Prison-5')">
Lv.6 [a6a6]
</button>
</div>
@ -1220,7 +1227,7 @@
Dodge (Passive)
</div>
<div class="col">
<input type = "range" class = "a_slider" id = "agi_boost_armor" name = "agi-boost-armor" autocomplete = "off" min = '0' max = '150' value = '0' step = '1' onchange = "updateArmorPowderSpecials('agi_boost_armor', true)">
<input type = "range" class = "a_slider" id = "agi_boost_armor" name = "agi-boost-armor" autocomplete = "off" min = '0' max = '150' value = '0' step = '1' onchange = "update_armor_powder_specials('agi_boost_armor')">
<input type="text" id="agi_boost_armor_prev" autocomplete = "off" value="0" style = "display:none;">
<label id = "agi_boost_armor_label" for="agi-boost-armor">% Air Dmg Boost: 0</label>
</div>
@ -1255,28 +1262,30 @@
<div class="col-xl-3 mb-3 px-0">
<div class="row row-cols-1 gy-3 mb-4 text-center scaled-font">
<div class = "col">
<div class = "spell-display dark-5 rounded dark-shadow py-2 border border-dark" id="build-melee-statsAvg">melee</div>
<div class = "spell-display spell-expand dark-5 rounded dark-shadow py-2 border border-dark" id="build-melee-statsAvg">melee</div>
<div class = "spell-display dark-5 rounded-bottom py-2 dark-shadow" id = "build-melee-stats" style="display: none;"></div>
</div>
<div class = "col">
<div class = "col spell-display dark-5 rounded dark-shadow py-2 border border-dark" id="build-poison-stats">poison</div>
</div>
<div class = "col">
<div id="all-spells-display" class="row row-cols-1 gy-3 text-center scaled-font pe-0">
<div class = "col pe-0">
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell0-infoAvg">spell1</div>
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell0-info" style="display: none;">Spell 1</div>
</div>
<div class = "col">
<div class = "col pe-0">
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell1-infoAvg">spell2</div>
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell1-info" style="display: none;">Spell 2</div>
</div>
<div class = "col">
<div class = "col pe-0">
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell2-infoAvg">spell3</div>
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell2-info" style="display: none;">Spell 3</div>
</div>
<div class = "col">
<div class = "col pe-0">
<div class = "col spell-display spell-expand dark-5 rounded dark-shadow pt-2 border border-dark" id="spell3-infoAvg">spell4</div>
<div class = "col spell-display dark-5 rounded dark-shadow py-2" id = "spell3-info" style="display: none;">Spell 4</div>
</div>
</div>
<div class = "col">
<div class = "spell-display dark-5 rounded dark-shadow py-2 border border-dark" id = "powder-special-stats"></div>
</div>
@ -1304,7 +1313,7 @@
</div>
<div id="weapon-tooltip" class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3">
</div>
<div id="weaponTome1-tooltip" class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3">
<!--div id="weaponTome1-tooltip" class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3">
</div>
<div id="weaponTome2-tooltip" class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3">
</div>
@ -1317,7 +1326,7 @@
<div id="armorTome4-tooltip" class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3">
</div>
<div id="guildTome1-tooltip" class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3">
</div>
</div-->
<div class="rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark dark-shadow p-3" id = "build-order">
</div>
<div class = "rounded row row-cols-1 g-0 scaled-font float-tooltip border border-3 border-dark p-3" id = "set-info"></div>
@ -1327,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">
@ -1396,19 +1405,18 @@
<script src="https://cdn.jsdelivr.net/npm/macy@2"></script>
<script type="text/javascript" src="../js/utils.js"></script>
<script type="text/javascript" src="../js/build_utils.js"></script>
<script type="text/javascript" src="../js/computation_graph.js"></script>
<!-- <script type="text/javascript" src="../js/icons.js"></script> -->
<script type="text/javascript" src="../js/sq2icons.js"></script>
<script type="text/javascript" src="../js/powders.js"></script>
<script type="text/javascript" src="../js/skillpoints.js"></script>
<script type="text/javascript" src="../js/damage_calc.js"></script>
<script type="text/javascript" src="../js/atree_constants.js"></script>
<script type="text/javascript" src="../js/atree_constants_min.js"></script>
<script type="text/javascript" src="../js/display_constants.js"></script>
<script type="text/javascript" src="../js/sq2display_constants.js"></script>
<script type="text/javascript" src="../js/display.js"></script>
<script type="text/javascript" src="../js/sq2display.js"></script>
<script type="text/javascript" src="../js/query.js"></script>
<script type="text/javascript" src="../js/query_2.js"></script>
@ -1417,12 +1425,16 @@
<script type="text/javascript" src="../js/load_tome.js"></script>
<script type="text/javascript" src="../js/custom.js"></script>
<script type="text/javascript" src="../js/craft.js"></script>
<script type="text/javascript" src="../js/sq2build.js"></script>
<script type="text/javascript" src="../js/sq2builder.js"></script>
<script type="text/javascript" src="../js/build.js"></script>
<script type="text/javascript" src="../js/build_constants.js"></script>
<script type="text/javascript" src="../js/build_encode_decode.js"></script>
<script type="text/javascript" src="../js/atree.js"></script>
<script type="text/javascript" src="../js/builder.js"></script>
<script type="text/javascript" src="../js/builder_graph.js"></script>
<script type="text/javascript" src="../js/expr_parser.js"></script>
<script type="text/javascript" src="../js/items.js"></script>
<script type="text/javascript" src="../js/sq2items.js"></script>
<script type="text/javascript" src="../js/sq2bs.js"></script>
<script type="text/javascript" src="../js/optimize.js"></script>
</body>
</html>

View file

@ -211,7 +211,7 @@
</button>
</div>
<div class = "col-lg-3 col-sm-6">
<button class = "button rounded scaled-font fw-bold text-light dark-5" id = "craft-button" onclick = "copyRecipeHash()">
<button class = "button rounded scaled-font fw-bold text-light dark-5" id = "copy-hash-button" onclick = "copyRecipeHash()">
Copy Hash
</button>
</div>
@ -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">
@ -301,8 +301,6 @@
<script type="text/javascript" src="../js/damage_calc.js"></script>
<script type="text/javascript" src="../js/display_constants.js"></script>
<script type="text/javascript" src="../js/display.js"></script>
<script type="text/javascript" src="../js/sq2display_constants.js"></script>
<script type="text/javascript" src="../js/sq2display.js"></script>
<script type="text/javascript" src="../js/load_ing.js"></script>
<script type="text/javascript" src="../js/load.js"></script>
<script type="text/javascript" src="../js/craft.js"></script>

20
credits.txt Normal file
View file

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

View file

@ -474,14 +474,12 @@ a:hover {
transform: rotate(270deg);
}
/* atree hover */
.atree-node {
opacity: 75%;
.rotate-flip {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.atree-node:hover {
opacity: 100%;
}
.hide-scroll {
-ms-overflow-style: none; /* Internet Explorer 10+ */
@ -490,3 +488,13 @@ a:hover {
.hide-scroll::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
.atree-selected {
outline: 5px solid rgba(95, 214, 223, 0.8);
}
.atree-circle {
border-radius:50%;
-moz-border-radius:50%;
-webkit-border-radius:50%;
}

BIN
dev/builder_colorcode.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

12
dev/compute_graph.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -892,9 +892,70 @@
Last updated: 30 May 2022
</p>
</div>
<div class="row section" title="Wynnbuilder Internals (compute graph)">
<p>
This section is about how Wynnbuilder's main builder page processes user input and calculates results.
Might be useful if you want to script wynnbuilder or extend it! Or for wynnbuilder developers (internal docs).
</p>
<div class="row section" title="Why?">
<p>
Modeling wynnbuilder's internal computations as a directed graph has a few advantages:
</p>
<ul class = "indent">
<li>Each compute "node" is small(er); easier to debug.</li>
<li>Information flow is specified explicitly (easier to debug).</li>
<li>Easy to build caching for arbitrary computations (only calculate what u need)</li>
<li>Stateless builder! Abstract the entire builder as a chain of function calls</li>
<li>Makes for pretty pictures</li>
</ul>
</div>
<div class="row section" title="TODO ComputeNode details">
TODO
</div>
<p>
An overview of wynnbuilder's internal structure can be seen <a href = "./compute_graph.svg" target = "_blank">here</a>. Arrows indicate flow of information.
Colors correspond roughly as follows:
</p>
<img src="./builder_colorcode.png"/>
<p>
The overall logic flow is as follows:
<ul class = "indent">
<li>Item and Powder inputs are parsed. Powders are applied to items.</li>
<li>Items and level information are combined to make a build.</li>
<li>Information from input fields for skill points and edit IDs is collected into an ID bonuses table.</li>
<li>Information about active powder specials, strength boosts, etc. are collected into their own ID tables.</li>
<li>All of the above tables are merged with the build's stats table to produce the "Final" ID bonus table.</li>
<li>Which spell variant (think: major id) to use for each of the 4 spells is computed based on the build.</li>
<li>Spell damage is calculated, using the merged stat table, spell info, and weapon info.</li>
</ul>
</p>
<p>
Outputs are computed as follows:
<ul class = "indent">
<li>Input box highlights are computed from the items produced by item input box nodes.</li>
<li>Item display is computed from item input boxes.</li>
<li>Build hash/URL is computed from the build, and skillpoint assignment.</li>
<li>Spell damage is displayed based on calculated spell damage results.</li>
<li>Build stats are displayed by builder-stats-display (this same node also displays a bunch of stuff at the bottom of the screen...)</li>
</ul>
</p>
<div class="row section" title="Gotchas">
<p>
The build sets default skillpoints and edited IDs automatically, whenever a build item/level is updated.
This is done using "soft links" by two nodes shown in red (builder-skillpoint-setter and builder-id-setter).
</p>
<p>
A soft link is where something goes and manually marks nodes dirty and calls their update methods.
This is useful for these cases because the skillpoints and editable ID fields usually take their value from
user input, but in some cases we want to programatically set them.
</p>
<p>
For example another soft link (not shown) is used to implement the reset button.
</p>
</div>
</div>
<!-- <div class="row section" title="Test Section">
</div> -->
</div>
<script type="text/javascript" src="../js/dev.js"></script>
<script type="text/javascript" src="../js/sq2icons.js"></script>

View file

@ -62,8 +62,6 @@
<script type="text/javascript" src="/js/load_ing.js"></script>
<script type="text/javascript" src="/js/display_constants.js"></script>
<script type="text/javascript" src="/js/display.js"></script>
<script type="text/javascript" src="/js/sq2display_constants.js"></script>
<script type="text/javascript" src="/js/sq2display.js"></script>
<script type="text/javascript" src="/js/item.js"></script>
</body>
</html>

View file

@ -79,8 +79,6 @@
<script type="text/javascript" src="/js/damage_calc.js"></script>
<script type="text/javascript" src="/js/display_constants.js"></script>
<script type="text/javascript" src="/js/display.js"></script>
<script type="text/javascript" src="/js/sq2display_constants.js"></script>
<script type="text/javascript" src="/js/sq2display.js"></script>
<script type="text/javascript" src="/js/query_2.js"></script>
<script type="text/javascript" src="/js/expr_parser.js"></script>
<script type="text/javascript" src="/js/load.js"></script>

View file

@ -0,0 +1,5 @@
How to convert:
1. edit `atree_constants.js`
2. run `python3 ../py_script/atree-generateID.py
3. check that the site still works

View file

@ -119,6 +119,10 @@ function runAtlas() {
let center = [(at1[0]+at2[0])/2, (at1[1]+at2[1])/2 ];
if (Math.sqrt(((at2[1]+atlas2.vy) - (at1[1]+atlas1.vy))**2 + ((at2[0]+atlas2.vx) - (at1[0]+atlas1.vx))**2) < 2*r) {
//Play bruh sound effect
document.getElementById('bruh_sound_effect').play();
document.getElementById('bruh_sound_effect').currentTime = 0;
if(Math.sqrt( (at2[1]-at1[1])**2 + (at2[0]-at1[0])**2 ) < 2*r ) {//check for collision
//Move both away slightly - correct alg this time :)
atlas1.style.left = parseFloat(atlas1.style.left.replace("px","")) + (at1[0]-center[0]) * 2 * r / Math.sqrt(dx**2 + dy**2) + "px";

1029
js/atree.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

146
js/atree_ids.json Normal file
View file

@ -0,0 +1,146 @@
{
"Archer": {
"Arrow Shield": 0,
"Escape": 1,
"Arrow Bomb": 2,
"Heart Shatter": 3,
"Fire Creep": 4,
"Bryophyte Roots": 5,
"Nimble String": 6,
"Arrow Storm": 7,
"Guardian Angels": 8,
"Windy Feet": 9,
"Basaltic Trap": 10,
"Windstorm": 11,
"Grappling Hook": 12,
"Implosion": 13,
"Twain's Arc": 14,
"Fierce Stomp": 15,
"Scorched Earth": 16,
"Leap": 17,
"Shocking Bomb": 18,
"Mana Trap": 19,
"Escape Artist": 20,
"Initiator": 21,
"Call of the Hound": 22,
"Arrow Hurricane": 23,
"Geyser Stomp": 24,
"Crepuscular Ray": 25,
"Grape Bomb": 26,
"Tangled Traps": 27,
"Snow Storm": 28,
"All-Seeing Panoptes": 29,
"Minefield": 30,
"Bow Proficiency I": 31,
"Cheaper Arrow Bomb": 32,
"Cheaper Arrow Storm": 33,
"Cheaper Escape": 34,
"Earth Mastery": 35,
"Thunder Mastery": 36,
"Water Mastery": 37,
"Air Mastery": 38,
"Fire Mastery": 39,
"More Shields": 40,
"Stormy Feet": 41,
"Refined Gunpowder": 42,
"More Traps": 43,
"Better Arrow Shield": 44,
"Better Leap": 45,
"Better Guardian Angels": 46,
"Cheaper Arrow Storm (2)": 47,
"Precise Shot": 48,
"Cheaper Arrow Shield": 49,
"Rocket Jump": 50,
"Cheaper Escape (2)": 51,
"Stronger Hook": 52,
"Cheaper Arrow Bomb (2)": 53,
"Bouncing Bomb": 54,
"Homing Shots": 55,
"Shrapnel Bomb": 56,
"Elusive": 57,
"Double Shots": 58,
"Triple Shots": 59,
"Power Shots": 60,
"Focus": 61,
"More Focus": 62,
"More Focus (2)": 63,
"Traveler": 64,
"Patient Hunter": 65,
"Stronger Patient Hunter": 66,
"Frenzy": 67,
"Phantom Ray": 68,
"Arrow Rain": 69,
"Decimator": 70
},
"Warrior": {
"Bash": 0,
"Spear Proficiency 1": 1,
"Cheaper Bash": 2,
"Double Bash": 3,
"Charge": 4,
"Heavy Impact": 5,
"Vehement": 6,
"Tougher Skin": 7,
"Uppercut": 8,
"Cheaper Charge": 9,
"War Scream": 10,
"Earth Mastery": 11,
"Thunder Mastery": 12,
"Water Mastery": 13,
"Air Mastery": 14,
"Fire Mastery": 15,
"Quadruple Bash": 16,
"Fireworks": 17,
"Half-Moon Swipe": 18,
"Flyby Jab": 19,
"Flaming Uppercut": 20,
"Iron Lungs": 21,
"Generalist": 22,
"Counter": 23,
"Mantle of the Bovemists": 24,
"Bak'al's Grasp": 25,
"Spear Proficiency 2": 26,
"Cheaper Uppercut": 27,
"Aerodynamics": 28,
"Provoke": 29,
"Precise Strikes": 30,
"Air Shout": 31,
"Enraged Blow": 32,
"Flying Kick": 33,
"Stronger Mantle": 34,
"Manachism": 35,
"Boiling Blood": 36,
"Ragnarokkr": 37,
"Ambidextrous": 38,
"Burning Heart": 39,
"Stronger Bash": 40,
"Intoxicating Blood": 41,
"Comet": 42,
"Collide": 43,
"Rejuvenating Skin": 44,
"Uncontainable Corruption": 45,
"Radiant Devotee": 46,
"Whirlwind Strike": 47,
"Mythril Skin": 48,
"Armour Breaker": 49,
"Shield Strike": 50,
"Sparkling Hope": 51,
"Massive Bash": 52,
"Tempest": 53,
"Spirit of the Rabbit": 54,
"Massacre": 55,
"Axe Kick": 56,
"Radiance": 57,
"Cheaper Bash 2": 58,
"Cheaper War Scream": 59,
"Discombobulate": 60,
"Thunderclap": 61,
"Cyclone": 62,
"Second Chance": 63,
"Blood Pact": 64,
"Haemorrhage": 65,
"Brink of Madness": 66,
"Cheaper Uppercut 2": 67,
"Martyr": 68
}
}

View file

@ -100,238 +100,12 @@ class Build{
* @param {Number[]} powders : Powder application. List of lists of integers (powder IDs).
* In order: boots, Chestplate, Leggings, Boots, Weapon.
* @param {Object[]} inputerrors : List of instances of error-like classes.
*
* @param {Object[]} tomes: List of tomes.
* In order: 2x Weapon Mastery Tome, 4x Armor Mastery Tome, 1x Guild Tome.
* 2x Slaying Mastery Tome, 2x Dungeoneering Mastery Tome, 2x Gathering Mastery Tome are in game, but do not have "useful" stats (those that affect damage calculations or building)
*/
constructor(level,equipment, powders, externalStats, inputerrors=[]){
let errors = inputerrors;
//this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem).
this.craftedItems = [];
this.customItems = [];
// NOTE: powders is just an array of arrays of powder IDs. Not powder objects.
this.powders = powders;
if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") {
const helmet = itemMap.get(equipment[0]);
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
} else {
try {
let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined);
if (helmet.statMap.get("type") !== "helmet") {
throw new Error("Not a helmet");
}
this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots"));
helmet.statMap.set("powders",this.powders[0].slice());
this.helmet = helmet.statMap;
applyArmorPowders(this.helmet, this.powders[0]);
if (this.helmet.get("custom")) {
this.customItems.push(helmet);
} else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(helmet);
}
} catch (Error) {
const helmet = itemMap.get("No Helmet");
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
errors.push(new ItemNotFound(equipment[0], "helmet", true));
}
}
if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") {
const chestplate = itemMap.get(equipment[1]);
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
} else {
try {
let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined);
if (chestplate.statMap.get("type") !== "chestplate") {
throw new Error("Not a chestplate");
}
this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots"));
chestplate.statMap.set("powders",this.powders[1].slice());
this.chestplate = chestplate.statMap;
applyArmorPowders(this.chestplate, this.powders[1]);
if (this.chestplate.get("custom")) {
this.customItems.push(chestplate);
} else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(chestplate);
}
} catch (Error) {
console.log(Error);
const chestplate = itemMap.get("No Chestplate");
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
errors.push(new ItemNotFound(equipment[1], "chestplate", true));
}
}
if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") {
const leggings = itemMap.get(equipment[2]);
this.powders[2] = this.powders[2].slice(0,leggings.slots);
this.leggings = expandItem(leggings, this.powders[2]);
} else {
try {
let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined);
if (leggings.statMap.get("type") !== "leggings") {
throw new Error("Not a leggings");
}
this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots"));
leggings.statMap.set("powders",this.powders[2].slice());
this.leggings = leggings.statMap;
applyArmorPowders(this.leggings, this.powders[2]);
if (this.leggings.get("custom")) {
this.customItems.push(leggings);
} else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(leggings);
}
} catch (Error) {
const leggings = itemMap.get("No Leggings");
this.powders[2] = this.powders[2].slice(0,leggings.slots);
this.leggings = expandItem(leggings, this.powders[2]);
errors.push(new ItemNotFound(equipment[2], "leggings", true));
}
}
if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") {
const boots = itemMap.get(equipment[3]);
this.powders[3] = this.powders[3].slice(0,boots.slots);
this.boots = expandItem(boots, this.powders[3]);
} else {
try {
let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined);
if (boots.statMap.get("type") !== "boots") {
throw new Error("Not a boots");
}
this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots"));
boots.statMap.set("powders",this.powders[3].slice());
this.boots = boots.statMap;
applyArmorPowders(this.boots, this.powders[3]);
if (this.boots.get("custom")) {
this.customItems.push(boots);
} else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(boots);
}
} catch (Error) {
const boots = itemMap.get("No Boots");
this.powders[3] = this.powders[3].slice(0,boots.slots);
this.boots = expandItem(boots, this.powders[3]);
errors.push(new ItemNotFound(equipment[3], "boots", true));
}
}
if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") {
const ring = itemMap.get(equipment[4]);
this.ring1 = expandItem(ring, []);
}else{
try {
let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined);
if (ring.statMap.get("type") !== "ring") {
throw new Error("Not a ring");
}
this.ring1 = ring.statMap;
if (this.ring1.get("custom")) {
this.customItems.push(ring);
} else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(ring);
}
} catch (Error) {
const ring = itemMap.get("No Ring 1");
this.ring1 = expandItem(ring, []);
errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring"));
}
}
if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") {
const ring = itemMap.get(equipment[5]);
this.ring2 = expandItem(ring, []);
}else{
try {
let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined);
if (ring.statMap.get("type") !== "ring") {
throw new Error("Not a ring");
}
this.ring2 = ring.statMap;
if (this.ring2.get("custom")) {
this.customItems.push(ring);
} else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(ring);
}
} catch (Error) {
const ring = itemMap.get("No Ring 2");
this.ring2 = expandItem(ring, []);
errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring"));
}
}
if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") {
const bracelet = itemMap.get(equipment[6]);
this.bracelet = expandItem(bracelet, []);
}else{
try {
let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined);
if (bracelet.statMap.get("type") !== "bracelet") {
throw new Error("Not a bracelet");
}
this.bracelet = bracelet.statMap;
if (this.bracelet.get("custom")) {
this.customItems.push(bracelet);
} else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(bracelet);
}
} catch (Error) {
const bracelet = itemMap.get("No Bracelet");
this.bracelet = expandItem(bracelet, []);
errors.push(new ItemNotFound(equipment[6], "bracelet", true));
}
}
if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") {
const necklace = itemMap.get(equipment[7]);
this.necklace = expandItem(necklace, []);
}else{
try {
let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined);
if (necklace.statMap.get("type") !== "necklace") {
throw new Error("Not a necklace");
}
this.necklace = necklace.statMap;
if (this.necklace.get("custom")) {
this.customItems.push(necklace);
} else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(necklace);
}
} catch (Error) {
const necklace = itemMap.get("No Necklace");
this.necklace = expandItem(necklace, []);
errors.push(new ItemNotFound(equipment[7], "necklace", true));
}
}
if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") {
const weapon = itemMap.get(equipment[8]);
this.powders[4] = this.powders[4].slice(0,weapon.slots);
this.weapon = expandItem(weapon, this.powders[4]);
if (equipment[8] !== "No Weapon") {
document.getElementsByClassName("powder-specials")[0].style.display = "grid";
} else {
document.getElementsByClassName("powder-specials")[0].style.display = "none";
}
}else{
try {
let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined);
if (weapon.statMap.get("category") !== "weapon") {
throw new Error("Not a weapon");
}
this.weapon = weapon.statMap;
if (this.weapon.get("custom")) {
this.customItems.push(weapon);
} else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(weapon);
}
this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots"));
this.weapon.set("powders",this.powders[4].slice());
document.getElementsByClassName("powder-specials")[0].style.display = "grid";
} catch (Error) {
const weapon = itemMap.get("No Weapon");
this.powders[4] = this.powders[4].slice(0,weapon.slots);
this.weapon = expandItem(weapon, this.powders[4]);
document.getElementsByClassName("powder-specials")[0].style.display = "none";
errors.push(new ItemNotFound(equipment[8], "weapon", true));
}
}
//console.log(this.craftedItems)
constructor(level, items, weapon){
if (level < 1) { //Should these be constants?
this.level = 1;
@ -348,11 +122,13 @@ class Build{
document.getElementById("level-choice").value = this.level;
this.availableSkillpoints = levelToSkillPoints(this.level);
this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ];
this.equipment = items;
this.weapon = weapon;
this.items = this.equipment.concat([this.weapon]);
// return [equip_order, best_skillpoints, final_skillpoints, best_total];
let result = calculate_skillpoints(this.equipment, this.weapon);
console.log(result);
// calc skillpoints requires statmaps only
let result = calculate_skillpoints(this.equipment.map((x) => x.statMap), this.weapon.statMap);
this.equip_order = result[0];
// How many skillpoints the player had to assign (5 number)
this.base_skillpoints = result[1];
@ -362,21 +138,7 @@ class Build{
this.assigned_skillpoints = result[3];
this.activeSetCounts = result[4];
// For strength boosts like warscream, vanish, etc.
this.damageMultiplier = 1.0;
this.defenseMultiplier = 1.0;
// For other external boosts ;-;
this.externalStats = externalStats;
this.initBuildStats();
// Remove every error before adding specific ones
for (let i of document.getElementsByClassName("error")) {
i.textContent = "";
}
this.errors = errors;
if (errors.length > 0) this.errored = true;
}
/*Returns build in string format
@ -385,138 +147,62 @@ class Build{
return [this.equipment,this.weapon].flat();
}
/* Getters */
getSpellCost(spellIdx, cost) {
return Math.max(1, this.getBaseSpellCost(spellIdx, cost));
}
getBaseSpellCost(spellIdx, cost) {
cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2])));
cost += this.statMap.get("spRaw"+spellIdx);
return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100));
}
/* Get melee stats for build.
Returns an array in the order:
*/
getMeleeStats(){
const stats = this.statMap;
if (this.weapon.get("tier") === "Crafted") {
stats.set("damageBases", [this.weapon.get("nDamBaseHigh"),this.weapon.get("eDamBaseHigh"),this.weapon.get("tDamBaseHigh"),this.weapon.get("wDamBaseHigh"),this.weapon.get("fDamBaseHigh"),this.weapon.get("aDamBaseHigh")]);
}
let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier");
if(adjAtkSpd > 6){
adjAtkSpd = 6;
}else if(adjAtkSpd < 0){
adjAtkSpd = 0;
}
let damage_mult = 1;
if (this.weapon.get("type") === "relik") {
damage_mult = 0.99; // CURSE YOU WYNNCRAFT
//One day we will create WynnWynn and no longer have shaman 99% melee injustice.
//In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams.
}
// 0spellmult for melee damage.
let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct") + this.externalStats.get("mdPct"), 0, this.weapon, this.total_skillpoints, damage_mult * this.damageMultiplier, this.externalStats);
let dex = this.total_skillpoints[1];
let totalDamNorm = results[0];
let totalDamCrit = results[1];
totalDamNorm.push(1-skillPointsToPercentage(dex));
totalDamCrit.push(skillPointsToPercentage(dex));
let damages_results = results[2];
let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2])
+(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2;
//Now do math
let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd];
let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd];
let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex)));
//[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit]
return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]);
}
/*
Get all defensive stats for this build.
*/
getDefenseStats(){
const stats = this.statMap;
let defenseStats = [];
let def_pct = skillPointsToPercentage(this.total_skillpoints[3]);
let agi_pct = skillPointsToPercentage(this.total_skillpoints[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 = classDefenseMultipliers.get(this.weapon.get("type"));
ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier));
ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier));
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)*(2-defMult)*(2-this.defenseMultiplier));
ehpr[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier));
defenseStats.push(ehpr);
//skp stats
defenseStats.push([ (1 - ((1-def_pct) * (2 - this.defenseMultiplier)))*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;
}
/* Get all stats for this build. Stores in this.statMap.
@pre The build itself should be valid. No checking of validity of pieces is done here.
*/
initBuildStats(){
let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "def", "agi"];
let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "def", "agi", "damMobs", "defMobs"];
let must_ids = [
"eMdPct","eMdRaw","eSdPct","eSdRaw","eDamPct","eDamRaw","eDamAddMin","eDamAddMax",
"tMdPct","tMdRaw","tSdPct","tSdRaw","tDamPct","tDamRaw","tDamAddMin","tDamAddMax",
"wMdPct","wMdRaw","wSdPct","wSdRaw","wDamPct","wDamRaw","wDamAddMin","wDamAddMax",
"fMdPct","fMdRaw","fSdPct","fSdRaw","fDamPct","fDamRaw","fDamAddMin","fDamAddMax",
"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
]
//Create a map of this build's stats
let statMap = new Map();
statMap.set("defMultiplier", 1);
for (const staticID of staticIDs) {
statMap.set(staticID, 0);
}
for (const staticID of must_ids) {
statMap.set(staticID, 0);
}
statMap.set("hp", levelToHPBase(this.level));
let major_ids = new Set();
for (const item of this.items){
for (let [id, value] of item.get("maxRolls")) {
const item_stats = item.statMap;
for (let [id, value] of item_stats.get("maxRolls")) {
if (staticIDs.includes(id)) {
continue;
}
statMap.set(id,(statMap.get(id) || 0)+value);
}
for (const staticID of staticIDs) {
if (item.get(staticID)) {
statMap.set(staticID, statMap.get(staticID) + item.get(staticID));
if (item_stats.get(staticID)) {
statMap.set(staticID, statMap.get(staticID) + item_stats.get(staticID));
}
}
if (item.get("majorIds")) {
for (const major_id of item.get("majorIds")) {
if (item_stats.get("majorIds")) {
for (const major_id of item_stats.get("majorIds")) {
major_ids.add(major_id);
}
}
}
statMap.set('damageMultiplier', 1 + (statMap.get('damMobs') / 100));
statMap.set('defMultiplier', 1 - (statMap.get('defMobs') / 100));
statMap.set("activeMajorIDs", major_ids);
for (const [setName, count] of this.activeSetCounts) {
const bonus = sets[setName].bonuses[count-1];
const bonus = sets.get(setName).bonuses[count-1];
for (const id in bonus) {
if (skp_order.includes(id)) {
// pass. Don't include skillpoints in ids
@ -529,27 +215,8 @@ class Build{
statMap.set("poisonPct", 100);
// The stuff relevant for damage calculation!!! @ferricles
statMap.set("atkSpd", this.weapon.get("atkSpd"));
statMap.set("atkSpd", this.weapon.statMap.get("atkSpd"));
for (const x of skp_elements) {
this.externalStats.set(x + "DamPct", 0);
}
this.externalStats.set("mdPct", 0);
this.externalStats.set("sdPct", 0);
this.externalStats.set("damageBonus", [0, 0, 0, 0, 0]);
this.externalStats.set("defBonus",[0, 0, 0, 0, 0]);
this.externalStats.set("poisonPct", 0);
this.statMap = statMap;
this.aggregateStats();
}
aggregateStats() {
let statMap = this.statMap;
statMap.set("damageRaw", [this.weapon.get("nDam"), this.weapon.get("eDam"), this.weapon.get("tDam"), this.weapon.get("wDam"), this.weapon.get("fDam"), this.weapon.get("aDam")]);
statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]);
statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]);
statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]);
statMap.set("defMult", classDefenseMultipliers.get(this.weapon.get("type")));
}
}

View file

@ -1,550 +0,0 @@
const classDefenseMultipliers = new Map([ ["relik",0.50], ["bow",0.60], ["wand", 0.80], ["dagger", 1.0], ["spear",1.20] ]);
/**
* @description Error to catch items that don't exist.
* @module ItemNotFound
*/
class ItemNotFound {
/**
* @class
* @param {String} item the item name entered
* @param {String} type the type of item
* @param {Boolean} genElement whether to generate an element from inputs
* @param {String} override override for item type
*/
constructor(item, type, genElement, override) {
/**
* @public
* @type {String}
*/
this.message = `Cannot find ${override||type} named ${item}`;
if (genElement)
/**
* @public
* @type {Element}
*/
this.element = document.getElementById(`${type}-choice`).parentElement.querySelectorAll("p.error")[0];
else
this.element = document.createElement("div");
}
}
/**
* @description Error to catch incorrect input.
* @module IncorrectInput
*/
class IncorrectInput {
/**
* @class
* @param {String} input the inputted text
* @param {String} format the correct format
* @param {String} sibling the id of the error node's sibling
*/
constructor(input, format, sibling) {
/**
* @public
* @type {String}
*/
this.message = `${input} is incorrect. Example: ${format}`;
/**
* @public
* @type {String}
*/
this.id = sibling;
}
}
/**
* @description Error that inputs an array of items to generate errors of.
* @module ListError
* @extends Error
*/
class ListError extends Error {
/**
* @class
* @param {Array} errors array of errors
*/
constructor(errors) {
let ret = [];
if (typeof errors[0] == "string") {
super(errors[0]);
} else {
super(errors[0].message);
}
for (let i of errors) {
if (typeof i == "string") {
ret.push(new Error(i));
} else {
ret.push(i);
}
}
/**
* @public
* @type {Object[]}
*/
this.errors = ret;
}
}
/*Class that represents a wynn player's build.
*/
class Build{
/**
* @description Construct a build.
* @param {Number} level : Level of the player.
* @param {String[]} equipment : List of equipment names that make up the build.
* In order: boots, Chestplate, Leggings, Boots, Ring1, Ring2, Brace, Neck, Weapon.
* @param {Number[]} powders : Powder application. List of lists of integers (powder IDs).
* In order: boots, Chestplate, Leggings, Boots, Weapon.
* @param {Object[]} inputerrors : List of instances of error-like classes.
*/
constructor(level,equipment, powders, externalStats, inputerrors=[]){
let errors = inputerrors;
//this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem).
this.craftedItems = [];
this.customItems = [];
// NOTE: powders is just an array of arrays of powder IDs. Not powder objects.
this.powders = powders;
if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") {
const helmet = itemMap.get(equipment[0]);
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
} else {
try {
//let boots = getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : (getCustomFromHash(equipment[0])? getCustomFromHash(equipment[0]) : undefined);
let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined);
if (helmet.statMap.get("type") !== "helmet") {
throw new Error("Not a helmet");
}
this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots"));
helmet.statMap.set("powders",this.powders[0].slice());
helmet.applyPowders();
this.helmet = helmet.statMap;
if (this.helmet.get("custom")) {
this.customItems.push(helmet);
} else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(helmet);
}
} catch (Error) {
//console.log(Error); //fix
const helmet = itemMap.get("No Helmet");
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
errors.push(new ItemNotFound(equipment[0], "helmet", true));
}
}
if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") {
const chestplate = itemMap.get(equipment[1]);
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
} else {
try {
let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined);
if (chestplate.statMap.get("type") !== "chestplate") {
throw new Error("Not a chestplate");
}
this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots"));
chestplate.statMap.set("powders",this.powders[1].slice());
chestplate.applyPowders();
this.chestplate = chestplate.statMap;
if (this.chestplate.get("custom")) {
this.customItems.push(chestplate);
} else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(chestplate);
}
} catch (Error) {
const chestplate = itemMap.get("No Chestplate");
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
errors.push(new ItemNotFound(equipment[1], "chestplate", true));
}
}
if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") {
const leggings = itemMap.get(equipment[2]);
this.powders[2] = this.powders[2].slice(0,leggings.slots);
this.leggings = expandItem(leggings, this.powders[2]);
} else {
try {
let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined);
if (leggings.statMap.get("type") !== "leggings") {
throw new Error("Not a leggings");
}
this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots"));
leggings.statMap.set("powders",this.powders[2].slice());
leggings.applyPowders();
this.leggings = leggings.statMap;
if (this.leggings.get("custom")) {
this.customItems.push(leggings);
} else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(leggings);
}
} catch (Error) {
const leggings = itemMap.get("No Leggings");
this.powders[2] = this.powders[2].slice(0,leggings.slots);
this.leggings = expandItem(leggings, this.powders[2]);
errors.push(new ItemNotFound(equipment[2], "leggings", true));
}
}
if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") {
const boots = itemMap.get(equipment[3]);
this.powders[3] = this.powders[3].slice(0,boots.slots);
this.boots = expandItem(boots, this.powders[3]);
} else {
try {
let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined);
if (boots.statMap.get("type") !== "boots") {
throw new Error("Not a boots");
}
this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots"));
boots.statMap.set("powders",this.powders[3].slice());
boots.applyPowders();
this.boots = boots.statMap;
console.log(boots);
if (this.boots.get("custom")) {
this.customItems.push(boots);
} else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(boots);
}
} catch (Error) {
const boots = itemMap.get("No Boots");
this.powders[3] = this.powders[3].slice(0,boots.slots);
this.boots = expandItem(boots, this.powders[3]);
errors.push(new ItemNotFound(equipment[3], "boots", true));
}
}
if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") {
const ring = itemMap.get(equipment[4]);
this.ring1 = expandItem(ring, []);
}else{
try {
let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined);
if (ring.statMap.get("type") !== "ring") {
throw new Error("Not a ring");
}
this.ring1 = ring.statMap;
if (this.ring1.get("custom")) {
this.customItems.push(ring);
} else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(ring);
}
} catch (Error) {
const ring = itemMap.get("No Ring 1");
this.ring1 = expandItem(ring, []);
errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring"));
}
}
if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") {
const ring = itemMap.get(equipment[5]);
this.ring2 = expandItem(ring, []);
}else{
try {
let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined);
if (ring.statMap.get("type") !== "ring") {
throw new Error("Not a ring");
}
this.ring2 = ring.statMap;
if (this.ring2.get("custom")) {
this.customItems.push(ring);
} else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(ring);
}
} catch (Error) {
const ring = itemMap.get("No Ring 2");
this.ring2 = expandItem(ring, []);
errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring"));
}
}
if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") {
const bracelet = itemMap.get(equipment[6]);
this.bracelet = expandItem(bracelet, []);
}else{
try {
let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined);
if (bracelet.statMap.get("type") !== "bracelet") {
throw new Error("Not a bracelet");
}
this.bracelet = bracelet.statMap;
if (this.bracelet.get("custom")) {
this.customItems.push(bracelet);
} else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(bracelet);
}
} catch (Error) {
const bracelet = itemMap.get("No Bracelet");
this.bracelet = expandItem(bracelet, []);
errors.push(new ItemNotFound(equipment[6], "bracelet", true));
}
}
if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") {
const necklace = itemMap.get(equipment[7]);
this.necklace = expandItem(necklace, []);
}else{
try {
let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined);
if (necklace.statMap.get("type") !== "necklace") {
throw new Error("Not a necklace");
}
this.necklace = necklace.statMap;
if (this.necklace.get("custom")) {
this.customItems.push(necklace);
} else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(necklace);
}
} catch (Error) {
const necklace = itemMap.get("No Necklace");
this.necklace = expandItem(necklace, []);
errors.push(new ItemNotFound(equipment[7], "necklace", true));
}
}
if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") {
const weapon = itemMap.get(equipment[8]);
this.powders[4] = this.powders[4].slice(0,weapon.slots);
this.weapon = expandItem(weapon, this.powders[4]);
if (equipment[8] !== "No Weapon") {
document.getElementsByClassName("powder-specials")[0].style.display = "grid";
} else {
document.getElementsByClassName("powder-specials")[0].style.display = "none";
}
}else{
try {
let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined);
if (weapon.statMap.get("category") !== "weapon") {
throw new Error("Not a weapon");
}
this.weapon = weapon.statMap;
if (this.weapon.get("custom")) {
this.customItems.push(weapon);
} else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(weapon);
}
this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots"));
this.weapon.set("powders",this.powders[4].slice());
document.getElementsByClassName("powder-specials")[0].style.display = "grid";
} catch (Error) {
const weapon = itemMap.get("No Weapon");
this.powders[4] = this.powders[4].slice(0,weapon.slots);
this.weapon = expandItem(weapon, this.powders[4]);
document.getElementsByClassName("powder-specials")[0].style.display = "none";
errors.push(new ItemNotFound(equipment[8], "weapon", true));
}
}
//console.log(this.craftedItems)
if (level < 1) { //Should these be constants?
this.level = 1;
} else if (level > 106) {
this.level = 106;
} else if (level <= 106 && level >= 1) {
this.level = level;
} else if (typeof level === "string") {
this.level = level;
errors.push(new IncorrectInput(level, "a number", "level-choice"));
} else {
errors.push("Level is not a string or number.");
}
document.getElementById("level-choice").value = this.level;
this.availableSkillpoints = levelToSkillPoints(this.level);
this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ];
this.items = this.equipment.concat([this.weapon]);
// return [equip_order, best_skillpoints, final_skillpoints, best_total];
let result = calculate_skillpoints(this.equipment, this.weapon);
console.log(result);
this.equip_order = result[0];
this.base_skillpoints = result[1];
this.total_skillpoints = result[2];
this.assigned_skillpoints = result[3];
this.activeSetCounts = result[4];
// For strength boosts like warscream, vanish, etc.
this.damageMultiplier = 1.0;
this.defenseMultiplier = 1.0;
// For other external boosts ;-;
this.externalStats = externalStats;
this.initBuildStats();
// Remove every error before adding specific ones
for (let i of document.getElementsByClassName("error")) {
i.textContent = "";
}
this.errors = errors;
if (errors.length > 0) this.errored = true;
}
/*Returns build in string format
*/
toString(){
return [this.equipment,this.weapon].flat();
}
/* Getters */
/* Get total health for build.
*/
getSpellCost(spellIdx, cost) {
cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2])));
cost = Math.max(0, Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100)));
return Math.max(1, cost + this.statMap.get("spRaw"+spellIdx));
}
/* Get melee stats for build.
Returns an array in the order:
*/
getMeleeStats(){
const stats = this.statMap;
if (this.weapon.get("tier") === "Crafted") {
stats.set("damageBases", [this.weapon.get("nDamBaseHigh"),this.weapon.get("eDamBaseHigh"),this.weapon.get("tDamBaseHigh"),this.weapon.get("wDamBaseHigh"),this.weapon.get("fDamBaseHigh"),this.weapon.get("aDamBaseHigh")]);
}
let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier");
if(adjAtkSpd > 6){
adjAtkSpd = 6;
}else if(adjAtkSpd < 0){
adjAtkSpd = 0;
}
let damage_mult = 1;
if (this.weapon.get("type") === "relik") {
damage_mult = 0.99; // CURSE YOU WYNNCRAFT
//One day we will create WynnWynn and no longer have shaman 99% melee injustice.
//In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams.
}
// 0spellmult for melee damage.
let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct") + this.externalStats.get("mdPct"), 0, this.weapon, this.total_skillpoints, damage_mult * this.damageMultiplier, this.externalStats);
let dex = this.total_skillpoints[1];
let totalDamNorm = results[0];
let totalDamCrit = results[1];
totalDamNorm.push(1-skillPointsToPercentage(dex));
totalDamCrit.push(skillPointsToPercentage(dex));
let damages_results = results[2];
let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2])
+(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2;
//Now do math
let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd];
let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd];
let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex)));
//[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit]
return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]);
}
/*
Get all defensive stats for this build.
*/
getDefenseStats(){
const stats = this.statMap;
let defenseStats = [];
let def_pct = skillPointsToPercentage(this.total_skillpoints[3]);
let agi_pct = skillPointsToPercentage(this.total_skillpoints[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 = classDefenseMultipliers.get(this.weapon.get("type"));
ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier));
ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier));
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)*(2-defMult)*(2-this.defenseMultiplier));
ehpr[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier));
defenseStats.push(ehpr);
//skp stats
defenseStats.push([ (1 - ((1-def_pct) * (2 - this.defenseMultiplier)))*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;
}
/* Get all stats for this build. Stores in this.statMap.
@pre The build itself should be valid. No checking of validity of pieces is done here.
*/
initBuildStats(){
let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef"];
//Create a map of this build's stats
let statMap = new Map();
for (const staticID of staticIDs) {
statMap.set(staticID, 0);
}
statMap.set("hp", levelToHPBase(this.level));
let major_ids = new Set();
for (const item of this.items){
for (let [id, value] of item.get("maxRolls")) {
statMap.set(id,(statMap.get(id) || 0)+value);
}
for (const staticID of staticIDs) {
if (item.get(staticID)) {
statMap.set(staticID, statMap.get(staticID) + item.get(staticID));
}
}
if (item.get("majorIds")) {
for (const majorID of item.get("majorIds")) {
major_ids.add(majorID);
}
}
}
statMap.set("activeMajorIDs", major_ids);
for (const [setName, count] of this.activeSetCounts) {
const bonus = sets[setName].bonuses[count-1];
for (const id in bonus) {
if (skp_order.includes(id)) {
// pass. Don't include skillpoints in ids
}
else {
statMap.set(id,(statMap.get(id) || 0)+bonus[id]);
}
}
}
statMap.set("poisonPct", 100);
// The stuff relevant for damage calculation!!! @ferricles
statMap.set("atkSpd", this.weapon.get("atkSpd"));
for (const x of skp_elements) {
this.externalStats.set(x + "DamPct", 0);
}
this.externalStats.set("mdPct", 0);
this.externalStats.set("sdPct", 0);
this.externalStats.set("damageBonus", [0, 0, 0, 0, 0]);
this.externalStats.set("defBonus",[0, 0, 0, 0, 0]);
this.externalStats.set("poisonPct", 0);
this.statMap = statMap;
this.aggregateStats();
}
aggregateStats() {
let statMap = this.statMap;
statMap.set("damageRaw", [this.weapon.get("nDam"), this.weapon.get("eDam"), this.weapon.get("tDam"), this.weapon.get("wDam"), this.weapon.get("fDam"), this.weapon.get("aDam")]);
statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]);
statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]);
statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]);
statMap.set("defMult", classDefenseMultipliers.get(this.weapon.get("type")));
}
}

107
js/build_constants.js Normal file
View file

@ -0,0 +1,107 @@
/**
* I kinda lied. Theres some listener stuff in here
* but its mostly constants for builder page specifically.
*/
const url_tag = location.hash.slice(1);
const BUILD_VERSION = "7.0.19";
let player_build;
// THIS IS SUPER DANGEROUS, WE SHOULD NOT BE KEEPING THIS IN SO MANY PLACES
let editable_item_fields = [ "sdPct", "sdRaw", "mdPct", "mdRaw", "poison",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct",
"hprRaw", "hprPct", "hpBonus", "atkTier",
"spPct1", "spRaw1", "spPct2", "spRaw2",
"spPct3", "spRaw3", "spPct4", "spRaw4" ];
let editable_elems = [];
for (let i of editable_item_fields) {
let elem = document.getElementById(i);
elem.addEventListener("change", (event) => {
elem.classList.add("highlight");
});
editable_elems.push(elem);
}
for (let i of skp_order) {
let elem = document.getElementById(i+"-skp");
elem.addEventListener("change", (event) => {
elem.classList.add("highlight");
});
editable_elems.push(elem);
}
function clear_highlights() {
for (let i of editable_elems) {
i.classList.remove("highlight");
}
}
let equipment_fields = [
"helmet",
"chestplate",
"leggings",
"boots",
"ring1",
"ring2",
"bracelet",
"necklace",
"weapon"
];
let tome_fields = [
"weaponTome1",
"weaponTome2",
"armorTome1",
"armorTome2",
"armorTome3",
"armorTome4",
"guildTome1",
]
let equipment_names = [
"Helmet",
"Chestplate",
"Leggings",
"Boots",
"Ring 1",
"Ring 2",
"Bracelet",
"Necklace",
"Weapon"
];
let tome_names = [
"Weapon Tome",
"Weapon Tome",
"Armor Tome",
"Armor Tome",
"Armor Tome",
"Armor Tome",
"Guild Tome",
]
let equipment_inputs = equipment_fields.map(x => x + "-choice");
let build_fields = equipment_fields.map(x => x+"-tooltip");
let tomeInputs = tome_fields.map(x => x + "-choice");
let powder_inputs = [
"helmet-powder",
"chestplate-powder",
"leggings-powder",
"boots-powder",
"weapon-powder",
];
let weapon_keys = ['dagger', 'wand', 'bow', 'relik', 'spear'];
let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots'];
let accessory_keys= ['ring1', 'ring2', 'bracelet', 'necklace'];
let powderable_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'weapon'];
let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon'];
let tome_keys = ['weaponTome1', 'weaponTome2', 'armorTome1', 'armorTome2', 'armorTome3', 'armorTome4', 'guildTome1'];
let spell_disp = ['build-melee-stats', 'spell0-info', 'spell1-info', 'spell2-info', 'spell3-info'];
let other_disp = ['build-order', 'set-info', 'int-info'];

290
js/build_encode_decode.js Normal file
View file

@ -0,0 +1,290 @@
function parsePowdering(powder_info) {
// TODO: Make this run in linear instead of quadratic time... ew
let powdering = [];
for (let i = 0; i < 5; ++i) {
let powders = "";
let n_blocks = Base64.toInt(powder_info.charAt(0));
// console.log(n_blocks + " blocks");
powder_info = powder_info.slice(1);
for (let j = 0; j < n_blocks; ++j) {
let block = powder_info.slice(0,5);
let six_powders = Base64.toInt(block);
for (let k = 0; k < 6 && six_powders != 0; ++k) {
powders += powderNames.get((six_powders & 0x1f) - 1);
six_powders >>>= 5;
}
powder_info = powder_info.slice(5);
}
powdering[i] = powders;
}
return [powdering, powder_info];
}
let atree_data = null;
/*
* Populate fields based on url, and calculate build.
*/
function decodeBuild(url_tag) {
if (url_tag) {
//default values
let equipment = [null, null, null, null, null, null, null, null, null];
let tomes = [null, null, null, null, null, null, null];
let powdering = ["", "", "", "", ""];
let info = url_tag.split("_");
let version = info[0];
let save_skp = false;
let skillpoints = [0, 0, 0, 0, 0];
let level = 106;
let version_number = parseInt(version)
//equipment (items)
// TODO: use filters
if (version_number < 4) {
let equipments = info[1];
for (let i = 0; i < 9; ++i ) {
let equipment_str = equipments.slice(i*3,i*3+3);
equipment[i] = getItemNameFromID(Base64.toInt(equipment_str));
}
info[1] = equipments.slice(27);
}
else if (version_number == 4) {
let info_str = info[1];
let start_idx = 0;
for (let i = 0; i < 9; ++i ) {
if (info_str.charAt(start_idx) === "-") {
equipment[i] = "CR-"+info_str.slice(start_idx+1, start_idx+18);
start_idx += 18;
}
else {
let equipment_str = info_str.slice(start_idx, start_idx+3);
equipment[i] = getItemNameFromID(Base64.toInt(equipment_str));
start_idx += 3;
}
}
info[1] = info_str.slice(start_idx);
}
else if (version_number <= 7) {
let info_str = info[1];
let start_idx = 0;
for (let i = 0; i < 9; ++i ) {
if (info_str.slice(start_idx,start_idx+3) === "CR-") {
equipment[i] = info_str.slice(start_idx, start_idx+20);
start_idx += 20;
} else if (info_str.slice(start_idx+3,start_idx+6) === "CI-") {
let len = Base64.toInt(info_str.slice(start_idx,start_idx+3));
equipment[i] = info_str.slice(start_idx+3,start_idx+3+len);
start_idx += (3+len);
} else {
let equipment_str = info_str.slice(start_idx, start_idx+3);
equipment[i] = getItemNameFromID(Base64.toInt(equipment_str));
start_idx += 3;
}
}
info[1] = info_str.slice(start_idx);
}
//constant in all versions
for (let i in equipment) {
setValue(equipment_inputs[i], equipment[i]);
}
//level, skill point assignments, and powdering
if (version_number == 1) {
let powder_info = info[1];
let res = parsePowdering(powder_info);
powdering = res[0];
} else if (version_number == 2) {
save_skp = true;
let skillpoint_info = info[1].slice(0, 10);
for (let i = 0; i < 5; ++i ) {
skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2));
}
let powder_info = info[1].slice(10);
let res = parsePowdering(powder_info);
powdering = res[0];
} else if (version_number <= 7){
level = Base64.toInt(info[1].slice(10,12));
setValue("level-choice",level);
save_skp = true;
let skillpoint_info = info[1].slice(0, 10);
for (let i = 0; i < 5; ++i ) {
skillpoints[i] = Base64.toIntSigned(skillpoint_info.slice(i*2,i*2+2));
}
let powder_info = info[1].slice(12);
let res = parsePowdering(powder_info);
powdering = res[0];
info[1] = res[1];
}
// Tomes.
if (version >= 6) {
//tome values do not appear in anything before v6.
for (let i in tomes) {
let tome_str = info[1].charAt(i);
let tome_name = getTomeNameFromID(Base64.toInt(tome_str));
console.log(tome_name);
setValue(tomeInputs[i], tome_name);
}
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]);
}
for (let i in skillpoints) {
setValue(skp_order[i] + "-skp", skillpoints[i]);
}
}
}
/* Stores the entire build in a string using B64 encoding and adds it to the URL.
*/
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 = "";
for (const item of build.items) {
if (item.statMap.get("custom")) {
let custom = "CI-"+encodeCustom(item, true);
build_string += Base64.fromIntN(custom.length, 3) + custom;
build_version = Math.max(build_version, 5);
} else if (item.statMap.get("crafted")) {
build_string += "CR-"+encodeCraft(item);
} else if (item.statMap.get("category") === "tome") {
let tome_id = item.statMap.get("id");
if (tome_id <= 60) {
// valid normal tome. ID 61-63 is for NONE tomes.
build_version = Math.max(build_version, 6);
}
tome_string += Base64.fromIntN(tome_id, 1);
} else {
build_string += Base64.fromIntN(item.statMap.get("id"), 3);
}
}
for (const skp of skillpoints) {
build_string += Base64.fromIntN(skp, 2); // Maximum skillpoints: 2048
}
build_string += Base64.fromIntN(build.level, 2);
for (const _powderset of powders) {
let n_bits = Math.ceil(_powderset.length / 6);
build_string += Base64.fromIntN(n_bits, 1); // Hard cap of 378 powders.
// Slice copy.
let powderset = _powderset.slice();
while (powderset.length != 0) {
let firstSix = powderset.slice(0,6).reverse();
let powder_hash = 0;
for (const powder of firstSix) {
powder_hash = (powder_hash << 5) + 1 + powder; // LSB will be extracted first.
}
build_string += Base64.fromIntN(powder_hash, 5);
powderset = powderset.slice(6);
}
}
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;
}
}
function copyBuild() {
copyTextToClipboard(url_base+location.hash);
document.getElementById("copy-button").textContent = "Copied!";
}
function shareBuild(build) {
if (build) {
let text = url_base+location.hash+"\n"+
"WynnBuilder build:\n"+
"> "+build.items[0].statMap.get("displayName")+"\n"+
"> "+build.items[1].statMap.get("displayName")+"\n"+
"> "+build.items[2].statMap.get("displayName")+"\n"+
"> "+build.items[3].statMap.get("displayName")+"\n"+
"> "+build.items[4].statMap.get("displayName")+"\n"+
"> "+build.items[5].statMap.get("displayName")+"\n"+
"> "+build.items[6].statMap.get("displayName")+"\n"+
"> "+build.items[7].statMap.get("displayName")+"\n"+
"> "+build.items[15].statMap.get("displayName")+" ["+build_powders[4].map(x => powderNames.get(x)).join("")+"]";
copyTextToClipboard(text);
document.getElementById("share-button").textContent = "Copied!";
}
}
/**
* Ability tree encode and decode functions
*
* Based on a traversal, basically only uses bits to represent the nodes that are on (and "dark" outgoing edges).
* credit: SockMower
*/
/**
* Return: BitVector
*/
function encode_atree(atree, atree_state) {
let ret_vec = new BitVector(0, 0);
function traverse(head, atree_state, visited, ret) {
for (const child of head.children) {
if (visited.has(child.ability.id)) { continue; }
visited.set(child.ability.id, true);
if (atree_state.get(child.ability.id).active) {
ret.append(1, 1);
traverse(child, atree_state, visited, ret);
}
else {
ret.append(0, 1);
}
}
}
traverse(atree[0], atree_state, new Map(), ret_vec);
return ret_vec;
}
/**
* Return: List of active nodes
*/
function decode_atree(atree, bits) {
let i = 0;
let ret = [];
ret.push(atree[0]);
function traverse(head, visited, ret) {
for (const child of head.children) {
if (visited.has(child.ability.id)) { continue; }
visited.set(child.ability.id, true);
if (bits.read_bit(i)) {
i += 1;
ret.push(child);
traverse(child, visited, ret);
}
else {
i += 1;
}
}
}
traverse(atree[0], new Map(), ret);
return ret;
}

View file

@ -51,28 +51,247 @@ const armorTypes = [ "helmet", "chestplate", "leggings", "boots" ];
const accessoryTypes = [ "ring", "bracelet", "necklace" ];
const weaponTypes = [ "wand", "spear", "bow", "dagger", "relik" ];
const consumableTypes = [ "potion", "scroll", "food"];
const tomeTypes = ["armorTome", "weaponTome", "guildTome"]; //"dungeonTome", "gatheringTome", "slayingTome"
const tome_types = ['weaponTome', 'armorTome', 'guildTome'];
const attackSpeeds = ["SUPER_SLOW", "VERY_SLOW", "SLOW", "NORMAL", "FAST", "VERY_FAST", "SUPER_FAST"];
const baseDamageMultiplier = [ 0.51, 0.83, 1.5, 2.05, 2.5, 3.1, 4.3 ];
//0.51, 0.82, 1.50, 2.05, 2.50, 3.11, 4.27
const classes = ["Warrior", "Assassin", "Mage", "Archer", "Shaman"];
const wep_to_class = new Map([["dagger", "Assassin"], ["spear", "Warrior"], ["wand", "Mage"], ["bow", "Archer"], ["relik", "Shaman"]])
const tiers = ["Normal", "Unique", "Rare", "Legendary", "Fabled", "Mythic", "Set", "Crafted"] //I'm not sure why you would make a custom crafted but if you do you should be able to use it w/ the correct powder formula
const types = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(consumableTypes).concat(tomeTypes).map(x => x.substring(0,1).toUpperCase() + x.substring(1));
const all_types = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(consumableTypes).concat(tome_types).map(x => x.substring(0,1).toUpperCase() + x.substring(1));
//weaponTypes.push("sword");
//console.log(types)
let itemTypes = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(tomeTypes);
let itemTypes = armorTypes.concat(accessoryTypes).concat(weaponTypes).concat(tome_types);
let elementIcons = ["\u2724","\u2726", "\u2749", "\u2739", "\u274b" ];
let skpReqs = skp_order.map(x => x + "Req");
let item_fields = [ "name", "displayName", "lore", "color", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "defReq", "agiReq", "hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "str", "dex", "int", "agi", "def", "thorns", "expd", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "fixID", "category", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rainbowRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd", "id", "majorIds", "dmgMobs", "defMobs"];
let item_fields = [ "name", "displayName", "lore", "color", "tier", "set", "slots", "type", "material", "drop", "quest", "restrict", "nDam", "fDam", "wDam", "aDam", "tDam", "eDam", "atkSpd", "hp", "fDef", "wDef", "aDef", "tDef", "eDef", "lvl", "classReq", "strReq", "dexReq", "intReq", "defReq", "agiReq", "hprPct", "mr", "sdPct", "mdPct", "ls", "ms", "xpb", "lb", "ref", "str", "dex", "int", "agi", "def", "thorns", "expd", "spd", "atkTier", "poison", "hpBonus", "spRegen", "eSteal", "hprRaw", "sdRaw", "mdRaw", "fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct", "fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct", "fixID", "category", "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4", "rSdRaw", "sprint", "sprintReg", "jh", "lq", "gXp", "gSpd", "id", "majorIds", "damMobs", "defMobs",
// wynn2 damages.
"eMdPct","eMdRaw","eSdPct","eSdRaw",/*"eDamPct"*/,"eDamRaw","eDamAddMin","eDamAddMax",
"tMdPct","tMdRaw","tSdPct","tSdRaw",/*"tDamPct"*/,"tDamRaw","tDamAddMin","tDamAddMax",
"wMdPct","wMdRaw","wSdPct","wSdRaw",/*"wDamPct"*/,"wDamRaw","wDamAddMin","wDamAddMax",
"fMdPct","fMdRaw","fSdPct","fSdRaw",/*"fDamPct"*/,"fDamRaw","fDamAddMin","fDamAddMax",
"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
"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" ]
//File reading for ID translations for JSON purposes
let reversetranslations = new Map();
let translations = new Map([["name", "name"], ["displayName", "displayName"], ["tier", "tier"], ["set", "set"], ["sockets", "slots"], ["type", "type"], ["dropType", "drop"], ["quest", "quest"], ["restrictions", "restrict"], ["damage", "nDam"], ["fireDamage", "fDam"], ["waterDamage", "wDam"], ["airDamage", "aDam"], ["thunderDamage", "tDam"], ["earthDamage", "eDam"], ["attackSpeed", "atkSpd"], ["health", "hp"], ["fireDefense", "fDef"], ["waterDefense", "wDef"], ["airDefense", "aDef"], ["thunderDefense", "tDef"], ["earthDefense", "eDef"], ["level", "lvl"], ["classRequirement", "classReq"], ["strength", "strReq"], ["dexterity", "dexReq"], ["intelligence", "intReq"], ["agility", "agiReq"], ["defense", "defReq"], ["healthRegen", "hprPct"], ["manaRegen", "mr"], ["spellDamage", "sdPct"], ["damageBonus", "mdPct"], ["lifeSteal", "ls"], ["manaSteal", "ms"], ["xpBonus", "xpb"], ["lootBonus", "lb"], ["reflection", "ref"], ["strengthPoints", "str"], ["dexterityPoints", "dex"], ["intelligencePoints", "int"], ["agilityPoints", "agi"], ["defensePoints", "def"], ["thorns", "thorns"], ["exploding", "expd"], ["speed", "spd"], ["attackSpeedBonus", "atkTier"], ["poison", "poison"], ["healthBonus", "hpBonus"], ["soulPoints", "spRegen"], ["emeraldStealing", "eSteal"], ["healthRegenRaw", "hprRaw"], ["spellDamageRaw", "sdRaw"], ["damageBonusRaw", "mdRaw"], ["bonusFireDamage", "fDamPct"], ["bonusWaterDamage", "wDamPct"], ["bonusAirDamage", "aDamPct"], ["bonusThunderDamage", "tDamPct"], ["bonusEarthDamage", "eDamPct"], ["bonusFireDefense", "fDefPct"], ["bonusWaterDefense", "wDefPct"], ["bonusAirDefense", "aDefPct"], ["bonusThunderDefense", "tDefPct"], ["bonusEarthDefense", "eDefPct"], ["type", "type"], ["identified", "fixID"], ["skin", "skin"], ["category", "category"], ["spellCostPct1", "spPct1"], ["spellCostRaw1", "spRaw1"], ["spellCostPct2", "spPct2"], ["spellCostRaw2", "spRaw2"], ["spellCostPct3", "spPct3"], ["spellCostRaw3", "spRaw3"], ["spellCostPct4", "spPct4"], ["spellCostRaw4", "spRaw4"], ["rainbowSpellDamageRaw", "rainbowRaw"], ["sprint", "sprint"], ["sprintRegen", "sprintReg"], ["jumpHeight", "jh"], ["lootQuality", "lq"], ["gatherXpBonus", "gXp"], ["gatherSpeed", "gSpd"]]);
//does not include dmgMobs (wep tomes) and defMobs (armor tomes)
let translations = new Map([["name", "name"], ["displayName", "displayName"], ["tier", "tier"], ["set", "set"], ["sockets", "slots"], ["type", "type"], ["dropType", "drop"], ["quest", "quest"], ["restrictions", "restrict"], ["damage", "nDam"], ["fireDamage", "fDam"], ["waterDamage", "wDam"], ["airDamage", "aDam"], ["thunderDamage", "tDam"], ["earthDamage", "eDam"], ["attackSpeed", "atkSpd"], ["health", "hp"], ["fireDefense", "fDef"], ["waterDefense", "wDef"], ["airDefense", "aDef"], ["thunderDefense", "tDef"], ["earthDefense", "eDef"], ["level", "lvl"], ["classRequirement", "classReq"], ["strength", "strReq"], ["dexterity", "dexReq"], ["intelligence", "intReq"], ["agility", "agiReq"], ["defense", "defReq"], ["healthRegen", "hprPct"], ["manaRegen", "mr"], ["spellDamage", "sdPct"], ["damageBonus", "mdPct"], ["lifeSteal", "ls"], ["manaSteal", "ms"], ["xpBonus", "xpb"], ["lootBonus", "lb"], ["reflection", "ref"], ["strengthPoints", "str"], ["dexterityPoints", "dex"], ["intelligencePoints", "int"], ["agilityPoints", "agi"], ["defensePoints", "def"], ["thorns", "thorns"], ["exploding", "expd"], ["speed", "spd"], ["attackSpeedBonus", "atkTier"], ["poison", "poison"], ["healthBonus", "hpBonus"], ["soulPoints", "spRegen"], ["emeraldStealing", "eSteal"], ["healthRegenRaw", "hprRaw"], ["spellDamageRaw", "sdRaw"], ["damageBonusRaw", "mdRaw"], ["bonusFireDamage", "fDamPct"], ["bonusWaterDamage", "wDamPct"], ["bonusAirDamage", "aDamPct"], ["bonusThunderDamage", "tDamPct"], ["bonusEarthDamage", "eDamPct"], ["bonusFireDefense", "fDefPct"], ["bonusWaterDefense", "wDefPct"], ["bonusAirDefense", "aDefPct"], ["bonusThunderDefense", "tDefPct"], ["bonusEarthDefense", "eDefPct"], ["type", "type"], ["identified", "fixID"], ["skin", "skin"], ["category", "category"], ["spellCostPct1", "spPct1"], ["spellCostRaw1", "spRaw1"], ["spellCostPct2", "spPct2"], ["spellCostRaw2", "spRaw2"], ["spellCostPct3", "spPct3"], ["spellCostRaw3", "spRaw3"], ["spellCostPct4", "spPct4"], ["spellCostRaw4", "spRaw4"], ["rainbowSpellDamageRaw", "rSdRaw"], ["sprint", "sprint"], ["sprintRegen", "sprintReg"], ["jumpHeight", "jh"], ["lootQuality", "lq"], ["gatherXpBonus", "gXp"], ["gatherSpeed", "gSpd"]]);
//does not include damMobs (wep tomes) and defMobs (armor tomes)
for (const [k, v] of translations) {
reversetranslations.set(v, k);
}
let nonRolledIDs = [
"name",
"lore",
"displayName",
"tier",
"set",
"slots",
"type",
"material",
"drop",
"quest",
"restrict",
"nDam", "fDam", "wDam", "aDam", "tDam", "eDam",
"atkSpd",
"hp",
"fDef", "wDef", "aDef", "tDef", "eDef",
"lvl",
"classReq",
"strReq", "dexReq", "intReq", "defReq", "agiReq",
"str", "dex", "int", "agi", "def",
"fixID",
"category",
"id",
"skillpoints",
"reqs",
"nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_",
"majorIds",
"damMobs",
"defMobs",
// wynn2 damages.
"eDamAddMin","eDamAddMax",
"tDamAddMin","tDamAddMax",
"wDamAddMin","wDamAddMax",
"fDamAddMin","fDamAddMax",
"aDamAddMin","aDamAddMax",
"nDamAddMin","nDamAddMax", // neutral which is now an element
"damAddMin","damAddMax", // all
"rDamAddMin","rDamAddMax" // rainbow (the "element" of all minus neutral).
];
let rolledIDs = [
"hprPct",
"mr",
"sdPct",
"mdPct",
"ls",
"ms",
"xpb",
"lb",
"ref",
"thorns",
"expd",
"spd",
"atkTier",
"poison",
"hpBonus",
"spRegen",
"eSteal",
"hprRaw",
"sdRaw",
"mdRaw",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct",
"spPct1", "spRaw1",
"spPct2", "spRaw2",
"spPct3", "spRaw3",
"spPct4", "spRaw4",
"pDamRaw",
"sprint",
"sprintReg",
"jh",
"lq",
"gXp",
"gSpd",
// wynn2 damages.
"eMdPct","eMdRaw","eSdPct","eSdRaw",/*"eDamPct"*/,"eDamRaw","eDamAddMin","eDamAddMax",
"tMdPct","tMdRaw","tSdPct","tSdRaw",/*"tDamPct"*/,"tDamRaw","tDamAddMin","tDamAddMax",
"wMdPct","wMdRaw","wSdPct","wSdRaw",/*"wDamPct"*/,"wDamRaw","wDamAddMin","wDamAddMax",
"fMdPct","fMdRaw","fSdPct","fSdRaw",/*"fDamPct"*/,"fDamRaw","fDamAddMin","fDamAddMax",
"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
];
let reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ];
/**
* Take an item with id list and turn it into a set of minrolls and maxrolls.
*/
function expandItem(item) {
let minRolls = new Map();
let maxRolls = new Map();
let expandedItem = new Map();
if (item.fixID) { //The item has fixed IDs.
expandedItem.set("fixID",true);
for (const id of rolledIDs) { //all rolled IDs are numerical
let val = (item[id] || 0);
minRolls.set(id,val);
maxRolls.set(id,val);
}
} else { //The item does not have fixed IDs.
for (const id of rolledIDs) {
let val = (item[id] || 0);
if (val > 0) { // positive rolled IDs
if (reversedIDs.includes(id)) {
maxRolls.set(id,idRound(val*0.3));
minRolls.set(id,idRound(val*1.3));
} else {
maxRolls.set(id,idRound(val*1.3));
minRolls.set(id,idRound(val*0.3));
}
} else if (val < 0) { //negative rolled IDs
if (reversedIDs.includes(id)) {
maxRolls.set(id,idRound(val*1.3));
minRolls.set(id,idRound(val*0.7));
}
else {
maxRolls.set(id,idRound(val*0.7));
minRolls.set(id,idRound(val*1.3));
}
}
else { // if val == 0
// NOTE: DO NOT remove this case! idRound behavior does not round to 0!
maxRolls.set(id,0);
minRolls.set(id,0);
}
}
}
for (const id of nonRolledIDs) {
expandedItem.set(id,item[id]);
}
expandedItem.set("minRolls",minRolls);
expandedItem.set("maxRolls",maxRolls);
return expandedItem;
}
class Item {
constructor(item_obj) {
this.statMap = expandItem(item_obj);
}
}
/* Takes in an ingredient object and returns an equivalent Map().
*/
function expandIngredient(ing) {
let expandedIng = new Map();
let mapIds = ['consumableIDs', 'itemIDs', 'posMods'];
for (const id of mapIds) {
let idMap = new Map();
for (const key of Object.keys(ing[id])) {
idMap.set(key, ing[id][key]);
}
expandedIng.set(id, idMap);
}
let normIds = ['lvl','name', 'displayName','tier','skills','id'];
for (const id of normIds) {
expandedIng.set(id, ing[id]);
}
if (ing['isPowder']) {
expandedIng.set("isPowder",ing['isPowder']);
expandedIng.set("pid",ing['pid']);
}
//now the actually hard one
let idMap = new Map();
idMap.set("minRolls", new Map());
idMap.set("maxRolls", new Map());
for (const field of ingFields) {
let val = (ing['ids'][field] || 0);
idMap.get("minRolls").set(field, val['minimum']);
idMap.get("maxRolls").set(field, val['maximum']);
}
expandedIng.set("ids",idMap);
return expandedIng;
}
/* Takes in a recipe object and returns an equivalent Map().
*/
function expandRecipe(recipe) {
let expandedRecipe = new Map();
let normIDs = ["name", "skill", "type","id"];
for (const id of normIDs) {
expandedRecipe.set(id,recipe[id]);
}
let rangeIDs = ["durability","lvl", "healthOrDamage", "duration", "basicDuration"];
for (const id of rangeIDs) {
if(recipe[id]){
expandedRecipe.set(id, [recipe[id]['minimum'], recipe[id]['maximum']]);
} else {
expandedRecipe.set(id, [0,0]);
}
}
expandedRecipe.set("materials", [ new Map([ ["item", recipe['materials'][0]['item']], ["amount", recipe['materials'][0]['amount']] ]) , new Map([ ["item", recipe['materials'][1]['item']], ["amount",recipe['materials'][1]['amount'] ] ]) ]);
return expandedRecipe;
}
/*An independent helper function that rounds a rolled ID to the nearest integer OR brings the roll away from 0.
* @param id
*/
function idRound(id){
rounded = Math.round(id);
if(rounded == 0){
return 1; //this is a hack, will need changing along w/ rest of ID system if anything changes
}else{
return rounded;
}
}

File diff suppressed because it is too large Load diff

1160
js/builder_graph.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,122 +1,263 @@
let _ALL_NODES = new Map();
let all_nodes = [];
class ComputeNode {
/***
/**
* Make a generic compute node.
* Adds the node to the global map of nodenames to nodes (for calling from html listeners).
*
* @param name : Name of the node (string). Must be unique. Must "fit in" a JS string (terminated by single quotes).
*/
constructor(name) {
if (_ALL_NODES.has(name)) {
throw 'Duplicate node name: ' + name;
}
_ALL_NODES.set(name, this)
this.inputs = [];
this.inputs = []; // parent nodes
this.input_translation = new Map();
this.children = [];
this.value = 0;
this.value = null;
this.name = name;
this.update_task = null;
this.update_time = Date.now();
this.fail_cb = false; // Set to true to force updates even if parent failed.
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);
}
/***
/**
* Request update of this compute node. Pushes updates to children.
*/
update(timestamp) {
if (timestamp <= this.update_time) {
update() {
if (this.inputs_dirty_count != 0) {
return;
}
this.update_time = timestamp;
let value_map = Map();
if (this.dirty === 0) {
return;
}
if (this.dirty == 2) {
let calc_inputs = new Map();
for (const input of this.inputs) {
value_map.set(input.name, input.get_value());
calc_inputs.set(this.input_translation.get(input.name), input.value);
}
this.value = this.compute_func();
this.value = this.compute_func(calc_inputs);
}
this.dirty = 0;
for (const child of this.children) {
child.update();
child.mark_input_clean(this.name, this.value);
}
return this;
}
/**
* Mark parent as not dirty. Propagates calculation if all inputs are present.
*/
mark_input_clean(input_name, value) {
if (value !== null || this.fail_cb) {
if (this.inputs_dirty.get(input_name)) {
this.inputs_dirty.set(input_name, false);
this.inputs_dirty_count -= 1;
}
if (this.inputs_dirty_count === 0) {
this.update();
}
}
}
/***
mark_input_dirty(input_name) {
if (!this.inputs_dirty.get(input_name)) {
this.inputs_dirty.set(input_name, true);
this.inputs_dirty_count += 1;
}
}
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(dirty_state);
}
}
return this;
}
/**
* Get value of this compute node. Can't trigger update cascades (push based update, not pull based.)
*/
get_value() {
return this.value
}
/***
/**
* Abstract method for computing something. Return value is set into this.value
*/
compute_func() {
compute_func(input_map) {
throw "no compute func specified";
}
link_to(parent_node) {
/**
* Add link to a parent compute node, optionally with an alias.
*/
link_to(parent_node, link_name) {
this.inputs.push(parent_node)
link_name = (link_name !== undefined) ? link_name : parent_node.name;
this.input_translation.set(parent_node.name, link_name);
if (parent_node.dirty || (parent_node.value === null && !this.fail_cb)) {
this.inputs_dirty_count += 1;
this.inputs_dirty.set(parent_node.name, true);
}
parent_node.children.push(this);
return this;
}
/**
* Delete a link to a parent node.
* TODO: time complexity of list deletion (not super relevant but it hurts my soul)
*/
remove_link(parent_node) {
const idx = this.inputs.indexOf(parent_node); // Get idx
this.inputs.splice(idx, 1); // remove element
this.input_translation.delete(parent_node.name);
const was_dirty = this.inputs_dirty.get(parent_node.name);
this.inputs_dirty.delete(parent_node.name);
if (was_dirty) {
this.inputs_dirty_count -= 1;
}
const idx2 = parent_node.children.indexOf(this);
parent_node.children.splice(idx2, 1);
return this;
}
}
/***
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.
*
* @param node_name : ComputeNode name to schedule an update for.
* @param node : ComputeNode to schedule an update for.
*/
function calcSchedule(node_name) {
node = _ALL_NODES.get(node_name);
function calcSchedule(node, timeout) {
if (node.update_task !== null) {
clearTimeout(node.update_task);
}
node.mark_dirty();
node.update_task = setTimeout(function() {
const timestamp = Date.now();
node.update(timestamp);
node.update();
node.update_task = null;
}, 500);
}, timeout);
}
/***
* Node for getting an item's stats from an item input field.
*/
class ItemStats extends ComputeNode {
/***
* 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) {
class PrintNode extends ComputeNode {
constructor(name) {
super(name);
this.input_field.setAttribute("onInput", "calcSchedule('"+name+"');");
this.input_field = item_input_field;
this.none_item = none_item;
this.fail_cb = true;
}
compute_func() {
// built on the assumption of no one will type in CI/CR letter by letter
let item_text = this.input_field.value;
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 = itemMap.get(item_text);
}
else if (tomeMap.has(item_text)) {
item = tomeMap.get(item_text);
}
if (!item) {
return this.none_item;
}
return item;
compute_func(input_map) {
console.log([this.name, input_map]);
return null;
}
}
/**
* Node for getting an input from an input field.
* Fires updates whenever the input field is updated.
*
* Signature: InputNode() => str
*/
class InputNode extends ComputeNode {
constructor(name, input_field) {
super(name);
this.input_field = input_field;
this.input_field.addEventListener("input", () => calcSchedule(this, 500));
this.input_field.addEventListener("change", () => calcSchedule(this, 5));
//calcSchedule(this); Manually fire first update for better control
}
compute_func(input_map) {
return this.input_field.value;
}
}
/**
* Passthrough node for simple aggregation.
* Unfortunately if you use this too much you get layers and layers of maps...
*
* Signature: PassThroughNode(**kwargs) => Map[...]
*/
class PassThroughNode extends ComputeNode {
constructor(name) {
super(name);
this.breakout_nodes = new Map();
}
compute_func(input_map) {
return input_map;
}
/**
* Get a ComputeNode that will "break out" one part of this aggregation input.
* There is some overhead to this operation because ComputeNode is not exactly a free abstraction... oof
* Also you will recv updates whenever any input that is part of the aggregation changes even
* if the specific sub-input didn't change.
*
* Parameters:
* sub-input: The key to listen to
*/
get_node(sub_input) {
if (this.breakout_nodes.has(sub_input)) {
return this.breakout_nodes.get(sub_input);
}
const _name = this.name;
const ret = new (class extends ComputeNode {
constructor() { super('passthrough-'+_name+'-'+sub_input); }
compute_func(input_map) { return input_map.get(_name).get(sub_input); }
})().link_to(this);
this.breakout_nodes.set(sub_input, ret);
return ret;
}
}

View file

@ -170,7 +170,6 @@ class Craft{
statMap.set(e + "Dam", "0-0");
statMap.set(e + "DamLow", "0-0");
}
//statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]);
statMap.set("category","weapon");
statMap.set("atkSpd",this.atkSpd);
}
@ -190,7 +189,6 @@ class Craft{
let amounts = this.recipe.get("materials").map(x=> x.get("amount"));
//Mat Multipliers - should work!
matmult = (tierToMult[tiers[0]]*amounts[0] + tierToMult[tiers[1]]*amounts[1]) / (amounts[0]+amounts[1]);
console.log(matmult);
let low = this.recipe.get("healthOrDamage")[0];
let high = this.recipe.get("healthOrDamage")[1];
@ -382,12 +380,10 @@ class Craft{
statMap.set("reqs",[0,0,0,0,0]);
statMap.set("skillpoints", [0,0,0,0,0]);
statMap.set("damageBonus",[0,0,0,0,0]);
for (const e in skp_order) {
statMap.set(skp_order[e], statMap.get("maxRolls").has(skp_order[e]) ? statMap.get("maxRolls").get(skp_order[e]) : 0);
statMap.get("skillpoints")[e] = statMap.get("maxRolls").has(skp_order[e]) ? statMap.get("maxRolls").get(skp_order[e]) : 0;
statMap.get("reqs")[e] = statMap.has(skp_order[e]+"Req") && !consumableTypes.includes(statMap.get("type"))? statMap.get(skp_order[e]+"Req") : 0;
statMap.get("damageBonus")[e] = statMap.has(skp_order[e]+"DamPct") ? statMap.get(skp_order[e]+"DamPct") : 0;
}
for (const id of rolledIDs) {
if (statMap.get("minRolls").has(id)) {

View file

@ -172,16 +172,17 @@ function calculateCraft() {
document.getElementById("mat-2").textContent = recipe.get("materials")[1].get("item").split(" ").slice(1).join(" ") + " Tier:";
//Display Recipe Stats
displaysq2RecipeStats(player_craft, "recipe-stats");
displayRecipeStats(player_craft, "recipe-stats");
//Display Craft Stats
// displayCraftStats(player_craft, "craft-stats");
let mock_item = player_craft.statMap;
displaysq2ExpandedItem(mock_item, "craft-stats");
apply_weapon_powders(mock_item);
displayExpandedItem(mock_item, "craft-stats");
//Display Ingredients' Stats
for (let i = 1; i < 7; i++) {
displaysq2ExpandedIngredient(player_craft.ingreds[i-1] , "ing-"+i+"-stats");
displayExpandedIngredient(player_craft.ingreds[i-1] , "ing-"+i+"-stats");
}
//Display Warnings - only ingred type warnings for now
let warning_elem = document.getElementById("craft-warnings");
@ -264,7 +265,7 @@ function populateFields() {
*/
function copyRecipeHash() {
if (player_craft) {
copyTextToClipboard("CR-"+location.hash);
copyTextToClipboard("CR-"+location.hash.slice(1));
document.getElementById("copy-hash-button").textContent = "Copied!";
}
}
@ -341,7 +342,7 @@ function toggleMaterial(buttonId) {
*/
function updateCraftedImage() {
let input = document.getElementById("recipe-choice");
if (item_types.includes(input.value)) {
if (all_types.includes(input.value)) {
document.getElementById("recipe-img").src = "../media/items/" + (newIcons ? "new/":"old/") + "generic-" + input.value.toLowerCase() + ".png";
}
@ -364,4 +365,8 @@ function resetFields() {
calculateCraft();
}
load_ing_init(init_crafter);
(async function() {
let load_promises = [ load_ing_init() ];
await Promise.all(load_promises);
init_crafter();
})();

View file

@ -175,6 +175,7 @@ function getCustomFromHash(hash) {
}
}
statMap.set("hash", "CI-" + name);
statMap.set("custom", true);
return new Custom(statMap);
}
} catch (error) {

View file

@ -370,7 +370,7 @@ function useBaseItem(elem) {
//Check items db.
for (const [name,itemObj] of itemMap) {
if (itemName === name) {
baseItem = expandItem(itemObj, []);
baseItem = expandItem(itemObj);
break;
}
}

View file

@ -1,190 +1,297 @@
const damageMultipliers = new Map([ ["allytotem", .15], ["yourtotem", .35], ["vanish", 0.80], ["warscream", 0.10], ["bash", 0.50] ]);
// Calculate spell damage given a spell elemental conversion table, and a spell multiplier.
// If spell mult is 0, its melee damage and we don't multiply by attack speed.
// externalStats should be a map
function calculateSpellDamage(stats, spellConversions, rawModifier, pctModifier, spellMultiplier, weapon, total_skillpoints, damageMultiplier, externalStats) {
let buildStats = new Map(stats);
let tooltipinfo = new Map();
//6x for damages, normal min normal max crit min crit max
let damageformulas = [["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "],["Min: = ","Max: = ","Min: = ","Max: = "]];
if(externalStats) { //if nothing is passed in, then this hopefully won't trigger
for (const entry of externalStats) {
const key = entry[0];
const value = entry[1];
if (typeof value === "number") {
buildStats.set(key, buildStats.get(key) + value);
} else if (Array.isArray(value)) {
arr = [];
for (let j = 0; j < value.length; j++) {
arr[j] = buildStats.get(key)[j] + value[j];
function get_base_dps(item) {
const attack_speed_mult = baseDamageMultiplier[attackSpeeds.indexOf(item.get("atkSpd"))];
//SUPER JANK @HPP PLS FIX
if (item.get("tier") !== "Crafted") {
let total_damage = 0;
for (const damage_k of damage_keys) {
damages = item.get(damage_k);
total_damage += damages[0] + damages[1];
}
buildStats.set(key, arr);
}
}
}
let powders = weapon.get("powders").slice();
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
let damages = [];
const rawDamages = buildStats.get("damageRaw");
for (let i = 0; i < rawDamages.length; i++) {
const damage_vals = rawDamages[i].split("-").map(Number);
damages.push(damage_vals);
}
// Applying spell conversions
let neutralBase = damages[0].slice();
let neutralRemainingRaw = damages[0].slice();
//powder application for custom crafted weapons is inherently fucked because there is no base. Unsure what to do.
//Powder application for Crafted weapons - this implementation is RIGHT YEAAAAAAAAA
//1st round - apply each as ingred, 2nd round - apply as normal
if (weapon.get("tier") === "Crafted") {
let damageBases = buildStats.get("damageBases").slice();
for (const p of powders.concat(weapon.get("ingredPowders"))) {
let powder = powderStats[p]; //use min, max, and convert
let element = Math.floor((p+0.01)/6); //[0,4], the +0.01 attempts to prevent division error
let diff = Math.floor(damageBases[0] * powder.convert/100);
damageBases[0] -= diff;
damageBases[element+1] += diff + Math.floor( (powder.min + powder.max) / 2 );
}
//update all damages
if(!weapon.get("custom")) {
for (let i = 0; i < damages.length; i++) {
damages[i] = [Math.floor(damageBases[i] * 0.9), Math.floor(damageBases[i] * 1.1)];
}
}
neutralRemainingRaw = damages[0].slice();
neutralBase = damages[0].slice();
}
for (let i = 0; i < 5; ++i) {
let conversionRatio = spellConversions[i+1]/100;
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);
let max_diff = Math.min(neutralRemainingRaw[1], conversionRatio * neutralBase[1]);
damages[i+1][0] = Math.floor(round_near(damages[i+1][0] + min_diff));
damages[i+1][1] = Math.floor(round_near(damages[i+1][1] + max_diff));
neutralRemainingRaw[0] = Math.floor(round_near(neutralRemainingRaw[0] - min_diff));
neutralRemainingRaw[1] = Math.floor(round_near(neutralRemainingRaw[1] - max_diff));
}
//apply powders to weapon
for (const powderID of powders) {
const powder = powderStats[powderID];
// Bitwise to force conversion to integer (integer division).
const element = (powderID/6) | 0;
let conversionRatio = powder.convert/100;
if (neutralRemainingRaw[1] > 0) {
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);
let max_diff = Math.min(neutralRemainingRaw[1], conversionRatio * neutralBase[1]);
damages[element+1][0] = Math.floor(round_near(damages[element+1][0] + min_diff));
damages[element+1][1] = Math.floor(round_near(damages[element+1][1] + max_diff));
neutralRemainingRaw[0] = Math.floor(round_near(neutralRemainingRaw[0] - min_diff));
neutralRemainingRaw[1] = Math.floor(round_near(neutralRemainingRaw[1] - max_diff));
}
damages[element+1][0] += powder.min;
damages[element+1][1] += powder.max;
}
//console.log(tooltipinfo);
damages[0] = neutralRemainingRaw;
tooltipinfo.set("damageBases", damages);
let damageMult = damageMultiplier;
let melee = false;
// If we are doing melee calculations:
tooltipinfo.set("dmgMult", damageMult);
if (spellMultiplier == 0) {
spellMultiplier = 1;
melee = true;
return total_damage * attack_speed_mult / 2;
}
else {
tooltipinfo.set("dmgMult", `(${tooltipinfo.get("dmgMult")} * ${spellMultiplier} * ${baseDamageMultiplier[attackSpeeds.indexOf(buildStats.get("atkSpd"))]})`)
damageMult *= spellMultiplier * baseDamageMultiplier[attackSpeeds.indexOf(buildStats.get("atkSpd"))];
let total_damage_min = 0;
let total_damage_max = 0;
for (const damage_k of damage_keys) {
damages = item.get(damage_k);
total_damage_min += damages[0][0] + damages[0][1];
total_damage_max += damages[1][0] + damages[1][1];
}
//console.log(damages);
//console.log(damageMult);
tooltipinfo.set("rawModifier", `(${rawModifier} * ${spellMultiplier} * ${damageMultiplier})`);
rawModifier *= spellMultiplier * damageMultiplier;
let totalDamNorm = [0, 0];
let totalDamCrit = [0, 0];
let damages_results = [];
// 0th skillpoint is strength, 1st is dex.
let str = total_skillpoints[0];
let strBoost = 1 + skillPointsToPercentage(str);
if(!melee){
let baseDam = rawModifier * strBoost;
let baseDamCrit = rawModifier * (1 + strBoost);
totalDamNorm = [baseDam, baseDam];
totalDamCrit = [baseDamCrit, baseDamCrit];
for (let arr of damageformulas) {
arr = arr.map(x => x + " + " +tooltipinfo.get("rawModifier"));
total_damage_min = attack_speed_mult * total_damage_min / 2;
total_damage_max = attack_speed_mult * total_damage_max / 2;
return [total_damage_min, total_damage_max];
}
}
let staticBoost = (pctModifier / 100.);
tooltipinfo.set("staticBoost", `${(pctModifier/ 100.).toFixed(2)}`);
tooltipinfo.set("skillBoost",["","","","","",""]);
let skillBoost = [0];
for (let i in total_skillpoints) {
skillBoost.push(skillPointsToPercentage(total_skillpoints[i]) + buildStats.get("damageBonus")[i] / 100.);
tooltipinfo.get("skillBoost")[parseInt(i,10)+1] = `(${skillPointsToPercentage(total_skillpoints[i]).toFixed(2)} + ${(buildStats.get("damageBonus")[i]/100.).toFixed(2)})`
}
tooltipinfo.get("skillBoost")[0] = undefined;
for (let i in damages) {
let damageBoost = 1 + skillBoost[i] + staticBoost;
tooltipinfo.set("damageBoost", `(1 + ${(tooltipinfo.get("skillBoost")[i] ? tooltipinfo.get("skillBoost")[i] + " + " : "")} ${tooltipinfo.get("staticBoost")})`)
damages_results.push([
Math.max(damages[i][0] * strBoost * Math.max(damageBoost,0) * damageMult, 0), // Normal min
Math.max(damages[i][1] * strBoost * Math.max(damageBoost,0) * damageMult, 0), // Normal max
Math.max(damages[i][0] * (strBoost + 1) * Math.max(damageBoost,0) * damageMult, 0), // Crit min
Math.max(damages[i][1] * (strBoost + 1) * Math.max(damageBoost,0) * damageMult, 0), // Crit max
]);
damageformulas[i][0] += `(max((${tooltipinfo.get("damageBases")[i][0]} * ${strBoost} * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))`
damageformulas[i][1] += `(max((${tooltipinfo.get("damageBases")[i][1]} * ${strBoost} * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))`
damageformulas[i][2] += `(max((${tooltipinfo.get("damageBases")[i][0]} * ${strBoost} * 2 * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))`
damageformulas[i][3] += `(max((${tooltipinfo.get("damageBases")[i][1]} * ${strBoost} * 2 * max(${tooltipinfo.get("damageBoost")}, 0) * ${tooltipinfo.get("dmgMult")}), 0))`
totalDamNorm[0] += damages_results[i][0];
totalDamNorm[1] += damages_results[i][1];
totalDamCrit[0] += damages_results[i][2];
totalDamCrit[1] += damages_results[i][3];
}
if (melee) {
totalDamNorm[0] += Math.max(strBoost*rawModifier, -damages_results[0][0]);
totalDamNorm[1] += Math.max(strBoost*rawModifier, -damages_results[0][1]);
totalDamCrit[0] += Math.max((strBoost+1)*rawModifier, -damages_results[0][2]);
totalDamCrit[1] += Math.max((strBoost+1)*rawModifier, -damages_results[0][3]);
}
damages_results[0][0] += strBoost*rawModifier;
damages_results[0][1] += strBoost*rawModifier;
damages_results[0][2] += (strBoost + 1)*rawModifier;
damages_results[0][3] += (strBoost + 1)*rawModifier;
for (let i = 0; i < 2; i++) {
damageformulas[0][i] += ` + (${strBoost} * ${tooltipinfo.get("rawModifier")})`
}
for (let i = 2; i < 4; i++) {
damageformulas[0][i] += ` + (2 * ${strBoost} * ${tooltipinfo.get("rawModifier")})`
}
if (totalDamNorm[0] < 0) totalDamNorm[0] = 0;
if (totalDamNorm[1] < 0) totalDamNorm[1] = 0;
if (totalDamCrit[0] < 0) totalDamCrit[0] = 0;
if (totalDamCrit[1] < 0) totalDamCrit[1] = 0;
tooltipinfo.set("damageformulas", damageformulas);
return [totalDamNorm, totalDamCrit, damages_results, tooltipinfo];
}
function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, ignore_speed=false) {
// TODO: Roll all the loops together maybe
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
// 1. Get weapon damage (with powders).
let weapon_damages;
if (weapon.get('tier') === 'Crafted') {
weapon_damages = damage_keys.map(x => weapon.get(x)[1]);
}
else {
weapon_damages = damage_keys.map(x => weapon.get(x));
}
let present = weapon.get(damage_present_key);
// 2. Conversions.
// 2.1. First, apply neutral conversion (scale weapon damage). Keep track of total weapon damage here.
let damages = [];
const neutral_convert = conversions[0] / 100;
let weapon_min = 0;
let weapon_max = 0;
for (const damage of weapon_damages) {
let min_dmg = damage[0] * neutral_convert;
let max_dmg = damage[1] * neutral_convert;
damages.push([min_dmg, max_dmg]);
weapon_min += damage[0];
weapon_max += damage[1];
}
// 2.2. Next, apply elemental conversions using damage computed in step 1.1.
// Also, track which elements are present. (Add onto those present in the weapon itself.)
let total_convert = 0; //TODO get confirmation that this is how raw works.
for (let i = 1; i <= 5; ++i) {
if (conversions[i] > 0) {
const conv_frac = conversions[i]/100;
damages[i][0] += conv_frac * weapon_min;
damages[i][1] += conv_frac * weapon_max;
present[i] = true;
total_convert += conv_frac
}
}
// Also theres prop and rainbow!!
const damage_elements = ['n'].concat(skp_elements); // netwfa
if (!ignore_speed) {
// 3. Apply attack speed multiplier. Ignored for melee single hit
const attack_speed_mult = baseDamageMultiplier[attackSpeeds.indexOf(weapon.get("atkSpd"))];
for (let i = 0; i < 6; ++i) {
damages[i][0] *= attack_speed_mult;
damages[i][1] *= attack_speed_mult;
}
}
// 4. Add additive damage. TODO: Is there separate additive damage?
for (let i = 0; i < 6; ++i) {
if (present[i]) {
damages[i][0] += stats.get(damage_elements[i]+'DamAddMin');
damages[i][1] += stats.get(damage_elements[i]+'DamAddMax');
}
}
// 5. ID bonus.
let specific_boost_str = 'Md';
if (use_spell_damage) {
specific_boost_str = 'Sd';
}
// 5.1: %boost application
let skill_boost = [0]; // no neutral skillpoint booster
for (const skp of skp_order) {
skill_boost.push(skillPointsToPercentage(stats.get(skp)));
}
let static_boost = (stats.get(specific_boost_str.toLowerCase()+'Pct') + stats.get('damPct')) / 100;
// These do not count raw damage. I think. Easy enough to change
let total_min = 0;
let total_max = 0;
for (let i in damages) {
let damage_prefix = damage_elements[i] + specific_boost_str;
let damageBoost = 1 + skill_boost[i] + static_boost
+ ((stats.get(damage_prefix+'Pct') + stats.get(damage_elements[i]+'DamPct')) /100);
damages[i][0] *= Math.max(damageBoost, 0);
damages[i][1] *= Math.max(damageBoost, 0);
// Collect total damage post %boost
total_min += damages[i][0];
total_max += damages[i][1];
}
let total_elem_min = total_min - damages[0][0];
let total_elem_max = total_max - damages[0][1];
// 5.2: Raw application.
let prop_raw = stats.get(specific_boost_str.toLowerCase()+'Raw') + stats.get('damRaw');
let rainbow_raw = stats.get('r'+specific_boost_str+'Raw') + stats.get('rDamRaw');
for (let i in damages) {
let damages_obj = damages[i];
let damage_prefix = damage_elements[i] + specific_boost_str;
// Normie raw
let raw_boost = 0;
if (present[i]) {
raw_boost += stats.get(damage_prefix+'Raw') + stats.get(damage_elements[i]+'DamRaw');
}
// Next, rainraw and propRaw
let min_boost = raw_boost;
let max_boost = raw_boost;
if (total_max > 0) { // TODO: what about total negative all raw?
if (total_elem_min > 0) {
min_boost += (damages_obj[0] / total_min) * prop_raw;
}
max_boost += (damages_obj[1] / total_max) * prop_raw;
}
if (i != 0 && total_elem_max > 0) { // rainraw TODO above
if (total_elem_min > 0) {
min_boost += (damages_obj[0] / total_elem_min) * rainbow_raw;
}
max_boost += (damages_obj[1] / total_elem_max) * rainbow_raw;
}
damages_obj[0] += min_boost * total_convert;
damages_obj[1] += max_boost * total_convert;
}
// 6. Strength boosters
// str/dex, as well as any other mutually multiplicative effects
let strBoost = 1 + skill_boost[1];
let total_dam_norm = [0, 0];
let total_dam_crit = [0, 0];
let damages_results = [];
const damage_mult = stats.get("damageMultiplier");
for (const damage of damages) {
const res = [
damage[0] * strBoost * damage_mult, // Normal min
damage[1] * strBoost * damage_mult, // Normal max
damage[0] * (strBoost + 1) * damage_mult, // Crit min
damage[1] * (strBoost + 1) * damage_mult, // Crit max
];
damages_results.push(res);
total_dam_norm[0] += res[0];
total_dam_norm[1] += res[1];
total_dam_crit[0] += res[2];
total_dam_crit[1] += res[3];
}
if (total_dam_norm[0] < 0) total_dam_norm[0] = 0;
if (total_dam_norm[1] < 0) total_dam_norm[1] = 0;
if (total_dam_crit[0] < 0) total_dam_crit[0] = 0;
if (total_dam_crit[1] < 0) total_dam_crit[1] = 0;
return [total_dam_norm, total_dam_crit, damages_results];
}
/*
Spell schema:
spell: {
name: str internal string name for the spell. Unique identifier, also display
cost: Optional[int] ignored for spells that are not id 1-4
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"
use_atkspd: Optional[bool] [DEFAULT: true] true to factor attack speed, false otherwise.
display: Optional[str] [DEFAULT: "total"] "total" to sum all parts. Or, the name of a spell part
parts: List[part] Parts of this spell (different stuff the spell does basically)
}
NOTE: when using `replace_spell` on an existing spell, all fields become optional.
Specified fields overwrite existing fields; unspecified fields are left unchanged.
There are three possible spell "part" types: damage, heal, and total.
part: spell_damage | spell_heal | spell_total
spell_damage: {
name: str != "total" Name of the part.
type: "damage" [TODO: DEPRECATED/REMOVE] flag signaling what type of part it is. Can infer from fields
multipliers: array[num, 6] floating point spellmults (though supposedly wynn only supports integer mults)
}
spell_heal: {
name: str != "total" Name of the part.
type: "heal" [TODO: DEPRECATED/REMOVE] flag signaling what type of part it is. Can infer from fields
power: num floating point healing power (1 is 100% of max hp).
}
spell_total: {
name: str != "total" Name of the part.
type: "total" [TODO: DEPRECATED/REMOVE] flag signaling what type of part it is. Can infer from fields
hits: Map[str, num] Keys are other part names, numbers are the multipliers. Undefined behavior if subparts
are not the same type of spell. Can only pull from spells defined before it.
}
Before passing to display, use the following structs.
NOTE: total is collapsed into damage or healing.
spell_damage: {
type: "damage" Internal use
name: str Display name of part. Should be human readable
normal_min: array[num, 6] floating point damages (no crit, min), can be less than zero. Order: NETWFA
normal_max: array[num, 6] floating point damages (no crit, max)
normal_total: array[num, 2] (min, max) noncrit total damage (not negative)
crit_min: array[num, 6] floating point damages (crit, min), can be less than zero. Order: NETWFA
crit_max: array[num, 6] floating point damages (crit, max)
crit_total: array[num, 2] (min, max) crit total damage (not negative)
}
spell_heal: {
type: "heal" Internal use
name: str Display name of part. Should be human readable
heal_amount: num floating point HP healed (self)
}
*/
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?
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...
base_spell: 1,
display: "Total Heal",
parts: [
{ name: "First Pulse", power: 0.12 },
{ name: "Second and Third Pulses", power: 0.06 },
{ name: "Total Heal", hits: { "First Pulse": 1, "Second and Third Pulses": 2 } }
]
}],
spear: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Melee", // TODO: name for melee attacks?
base_spell: 0,
scaling: "melee", use_atkspd: false,
display: "Melee",
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}],
bow: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Bow Shot", // TODO: name for melee attacks?
base_spell: 0,
scaling: "melee", use_atkspd: false,
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?
base_spell: 0,
scaling: "melee", use_atkspd: false,
display: "Melee",
parts: [{ name: "Melee", multipliers: [100, 0, 0, 0, 0, 0] }]
}],
relik: [{
type: "replace_spell", // not needed but makes this usable as an "abil part"
name: "Relik Melee", // TODO: name for melee attacks?
base_spell: 0,
spell_type: "damage",
scaling: "melee", use_atkspd: false,
display: "Total",
parts: [
{ name: "Single Beam", multipliers: [33, 0, 0, 0, 0, 0] },
{ name: "Total", hits: { "Single Beam": 3 } }
]
}]
};
const spell_table = {
"wand": [

File diff suppressed because it is too large Load diff

View file

@ -1,99 +1,3 @@
let nonRolledIDs = [
"name",
"lore",
"displayName",
"tier",
"set",
"slots",
"type",
"material",
"drop",
"quest",
"restrict",
"nDam",
"fDam",
"wDam",
"aDam",
"tDam",
"eDam",
"atkSpd",
"hp",
"fDef",
"wDef",
"aDef",
"tDef",
"eDef",
"lvl",
"classReq",
"strReq",
"dexReq",
"intReq",
"defReq",
"agiReq","str",
"dex",
"int",
"agi",
"def",
"fixID",
"category",
"id",
"skillpoints",
"reqs",
"nDam_",
"fDam_",
"wDam_",
"aDam_",
"tDam_",
"eDam_",
"majorIds"];
let rolledIDs = [
"hprPct",
"mr",
"sdPct",
"mdPct",
"ls",
"ms",
"xpb",
"lb",
"ref",
"thorns",
"expd",
"spd",
"atkTier",
"poison",
"hpBonus",
"spRegen",
"eSteal",
"hprRaw",
"sdRaw",
"mdRaw",
"fDamPct",
"wDamPct",
"aDamPct",
"tDamPct",
"eDamPct",
"fDefPct",
"wDefPct",
"aDefPct",
"tDefPct",
"eDefPct",
"spPct1",
"spRaw1",
"spPct2",
"spRaw2",
"spPct3",
"spRaw3",
"spPct4",
"spRaw4",
"rainbowRaw",
"sprint",
"sprintReg",
"jh",
"lq",
"gXp",
"gSpd"
];
let reversedIDs = [ "spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4" ];
let colorMap = new Map(
[
["Normal", "#fff"],
@ -305,23 +209,24 @@ let posModSuffixes = {
/*
* Display commands
*/
let build_overall_display_commands = [
"#table",
let build_all_display_commands = [
"#defense-stats",
"str", "dex", "int", "def", "agi",
"!spacer",
"mr", "ms",
"hprRaw", "hprPct",
"ls",
"sdRaw", "sdPct",
"mdRaw", "mdPct",
"ref", "thorns",
"ls",
"poison",
"expd",
"spd",
"atkTier",
"!elemental",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"!elemental",
"spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4",
"atkTier",
"poison",
"ref", "thorns",
"expd",
"spd",
"rainbowRaw",
"sprint", "sprintReg",
"jh",
@ -331,26 +236,51 @@ let build_overall_display_commands = [
"gXp", "gSpd",
];
let item_display_commands = [
"#cdiv",
let build_offensive_display_commands = [
"str", "dex", "int", "def", "agi",
"mr", "ms",
"sdRaw", "sdPct",
"mdRaw", "mdPct",
"ref", "thorns",
"ls",
"poison",
"expd",
"spd",
"atkTier",
"rainbowRaw",
"!elemental",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"!elemental",
"spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4",
];
let build_basic_display_commands = [
'#defense-stats',
// defense stats [hp, ehp, hpr, ]
// "sPot", // base * atkspd + spell raws
// melee potential
// "mPot", // melee% * (base * atkspd) + melee raws
"mr", "ms",
"ls",
"poison",
"spd",
"atkTier",
]
let sq2_item_display_commands = [
"displayName",
//"type", //REPLACE THIS WITH SKIN
"#ldiv",
"atkSpd",
"#ldiv",
"!elemental",
"hp",
"nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_",
"!spacer",
"fDef", "wDef", "aDef", "tDef", "eDef",
"!elemental",
"#ldiv",
"classReq",
"lvl",
"strReq", "dexReq", "intReq", "defReq","agiReq",
"#ldiv",
"!spacer",
"str", "dex", "int", "def", "agi",
"#table",
"str", "dex", "int", "def", "agi", //jank lmao
"hpBonus",
"hprRaw", "hprPct",
"sdRaw", "sdPct",
@ -374,11 +304,33 @@ let item_display_commands = [
"spRegen",
"eSteal",
"gXp", "gSpd",
"#ldiv",
"majorIds",
"!spacer",
"slots",
"!spacer",
"set",
"lore",
"quest",
"restrict"
];
let sq2_ing_display_order = [
"displayName", //tier will be displayed w/ name
"!spacer",
"ids",
"!spacer",
"posMods",
"itemIDs",
"consumableIDs",
"!spacer",
"lvl",
"skills",
]
let elem_colors = [
"#00AA00",
"#FFFF55",
"#55FFFF",
"#FF5555",
"#FFFFFF"
]

View file

@ -213,14 +213,14 @@ function redraw(data) {
let tier_mod = tiers_mod.get(tier);
let y_max = baseline_y.map(x => 2.1*x*tier_mod*type_mod);
let y_min = baseline_y.map(x => 2.0*x*tier_mod*type_mod);
line_top.datum(zip(baseline_x, y_max))
line_top.datum(zip2(baseline_x, y_max))
.attr("fill", "none")
.attr("stroke", d => colorMap.get(tier))
.attr("d", d3.line()
.x(function(d) { return x(d[0]) })
.y(function(d) { return y(d[1]) })
)
line_bot.datum(zip(baseline_x, y_min))
line_bot.datum(zip2(baseline_x, y_min))
.attr("fill", "none")
.attr("stroke", d => colorMap.get(tier))
.attr("d", d3.line()

View file

@ -239,7 +239,7 @@ function init_items2() {
const itemListFooter = document.getElementById('item-list-footer');
// compile the search db from the item db
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i, [])]);
const searchDb = items.filter(i => !i.remapID).map(i => [i, expandItem(i)]);
// init item list elements
const ITEM_LIST_SIZE = 64;

View file

@ -6,7 +6,7 @@ let reload = false;
let load_complete = false;
let load_in_progress = false;
let items;
let sets;
let sets = new Map();
let itemMap;
let idMap;
let redirectMap;
@ -14,42 +14,46 @@ let itemLists = new Map();
/*
* Load item set from local DB. Calls init() on success.
*/
async function load_local(init_func) {
async function load_local() {
return new Promise(function(resolve, reject) {
let get_tx = db.transaction(['item_db', 'set_db'], 'readonly');
let sets_store = get_tx.objectStore('set_db');
let get_store = get_tx.objectStore('item_db');
let request = get_store.getAll();
request.onerror = function(event) {
console.log("Could not read local item db...");
reject("Could not read local item db...");
}
request.onsuccess = function(event) {
console.log("Successfully read local item db.");
items = request.result;
//console.log(items);
let request2 = sets_store.openCursor();
sets = {};
request2.onerror = function(event) {
console.log("Could not read local set db...");
}
// key-value iteration (hpp don't break this again)
// https://stackoverflow.com/questions/47931595/indexeddb-getting-all-data-with-keys
let request2 = sets_store.openCursor();
request2.onerror = function(event) {
reject("Could not read local set db...");
}
request2.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
sets[cursor.primaryKey] = cursor.value;
let key = cursor.primaryKey;
let value = cursor.value;
sets.set(key, value);
cursor.continue();
}
else {
// no more results
console.log("Successfully read local set db.");
//console.log(sets);
}
};
get_tx.oncomplete = function(event) {
items = request.result;
init_maps();
init_func();
load_complete = true;
}
}
}
await get_tx.complete;
db.close();
resolve();
}
});
}
/*
@ -91,7 +95,7 @@ function clean_item(item) {
/*
* Load item set from remote DB (aka a big json file). Calls init() on success.
*/
async function load(init_func) {
async function load() {
let getUrl = window.location;
let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1];
@ -99,17 +103,7 @@ async function load(init_func) {
let url = baseUrl + "/compress.json?"+new Date();
let result = await (await fetch(url)).json();
items = result.items;
sets = result.sets;
// let clear_tx = db.transaction(['item_db', 'set_db'], 'readwrite');
// let clear_items = clear_tx.objectStore('item_db');
// let clear_sets = clear_tx.objectStore('item_db');
//
// await clear_items.clear();
// await clear_sets.clear();
// await clear_tx.complete;
let sets_ = result.sets;
let add_tx = db.transaction(['item_db', 'set_db'], 'readwrite');
add_tx.onabort = function(e) {
@ -127,54 +121,47 @@ async function load(init_func) {
add_promises.push(req);
}
let sets_store = add_tx.objectStore('set_db');
for (const set in sets) {
add_promises.push(sets_store.add(sets[set], set));
for (const set in sets_) {
add_promises.push(sets_store.add(sets_[set], set));
sets.set(set, sets_[set]);
}
add_promises.push(add_tx.complete);
Promise.all(add_promises).then((values) => {
await Promise.all(add_promises);
init_maps();
init_func();
load_complete = true;
});
// DB not closed? idfk man
db.close();
}
function load_init(init_func) {
if (load_complete) {
console.log("Item db already loaded, skipping load sequence");
init_func();
return;
}
async function load_init() {
return new Promise((resolve, reject) => {
let request = window.indexedDB.open('item_db', DB_VERSION);
request.onerror = function() {
console.log("DB failed to open...");
reject("DB failed to open...");
};
request.onsuccess = function() {
(async function() {
request.onsuccess = async function() {
db = request.result;
if (!reload) {
console.log("Using stored data...")
load_local(init_func);
}
else {
if (load_in_progress) {
while (!load_complete) {
await sleep(100);
}
console.log("Skipping load...")
init_func();
}
else {
// Not 100% safe... whatever!
load_in_progress = true
if (reload) {
console.log("Using new data...")
load(init_func);
await load();
}
else {
console.log("Using stored data...")
await load_local();
}
}
})()
}
resolve();
};
request.onupgradeneeded = function(e) {
reload = true;
@ -198,20 +185,16 @@ function load_init(init_func) {
db.createObjectStore('set_db');
console.log("DB setup complete...");
}
};
});
}
function init_maps() {
//warp
itemMap = new Map();
/* Mapping from item names to set names. */
idMap = new Map();
redirectMap = new Map();
for (const it of itemTypes) {
// List of 'raw' "none" items (No Helmet, etc), in order helmet, chestplate... ring1, ring2, brace, neck, weapon.
for (const it of itemTypes) {
itemLists.set(it, []);
}
}
let noneItems = [
let none_items = [
["armor", "helmet", "No Helmet"],
["armor", "chestplate", "No Chestplate"],
["armor", "leggings", "No Leggings"],
@ -221,13 +204,13 @@ function init_maps() {
["accessory", "bracelet", "No Bracelet"],
["accessory", "necklace", "No Necklace"],
["weapon", "dagger", "No Weapon"],
];
for (let i = 0; i < noneItems.length; i++) {
];
for (let i = 0; i < none_items.length; i++) {
let item = Object();
item.slots = 0;
item.category = noneItems[i][0];
item.type = noneItems[i][1];
item.name = noneItems[i][2];
item.category = none_items[i][0];
item.type = none_items[i][1];
item.name = none_items[i][2];
item.displayName = item.name;
item.set = null;
item.quest = null;
@ -245,15 +228,22 @@ function init_maps() {
item.aDam = "0-0";
clean_item(item);
noneItems[i] = item;
}
items = items.concat(noneItems);
none_items[i] = item;
}
function init_maps() {
//warp
itemMap = new Map();
/* Mapping from item names to set names. */
idMap = new Map();
redirectMap = new Map();
items = items.concat(none_items);
//console.log(items);
for (const item of items) {
if (item.remapID === undefined) {
itemLists.get(item.type).push(item.displayName);
itemMap.set(item.displayName, item);
if (noneItems.includes(item)) {
if (none_items.includes(item)) {
idMap.set(item.id, "");
}
else {

View file

@ -4,6 +4,7 @@ const ING_DB_VERSION = 13;
let idb;
let ireload = false;
let iload_in_progress = false;
let iload_complete = false;
let ings;
let recipes;
@ -20,32 +21,34 @@ let recipeIDMap;
/*
* Load item set from local DB. Calls init() on success.
*/
async function ing_load_local(init_func) {
console.log("IngMap is: \n " + ingMap);
async function ing_load_local() {
return new Promise(function(resolve, reject) {
let get_tx = idb.transaction(['ing_db', 'recipe_db'], 'readonly');
let ings_store = get_tx.objectStore('ing_db');
let recipes_store = get_tx.objectStore('recipe_db');
let request3 = ings_store.getAll();
request3.onerror = function(event) {
console.log("Could not read local ingredient db...");
reject("Could not read local ingredient db...");
}
request3.onsuccess = function(event) {
console.log("Successfully read local ingredient db.");
ings = request3.result;
}
let request4 = recipes_store.getAll();
request4.onerror = function(event) {
console.log("Could not read local recipe db...");
reject("Could not read local recipe db...");
}
request4.onsuccess = function(event) {
console.log("Successfully read local recipe db.");
}
get_tx.oncomplete = function(event) {
ings = request3.result;
recipes = request4.result;
init_ing_maps();
init_func();
iload_complete = true;
}
}
await get_tx.complete;
idb.close();
resolve()
}
});
}
function clean_ing(ing) {
@ -59,11 +62,12 @@ function clean_ing(ing) {
/*
* Load item set from remote DB (aka a big json file). Calls init() on success.
*/
async function load_ings(init_func) {
async function load_ings() {
let getUrl = window.location;
let baseUrl = getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.split('/')[1];
let url = baseUrl + "/ingreds_compress.json";
let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1];
// "Random" string to prevent caching!
let url = baseUrl + "/ingreds_compress.json?"+new Date();
url = url.replace(/\w+.html/, "") ;
let result = await (await fetch(url)).json();
@ -97,35 +101,40 @@ async function load_ings(init_func) {
}
add_promises.push(add_tx2.complete);
add_promises.push(add_tx3.complete);
Promise.all(add_promises).then((values) => {
await Promise.all(add_promises);
init_ing_maps();
init_func();
iload_complete = true;
});
// DB not closed? idfk man
idb.close();
}
function load_ing_init(init_func) {
if (iload_complete) {
console.log("Ingredient db already loaded, skipping load sequence");
init_func();
return;
}
async function load_ing_init() {
return new Promise((resolve, reject) => {
let request = window.indexedDB.open("ing_db", ING_DB_VERSION)
request.onerror = function() {
console.log("DB failed to open...");
reject("DB failed to open...");
}
request.onsuccess = function() {
request.onsuccess = async function() {
idb = request.result;
if (!ireload) {
console.log("Using stored data...")
ing_load_local(init_func);
if (iload_in_progress) {
while (!iload_complete) {
await sleep(100);
}
console.log("Skipping load...")
}
else {
iload_in_progress = true
if (ireload) {
console.log("Using new data...")
load_ings(init_func);
await load_ings();
}
else {
console.log("Using stored data...")
await ing_load_local();
}
}
resolve();
}
request.onupgradeneeded = function(e) {
@ -150,6 +159,7 @@ function load_ing_init(init_func) {
console.log("DB setup complete...");
}
});
}
function init_ing_maps() {
@ -222,4 +232,5 @@ function init_ing_maps() {
recipeList.push(recipe["name"]);
recipeIDMap.set(recipe["id"],recipe["name"]);
}
console.log(ingMap);
}

View file

@ -1,4 +1,4 @@
const TOME_DB_VERSION = 1;
const TOME_DB_VERSION = 3;
// @See https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/video-store/index.jsA
let tdb;
@ -13,29 +13,31 @@ let tomeLists = new Map();
/*
* Load tome set from local DB. Calls init() on success.
*/
async function load_tome_local(init_func) {
async function load_tome_local() {
return new Promise(function(resolve, reject) {
let get_tx = tdb.transaction(['tome_db'], 'readonly');
let get_store = get_tx.objectStore('tome_db');
let request = get_store.getAll();
request.onerror = function(event) {
console.log("Could not read local tome db...");
reject("Could not read local tome db...");
}
request.onsuccess = function(event) {
console.log("Successfully read local tome db.");
tomes = request.result;
init_tome_maps();
init_func();
tload_complete = true;
}
await get_tx.complete;
get_tx.oncomplete = function(event) {
tomes = request.result;
init_tome_maps();
tload_complete = true;
tdb.close();
resolve();
}
});
}
/*
* Load tome set from remote DB (json). Calls init() on success.
*/
async function load_tome(init_func) {
async function load_tome() {
let getUrl = window.location;
let baseUrl = getUrl.protocol + "//" + getUrl.host + "/";// + getUrl.pathname.split('/')[1];
@ -60,49 +62,42 @@ async function load_tome(init_func) {
};
add_promises.push(req);
}
Promise.all(add_promises).then((values) => {
add_promises.push(add_tx.complete);
await Promise.all(add_promises);
init_tome_maps();
init_func();
tload_complete = true;
});
// DB not closed? idfk man
tdb.close();
}
function load_tome_init(init_func) {
if (tload_complete) {
console.log("Tome db already loaded, skipping load sequence");
init_func();
return;
}
async function load_tome_init() {
return new Promise((resolve, reject) => {
let request = window.indexedDB.open('tome_db', TOME_DB_VERSION);
request.onerror = function() {
console.log("DB failed to open...");
reject("DB failed to open...");
};
request.onsuccess = function() {
(async function() {
request.onsuccess = async function() {
tdb = request.result;
if (!treload) {
console.log("Using stored data...")
load_tome_local(init_func);
}
else {
if (tload_in_progress) {
while (!tload_complete) {
await sleep(100);
}
console.log("Skipping load...")
init_func();
}
else {
// Not 100% safe... whatever!
tload_in_progress = true
if (treload) {
console.log("Using new data...")
load_tome(init_func);
await load_tome();
}
else {
console.log("Using stored data...")
await load_tome_local();
}
}
})()
resolve();
}
request.onupgradeneeded = function(e) {
@ -121,8 +116,14 @@ function load_tome_init(init_func) {
console.log("DB setup complete...");
}
});
}
let none_tomes = [
["tome", "weaponTome", "No Weapon Tome"],
["tome", "armorTome", "No Armor Tome"],
["tome", "guildTome", "No Guild Tome"]
];
function init_tome_maps() {
//warp
tomeMap = new Map();
@ -130,21 +131,16 @@ function init_tome_maps() {
tomeIDMap = new Map();
tomeRedirectMap = new Map();
for (const it of tomeTypes) {
for (const it of tome_types) {
tomeLists.set(it, []);
}
let noneTomes = [
["tome", "weaponTome", "No Weapon Tome"],
["tome", "armorTome", "No Armor Tome"],
["tome", "guildTome", "No Guild Tome"]
];
for (let i = 0; i < 3; i++) {
let tome = Object();
tome.slots = 0;
tome.category = noneTomes[i][0];
tome.type = noneTomes[i][1];
tome.name = noneTomes[i][2];
tome.category = none_tomes[i][0];
tome.type = none_tomes[i][1];
tome.name = none_tomes[i][2];
tome.displayName = tome.name;
tome.set = null;
tome.quest = null;
@ -163,14 +159,14 @@ function init_tome_maps() {
//dependency - load.js
clean_item(tome);
noneTomes[i] = tome;
none_tomes[i] = tome;
}
tomes = tomes.concat(noneTomes);
tomes = tomes.concat(none_tomes);
for (const tome of tomes) {
if (tome.remapID === undefined) {
tomeLists.get(tome.type).push(tome.displayName);
tomeMap.set(tome.displayName, tome);
if (noneTomes.includes(tome)) {
if (none_tomes.includes(tome)) {
tomeIDMap.set(tome.id, "");
}
else {

100
js/optimize.js Normal file
View file

@ -0,0 +1,100 @@
function optimizeStrDex() {
if (!player_build) {
return;
}
const skillpoints = skp_inputs.map(x => x.value); // JANK
let total_assigned = 0;
const min_assigned = player_build.base_skillpoints;
const base_totals = player_build.total_skillpoints;
let base_skillpoints = [];
for (let i in skp_order){ //big bren
const assigned = skillpoints[i] - base_totals[i] + min_assigned[i]
base_skillpoints.push(assigned);
total_assigned += assigned;
}
const remaining = levelToSkillPoints(player_build.level) - total_assigned;
const max_str_boost = 100 - base_skillpoints[0];
const max_dex_boost = 100 - base_skillpoints[1];
if (Math.min(remaining, max_str_boost, max_dex_boost) < 0) return; // Unwearable
let str_bonus = remaining;
let dex_bonus = 0;
let best_skillpoints = skillpoints;
let best_damage = 0;
for (let i = 0; i <= remaining; ++i) {
let total_skillpoints = skillpoints.slice();
total_skillpoints[0] += Math.min(max_str_boost, str_bonus);
total_skillpoints[1] += Math.min(max_dex_boost, dex_bonus);
// Calculate total 3rd spell damage
let spell = spell_table[player_build.weapon.statMap.get("type")][2];
const stats = player_build.statMap;
let critChance = skillPointsToPercentage(total_skillpoints[1]);
let save_damages = [];
let spell_parts;
if (spell.parts) {
spell_parts = spell.parts;
}
else {
spell_parts = spell.variants.DEFAULT;
for (const majorID of stats.get("activeMajorIDs")) {
if (majorID in spell.variants) {
spell_parts = spell.variants[majorID];
break;
}
}
}
let total_damage = 0;
for (const part of spell_parts) {
if (part.type === "damage") {
let tmp_conv = [];
for (let i in part.conversion) {
tmp_conv.push(part.conversion[i] * part.multiplier);
}
let _results = calculateSpellDamage(stats, player_build.weapon.statMap, tmp_conv, true);
let totalDamNormal = _results[0];
let totalDamCrit = _results[1];
let results = _results[2];
let tooltipinfo = _results[3];
for (let i = 0; i < 6; ++i) {
for (let j in results[i]) {
results[i][j] = results[i][j].toFixed(2);
}
}
let nonCritAverage = (totalDamNormal[0]+totalDamNormal[1])/2 || 0;
let critAverage = (totalDamCrit[0]+totalDamCrit[1])/2 || 0;
let averageDamage = (1-critChance)*nonCritAverage+critChance*critAverage || 0;
save_damages.push(averageDamage);
if (part.summary == true) {
total_damage = averageDamage;
}
} else if (part.type === "total") {
total_damage = 0;
for (let i in part.factors) {
total_damage += save_damages[i] * part.factors[i];
}
}
} // END Calculate total 3rd spell damage (total_damage)
if (total_damage > best_damage) {
best_damage = total_damage;
best_skillpoints = total_skillpoints.slice();
}
str_bonus -= 1;
dex_bonus += 1;
}
console.log(best_skillpoints);
// TODO do not merge for performance reasons
for (let i in skp_order) {
skp_inputs[i].input_field.value = best_skillpoints[i];
skp_inputs[i].mark_dirty();
}
for (let i in skp_order) {
skp_inputs[i].update();
}
}

View file

@ -61,3 +61,134 @@ let powderSpecialStats = [
_ps("Courage",new Map([ ["Duration", [6,6.5,7,7.5,8]],["Damage", [75,87.5,100,112.5,125]],["Damage Boost", [70,90,110,130,150]] ]),"Endurance",new Map([ ["Damage", [2,3,4,5,6]],["Duration", [8,8,8,8,8]],["Description", "Hit Taken"] ]),200), //f
_ps("Wind Prison",new Map([ ["Duration", [3,3.5,4,4.5,5]],["Damage Boost", [400,450,500,550,600]],["Knockback", [8,12,16,20,24]] ]),"Dodge",new Map([ ["Damage",[2,3,4,5,6]],["Duration",[2,3,4,5,6]],["Description","Near Mobs"] ]),150) //a
];
/**
* Apply armor powders.
* Encoding shortcut assumes that all powders give +def to one element
* and -def to the element "behind" it in cycle ETWFA, which is true
* as of now and unlikely to change in the near future.
*/
function applyArmorPowders(expandedItem) {
const powders = expandedItem.get('powders');
for(const id of powders){
let powder = powderStats[id];
let name = powderNames.get(id).charAt(0);
let prevName = skp_elements[(skp_elements.indexOf(name) + 4 )% 5];
expandedItem.set(name+"Def", (expandedItem.get(name+"Def") || 0) + powder["defPlus"]);
expandedItem.set(prevName+"Def", (expandedItem.get(prevName+"Def") || 0) - powder["defMinus"]);
}
}
const damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ];
const damage_present_key = 'damagePresent';
/**
* Apply weapon powders. MUTATES THE ITEM!
* Adds entries for `damage_keys` and `damage_present_key`
* For normal items, `damage_keys` is 6x2 list (elem: [min, max])
* For crafted items, `damage_keys` is 6x2x2 list (elem: [minroll: [min, max], maxroll: [min, max]])
*/
function apply_weapon_powders(item) {
let present;
if (item.get("tier") !== "Crafted") {
let weapon_result = calc_weapon_powder(item);
let damages = weapon_result[0];
present = weapon_result[1];
for (const i in damage_keys) {
item.set(damage_keys[i], damages[i]);
}
} else {
let base_low = [item.get("nDamBaseLow"),item.get("eDamBaseLow"),item.get("tDamBaseLow"),item.get("wDamBaseLow"),item.get("fDamBaseLow"),item.get("aDamBaseLow")];
let results_low = calc_weapon_powder(item, base_low);
let damage_low = results_low[0];
let base_high = [item.get("nDamBaseHigh"),item.get("eDamBaseHigh"),item.get("tDamBaseHigh"),item.get("wDamBaseHigh"),item.get("fDamBaseHigh"),item.get("aDamBaseHigh")];
let results_high = calc_weapon_powder(item, base_high);
let damage_high = results_high[0];
present = results_high[1];
for (const i in damage_keys) {
item.set(damage_keys[i], [damage_low[i], damage_high[i]]);
}
}
item.set(damage_present_key, present);
}
/**
* Calculate weapon damage from powder.
*
* Params:
* weapon: Weapon to apply powder to
* damageBases: used by crafted
*
* Return:
* [damages, damage_present]
*/
function calc_weapon_powder(weapon, damageBases) {
let powders = weapon.get("powders").slice();
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
let damages = [
weapon.get('nDam').split('-').map(Number),
weapon.get('eDam').split('-').map(Number),
weapon.get('tDam').split('-').map(Number),
weapon.get('wDam').split('-').map(Number),
weapon.get('fDam').split('-').map(Number),
weapon.get('aDam').split('-').map(Number)
];
// Applying spell conversions
let neutralBase = damages[0].slice();
let neutralRemainingRaw = damages[0].slice();
//powder application for custom crafted weapons is inherently fucked because there is no base. Unsure what to do.
//Powder application for Crafted weapons - this implementation is RIGHT YEAAAAAAAAA
//1st round - apply each as ingred, 2nd round - apply as normal
if (weapon.get("tier") === "Crafted" && !weapon.get("custom")) {
for (const p of powders.concat(weapon.get("ingredPowders"))) {
let powder = powderStats[p]; //use min, max, and convert
let element = Math.floor((p+0.01)/6); //[0,4], the +0.01 attempts to prevent division error
let diff = Math.floor(damageBases[0] * powder.convert/100);
damageBases[0] -= diff;
damageBases[element+1] += diff + Math.floor( (powder.min + powder.max) / 2 );
}
//update all damages
for (let i = 0; i < damages.length; i++) {
damages[i] = [Math.floor(damageBases[i] * 0.9), Math.floor(damageBases[i] * 1.1)];
}
neutralRemainingRaw = damages[0].slice();
neutralBase = damages[0].slice();
}
//apply powders to weapon
for (const powderID of powders) {
const powder = powderStats[powderID];
// Bitwise to force conversion to integer (integer division).
const element = (powderID/6) | 0;
let conversionRatio = powder.convert/100;
if (neutralRemainingRaw[1] > 0) {
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);
let max_diff = Math.min(neutralRemainingRaw[1], conversionRatio * neutralBase[1]);
//damages[element+1][0] = Math.floor(round_near(damages[element+1][0] + min_diff));
//damages[element+1][1] = Math.floor(round_near(damages[element+1][1] + max_diff));
//neutralRemainingRaw[0] = Math.floor(round_near(neutralRemainingRaw[0] - min_diff));
//neutralRemainingRaw[1] = Math.floor(round_near(neutralRemainingRaw[1] - max_diff));
damages[element+1][0] += min_diff;
damages[element+1][1] += max_diff;
neutralRemainingRaw[0] -= min_diff;
neutralRemainingRaw[1] -= max_diff;
}
damages[element+1][0] += powder.min;
damages[element+1][1] += powder.max;
}
// The ordering of these two blocks decides whether neutral is present when converted away or not.
let present_elements = []
for (const damage of damages) {
present_elements.push(damage[1] > 0);
}
// The ordering of these two blocks decides whether neutral is present when converted away or not.
damages[0] = neutralRemainingRaw;
return [damages, present_elements];
}

205
js/render_compute_graph.js Normal file
View file

@ -0,0 +1,205 @@
// Set-up the export button
function set_export_button(svg, button_id, output_id) {
d3.select('#'+button_id).on('click', function(){
//get svg source.
var serializer = new XMLSerializer();
var source = serializer.serializeToString(svg.node());
console.log(source);
source = source.replace(/^<g/, '<svg');
source = source.replace(/<\/g>$/, '</svg>');
//add name spaces.
if(!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
}
if(!source.match(/^<svg[^>]+"http\:\/\/www\.w3\.org\/1999\/xlink"/)){
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
}
//add xml declaration
source = '<?xml version="1.0" standalone="no"?>\r\n' + source;
//convert svg source to URI data scheme.
var url = "data:image/svg+xml;charset=utf-8,"+encodeURIComponent(source);
//set url value to a element's href attribute.
document.getElementById(output_id).href = url;
});
}
d3.select("#graph_body")
.append("div")
.attr("style", "width: 100%; height: 100%; min-height: 0px; flex-grow: 1")
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content-responsive", true);
let graph = d3.select("svg");
let svg = graph.append('g');
let margin = {top: 20, right: 20, bottom: 35, left: 40};
function bbox() {
let ret = graph.node().parentNode.getBoundingClientRect();
return ret;
}
let _bbox = bbox();
const colors = ['aqua', 'yellow', 'fuchsia', 'white', 'teal', 'olive', 'purple', 'gray', 'blue', 'lime', 'red', 'silver', 'navy', 'green', 'maroon'];
const n_colors = colors.length;
const view = svg.append("rect")
.attr("class", "view")
.attr("x", 0)
.attr("y", 0);
function convert_data(nodes_raw) {
let edges = [];
let node_id = new Map();
nodes = [];
for (let i in nodes_raw) {
node_id.set(nodes_raw[i], i);
nodes.push({id: i, color: 0, data: nodes_raw[i]});
}
for (const node of nodes_raw) {
const to = node_id.get(node);
for (const input of node.inputs) {
const from = node_id.get(input);
let name = input.name;
let link_name = node.input_translation.get(name);
edges.push({
source: from,
target: to,
name: link_name
});
}
}
return {
nodes: nodes,
links: edges
}
}
function create_svg(data, redraw_func) {
// Initialize the links
var link = svg
.selectAll("line")
.data(data.links)
.enter()
.append("line")
.style("stroke", "#aaa")
// Initialize the nodes
let node = svg
.selectAll("g")
.data(data.nodes);
let node_enter = node.enter()
.append('g')
let circles = node_enter.append("circle")
.attr("r", 20)
.style("fill", ({id, color, data}) => colors[color])
node_enter.append('text')
.attr("dx", -20)
.attr("dy", -22)
.style('fill', 'white')
.text(({id, color, data}) => data.name);
// Let's list the force we wanna apply on the network
var simulation = d3.forceSimulation(data.nodes) // Force algorithm is applied to data.nodes
.force("link", d3.forceLink().strength(0.1) // This force provides links between nodes
.id(function(d) { return d.id; }) // This provide the id of a node
.links(data.links) // and this the list of links
)
.force("charge", d3.forceManyBody().strength(-400)) // This adds repulsion between nodes. Play with the -400 for the repulsion strength
//.force("center", d3.forceCenter(_bbox.width / 2, _bbox.height / 2).strength(0.1)) // This force attracts nodes to the center of the svg area
.on("tick", ticked);
// This function is run at each iteration of the force algorithm, updating the nodes position.
let scale_transform = {k: 1, x: 0, y: 0}
function ticked() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node_enter.attr("transform", function (d) { return 'translate('+scale_transform.x+','+scale_transform.y+') scale('+scale_transform.k+') translate('+d.x+','+d.y+')' })
}
const drag = d3.drag()
.on("start", dragstart)
.on("drag", dragged);
node_enter.call(drag).on('click', click);
function click(event, d) {
if (event.ctrlKey) {
// Color cycle.
d.color = (d.color + 1) % n_colors;
d3.select(this).selectAll('circle').style("fill", ({id, color, data}) => colors[color])
}
else {
delete d.fx;
delete d.fy;
d3.select(this).classed("fixed", false);
simulation.alpha(0.5).restart();
}
}
function dragstart() {
d3.select(this).classed("fixed", true);
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
simulation.alpha(0.5).restart();
}
const zoom = d3.zoom()
.scaleExtent([0.01, 10])
.translateExtent([[-10000, -10000], [10000, 10000]])
.filter(filter)
.on("zoom", zoomed);
view.call(zoom);
function zoomed({ transform }) {
link.attr('transform', transform);
scale_transform = transform;
node_enter.attr("transform", function (d) { return 'translate('+scale_transform.x+','+scale_transform.y+') scale('+scale_transform.k+') translate('+d.x+','+d.y+')' })
redraw_func();
}
// prevent scrolling then apply the default filter
function filter(event) {
event.preventDefault();
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
}
}
set_export_button(svg, 'saveButton', 'saveLink');
(async function() {
// JANKY
while (edit_id_output === undefined) {
await sleep(500);
}
function redraw() {
_bbox = bbox();
graph.attr("viewBox", [0, 0, _bbox.width, _bbox.height]);
view.attr("width", _bbox.width - 1)
.attr("height", _bbox.height - 1);
}
d3.select(window)
.on("resize", function() {
redraw();
});
redraw();
const data = convert_data(all_nodes);
create_svg(data, redraw);
console.log("render");
})();

View file

@ -31,14 +31,14 @@ function calculate_skillpoints(equipment, weapon) {
let setCount = activeSetCounts.get(setName);
let old_bonus = {};
if (setCount) {
old_bonus = sets[setName].bonuses[setCount-1];
old_bonus = sets.get(setName).bonuses[setCount-1];
activeSetCounts.set(setName, setCount + 1);
}
else {
setCount = 0;
activeSetCounts.set(setName, 1);
}
const new_bonus = sets[setName].bonuses[setCount];
const new_bonus = sets.get(setName).bonuses[setCount];
//let skp_order = ["str","dex","int","def","agi"];
for (const i in skp_order) {
const delta = (new_bonus[skp_order[i]] || 0) - (old_bonus[skp_order[i]] || 0);
@ -74,8 +74,8 @@ function calculate_skillpoints(equipment, weapon) {
if (setName) { // undefined/null means no set.
const setCount = activeSetCounts.get(setName);
if (setCount) {
const old_bonus = sets[setName].bonuses[setCount-1];
const new_bonus = sets[setName].bonuses[setCount];
const old_bonus = sets.get(setName).bonuses[setCount-1];
const new_bonus = sets.get(setName).bonuses[setCount];
//let skp_order = ["str","dex","int","def","agi"];
for (const i in skp_order) {
const set_delta = (new_bonus[skp_order[i]] || 0) - (old_bonus[skp_order[i]] || 0);

View file

@ -1,632 +0,0 @@
let weapon_keys = ['dagger', 'wand', 'bow', 'relik', 'spear'];
let armor_keys = ['helmet', 'chestplate', 'leggings', 'boots'];
let skp_keys = ['str', 'dex', 'int', 'def', 'agi'];
let accessory_keys= ['ring1', 'ring2', 'bracelet', 'necklace'];
let powderable_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'weapon'];
let equipment_keys = ['helmet', 'chestplate', 'leggings', 'boots', 'ring1', 'ring2', 'bracelet', 'necklace', 'weapon'].concat(tome_keys);
let powder_keys = ['e', 't', 'w', 'f', 'a'];
let spell_disp = ['spell0-info', 'spell1-info', 'spell2-info', 'spell3-info'];
let other_disp = ['build-order', 'set-info', 'int-info'];
document.addEventListener('DOMContentLoaded', function() {
for (const eq of equipment_keys) {
document.querySelector("#"+eq+"-choice").setAttribute("oninput", "update_field('"+ eq +"'); calcBuildSchedule();");
document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');"); //toggle_plus_minus('" + eq + "-pm');
}
for (const eq of powderable_keys) {
document.querySelector("#"+eq+"-powder").setAttribute("oninput", "calcBuildSchedule(); update_field('"+ eq +"');");
}
for (const eq of tome_keys) {
document.querySelector("#" + eq + "-choice").setAttribute("oninput", "update_field('" + eq + "'); calcBuildSchedule();");
document.querySelector("#"+eq+"-tooltip").setAttribute("onclick", "collapse_element('#"+ eq +"-tooltip');");
}
for (const i of spell_disp) {
document.querySelector("#"+i+"Avg").setAttribute("onclick", "toggle_spell_tab('"+i+"')");
}
document.querySelector("#level-choice").setAttribute("oninput", "calcBuildSchedule()")
document.querySelector("#weapon-choice").setAttribute("oninput", document.querySelector("#weapon-choice").getAttribute("oninput") + "resetArmorPowderSpecials();");
// document.querySelector("#edit-IDs-button").setAttribute("onclick", "toggle_edit_id_tab()");
let skp_fields = document.getElementsByClassName("skp-update");
for (i = 0; i < skp_fields.length; i++) {
skp_fields[i].setAttribute("oninput", "updateStatSchedule()");
}
let masonry = Macy({
container: "#masonry-container",
columns: 1,
mobileFirst: true,
breakAt: {
1200: 4,
},
margin: {
x: 20,
y: 20,
}
});
let search_masonry = Macy({
container: "#search-results",
columns: 1,
mobileFirst: true,
breakAt: {
1200: 4,
},
margin: {
x: 20,
y: 20,
}
});
document.querySelector("#search-container").addEventListener("keyup", function(event) {
if (event.key === "Escape") {
document.querySelector("#search-container").style.display = "none";
};
});
construct_AT(document.getElementById("atree-ui"), atree_example);
document.getElementById("atree-dropdown").style.display = "none";
});
// phanta scheduler
let calcBuildTask = null;
let updateStatTask = null;
let doSearchTask = null;
function calcBuildSchedule(){
if (calcBuildTask !== null) {
clearTimeout(calcBuildTask);
}
calcBuildTask = setTimeout(function(){
calcBuildTask = null;
resetEditableIDs();
calculateBuild();
}, 500);
}
function updateStatSchedule(){
if (updateStatTask !== null) {
clearTimeout(updateStatTask);
}
updateStatTask = setTimeout(function(){
updateStatTask = null;
updateStats();
}, 500);
}
function doSearchSchedule(){
console.log("Search Schedule called");
if (doSearchTask !== null) {
clearTimeout(doSearchTask);
}
doSearchTask = setTimeout(function(){
doSearchTask = null;
doItemSearch();
window.dispatchEvent(new Event('resize'));
}, 500);
}
function sq2ResetFields(){
for (let i in powderInputs) {
setValue(powderInputs[i], "");
}
for (let i in equipmentInputs) {
setValue(equipmentInputs[i], "");
}
for (let i in tomeInputs) {
setValue(tomeInputs[i], "");
}
setValue("str-skp", "0");
setValue("dex-skp", "0");
setValue("int-skp", "0");
setValue("def-skp", "0");
setValue("agi-skp", "0");
setValue("level-choice", "106");
location.hash = "";
calculateBuild();
}
// equipment field dynamic styling
function update_field(field) {
// built on the assumption of no one will type in CI/CR letter by letter
// resets
document.querySelector("#"+field+"-choice").classList.remove("text-light", "is-invalid", 'Normal', 'Unique', 'Rare', 'Legendary', 'Fabled', 'Mythic', 'Set', 'Crafted', 'Custom');
document.querySelector("#"+field+"-choice").classList.add("text-light");
document.querySelector("#" + field + "-img").classList.remove('Normal-shadow', 'Unique-shadow', 'Rare-shadow', 'Legendary-shadow', 'Fabled-shadow', 'Mythic-shadow', 'Set-shadow', 'Crafted-shadow', 'Custom-shadow');
item = document.querySelector("#"+field+"-choice").value
let powder_slots;
let tier;
let category;
let type;
// get item info
if (item.slice(0, 3) == "CI-") {
item = getCustomFromHash(item);
powder_slots = item.statMap.get("slots");
tier = item.statMap.get("tier");
category = item.statMap.get("category");
type = item.statMap.get("type");
}
else if (item.slice(0, 3) == "CR-") {
item = getCraftFromHash(item);
powder_slots = item.statMap.get("slots");
tier = item.statMap.get("tier");
category = item.statMap.get("category");
type = item.statMap.get("type");
}
else if (itemMap.get(item)) {
item = itemMap.get(item);
if (!item) {return false;}
powder_slots = item.slots;
tier = item.tier;
category = item.category;
type = item.type;
}
else if (tomeMap.get(item)) {
tome = tomeMap.get(item);
if (!tome) {return false;}
powder_slots = 0;
tier = tome.tier;
category = tome.category;
type = tome.type;
}
else {
// item not found
document.querySelector("#"+field+"-choice").classList.add("text-light");
if (item) { document.querySelector("#"+field+"-choice").classList.add("is-invalid"); }
/*if (!accessory_keys.contains(type.toLowerCase())) {
document.querySelector("#"+type+"-powder").disabled = true;
}*/
return false;
}
if ((type != field.replace(/[0-9]/g, '')) && (category != field.replace(/[0-9]/g, ''))) {
document.querySelector("#"+field+"-choice").classList.add("text-light");
if (item) { document.querySelector("#"+field+"-choice").classList.add("is-invalid"); }
//document.querySelector("#"+equipment_keys[i]+"-powder").disabled = true;
return false;
}
// set item color
document.querySelector("#"+field+"-choice").classList.add(tier);
document.querySelector("#"+field+"-img").classList.add(tier + "-shadow");
if (powderable_keys.includes(field)) {
// set powder slots
document.querySelector("#"+field+"-powder").setAttribute("placeholder", powder_slots+" slots");
if (powder_slots == 0) {
document.querySelector("#"+field+"-powder").disabled = true;
} else {
document.querySelector("#"+field+"-powder").disabled = false;
}
// powder error handling
document.querySelector("#" + field + "-powder").classList.remove("is-invalid");
let powder_string = document.querySelector("#"+field+"-powder").value;
if (powder_string.length % 2 != 0 || powder_string.length / 2 > powder_slots) {
document.querySelector("#"+field+"-powder").classList.add("is-invalid");
} else {
for (i = 0; i < powder_string.length / 2; i++) {
if (powder_keys.includes(powder_string.substring(i*2, i*2+2).split("")[0]) == false || isNaN(powder_string.substring(i*2, i*2+2).split("")[1]) || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) < 1 || parseInt(powder_string.substring(i*2, i*2+2).split("")[1]) > 6) {
document.querySelector("#"+field+"-powder").classList.add("is-invalid");
}
}
};
}
// set weapon img
if (category == 'weapon') {
document.querySelector("#weapon-img").setAttribute('src', '../media/items/new/generic-'+type+'.png');
}
}
/* tabulars | man i hate this code but too lazy to fix /shrug */
let tabs = ['overall-stats', 'offensive-stats', 'defensive-stats'];
function show_tab(tab) {
//console.log(itemFilters)
//hide all tabs, then show the tab of the div clicked and highlight the correct button
for (const i in tabs) {
document.querySelector("#" + tabs[i]).style.display = "none";
document.getElementById("tab-" + tabs[i].split("-")[0] + "-btn").classList.remove("selected-btn");
}
document.querySelector("#" + tab).style.display = "";
document.getElementById("tab-" + tab.split("-")[0] + "-btn").classList.add("selected-btn");
}
function toggle_spell_tab(tab) {
let arrow_img = document.querySelector("#" + "arrow_" + tab + "Avg");
if (document.querySelector("#"+tab).style.display == "none") {
document.querySelector("#"+tab).style.display = "";
arrow_img.src = arrow_img.src.replace("down", "up");
} else {
document.querySelector("#"+tab).style.display = "none";
arrow_img.src = arrow_img.src.replace("up", "down");
}
}
function toggle_boost_tab(tab) {
for (const i of skp_keys) {
document.querySelector("#"+i+"-boost").style.display = "none";
document.getElementById(i + "-boost-tab").classList.remove("selected-btn");
}
document.querySelector("#"+tab+"-boost").style.display = "";
document.getElementById(tab + "-boost-tab").classList.add("selected-btn");
}
// toggle tab
function toggle_tab(tab) {
if (document.querySelector("#"+tab).style.display == "none") {
document.querySelector("#"+tab).style.display = "";
} else {
document.querySelector("#"+tab).style.display = "none";
}
}
function collapse_element(elmnt) {
elem_list = document.querySelector(elmnt).children;
if (elem_list) {
for (elem of elem_list) {
if (elem.classList.contains("no-collapse")) { continue; }
if (elem.style.display == "none") {
elem.style.display = "";
} else {
elem.style.display = "none";
}
}
}
// macy quirk
window.dispatchEvent(new Event('resize'));
// weird bug where display: none overrides??
document.querySelector(elmnt).style.removeProperty('display');
}
// search misc
function set_item(item) {
document.querySelector("#search-container").style.display = "none";
let type;
// if (!player_build) {return false;}
if (item.get("category") === "weapon") {
type = "weapon";
} else if (item.get("type") === "ring") {
if (!document.querySelector("#ring1-choice").value) {
type = "ring1";
} else {
type = "ring2";
}
} else {
type = item.get("type");
}
document.querySelector("#"+type+"-choice").value = item.get("displayName");
calcBuildSchedule();
update_field(type);
}
// disable boosts
function reset_powder_specials() {
let specials = ["Quake", "Chain_Lightning", "Curse", "Courage", "Wind_Prison"]
for (const special of specials) {
for (i = 1; i < 6; i++) {
if (document.querySelector("#"+special+"-"+i).classList.contains("toggleOn")) {
document.querySelector("#"+special+"-"+i).classList.remove("toggleOn");
}
}
}
}
// autocomplete initialize
function init_autocomplete() {
let dropdowns = new Map()
for (const eq of equipment_keys) {
if (tome_keys.includes(eq)) {
continue;
}
// build dropdown
let item_arr = [];
if (eq == 'weapon') {
for (const weaponType of weapon_keys) {
for (const weapon of itemLists.get(weaponType)) {
let item_obj = itemMap.get(weapon);
if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") {
continue;
}
if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) {
continue;
}
item_arr.push(weapon);
}
}
} else {
for (const item of itemLists.get(eq.replace(/[0-9]/g, ''))) {
let item_obj = itemMap.get(item);
if (item_obj["restrict"] && item_obj["restrict"] === "DEPRECATED") {
continue;
}
if (item_obj["name"] == 'No '+ eq.charAt(0).toUpperCase() + eq.slice(1)) {
continue;
}
item_arr.push(item)
}
}
// create dropdown
dropdowns.set(eq, new autoComplete({
data: {
src: item_arr
},
selector: "#"+ eq +"-choice",
wrapper: false,
resultsList: {
maxResults: 1000,
tabSelect: true,
noResults: true,
class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm",
element: (list, data) => {
// dynamic result loc
let position = document.getElementById(eq+'-dropdown').getBoundingClientRect();
list.style.top = position.bottom + window.scrollY +"px";
list.style.left = position.x+"px";
list.style.width = position.width+"px";
list.style.maxHeight = position.height * 2 +"px";
if (!data.results.length) {
message = document.createElement('li');
message.classList.add('scaled-font');
message.textContent = "No results found!";
list.prepend(message);
}
},
},
resultItem: {
class: "scaled-font search-item",
selected: "dark-5",
element: (item, data) => {
item.classList.add(itemMap.get(data.value).tier);
},
},
events: {
input: {
selection: (event) => {
if (event.detail.selection.value) {
event.target.value = event.detail.selection.value;
}
update_field(eq);
calcBuildSchedule();
},
},
}
}));
}
for (const eq of tome_keys) {
// build dropdown
let tome_arr = [];
for (const tome of tomeLists.get(eq.replace(/[0-9]/g, ''))) {
let tome_obj = tomeMap.get(tome);
if (tome_obj["restrict"] && tome_obj["restrict"] === "DEPRECATED") {
continue;
}
//this should suffice for tomes - jank
if (tome_obj["name"].includes('No ' + eq.charAt(0).toUpperCase())) {
continue;
}
let tome_name = tome;
tome_arr.push(tome_name);
}
// create dropdown
dropdowns.set(eq, new autoComplete({
data: {
src: tome_arr
},
selector: "#"+ eq +"-choice",
wrapper: false,
resultsList: {
maxResults: 1000,
tabSelect: true,
noResults: true,
class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm",
element: (list, data) => {
// dynamic result loc
let position = document.getElementById(eq+'-dropdown').getBoundingClientRect();
list.style.top = position.bottom + window.scrollY +"px";
list.style.left = position.x+"px";
list.style.width = position.width+"px";
list.style.maxHeight = position.height * 2 +"px";
if (!data.results.length) {
message = document.createElement('li');
message.classList.add('scaled-font');
message.textContent = "No results found!";
list.prepend(message);
}
},
},
resultItem: {
class: "scaled-font search-item",
selected: "dark-5",
element: (tome, data) => {
tome.classList.add(tomeMap.get(data.value).tier);
},
},
events: {
input: {
selection: (event) => {
if (event.detail.selection.value) {
event.target.value = event.detail.selection.value;
}
update_field(eq);
calcBuildSchedule();
},
},
}
}));
}
let filter_loc = ["filter1", "filter2", "filter3", "filter4"];
for (const i of filter_loc) {
dropdowns.set(i+"-choice", new autoComplete({
data: {
src: sq2ItemFilters,
},
selector: "#"+i+"-choice",
wrapper: false,
resultsList: {
tabSelect: true,
noResults: true,
class: "search-box dark-7 rounded-bottom px-2 fw-bold dark-shadow-sm",
element: (list, data) => {
// dynamic result loc
console.log(i);
list.style.zIndex = "100";
let position = document.getElementById(i+"-dropdown").getBoundingClientRect();
window_pos = document.getElementById("search-container").getBoundingClientRect();
list.style.top = position.bottom - window_pos.top + 5 +"px";
list.style.left = position.x - window_pos.x +"px";
list.style.width = position.width+"px";
if (!data.results.length) {
message = document.createElement('li');
message.classList.add('scaled-font');
message.textContent = "No filters found!";
list.prepend(message);
}
},
},
resultItem: {
class: "scaled-font search-item",
selected: "dark-5",
},
events: {
input: {
selection: (event) => {
if (event.detail.selection.value) {
event.target.value = event.detail.selection.value;
}
doSearchSchedule();
},
},
}
}));
}
}
// atree parsing
function construct_AT(elem, tree) {
for (let i = 0; i < tree.length; i++) {
let node = tree[i];
// create rows if not exist
if (document.getElementById("atree-row-" + node.row) == null) {
for (let j = 0; j <= node.row; j++) {
if (document.getElementById("atree-row-" + j) == null) {
let row = document.createElement('div');
row.classList.add("row");
row.id = "atree-row-" + j;
row.style.height = elem.getBoundingClientRect().width / 5 + "px";
for (let k = 0; k < 5; k++) {
col = document.createElement('div');
col.classList.add('col', 'px-0');
row.appendChild(col);
};
elem.appendChild(row);
};
};
};
// create node
let node_elem = document.createElement('div')
node_elem.style = "background-image: url('" + node.image + "'); background-size: cover; width: 100%; height: 100%;";
if (node.connector && node.rotate != 0) {
node_elem.classList.add("rotate-" + node.rotate);
};
// add tooltip
if (!node.connector) {
node_elem.addEventListener('mouseover', function(e) {
if (e.target !== this) {return;}
let tooltip = this.children[0];
tooltip.style.top = this.getBoundingClientRect().bottom + window.scrollY * 1.02 + "px";
tooltip.style.left = this.parentElement.parentElement.getBoundingClientRect().left + (elem.getBoundingClientRect().width * .05 / 2) + "px";
tooltip.style.display = "block";
});
node_elem.addEventListener('mouseout', function(e) {
if (e.target !== this) {return;}
let tooltip = this.children[0];
tooltip.style.display = "none";
});
node_elem.classList.add("atree-node");
let active_tooltip = document.createElement('div');
active_tooltip.classList.add("rounded-bottom", "dark-7", "border");
active_tooltip.style.width = elem.getBoundingClientRect().width * .95 + "px";
active_tooltip.style.display = "none";
// tooltip text formatting
let active_tooltip_title = document.createElement('b');
active_tooltip_title.classList.add("scaled-font");
active_tooltip_title.textContent = node.title;
let active_tooltip_text = document.createElement('p');
active_tooltip_text.classList.add("scaled-font-sm");
active_tooltip_text.textContent = node.desc;
active_tooltip.appendChild(active_tooltip_title);
active_tooltip.appendChild(active_tooltip_text);
node_tooltip = active_tooltip.cloneNode(true);
active_tooltip.id = "atree-ab-" + node.title.replaceAll(" ", "");
node_tooltip.style.position = "absolute";
node_tooltip.style.zIndex = "100";
node_elem.appendChild(node_tooltip);
document.getElementById("atree-active").appendChild(active_tooltip);
node_elem.addEventListener('click', function(e) {
if (e.target !== this) {return;}
let tooltip = document.getElementById("atree-ab-" + node.title.replaceAll(" ", ""));
if (tooltip.style.display == "block") {
tooltip.style.display = "none";
this.classList.add("atree-node");
}
else {
tooltip.style.display = "block";
this.classList.remove("atree-node");
}
});
};
document.getElementById("atree-row-" + node.row).children[node.col].appendChild(node_elem);
};
};

View file

@ -1,692 +0,0 @@
const classDefenseMultipliers = new Map([ ["relik",0.50], ["bow",0.60], ["wand", 0.80], ["dagger", 1.0], ["spear",1.20], ["sword", 1.10]]);
/**
* @description Error to catch items that don't exist.
* @module ItemNotFound
*/
class ItemNotFound {
/**
* @class
* @param {String} item the item name entered
* @param {String} type the type of item
* @param {Boolean} genElement whether to generate an element from inputs
* @param {String} override override for item type
*/
constructor(item, type, genElement, override) {
/**
* @public
* @type {String}
*/
this.message = `Cannot find ${override||type} named ${item}`;
if (genElement)
/**
* @public
* @type {Element}
*/
this.element = document.getElementById(`${type}-choice`).parentElement.querySelectorAll("p.error")[0];
else
this.element = document.createElement("div");
}
}
/**
* @description Error to catch incorrect input.
* @module IncorrectInput
*/
class IncorrectInput {
/**
* @class
* @param {String} input the inputted text
* @param {String} format the correct format
* @param {String} sibling the id of the error node's sibling
*/
constructor(input, format, sibling) {
/**
* @public
* @type {String}
*/
this.message = `${input} is incorrect. Example: ${format}`;
/**
* @public
* @type {String}
*/
this.id = sibling;
}
}
/**
* @description Error that inputs an array of items to generate errors of.
* @module ListError
* @extends Error
*/
class ListError extends Error {
/**
* @class
* @param {Array} errors array of errors
*/
constructor(errors) {
let ret = [];
if (typeof errors[0] == "string") {
super(errors[0]);
} else {
super(errors[0].message);
}
for (let i of errors) {
if (typeof i == "string") {
ret.push(new Error(i));
} else {
ret.push(i);
}
}
/**
* @public
* @type {Object[]}
*/
this.errors = ret;
}
}
/*Class that represents a wynn player's build.
*/
class Build{
/**
* @description Construct a build.
* @param {Number} level : Level of the player.
* @param {String[]} equipment : List of equipment names that make up the build.
* In order: boots, Chestplate, Leggings, Boots, Ring1, Ring2, Brace, Neck, Weapon.
* @param {Number[]} powders : Powder application. List of lists of integers (powder IDs).
* In order: boots, Chestplate, Leggings, Boots, Weapon.
* @param {Object[]} inputerrors : List of instances of error-like classes.
*
* @param {Object[]} tomes: List of tomes.
* In order: 2x Weapon Mastery Tome, 4x Armor Mastery Tome, 1x Guild Tome.
* 2x Slaying Mastery Tome, 2x Dungeoneering Mastery Tome, 2x Gathering Mastery Tome are in game, but do not have "useful" stats (those that affect damage calculations or building)
*/
constructor(level,equipment, powders, externalStats, inputerrors=[], tomes){
let errors = inputerrors;
//this contains the Craft objects, if there are any crafted items. this.boots, etc. will contain the statMap of the Craft (which is built to be an expandedItem).
this.craftedItems = [];
this.customItems = [];
// NOTE: powders is just an array of arrays of powder IDs. Not powder objects.
this.powders = powders;
if(itemMap.get(equipment[0]) && itemMap.get(equipment[0]).type === "helmet") {
const helmet = itemMap.get(equipment[0]);
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
} else {
try {
let helmet = getCustomFromHash(equipment[0]) ? getCustomFromHash(equipment[0]) : (getCraftFromHash(equipment[0]) ? getCraftFromHash(equipment[0]) : undefined);
if (helmet.statMap.get("type") !== "helmet") {
throw new Error("Not a helmet");
}
this.powders[0] = this.powders[0].slice(0,helmet.statMap.get("slots"));
helmet.statMap.set("powders",this.powders[0].slice());
this.helmet = helmet.statMap;
applyArmorPowders(this.helmet, this.powders[0]);
if (this.helmet.get("custom")) {
this.customItems.push(helmet);
} else if (this.helmet.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(helmet);
}
} catch (Error) {
const helmet = itemMap.get("No Helmet");
this.powders[0] = this.powders[0].slice(0,helmet.slots);
this.helmet = expandItem(helmet, this.powders[0]);
errors.push(new ItemNotFound(equipment[0], "helmet", true));
}
}
if(itemMap.get(equipment[1]) && itemMap.get(equipment[1]).type === "chestplate") {
const chestplate = itemMap.get(equipment[1]);
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
} else {
try {
let chestplate = getCustomFromHash(equipment[1]) ? getCustomFromHash(equipment[1]) : (getCraftFromHash(equipment[1]) ? getCraftFromHash(equipment[1]) : undefined);
if (chestplate.statMap.get("type") !== "chestplate") {
throw new Error("Not a chestplate");
}
this.powders[1] = this.powders[1].slice(0,chestplate.statMap.get("slots"));
chestplate.statMap.set("powders",this.powders[1].slice());
this.chestplate = chestplate.statMap;
applyArmorPowders(this.chesplate, this.powders[1]);
if (this.chestplate.get("custom")) {
this.customItems.push(chestplate);
} else if (this.chestplate.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(chestplate);
}
} catch (Error) {
console.log(Error);
const chestplate = itemMap.get("No Chestplate");
this.powders[1] = this.powders[1].slice(0,chestplate.slots);
this.chestplate = expandItem(chestplate, this.powders[1]);
errors.push(new ItemNotFound(equipment[1], "chestplate", true));
}
}
if (itemMap.get(equipment[2]) && itemMap.get(equipment[2]).type === "leggings") {
const leggings = itemMap.get(equipment[2]);
this.powders[2] = this.powders[2].slice(0,leggings.slots);
this.leggings = expandItem(leggings, this.powders[2]);
} else {
try {
let leggings = getCustomFromHash(equipment[2]) ? getCustomFromHash(equipment[2]) : (getCraftFromHash(equipment[2]) ? getCraftFromHash(equipment[2]) : undefined);
if (leggings.statMap.get("type") !== "leggings") {
throw new Error("Not a leggings");
}
this.powders[2] = this.powders[2].slice(0,leggings.statMap.get("slots"));
leggings.statMap.set("powders",this.powders[2].slice());
this.leggings = leggings.statMap;
applyArmorPowders(this.leggings, this.powders[2]);
if (this.leggings.get("custom")) {
this.customItems.push(leggings);
} else if (this.leggings.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(leggings);
}
} catch (Error) {
const leggings = itemMap.get("No Leggings");
this.powders[2] = this.powders[2].slice(0,leggings.slots);
this.leggings = expandItem(leggings, this.powders[2]);
errors.push(new ItemNotFound(equipment[2], "leggings", true));
}
}
if (itemMap.get(equipment[3]) && itemMap.get(equipment[3]).type === "boots") {
const boots = itemMap.get(equipment[3]);
this.powders[3] = this.powders[3].slice(0,boots.slots);
this.boots = expandItem(boots, this.powders[3]);
} else {
try {
let boots = getCustomFromHash(equipment[3]) ? getCustomFromHash(equipment[3]) : (getCraftFromHash(equipment[3]) ? getCraftFromHash(equipment[3]) : undefined);
if (boots.statMap.get("type") !== "boots") {
throw new Error("Not a boots");
}
this.powders[3] = this.powders[3].slice(0,boots.statMap.get("slots"));
boots.statMap.set("powders",this.powders[3].slice());
this.boots = boots.statMap;
applyArmorPowders(this.boots, this.powders[3]);
if (this.boots.get("custom")) {
this.customItems.push(boots);
} else if (this.boots.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(boots);
}
} catch (Error) {
const boots = itemMap.get("No Boots");
this.powders[3] = this.powders[3].slice(0,boots.slots);
this.boots = expandItem(boots, this.powders[3]);
errors.push(new ItemNotFound(equipment[3], "boots", true));
}
}
if(itemMap.get(equipment[4]) && itemMap.get(equipment[4]).type === "ring") {
const ring = itemMap.get(equipment[4]);
this.ring1 = expandItem(ring, []);
}else{
try {
let ring = getCustomFromHash(equipment[4]) ? getCustomFromHash(equipment[4]) : (getCraftFromHash(equipment[4]) ? getCraftFromHash(equipment[4]) : undefined);
if (ring.statMap.get("type") !== "ring") {
throw new Error("Not a ring");
}
this.ring1 = ring.statMap;
if (this.ring1.get("custom")) {
this.customItems.push(ring);
} else if (this.ring1.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(ring);
}
} catch (Error) {
const ring = itemMap.get("No Ring 1");
this.ring1 = expandItem(ring, []);
errors.push(new ItemNotFound(equipment[4], "ring1", true, "ring"));
}
}
if(itemMap.get(equipment[5]) && itemMap.get(equipment[5]).type === "ring") {
const ring = itemMap.get(equipment[5]);
this.ring2 = expandItem(ring, []);
}else{
try {
let ring = getCustomFromHash(equipment[5]) ? getCustomFromHash(equipment[5]) : (getCraftFromHash(equipment[5]) ? getCraftFromHash(equipment[5]) : undefined);
if (ring.statMap.get("type") !== "ring") {
throw new Error("Not a ring");
}
this.ring2 = ring.statMap;
if (this.ring2.get("custom")) {
this.customItems.push(ring);
} else if (this.ring2.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(ring);
}
} catch (Error) {
const ring = itemMap.get("No Ring 2");
this.ring2 = expandItem(ring, []);
errors.push(new ItemNotFound(equipment[5], "ring2", true, "ring"));
}
}
if(itemMap.get(equipment[6]) && itemMap.get(equipment[6]).type === "bracelet") {
const bracelet = itemMap.get(equipment[6]);
this.bracelet = expandItem(bracelet, []);
}else{
try {
let bracelet = getCustomFromHash(equipment[6]) ? getCustomFromHash(equipment[6]) : (getCraftFromHash(equipment[6]) ? getCraftFromHash(equipment[6]) : undefined);
if (bracelet.statMap.get("type") !== "bracelet") {
throw new Error("Not a bracelet");
}
this.bracelet = bracelet.statMap;
if (this.bracelet.get("custom")) {
this.customItems.push(bracelet);
} else if (this.bracelet.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(bracelet);
}
} catch (Error) {
const bracelet = itemMap.get("No Bracelet");
this.bracelet = expandItem(bracelet, []);
errors.push(new ItemNotFound(equipment[6], "bracelet", true));
}
}
if(itemMap.get(equipment[7]) && itemMap.get(equipment[7]).type === "necklace") {
const necklace = itemMap.get(equipment[7]);
this.necklace = expandItem(necklace, []);
}else{
try {
let necklace = getCustomFromHash(equipment[7]) ? getCustomFromHash(equipment[7]) : (getCraftFromHash(equipment[7]) ? getCraftFromHash(equipment[7]) : undefined);
if (necklace.statMap.get("type") !== "necklace") {
throw new Error("Not a necklace");
}
this.necklace = necklace.statMap;
if (this.necklace.get("custom")) {
this.customItems.push(necklace);
} else if (this.necklace.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(necklace);
}
} catch (Error) {
const necklace = itemMap.get("No Necklace");
this.necklace = expandItem(necklace, []);
errors.push(new ItemNotFound(equipment[7], "necklace", true));
}
}
if(itemMap.get(equipment[8]) && itemMap.get(equipment[8]).category === "weapon") {
const weapon = itemMap.get(equipment[8]);
this.powders[4] = this.powders[4].slice(0,weapon.slots);
this.weapon = expandItem(weapon, this.powders[4]);
}else{
try {
let weapon = getCustomFromHash(equipment[8]) ? getCustomFromHash(equipment[8]) : (getCraftFromHash(equipment[8]) ? getCraftFromHash(equipment[8]) : undefined);
if (weapon.statMap.get("category") !== "weapon") {
throw new Error("Not a weapon");
}
this.weapon = weapon.statMap;
if (this.weapon.get("custom")) {
this.customItems.push(weapon);
} else if (this.weapon.get("crafted")) { //customs can also be crafted, but custom takes priority.
this.craftedItems.push(weapon);
}
this.powders[4] = this.powders[4].slice(0,this.weapon.get("slots"));
this.weapon.set("powders",this.powders[4].slice());
// document.getElementsByClassName("powder-specials")[0].style.display = "grid";
} catch (Error) {
const weapon = itemMap.get("No Weapon");
this.powders[4] = this.powders[4].slice(0,weapon.slots);
this.weapon = expandItem(weapon, this.powders[4]);
// document.getElementsByClassName("powder-specials")[0].style.display = "none";
errors.push(new ItemNotFound(equipment[8], "weapon", true));
}
}
//cannot craft tomes
if(tomeMap.get(tomes[0]) && tomeMap.get(tomes[0]).type === "weaponTome") {
const weaponTome1 = tomeMap.get(tomes[0]);
this.weaponTome1 = expandItem(weaponTome1, []);
} else {
try {
let weaponTome1 = getCustomFromHash(tomes[0]) ? getCustomFromHash(tomes[0]) : undefined;
if (weaponTome1.statMap.get("type") !== "weaponTome") {
throw new Error("Not a Weapon Tome");
}
if (this.weaponTome1.get("custom")) {
this.customItems.push(weaponTome1);
} //can't craft tomes
} catch (Error) {
const weaponTome1 = tomeMap.get("No Weapon Tome");
this.weaponTome1 = expandItem(weaponTome1, []);
errors.push(new ItemNotFound(tomes[0], "weaponTome1", true));
}
}
if(tomeMap.get(tomes[1]) && tomeMap.get(tomes[1]).type === "weaponTome") {
const weaponTome2 = tomeMap.get(tomes[1]);
this.weaponTome2 = expandItem(weaponTome2, []);
} else {
try {
let weaponTome2 = getCustomFromHash(tomes[1]) ? getCustomFromHash(tomes[1]) : undefined;
if (weaponTome2.statMap.get("type") !== "weaponTome") {
throw new Error("Not a Weapon Tome");
}
if (this.weaponTome2.get("custom")) {
this.customItems.push(weaponTome2);
} //can't craft tomes
} catch (Error) {
const weaponTome2 = tomeMap.get("No Weapon Tome");
this.weaponTome2 = expandItem(weaponTome2, []);
errors.push(new ItemNotFound(tomes[1], "weaponTome2", true));
}
}
if(tomeMap.get(tomes[2]) && tomeMap.get(tomes[2]).type === "armorTome") {
const armorTome1 = tomeMap.get(tomes[2]);
this.armorTome1 = expandItem(armorTome1, []);
} else {
try {
let armorTome1 = getCustomFromHash(tomes[2]) ? getCustomFromHash(tomes[2]) : undefined;
if (armorTome1.statMap.get("type") !== "armorTome") {
throw new Error("Not an Armor Tome");
}
if (this.armorTome1.get("custom")) {
this.customItems.push(armorTome1);
} //can't craft tomes
} catch (Error) {
const armorTome1 = tomeMap.get("No Armor Tome");
this.armorTome1 = expandItem(armorTome1, []);
errors.push(new ItemNotFound(tomes[2], "armorTome1", true));
}
}
if(tomeMap.get(tomes[3]) && tomeMap.get(tomes[3]).type === "armorTome") {
const armorTome2 = tomeMap.get(tomes[3]);
this.armorTome2 = expandItem(armorTome2, []);
} else {
try {
let armorTome2 = getCustomFromHash(tomes[3]) ? getCustomFromHash(tomes[3]) : undefined;
if (armorTome2.statMap.get("type") !== "armorTome") {
throw new Error("Not an Armor Tome");
}
if (this.armorTome2.get("custom")) {
this.customItems.push(armorTome2);
} //can't craft tomes
} catch (Error) {
const armorTome2 = tomeMap.get("No Armor Tome");
this.armorTome2 = expandItem(armorTome2, []);
errors.push(new ItemNotFound(tomes[3], "armorTome2", true));
}
}
if(tomeMap.get(tomes[4]) && tomeMap.get(tomes[4]).type === "armorTome") {
const armorTome3 = tomeMap.get(tomes[4]);
this.armorTome3 = expandItem(armorTome3, []);
} else {
try {
let armorTome3 = getCustomFromHash(tomes[4]) ? getCustomFromHash(tomes[4]) : undefined;
if (armorTome3.statMap.get("type") !== "armorTome") {
throw new Error("Not an Armor Tome");
}
if (this.armorTome3.get("custom")) {
this.customItems.push(armorTome3);
} //can't craft tomes
} catch (Error) {
const armorTome3 = tomeMap.get("No Armor Tome");
this.armorTome3 = expandItem(armorTome3, []);
errors.push(new ItemNotFound(tomes[4], "armorTome3", true));
}
}
if(tomeMap.get(tomes[5]) && tomeMap.get(tomes[5]).type === "armorTome") {
const armorTome4 = tomeMap.get(tomes[5]);
this.armorTome4 = expandItem(armorTome4, []);
} else {
try {
let armorTome4 = getCustomFromHash(tomes[5]) ? getCustomFromHash(tomes[5]) : undefined;
if (armorTome4.statMap.get("type") !== "armorTome") {
throw new Error("Not an Armor Tome");
}
if (this.armorTome4.get("custom")) {
this.customItems.push(armorTome4);
} //can't craft tomes
} catch (Error) {
const armorTome4 = tomeMap.get("No Armor Tome");
this.armorTome4 = expandItem(armorTome4, []);
errors.push(new ItemNotFound(tomes[5], "armorTome4", true));
}
}
if(tomeMap.get(tomes[6]) && tomeMap.get(tomes[6]).type === "guildTome") {
const guildTome1 = tomeMap.get(tomes[6]);
this.guildTome1 = expandItem(guildTome1, []);
} else {
try {
let guildTome1 = getCustomFromHash(tomes[6]) ? getCustomFromHash(tomes[6]) : undefined;
if (guildTome1.statMap.get("type") !== "guildTome1") {
throw new Error("Not an Guild Tome");
}
if (this.guildTome1.get("custom")) {
this.customItems.push(guildTome1);
} //can't craft tomes
} catch (Error) {
const guildTome1 = tomeMap.get("No Guild Tome");
this.guildTome1 = expandItem(guildTome1, []);
errors.push(new ItemNotFound(tomes[6], "guildTome1", true));
}
}
//console.log(this.craftedItems)
if (level < 1) { //Should these be constants?
this.level = 1;
} else if (level > 106) {
this.level = 106;
} else if (level <= 106 && level >= 1) {
this.level = level;
} else if (typeof level === "string") {
this.level = level;
errors.push(new IncorrectInput(level, "a number", "level-choice"));
} else {
errors.push("Level is not a string or number.");
}
document.getElementById("level-choice").value = this.level;
this.availableSkillpoints = levelToSkillPoints(this.level);
this.equipment = [ this.helmet, this.chestplate, this.leggings, this.boots, this.ring1, this.ring2, this.bracelet, this.necklace ];
this.tomes = [this.weaponTome1, this.weaponTome2, this.armorTome1, this.armorTome2, this.armorTome3, this.armorTome4, this.guildTome1];
this.items = this.equipment.concat([this.weapon]).concat(this.tomes);
// return [equip_order, best_skillpoints, final_skillpoints, best_total];
let result = calculate_skillpoints(this.equipment.concat(this.tomes), this.weapon);
console.log(result);
this.equip_order = result[0];
// How many skillpoints the player had to assign (5 number)
this.base_skillpoints = result[1];
// How many skillpoints the build ended up with (5 number)
this.total_skillpoints = result[2];
// How many skillpoints assigned (1 number, sum of base_skillpoints)
this.assigned_skillpoints = result[3];
this.activeSetCounts = result[4];
// For strength boosts like warscream, vanish, etc.
this.damageMultiplier = 1.0;
this.defenseMultiplier = 1.0;
// For other external boosts ;-;
this.externalStats = externalStats;
this.initBuildStats();
// Remove every error before adding specific ones
for (let i of document.getElementsByClassName("error")) {
i.textContent = "";
}
this.errors = errors;
if (errors.length > 0) this.errored = true;
}
/*Returns build in string format
*/
toString(){
return [this.equipment,this.weapon,this.tomes].flat();
}
/* Getters */
getSpellCost(spellIdx, cost) {
return Math.max(1, this.getBaseSpellCost(spellIdx, cost));
}
getBaseSpellCost(spellIdx, cost) {
cost = Math.ceil(cost * (1 - skillPointsToPercentage(this.total_skillpoints[2])));
cost += this.statMap.get("spRaw"+spellIdx);
return Math.floor(cost * (1 + this.statMap.get("spPct"+spellIdx) / 100));
}
/* Get melee stats for build.
Returns an array in the order:
*/
getMeleeStats(){
const stats = this.statMap;
if (this.weapon.get("tier") === "Crafted") {
stats.set("damageBases", [this.weapon.get("nDamBaseHigh"),this.weapon.get("eDamBaseHigh"),this.weapon.get("tDamBaseHigh"),this.weapon.get("wDamBaseHigh"),this.weapon.get("fDamBaseHigh"),this.weapon.get("aDamBaseHigh")]);
}
let adjAtkSpd = attackSpeeds.indexOf(stats.get("atkSpd")) + stats.get("atkTier");
if(adjAtkSpd > 6){
adjAtkSpd = 6;
}else if(adjAtkSpd < 0){
adjAtkSpd = 0;
}
let damage_mult = 1;
if (this.weapon.get("type") === "relik") {
damage_mult = 0.99; // CURSE YOU WYNNCRAFT
//One day we will create WynnWynn and no longer have shaman 99% melee injustice.
//In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams.
}
// 0spellmult for melee damage.
let results = calculateSpellDamage(stats, [100, 0, 0, 0, 0, 0], stats.get("mdRaw"), stats.get("mdPct") + this.externalStats.get("mdPct"), 0, this.weapon, this.total_skillpoints, damage_mult * this.damageMultiplier, this.externalStats);
let dex = this.total_skillpoints[1];
let totalDamNorm = results[0];
let totalDamCrit = results[1];
totalDamNorm.push(1-skillPointsToPercentage(dex));
totalDamCrit.push(skillPointsToPercentage(dex));
let damages_results = results[2];
let singleHitTotal = ((totalDamNorm[0]+totalDamNorm[1])*(totalDamNorm[2])
+(totalDamCrit[0]+totalDamCrit[1])*(totalDamCrit[2]))/2;
//Now do math
let normDPS = (totalDamNorm[0]+totalDamNorm[1])/2 * baseDamageMultiplier[adjAtkSpd];
let critDPS = (totalDamCrit[0]+totalDamCrit[1])/2 * baseDamageMultiplier[adjAtkSpd];
let avgDPS = (normDPS * (1 - skillPointsToPercentage(dex))) + (critDPS * (skillPointsToPercentage(dex)));
//[[n n n n] [e e e e] [t t t t] [w w w w] [f f f f] [a a a a] [lowtotal hightotal normalChance] [critlowtotal crithightotal critChance] normalDPS critCPS averageDPS adjAttackSpeed, singleHit]
return damages_results.concat([totalDamNorm,totalDamCrit,normDPS,critDPS,avgDPS,adjAtkSpd, singleHitTotal]).concat(results[3]);
}
/*
Get all defensive stats for this build.
*/
getDefenseStats(){
const stats = this.statMap;
let defenseStats = [];
let def_pct = skillPointsToPercentage(this.total_skillpoints[3]);
let agi_pct = skillPointsToPercentage(this.total_skillpoints[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 = classDefenseMultipliers.get(this.weapon.get("type"));
ehp[0] /= ((1-def_pct)*(1-agi_pct)*(2-defMult)*(2-this.defenseMultiplier));
ehp[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier));
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)*(2-defMult)*(2-this.defenseMultiplier));
ehpr[1] /= ((1-def_pct)*(2-defMult)*(2-this.defenseMultiplier));
defenseStats.push(ehpr);
//skp stats
defenseStats.push([ (1 - ((1-def_pct) * (2 - this.defenseMultiplier)))*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;
}
/* Get all stats for this build. Stores in this.statMap.
@pre The build itself should be valid. No checking of validity of pieces is done here.
*/
initBuildStats(){
let staticIDs = ["hp", "eDef", "tDef", "wDef", "fDef", "aDef", "str", "dex", "int", "def", "agi"];
//Create a map of this build's stats
let statMap = new Map();
for (const staticID of staticIDs) {
statMap.set(staticID, 0);
}
statMap.set("hp", levelToHPBase(this.level));
let major_ids = new Set();
for (const item of this.items){
for (let [id, value] of item.get("maxRolls")) {
if (staticIDs.includes(id)) {
continue;
}
statMap.set(id,(statMap.get(id) || 0)+value);
}
for (const staticID of staticIDs) {
if (item.get(staticID)) {
statMap.set(staticID, statMap.get(staticID) + item.get(staticID));
}
}
if (item.get("majorIds")) {
for (const major_id of item.get("majorIds")) {
major_ids.add(major_id);
}
}
}
statMap.set("activeMajorIDs", major_ids);
for (const [setName, count] of this.activeSetCounts) {
const bonus = sets[setName].bonuses[count-1];
for (const id in bonus) {
if (skp_order.includes(id)) {
// pass. Don't include skillpoints in ids
}
else {
statMap.set(id,(statMap.get(id) || 0)+bonus[id]);
}
}
}
statMap.set("poisonPct", 100);
// The stuff relevant for damage calculation!!! @ferricles
statMap.set("atkSpd", this.weapon.get("atkSpd"));
for (const x of skp_elements) {
this.externalStats.set(x + "DamPct", 0);
}
this.externalStats.set("mdPct", 0);
this.externalStats.set("sdPct", 0);
this.externalStats.set("damageBonus", [0, 0, 0, 0, 0]);
this.externalStats.set("defBonus",[0, 0, 0, 0, 0]);
this.externalStats.set("poisonPct", 0);
this.statMap = statMap;
this.aggregateStats();
}
aggregateStats() {
let statMap = this.statMap;
statMap.set("damageRaw", [this.weapon.get("nDam"), this.weapon.get("eDam"), this.weapon.get("tDam"), this.weapon.get("wDam"), this.weapon.get("fDam"), this.weapon.get("aDam")]);
statMap.set("damageBonus", [statMap.get("eDamPct"), statMap.get("tDamPct"), statMap.get("wDamPct"), statMap.get("fDamPct"), statMap.get("aDamPct")]);
statMap.set("defRaw", [statMap.get("eDef"), statMap.get("tDef"), statMap.get("wDef"), statMap.get("fDef"), statMap.get("aDef")]);
statMap.set("defBonus", [statMap.get("eDefPct"), statMap.get("tDefPct"), statMap.get("wDefPct"), statMap.get("fDefPct"), statMap.get("aDefPct")]);
statMap.set("defMult", classDefenseMultipliers.get(this.weapon.get("type")));
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,191 +0,0 @@
let powder_chars = [
'\u2724',
'\u2726',
'\u2749',
'\u2739',
'\u274b'
]
let subscript_nums = [
'\u2081',
'\u2082',
'\u2083',
'\u2084',
'\u2085',
'\u2086',
]
let skp_names = [
'str',
'dex',
'int',
'def',
'agi'
]
let elem_chars = [
'e',
't',
'w',
'f',
'a'
]
let elem_names = [
'earth',
'thunder',
'water',
'fire',
'air'
]
let elem_colors = [
"#00AA00",
"#FFFF55",
"#55FFFF",
"#FF5555",
"#FFFFFF"
]
let item_types = [
"Helmet",
"Chestplate",
"Leggings",
"Boots",
"Ring",
"Bracelet",
"Necklace",
"Dagger",
"Spear",
"Wand",
"Relik",
"Bow",
"Potion",
"Scroll",
"Food",
"Weapon Tome",
"Armor Tome",
"Guild Tome"
]
let tome_types = ['weaponTome', 'armorTome', 'guildTome'];
let tome_keys = ['weaponTome1', 'weaponTome2', 'armorTome1', 'armorTome2', 'armorTome3', 'armorTome4', 'guildTome1'];
/*
* Display commands
*/
let build_all_display_commands = [
"#defense-stats",
"str", "dex", "int", "def", "agi",
"mr", "ms",
"hprRaw", "hprPct",
"sdRaw", "sdPct",
"mdRaw", "mdPct",
"ref", "thorns",
"ls",
"poison",
"expd",
"spd",
"atkTier",
"!elemental",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"!elemental",
"spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4",
"rainbowRaw",
"sprint", "sprintReg",
"jh",
"xpb", "lb", "lq",
"spRegen",
"eSteal",
"gXp", "gSpd",
];
let build_offensive_display_commands = [
"str", "dex", "int", "def", "agi",
"mr", "ms",
"sdRaw", "sdPct",
"mdRaw", "mdPct",
"ref", "thorns",
"ls",
"poison",
"expd",
"spd",
"atkTier",
"rainbowRaw",
"!elemental",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"!elemental",
"spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4",
];
let build_basic_display_commands = [
'#defense-stats',
// defense stats [hp, ehp, hpr, ]
// "sPot", // base * atkspd + spell raws
// melee potential
// "mPot", // melee% * (base * atkspd) + melee raws
"mr", "ms",
"ls",
"poison",
"spd",
"atkTier",
]
let sq2_item_display_commands = [
"displayName",
"atkSpd",
"!elemental",
"hp",
"nDam_", "fDam_", "wDam_", "aDam_", "tDam_", "eDam_",
"!spacer",
"fDef", "wDef", "aDef", "tDef", "eDef",
"!elemental",
"classReq",
"lvl",
"strReq", "dexReq", "intReq", "defReq","agiReq",
"!spacer",
"str", "dex", "int", "def", "agi",
"hpBonus",
"hprRaw", "hprPct",
"sdRaw", "sdPct",
"mdRaw", "mdPct",
"mr", "ms",
"ref", "thorns",
"ls",
"poison",
"expd",
"spd",
"atkTier",
"!elemental",
"fDamPct", "wDamPct", "aDamPct", "tDamPct", "eDamPct",
"fDefPct", "wDefPct", "aDefPct", "tDefPct", "eDefPct",
"!elemental",
"spPct1", "spRaw1", "spPct2", "spRaw2", "spPct3", "spRaw3", "spPct4", "spRaw4",
"rainbowRaw",
"sprint", "sprintReg",
"jh",
"xpb", "lb", "lq",
"spRegen",
"eSteal",
"gXp", "gSpd",
"majorIds",
"!spacer",
"slots",
"!spacer",
"set",
"lore",
"quest",
"restrict"
];
let sq2_ing_display_order = [
"displayName", //tier will be displayed w/ name
"!spacer",
"ids",
"!spacer",
"posMods",
"itemIDs",
"consumableIDs",
"!spacer",
"lvl",
"skills",
]

View file

@ -250,7 +250,7 @@ function resetItemSearch() {
}
function init_items() {
items_expanded = items.filter( (i) => !("remapID" in i) ).map( (i) => expandItem(i, []) );
items_expanded = items.filter( (i) => !("remapID" in i) ).map( (i) => expandItem(i) );
}
load_init(init_items);

View file

@ -1,32 +1,8 @@
let getUrl = window.location;
const url_base = getUrl.protocol + "//" + getUrl.host + "/" + getUrl.pathname.split('/')[1];
const zip = (a, b) => a.map((k, i) => [k, b[i]]);
//updates all the OGP tags for a webpage. Should be called when build changes
function updateOGP() {
//update the embed URL
let url_elem = document.getElementById("ogp-url");
if (url_elem) {
url_elem.content = url_base+location.hash;
}
//update the embed text content
let build_elem = document.getElementById("ogp-build-list");
if (build_elem && player_build) {
let text = "WynnBuilder build:\n"+
"> "+player_build.helmet.get("displayName")+"\n"+
"> "+player_build.chestplate.get("displayName")+"\n"+
"> "+player_build.leggings.get("displayName")+"\n"+
"> "+player_build.boots.get("displayName")+"\n"+
"> "+player_build.ring1.get("displayName")+"\n"+
"> "+player_build.ring2.get("displayName")+"\n"+
"> "+player_build.bracelet.get("displayName")+"\n"+
"> "+player_build.necklace.get("displayName")+"\n"+
"> "+player_build.weapon.get("displayName")+" ["+player_build.weapon.get("powders").map(x => powderNames.get(x)).join("")+"]";
build_elem.content = text;
}
}
const zip2 = (a, b) => a.map((k, i) => [k, b[i]]);
const zip3 = (a, b, c) => a.map((k, i) => [k, b[i], c[i]]);
function clamp(num, low, high){
return Math.min(Math.max(num, low), high);
@ -217,9 +193,9 @@ Base64 = (function () {
*/
read_bit(idx) {
if (idx < 0 || idx >= this.length) {
throw new RangeError("Cannot read bit outside the range of the BitVector.");
throw new RangeError("Cannot read bit outside the range of the BitVector. ("+idx+" > "+this.length+")");
}
return ((this.bits[Math.floor(idx / 32)] & (1 << (idx % 32))) == 0 ? 0 : 1);
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.
@ -657,3 +633,127 @@ async function hardReload() {
function capitalizeFirst(str) {
return str[0].toUpperCase() + str.substring(1);
}
/** https://stackoverflow.com/questions/16839698/jquery-getscript-alternative-in-native-javascript
* If we ever want to write something that needs to import other js files
*/
const getScript = url => new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.async = true;
script.onerror = reject;
script.onload = script.onreadystatechange = function () {
const loadState = this.readyState;
if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
script.onload = script.onreadystatechange = null;
resolve();
}
document.head.appendChild(script);
})
/*
GENERIC TEST FUNCTIONS
*/
/** The generic assert function. Fails on all "false-y" values. Useful for non-object equality checks, boolean value checks, and existence checks.
*
* @param {*} arg - argument to assert.
* @param {String} msg - the error message to throw.
*/
function assert(arg, msg) {
if (!arg) {
throw new Error(msg ? msg : "Assert failed.");
}
}
/** Asserts object equality of the 2 parameters. For loose and strict asserts, use assert().
*
* @param {*} arg1 - first argument to compare.
* @param {*} arg2 - second argument to compare.
* @param {String} msg - the error message to throw.
*/
function assert_equals(arg1, arg2, msg) {
if (!Object.is(arg1, arg2)) {
throw new Error(msg ? msg : "Assert Equals failed. " + arg1 + " is not " + arg2 + ".");
}
}
/** Asserts object inequality of the 2 parameters. For loose and strict asserts, use assert().
*
* @param {*} arg1 - first argument to compare.
* @param {*} arg2 - second argument to compare.
* @param {String} msg - the error message to throw.
*/
function assert_not_equals(arg1, arg2, msg) {
if (Object.is(arg1, arg2)) {
throw new Error(msg ? msg : "Assert Not Equals failed. " + arg1 + " is " + arg2 + ".");
}
}
/** Asserts proximity between 2 arguments. Should be used for any floating point datatype.
*
* @param {*} arg1 - first argument to compare.
* @param {*} arg2 - second argument to compare.
* @param {Number} epsilon - the margin of error (<= del difference is ok). Defaults to -1E5.
* @param {String} msg - the error message to throw.
*/
function assert_near(arg1, arg2, epsilon = 1E-5, msg) {
if (Math.abs(arg1 - arg2) > epsilon) {
throw new Error(msg ? msg : "Assert Near failed. " + arg1 + " is not within " + epsilon + " of " + arg2 + ".");
}
}
/** Asserts that the input argument is null.
*
* @param {*} arg - the argument to test for null.
* @param {String} msg - the error message to throw.
*/
function assert_null(arg, msg) {
if (arg !== null) {
throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not null.");
}
}
/** Asserts that the input argument is undefined.
*
* @param {*} arg - the argument to test for undefined.
* @param {String} msg - the error message to throw.
*/
function assert_undefined(arg, msg) {
if (arg !== undefined) {
throw new Error(msg ? msg : "Assert Near failed. " + arg + " is not undefined.");
}
}
/** Asserts that there is an error when a callback function is run.
*
* @param {Function} func_binding - a function binding to run. Can be passed in with func.bind(null, arg1, ..., argn)
* @param {String} msg - the error message to throw.
*/
function assert_error(func_binding, msg) {
try {
func_binding();
} catch (err) {
return;
}
throw new Error(msg ? msg : "Function didn't throw an error.");
}
/**
* Deep copy object/array of basic types.
*/
function deepcopy(obj) {
if (typeof(obj) !== 'object' || obj === null) { // null or value type
return obj;
}
let ret = Array.isArray(obj) ? [] : {};
for (let key in obj) {
ret[key] = deepcopy(obj[key]);
}
return ret;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
media/atree/connect_c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
media/atree/connect_t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
media/atree/highlight_c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
media/atree/node_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

BIN
media/atree/node_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
media/atree/node_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

BIN
media/atree/node_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
media/atree/node_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

View file

@ -1,8 +1,6 @@
Process for getting new data:
1. run `python3 dump.py`. This will overwrite `dump.json` and `../ingreds.json`
2. Copy `../old clean.json` or `../compress.json` into `updated.json`
3. Run `python3 transform_merge.py`
4. Run `python3 ing_transform_combine.py`
5. Check validity (json differ or whatever)
6. Copy `clean.json` and `compress.json` into toplevel for usage
1. Get new data from API with `get.py`
2. Clean the data (may have to do manually) with the `process` related py files
3. Check validity (json differ or whatever)
4. Create clean and compress versions and copy them into toplevel for usage (can use `clean_json.py` and `compress_json.py` for this).

View file

@ -0,0 +1,66 @@
"""
Generate a JSON Ability Tree [atree_constants_idfied.json] with:
- All references replaced by numerical IDs
given a JSON Ability Tree with reference as string AND a JSON Ability Names to IDs.
"""
import json
# Ability names to IDs data
with open("atree_ids.json") as f:
id_data = json.loads(f.read())
# Ability tree data with reference as string
with open("atree_constants.json") as f:
atree_data = json.loads(f.read())
def translate_id(id_data, atree_data):
for _class, info in atree_data.items():
def translate(path, ref):
ref_dict = info
for x in path:
ref_dict = ref_dict[x]
ref_dict[ref] = id_data[_class][ref_dict[ref]]
for abil in range(len(info)):
info[abil]["id"] = id_data[_class][info[abil]["display_name"]]
for ref in range(len(info[abil]["parents"])):
translate([abil, "parents"], ref)
for ref in range(len(info[abil]["dependencies"])):
translate([abil, "dependencies"], ref)
for ref in range(len(info[abil]["blockers"])):
translate([abil, "blockers"], ref)
if "base_abil" in info[abil]:
base_abil_name = info[abil]["base_abil"]
if base_abil_name in id_data[_class]:
translate([abil], "base_abil")
if "effects" not in info[abil]:
print("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 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 and _input["abil"] in id_data[_class]:
_input["abil"] = id_data[_class][_input["abil"]]
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"]]
translate_id(id_data, atree_data)
with open('atree_constants_idfied.json', 'w', encoding='utf-8') as abil_dest:
json.dump(atree_data, abil_dest, ensure_ascii=False, indent=4)

View file

@ -0,0 +1,79 @@
"""
Generate a minified JSON Ability Tree [atree_constants_min.json] AND a minified .js form [atree_constants_min.js] of the Ability Tree with:
- All references replaced by numerical IDs
- Extra JSON File with Class: [Original name as key and Assigned IDs as value].
given [atree_constants.js] .js form of the Ability Tree with reference as string.
"""
import json
def translate_id(id_data, atree_data):
for _class, info in atree_data.items():
def translate(path, ref):
ref_dict = info
for x in path:
ref_dict = ref_dict[x]
ref_dict[ref] = id_data[_class][ref_dict[ref]]
for abil in range(len(info)):
info[abil]["id"] = id_data[_class][info[abil]["display_name"]]
for ref in range(len(info[abil]["parents"])):
translate([abil, "parents"], ref)
for ref in range(len(info[abil]["dependencies"])):
translate([abil, "dependencies"], ref)
for ref in range(len(info[abil]["blockers"])):
translate([abil, "blockers"], ref)
if "base_abil" in info[abil]:
base_abil_name = info[abil]["base_abil"]
if base_abil_name in id_data[_class]:
translate([abil], "base_abil")
if "effects" not in info[abil]:
print("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 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 and _input["abil"] in id_data[_class]:
_input["abil"] = id_data[_class][_input["abil"]]
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"]]
abilDict = {}
with open("atree_constants.js") as f:
data = f.read()
data = data.replace("const atrees = ", "")
data = json.loads(data)
for classType, info in data.items():
_id = 0
abilDict[classType] = {}
for abil in info:
abilDict[classType][abil["display_name"]] = _id
_id += 1
with open("atree_ids.json", "w", encoding='utf-8') as id_dest:
json.dump(abilDict, id_dest, ensure_ascii=False, indent=4)
translate_id(abilDict, data)
data_str = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
data_str = "const atrees=" + data_str
with open('atree_constants_min.js', 'w', encoding='utf-8') as abil_dest:
abil_dest.write(data_str)
with open('atree_constants_min.json', 'w', encoding='utf-8') as json_dest:
json.dump(data, json_dest, ensure_ascii=False, separators=(',', ':'))

146
py_script/atree-ids.json Normal file
View file

@ -0,0 +1,146 @@
{
"Archer": {
"Arrow Shield": 1,
"Escape": 2,
"Arrow Bomb": 3,
"Heart Shatter": 4,
"Fire Creep": 5,
"Bryophyte Roots": 6,
"Nimble String": 7,
"Arrow Storm": 8,
"Guardian Angels": 9,
"Windy Feet": 10,
"Basaltic Trap": 11,
"Windstorm": 12,
"Grappling Hook": 13,
"Implosion": 14,
"Twain's Arc": 15,
"Fierce Stomp": 16,
"Scorched Earth": 17,
"Leap": 18,
"Shocking Bomb": 19,
"Mana Trap": 20,
"Escape Artist": 21,
"Initiator": 22,
"Call of the Hound": 23,
"Arrow Hurricane": 24,
"Geyser Stomp": 25,
"Crepuscular Ray": 26,
"Grape Bomb": 27,
"Tangled Traps": 28,
"Snow Storm": 29,
"All-Seeing Panoptes": 30,
"Minefield": 31,
"Bow Proficiency I": 32,
"Cheaper Arrow Bomb": 33,
"Cheaper Arrow Storm": 34,
"Cheaper Escape": 35,
"Earth Mastery": 36,
"Thunder Mastery": 37,
"Water Mastery": 38,
"Air Mastery": 39,
"Fire Mastery": 40,
"More Shields": 41,
"Stormy Feet": 42,
"Refined Gunpowder": 43,
"More Traps": 44,
"Better Arrow Shield": 45,
"Better Leap": 46,
"Better Guardian Angels": 47,
"Cheaper Arrow Storm (2)": 48,
"Precise Shot": 49,
"Cheaper Arrow Shield": 50,
"Rocket Jump": 51,
"Cheaper Escape (2)": 52,
"Stronger Hook": 53,
"Cheaper Arrow Bomb (2)": 54,
"Bouncing Bomb": 55,
"Homing Shots": 56,
"Shrapnel Bomb": 57,
"Elusive": 58,
"Double Shots": 59,
"Triple Shots": 60,
"Power Shots": 61,
"Focus": 62,
"More Focus": 63,
"More Focus (2)": 64,
"Traveler": 65,
"Patient Hunter": 66,
"Stronger Patient Hunter": 67,
"Frenzy": 68,
"Phantom Ray": 69,
"Arrow Rain": 70,
"Decimator": 71
},
"Warrior": {
"Bash": 1,
"Spear Proficiency 1": 2,
"Cheaper Bash": 3,
"Double Bash": 4,
"Charge": 5,
"Heavy Impact": 6,
"Vehement": 7,
"Tougher Skin": 8,
"Uppercut": 9,
"Cheaper Charge": 10,
"War Scream": 11,
"Earth Mastery": 12,
"Thunder Mastery": 13,
"Water Mastery": 14,
"Air Mastery": 15,
"Fire Mastery": 16,
"Quadruple Bash": 17,
"Fireworks": 18,
"Half-Moon Swipe": 19,
"Flyby Jab": 20,
"Flaming Uppercut": 21,
"Iron Lungs": 22,
"Generalist": 23,
"Counter": 24,
"Mantle of the Bovemists": 25,
"Bak'al's Grasp": 26,
"Spear Proficiency 2": 27,
"Cheaper Uppercut": 28,
"Aerodynamics": 29,
"Provoke": 30,
"Precise Strikes": 31,
"Air Shout": 32,
"Enraged Blow": 33,
"Flying Kick": 34,
"Stronger Mantle": 35,
"Manachism": 36,
"Boiling Blood": 37,
"Ragnarokkr": 38,
"Ambidextrous": 39,
"Burning Heart": 40,
"Stronger Bash": 41,
"Intoxicating Blood": 42,
"Comet": 43,
"Collide": 44,
"Rejuvenating Skin": 45,
"Uncontainable Corruption": 46,
"Radiant Devotee": 47,
"Whirlwind Strike": 48,
"Mythril Skin": 49,
"Armour Breaker": 50,
"Shield Strike": 51,
"Sparkling Hope": 52,
"Massive Bash": 53,
"Tempest": 54,
"Spirit of the Rabbit": 55,
"Massacre": 56,
"Axe Kick": 57,
"Radiance": 58,
"Cheaper Bash 2": 59,
"Cheaper War Scream": 60,
"Discombobulate": 61,
"Thunderclap": 62,
"Cyclone": 63,
"Second Chance": 64,
"Blood Pact": 65,
"Haemorrhage": 66,
"Brink of Madness": 67,
"Cheaper Uppercut 2": 68,
"Martyr": 69
}
}

4967
py_script/atree-parse.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,5 @@
#parses all CI and creates a json file with all of them
import os
import re

View file

@ -1,3 +1,5 @@
#looks like something that hpp does with curl
import os
with open("ci.txt.2") as infile:

19
py_script/clean_json.py Normal file
View file

@ -0,0 +1,19 @@
'''
A generic file used for turning a json into a "clean" version of itself (human-friendly whitespace).
Clean files are useful for human reading and dev debugging.
Usage: python clean_json.py [infile rel path] [outfile rel path]
'''
if __name__ == "__main__":
import json
import argparse
parser = argparse.ArgumentParser(description="Pull data from wynn API.")
parser.add_argument('infile', help='input file to read data from')
parser.add_argument('outfile', help='output file to dump clean data into')
args = parser.parse_args()
infile, outfile = args.infile, args.outfile
json.dump(json.load(open(infile)), open(outfile, "w"), indent = 2)

View file

@ -1,8 +1,18 @@
import sys
import json
infile = sys.argv[1]
outfile = sys.argv[2]
if len(sys.argv) > 3 and sys.argv[3] == "decompress":
json.dump(json.load(open(infile)), open(outfile, "w"), indent=4)
else:
'''
A generic file used for turning a json into a compressed version of itself (minimal whitespaces).
Compressed files are useful for lowering the amount of data sent.
Usage: python compress_json.py [infile rel path] [outfile rel path]
'''
if __name__ == "__main__":
import json
import argparse
parser = argparse.ArgumentParser(description="Pull data from wynn API.")
parser.add_argument('infile', help='input file to read data from')
parser.add_argument('outfile', help='output file to dump clean data into')
args = parser.parse_args()
infile, outfile = args.infile, args.outfile
json.dump(json.load(open(infile)), open(outfile, "w"))

View file

@ -1,22 +0,0 @@
import requests
import json
import numpy as np
response = requests.get("https://api.wynncraft.com/public_api.php?action=itemDB&category=all")
with open("dump.json", "w") as outfile:
outfile.write(json.dumps(response.json()))
arr = np.array([])
for i in range(4):
response = requests.get("https://api.wynncraft.com/v2/ingredient/search/tier/" + str(i))
arr = np.append(arr, np.array(response.json()['data']))
with open("../ingreds.json", "w") as outfile:
outfile.write(json.dumps(list(arr)))
with open("../ingreds_compress.json", "w") as outfile:
outfile.write(json.dumps(list(arr)))
with open("../ingreds_clean.json", "w") as outfile:
json.dump(list(arr), outfile, indent = 2) #needs further cleaning

65
py_script/get.py Normal file
View file

@ -0,0 +1,65 @@
"""
Used to GET data from the Wynncraft API. Has shorthand options and allows
for requesting from a specific url.
Usage: python get.py [url or command] [outfile rel path]
Relevant page: https://docs.wynncraft.com/
"""
import argparse
import json
import numpy as np
import requests
parser = argparse.ArgumentParser(description="Pull data from wynn API.")
parser.add_argument('target', help='an API page, or preset [items, ings, recipes, terrs, maploc]')
parser.add_argument('outfile', help='output file to dump results into')
args = parser.parse_args()
req, outfile = args.target, args.outfile
CURR_WYNN_VERS = 2.0
#default to empty file output
response = {}
if req.lower() == "items":
response = requests.get("https://api.wynncraft.com/public_api.php?action=itemDB&category=all")
elif req.lower() == "ings":
response = {"ings":[]}
for i in range(4):
response['ings'].extend(requests.get("https://api.wynncraft.com/v2/ingredient/search/tier/" + str(i)).json()['data'])
elif req.lower() == "recipes":
temp = requests.get("https://api.wynncraft.com/v2/recipe/list")
response = {"recipes":[]}
for i in range(len(temp['data'])):
response["recipes"].extend(requests.get("https://api.wynncraft.com/v2/recipe/get/" + temp['data'][i]).json()['data'])
print("" + str(i) + " / " + str(len(temp['data'])))
elif req.lower() == "terrs":
response = requests.get("https://api.wynncraft.com/public_api.php?action=territoryList").json()['territories']
delkeys = ["territory","acquired","attacker"]
for t in response:
for key in delkeys:
del response[t][key]
response[t]["neighbors"] = []
#Dependency on a third-party manually-collected data source. May not update in sync with API.
terr_data = requests.get("https://gist.githubusercontent.com/kristofbolyai/87ae828ecc740424c0f4b3749b2287ed/raw/0735f2e8bb2d2177ba0e7e96ade421621070a236/territories.json").json()
for t in data:
response[t]["neighbors"] = data[t]["Routes"]
response[t]["resources"] = data[t]["Resources"]
response[t]["storage"] = data[t]["Storage"]
response[t]["emeralds"] = data[t]["Emeralds"]
response[t]["doubleemeralds"] = data[t]["DoubleEmerald"]
response[t]["doubleresource"] = data[t]["DoubleResource"]
elif req.lower() == "maploc":
response = requests.get('https://api.wynncraft.com/public_api.php?action=mapLocations')
else:
response = requests.get(req)
response['version'] = CURR_WYNN_VERS
json.dump(response, open(outfile, "w+"))

View file

@ -3646,5 +3646,11 @@
"Narcissist": 3648,
"Mask of the Spirits": 3649,
"Inhibitor": 3650,
"Spear of Testiness": 3651
"Spear of Testiness": 3651,
"Blue Wynnter Sweater": 3648,
"Green Wynnter Sweater": 3649,
"Purple Wynnter Sweater": 3650,
"Red Wynnter Sweater": 3651,
"Snowtread Boots": 3652,
"White Wynnter Sweater": 3653
}

View file

@ -1,3 +1,7 @@
"""
Used for grabbing image files at some point. Not used recently.
"""
import os
import json

View file

@ -1,4 +1,8 @@
"""Json diff checker for manual testing."""
"""
Json diff checker for manual testing - mainly debug
"""
import argparse
import json

View file

@ -1,3 +1,9 @@
"""
Used to parse a changelog at some point in the past. Could be used in the future.
Not a typically used file
"""
import json
import difflib

View file

@ -1,3 +1,9 @@
"""
Parses a set from a single file.
Usage: python parse_set_individual.py [infile]
"""
import sys
set_infile = sys.argv[1]

View file

@ -1,4 +0,0 @@
with open("sets.txt", "r") as setsFile:
sets_split = (x.split("'", 2)[1][2:] for x in setsFile.read().split("a href=")[1:])
with open("sets_list.txt", "w") as outFile:
outFile.write("\n".join(sets_split))

View file

@ -1,3 +1,7 @@
"""
Generates data for dps_vis
"""
import matplotlib.pyplot as plt
import json
import numpy as np

View file

@ -1,15 +1,30 @@
"""
Used to process the raw data about ingredients pulled from the API.
Usage:
- python process_ings.py [infile] [outfile]
OR
- python process_ings.py [infile and outfile]
"""
import json
with open("../ingreds.json", "r") as infile:
ing_data = json.loads(infile.read())
ings = ing_data
#this data does not have request :)
import sys
import os
if os.path.exists("../ing_map.json"):
with open("../ing_map.json","r") as ing_mapfile:
import base64
import argparse
parser = argparse.ArgumentParser(description="Process raw pulled ingredient data.")
parser.add_argument('infile', help='input file to read data from')
parser.add_argument('outfile', help='output file to dump clean data into')
args = parser.parse_args()
infile, outfile = args.infile, args.outfile
with open(infile, "r") as in_file:
ing_data = json.loads(in_file.read())
ings = ing_data['ings']
if os.path.exists("ing_map.json"):
with open("ing_map.json","r") as ing_mapfile:
ing_map = json.load(ing_mapfile)
else:
ing_map = {ing["name"]: i for i, ing in enumerate(ings)}
@ -146,8 +161,6 @@ ing_delete_keys = [
"skin"
]
print("loaded all files.")
for ing in ings:
for key in ing_delete_keys:
if key in ing:
@ -202,13 +215,10 @@ for ing in ings:
print(f'New Ingred: {ing["name"]}')
ing["id"] = ing_map[ing["name"]]
with open("../ingreds_clean.json", "w") as outfile:
json.dump(ing_data, outfile, indent = 2)
with open("../ingreds_compress.json", "w") as outfile:
json.dump(ing_data, outfile)
with open("../ing_map.json", "w") as ing_mapfile:
#save ing ids
with open("ing_map.json", "w+") as ing_mapfile:
json.dump(ing_map, ing_mapfile, indent = 2)
print('All ing jsons updated.')
#save ings
with open(outfile, "w+") as out_file:
json.dump(ing_data, out_file)

View file

@ -1,44 +1,35 @@
"""
Used to process the raw item data pulled from the API.
NOTE!!!!!!!
Usage:
- python process_items.py [infile] [outfile]
OR
- python process_items.py [infile and outfile]
DEMON TIDE 1.20 IS HARD CODED!
AMBIVALENCE IS REMOVED!
NOTE: id_map.json is due for change. Should be updated manually when Wynn2.0/corresponding WB version drops.
"""
import json
import sys
import os
import base64
import argparse
with open("dump.json", "r") as infile:
data = json.load(infile)
parser = argparse.ArgumentParser(description="Process raw pulled item data.")
parser.add_argument('infile', help='input file to read data from')
parser.add_argument('outfile', help='output file to dump clean data into')
args = parser.parse_args()
infile, outfile = args.infile, args.outfile
with open(infile, "r") as in_file:
data = json.loads(in_file.read())
with open("updated.json", "r") as oldfile:
old_data = json.load(oldfile)
items = data["items"]
old_items = old_data["items"]
if "request" in data:
del data["request"]
# import os
# sets = dict()
# for filename in os.listdir('sets'):
# if "json" not in filename:
# continue
# set_name = filename[1:].split(".")[0].replace("+", " ").replace("%27", "'")
# with open("sets/"+filename) as set_info:
# set_obj = json.load(set_info)
# for item in set_obj["items"]:
# item_set_map[item] = set_name
# sets[set_name] = set_obj
#
# data["sets"] = sets
data["sets"] = old_data["sets"]
item_set_map = dict()
for set_name, set_data in data["sets"].items():
for item_name in set_data["items"]:
item_set_map[item_name] = set_name
translate_mappings = {
#"name": "name",
@ -141,7 +132,12 @@ delete_keys = [
#"material"
]
with open("../clean.json", "r") as oldfile:
old_data = json.load(oldfile)
old_items = old_data['items']
id_map = {item["name"]: item["id"] for item in old_items}
with open("id_map.json", "r") as idmap_file:
id_map = json.load(idmap_file)
used_ids = set([v for k, v in id_map.items()])
max_id = 0
@ -150,8 +146,8 @@ known_item_names = set()
for item in items:
known_item_names.add(item["name"])
old_items_map = dict()
remap_items = []
old_items_map = dict()
for item in old_items:
if "remapID" in item:
remap_items.append(item)
@ -186,16 +182,18 @@ for item in items:
item_name = item["displayName"]
else:
item_name = item["name"]
if item_name in item_set_map:
item["set"] = item_set_map[item_name]
if item["name"] in old_items_map:
old_item = old_items_map[item["name"]]
if "hideSet" in old_item:
item["hideSet"] = old_item["hideSet"]
items.extend(remap_items)
with open("clean.json", "w") as outfile:
json.dump(data, outfile, indent=2)
with open("compress.json", "w") as outfile:
json.dump(data, outfile)
#write items back into data
data["items"] = items
#save id map
with open("id_map.json","w") as id_mapfile:
json.dump(id_map, id_mapfile, indent=2)
#write the data back to the outfile
with open(outfile, "w+") as out_file:
json.dump(data, out_file)

View file

@ -0,0 +1,59 @@
"""
Used to process the raw data about crafting recipes pulled from the API.
Usage:
- python process_recipes.py [infile] [outfile]
OR
- python process_recipes.py [infile and outfile]
"""
import json
import sys
import os
import base64
import argparse
parser = argparse.ArgumentParser(description="Process raw pulled recipe data.")
parser.add_argument('infile', help='input file to read data from')
parser.add_argument('outfile', help='output file to dump clean data into')
args = parser.parse_args()
infile, outfile = args.infile, args.outfile
with open(infile, "r") as in_file:
recipe_data = json.loads(in_file.read())
recipes = recipe_data["recipes"]
if os.path.exists("recipe_map.json"):
with open("recipe_map.json","r") as recipe_mapfile:
recipe_map = json.load(recipe_mapfile)
else:
recipe_map = {recipe["name"]: i for i, recipe in enumerate(recipes)}
recipe_translate_mappings = {
"level" : "lvl",
"id" : "name",
}
recipe_delete_keys = [ #lol
]
for recipe in recipes:
for key in recipe_delete_keys:
if key in recipe:
del recipe[key]
for k, v in recipe_translate_mappings.items():
if k in recipe:
recipe[v] = recipe[k]
del recipe[k]
if not (recipe["name"] in recipe_map):
recipe_map[recipe["name"]] = len(recipe_map)
print(f'New Recipe: {recipe["name"]}')
recipe["id"] = recipe_map[recipe["name"]]
#save recipe id map
with open("recipe_map.json", "w") as recipe_mapfile:
json.dump(recipe_map, recipe_mapfile, indent = 2)
#save recipe data
with open(outfile, "w+") as out_file:
json.dump(recipe_data, out_file)

Some files were not shown because too many files have changed in this diff Show more