main.js 93 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077
  1. /*
  2. * @licstart The following is the entire license notice for the JavaScript
  3. * code in this page.
  4. *
  5. * This file is part of oddjobs-dmg-calc.
  6. *
  7. * oddjobs-dmg-calc is free software: you can redistribute it and/or modify it
  8. * under the terms of the GNU Affero General Public License as published by the
  9. * Free Software Foundation, either version 3 of the License, or (at your
  10. * option) any later version.
  11. *
  12. * oddjobs-dmg-calc is distributed in the hope that it will be useful, but
  13. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  14. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
  15. * License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with oddjobs-dmg-calc. If not, see <https://www.gnu.org/licenses/>.
  19. *
  20. * @licend The above is the entire license notice for the JavaScript code in
  21. * this page.
  22. */
  23. import { ATTACK_LINES, ATTACK_REQS, attackIsElemental, attackName, attackPeriod, BAD_WEPS, chargeTypeFromValue, className, isHolySpell, isSummon, JOB_LVL_REQS, magicAttackPeriod, primaryStat, secondaryStat, SPELL_LINES, SPELL_LVL_REQS, spellName, weaponTypeName, chargeTypeToValue, } from "./data.js";
  24. import { truncClampedExpectation, truncClampedVariance } from "./math.js";
  25. import { Attack, Class, InputData, Spell, Stats, WeaponType, } from "./types.js";
  26. import { indefinite } from "./util.js";
  27. document.addEventListener("readystatechange", () => {
  28. if (document.readyState === "complete") {
  29. main();
  30. }
  31. });
  32. function main() {
  33. const saveAsButton = document.getElementById("save-as");
  34. const saveButton = document.getElementById("save");
  35. const loadButton = document.getElementById("load");
  36. const deleteButton = document.getElementById("delete");
  37. const exportButton = document.getElementById("export");
  38. const importButton = document.getElementById("import");
  39. const saveLoadDialog = document.getElementById("save-load-dialog");
  40. const currentlyLoaded = document.getElementById("currently-loaded");
  41. function currentlyLoadedName() {
  42. const cln = currentlyLoaded.childNodes[0];
  43. if (cln && cln.textContent && cln.textContent.trim()) {
  44. return cln.textContent.trim();
  45. }
  46. return undefined;
  47. }
  48. const strInput = document.getElementById("str");
  49. const dexInput = document.getElementById("dex");
  50. const intInput = document.getElementById("int");
  51. const lukInput = document.getElementById("luk");
  52. const totalWatkInput = document.getElementById("total-watk");
  53. const totalMatkInput = document.getElementById("total-matk");
  54. const echoInput = document.getElementById("echo");
  55. const masteryInput = document.getElementById("mastery");
  56. const skillDmgMultiInput = document.getElementById("skill-dmg-multi");
  57. const skillBasicAtkInput = document.getElementById("skill-basic-atk");
  58. const skillLinesInput = document.getElementById("skill-lines");
  59. const critProbInput = document.getElementById("crit-prob");
  60. const critDmgInput = document.getElementById("crit-dmg");
  61. const classInput = document.getElementById("class");
  62. const levelInput = document.getElementById("level");
  63. const weaponTypeInput = document.getElementById("weapon-type");
  64. const goodAnimProbInput = document.getElementById("good-anim-prob");
  65. const attackInput = document.getElementById("attack");
  66. const spellInput = document.getElementById("spell");
  67. const speedInput = document.getElementById("speed");
  68. const spellBoosterInput = document.getElementById("spell-booster");
  69. const eleAmpInput = document.getElementById("ele-amp");
  70. const eleBoostInput = document.getElementById("ele-boost");
  71. const eleWepInput = document.getElementById("ele-wep");
  72. const eleChargeInputs = Array.from(document.getElementsByName("ele-charge"));
  73. const eleChargeDmgInput = document.getElementById("ele-charge-dmg");
  74. const eleChargeLevelInput = document.getElementById("ele-charge-level");
  75. const caActiveInput = document.getElementById("ca-active");
  76. const caDmgInput = document.getElementById("ca-dmg");
  77. const caLevelInput = document.getElementById("ca-level");
  78. const caOrbsInput = document.getElementById("ca-orbs");
  79. const zerkActiveInput = document.getElementById("zerk-active");
  80. const zerkDmgInput = document.getElementById("zerk-dmg");
  81. const enemyWdefInput = document.getElementById("enemy-wdef");
  82. const enemyMdefInput = document.getElementById("enemy-mdef");
  83. const eleSusInput = document.getElementById("ele-sus");
  84. const enemyLevelInput = document.getElementById("enemy-level");
  85. const enemyCountInput = document.getElementById("enemy-count");
  86. const hitOrdInput = document.getElementById("hit-ord");
  87. const rangeOutput = document.getElementById("range");
  88. const critRangeOutput = document.getElementById("crit-range");
  89. const expectedPerHitOutput = document.getElementById("expected-per-hit");
  90. const sdPerHitOutput = document.getElementById("sd-per-hit");
  91. const cvPerHitOutput = document.getElementById("cv-per-hit");
  92. const totalRangeOutput = document.getElementById("total-range");
  93. const expectedPerHitTotalOutput = document.getElementById("expected-per-hit-total");
  94. const sdPerHitTotalOutput = document.getElementById("sd-per-hit-total");
  95. const cvPerHitTotalOutput = document.getElementById("cv-per-hit-total");
  96. const expectedDpsOutput = document.getElementById("expected-dps");
  97. const sdDpsOutput = document.getElementById("sd-dps");
  98. const cvDpsOutput = document.getElementById("cv-dps");
  99. const rangeMagicOutput = document.getElementById("range-magic");
  100. const critRangeMagicOutput = document.getElementById("crit-range-magic");
  101. const expectedPerHitMagicOutput = document.getElementById("expected-per-hit-magic");
  102. const sdPerHitMagicOutput = document.getElementById("sd-per-hit-magic");
  103. const cvPerHitMagicOutput = document.getElementById("cv-per-hit-magic");
  104. const totalRangeMagicOutput = document.getElementById("total-range-magic");
  105. const expectedPerHitTotalMagicOutput = document.getElementById("expected-per-hit-total-magic");
  106. const sdPerHitTotalMagicOutput = document.getElementById("sd-per-hit-total-magic");
  107. const cvPerHitTotalMagicOutput = document.getElementById("cv-per-hit-total-magic");
  108. const expectedDpsMagicOutput = document.getElementById("expected-dps-magic");
  109. const sdDpsMagicOutput = document.getElementById("sd-dps-magic");
  110. const cvDpsMagicOutput = document.getElementById("cv-dps-magic");
  111. const warningsDiv = document.getElementById("warnings-div");
  112. function readInputData() {
  113. let str = Math.max(parseInt(strInput.value, 10), 4);
  114. if (!Number.isFinite(str)) {
  115. str = 4;
  116. }
  117. strInput.value = "" + str;
  118. let dex = Math.max(parseInt(dexInput.value, 10), 4);
  119. if (!Number.isFinite(dex)) {
  120. dex = 4;
  121. }
  122. dexInput.value = "" + dex;
  123. let int = Math.max(parseInt(intInput.value, 10), 4);
  124. if (!Number.isFinite(int)) {
  125. int = 4;
  126. }
  127. intInput.value = "" + int;
  128. let luk = Math.max(parseInt(lukInput.value, 10), 4);
  129. if (!Number.isFinite(luk)) {
  130. luk = 4;
  131. }
  132. lukInput.value = "" + luk;
  133. let totalWatk = Math.max(parseInt(totalWatkInput.value, 10), 0);
  134. if (!Number.isFinite(totalWatk)) {
  135. totalWatk = 0;
  136. }
  137. totalWatkInput.value = "" + totalWatk;
  138. let totalMatk = Math.max(parseInt(totalMatkInput.value, 10), 0);
  139. if (!Number.isFinite(totalMatk)) {
  140. totalMatk = 0;
  141. }
  142. totalMatkInput.value = "" + totalMatk;
  143. let echo = Math.max(parseInt(echoInput.value, 10), 0);
  144. if (!Number.isFinite(echo)) {
  145. echo = 0;
  146. }
  147. echoInput.value = "" + echo;
  148. let mastery = Math.min(Math.max(parseInt(masteryInput.value, 10), 10), 90);
  149. if (!Number.isFinite(mastery)) {
  150. mastery = 10;
  151. }
  152. mastery -= mastery % 5;
  153. masteryInput.value = "" + mastery;
  154. let skillDmgMulti = Math.max(parseInt(skillDmgMultiInput.value, 10), 0);
  155. if (!Number.isFinite(skillDmgMulti)) {
  156. skillDmgMulti = 100;
  157. }
  158. skillDmgMultiInput.value = "" + skillDmgMulti;
  159. let skillBasicAtk = Math.max(parseInt(skillBasicAtkInput.value, 10), 0);
  160. if (!Number.isFinite(skillBasicAtk)) {
  161. skillBasicAtk = 10;
  162. }
  163. skillBasicAtkInput.value = "" + skillBasicAtk;
  164. let skillLines = Math.max(parseInt(skillLinesInput.value, 10), 1);
  165. if (!Number.isFinite(skillLines)) {
  166. skillLines = 1;
  167. }
  168. skillLinesInput.value = "" + skillLines;
  169. let critProb = Math.min(Math.max(parseInt(critProbInput.value, 10), 0), 100);
  170. if (!Number.isFinite(critProb)) {
  171. critProb = 0;
  172. }
  173. critProbInput.value = "" + critProb;
  174. let critDmg = Math.max(parseInt(critDmgInput.value, 10), 0);
  175. if (!Number.isFinite(critDmg)) {
  176. critDmg = 0;
  177. }
  178. critDmgInput.value = "" + critDmg;
  179. let clazz = parseInt(classInput.value, 10);
  180. if (!Number.isFinite(clazz) || !(clazz in Class)) {
  181. clazz = 0;
  182. }
  183. classInput.value = "" + clazz;
  184. let level = Math.min(Math.max(parseInt(levelInput.value, 10), 1), 200);
  185. if (!Number.isFinite(level)) {
  186. level = 30;
  187. }
  188. levelInput.value = "" + level;
  189. let wepType = parseInt(weaponTypeInput.value, 10);
  190. if (!Number.isFinite(wepType) || !(wepType in WeaponType)) {
  191. wepType = 30;
  192. }
  193. weaponTypeInput.value = "" + wepType;
  194. let goodAnimProb = Math.min(Math.max(parseFloat(goodAnimProbInput.value), 0), 100);
  195. if (!Number.isFinite(goodAnimProb)) {
  196. goodAnimProb = 60;
  197. }
  198. goodAnimProbInput.value = "" + goodAnimProb;
  199. let attack = parseInt(attackInput.value, 10);
  200. if (!Number.isFinite(attack) || !(attack in Attack)) {
  201. attack = 0;
  202. }
  203. attackInput.value = "" + attack;
  204. let spell = parseInt(spellInput.value, 10);
  205. if (!Number.isFinite(spell) || !(spell in Spell)) {
  206. spell = 0;
  207. }
  208. spellInput.value = "" + spell;
  209. let speed = Math.min(Math.max(parseInt(speedInput.value, 10), 2), 9);
  210. if (!Number.isFinite(speed)) {
  211. speed = 6;
  212. }
  213. speedInput.value = "" + speed;
  214. let spellBooster = Math.min(Math.max(parseInt(spellBoosterInput.value, 10), -2), 0);
  215. if (!Number.isFinite(spellBooster)) {
  216. spellBooster = 0;
  217. }
  218. spellBoosterInput.value = "" + spellBooster;
  219. let eleAmp = Math.max(parseInt(eleAmpInput.value, 10), 100);
  220. if (!Number.isFinite(eleAmp)) {
  221. eleAmp = 100;
  222. }
  223. eleAmpInput.value = "" + eleAmp;
  224. let eleBoost = Math.max(parseInt(eleBoostInput.value, 10), 0);
  225. if (!Number.isFinite(eleBoost)) {
  226. eleBoost = 0;
  227. }
  228. eleBoostInput.value = "" + eleBoost;
  229. let eleWep = Math.max(parseInt(eleWepInput.value, 10), 0);
  230. if (!Number.isFinite(eleWep)) {
  231. eleWep = 0;
  232. }
  233. eleWepInput.value = "" + eleWep;
  234. const eleChargeType = (() => {
  235. let eleChargeType = undefined;
  236. for (const eleChargeInput of eleChargeInputs) {
  237. if (eleChargeInput.checked) {
  238. eleChargeType = chargeTypeFromValue(eleChargeInput.value);
  239. break;
  240. }
  241. }
  242. if (eleChargeType === undefined) {
  243. eleChargeInputs.forEach(inp => (inp.checked = false));
  244. const noEleChargeInput = document.getElementById("no-ele-charge");
  245. noEleChargeInput.checked = true;
  246. return 0 /* ChargeType.None */;
  247. }
  248. return eleChargeType;
  249. })();
  250. let eleChargeDmg = Math.max(parseInt(eleChargeDmgInput.value, 10), 100);
  251. if (!Number.isFinite(eleChargeDmg)) {
  252. eleChargeDmg = 100;
  253. }
  254. eleChargeDmgInput.value = "" + eleChargeDmg;
  255. let eleChargeLevel = Math.min(Math.max(parseInt(eleChargeLevelInput.value, 10), 1), 30);
  256. if (!Number.isFinite(eleChargeLevel)) {
  257. eleChargeLevel = 1;
  258. }
  259. eleChargeLevelInput.value = "" + eleChargeLevel;
  260. const caActive = caActiveInput.checked;
  261. let caDmg = Math.max(parseInt(caDmgInput.value, 10), 100);
  262. if (!Number.isFinite(caDmg)) {
  263. caDmg = 104;
  264. }
  265. caDmgInput.value = "" + caDmg;
  266. let caLevel = Math.min(Math.max(parseInt(caLevelInput.value, 10), 1), 30);
  267. if (!Number.isFinite(caLevel)) {
  268. caLevel = 1;
  269. }
  270. caLevelInput.value = "" + caLevel;
  271. let caOrbs = Math.min(Math.max(parseInt(caOrbsInput.value, 10), 1), 10);
  272. if (!Number.isFinite(caOrbs)) {
  273. caOrbs = 1;
  274. }
  275. caOrbsInput.value = "" + caOrbs;
  276. const zerkActive = zerkActiveInput.checked;
  277. let zerkDmg = Math.max(parseInt(zerkDmgInput.value, 10), 100);
  278. if (!Number.isFinite(zerkDmg)) {
  279. zerkDmg = 100;
  280. }
  281. zerkDmgInput.value = "" + zerkDmg;
  282. let enemyWdef = parseInt(enemyWdefInput.value, 10);
  283. if (!Number.isFinite(enemyWdef)) {
  284. enemyWdef = 0;
  285. }
  286. enemyWdefInput.value = "" + enemyWdef;
  287. let enemyMdef = parseInt(enemyMdefInput.value, 10);
  288. if (!Number.isFinite(enemyMdef)) {
  289. enemyMdef = 0;
  290. }
  291. enemyMdefInput.value = "" + enemyMdef;
  292. let eleSus = Math.min(Math.max(parseFloat(eleSusInput.value), 0), 1.5);
  293. if (!Number.isFinite(eleSus) ||
  294. !(eleSus === 0 || eleSus === 0.5 || eleSus === 1 || eleSus === 1.5)) {
  295. eleSus = 1;
  296. }
  297. eleSusInput.value = "" + eleSus;
  298. let enemyLevel = Math.max(parseInt(enemyLevelInput.value, 10), 1);
  299. if (!Number.isFinite(enemyLevel)) {
  300. enemyLevel = 1;
  301. }
  302. enemyLevelInput.value = "" + enemyLevel;
  303. let enemyCount = Math.min(Math.max(parseInt(enemyCountInput.value, 10), 1), 15);
  304. if (!Number.isFinite(enemyCount)) {
  305. enemyCount = 1;
  306. }
  307. enemyCountInput.value = "" + enemyCount;
  308. let hitOrd = Math.min(Math.max(parseInt(hitOrdInput.value, 10), 1), 6);
  309. if (!Number.isFinite(hitOrd)) {
  310. hitOrd = 1;
  311. }
  312. hitOrdInput.value = "" + hitOrd;
  313. return new InputData(new Stats(str, dex, int, luk), totalWatk, totalMatk, echo / 100, mastery / 100, skillDmgMulti / 100, skillBasicAtk, skillLines, critProb / 100, critDmg / 100, clazz, level, wepType, goodAnimProb / 100, attack, spell, speed, spellBooster, eleAmp / 100, eleBoost / 100, eleWep / 100, eleChargeType, eleChargeDmg / 100, eleChargeLevel, caActive, caDmg, caLevel, caOrbs, zerkActive, zerkDmg / 100, enemyWdef, enemyMdef, eleSus, enemyLevel, enemyCount, hitOrd);
  314. }
  315. function writeInputData(inputData) {
  316. strInput.value = "" + inputData.stats.str;
  317. dexInput.value = "" + inputData.stats.dex;
  318. intInput.value = "" + inputData.stats.int;
  319. lukInput.value = "" + inputData.stats.luk;
  320. totalWatkInput.value = "" + inputData.totalWatk;
  321. totalMatkInput.value = "" + inputData.totalMatk;
  322. echoInput.value = "" + inputData.echo * 100;
  323. masteryInput.value = "" + inputData.mastery * 100;
  324. skillDmgMultiInput.value = "" + inputData.skillDmgMulti * 100;
  325. skillBasicAtkInput.value = "" + inputData.skillBasicAtk;
  326. skillLinesInput.value = "" + inputData.skillLines;
  327. critProbInput.value = "" + inputData.critProb * 100;
  328. critDmgInput.value = "" + inputData.critDmg * 100;
  329. classInput.value = "" + inputData.clazz;
  330. levelInput.value = "" + inputData.level;
  331. weaponTypeInput.value = "" + inputData.wepType;
  332. goodAnimProbInput.value = "" + inputData.goodAnimProb * 100;
  333. attackInput.value = "" + inputData.attack;
  334. spellInput.value = "" + inputData.spell;
  335. speedInput.value = "" + inputData.speed;
  336. spellBoosterInput.value = "" + inputData.spellBooster;
  337. eleAmpInput.value = "" + inputData.eleAmp * 100;
  338. eleBoostInput.value = "" + inputData.eleBoost * 100;
  339. eleWepInput.value = "" + inputData.eleWep * 100;
  340. eleChargeInputs.forEach(inp => (inp.checked = false));
  341. const chargeInput = document.getElementById(chargeTypeToValue(inputData.eleChargeType));
  342. chargeInput.checked = true;
  343. eleChargeDmgInput.value = "" + inputData.eleChargeDmg * 100;
  344. eleChargeLevelInput.value = "" + inputData.eleChargeLevel;
  345. caActiveInput.checked = inputData.caActive;
  346. caDmgInput.value = "" + inputData.caDmg;
  347. caLevelInput.value = "" + inputData.caLevel;
  348. caOrbsInput.value = "" + inputData.caOrbs;
  349. zerkActiveInput.checked = inputData.zerkActive;
  350. zerkDmgInput.value = "" + inputData.zerkDmg * 100;
  351. enemyWdefInput.value = "" + inputData.enemyWdef;
  352. enemyMdefInput.value = "" + inputData.enemyMdef;
  353. eleSusInput.value = "" + inputData.eleSus;
  354. enemyLevelInput.value = "" + inputData.enemyLevel;
  355. enemyCountInput.value = "" + inputData.enemyCount;
  356. hitOrdInput.value = "" + inputData.hitOrd;
  357. }
  358. function recalculate() {
  359. const inputData = readInputData();
  360. const critQ = 1 - inputData.critProb;
  361. recalculatePhys(inputData, critQ);
  362. recalculateMagic(inputData, critQ);
  363. recalculateWarnings(inputData);
  364. }
  365. function recalculatePhys(inputData, critQ) {
  366. const goodAnimProb = (() => {
  367. switch (inputData.attack) {
  368. case Attack.Rush:
  369. case Attack.CrusherLow:
  370. case Attack.CrusherHigh:
  371. case Attack.SomersaultKick:
  372. case Attack.AerialStrike:
  373. return swingProbToGoodAnimProb(inputData, 0);
  374. case Attack.Brandish:
  375. return 0.5;
  376. case Attack.Blast:
  377. return swingProbToGoodAnimProb(inputData, 0.6);
  378. case Attack.HeavensHammerXiuz:
  379. case Attack.HeavensHammerXiuzCorrected:
  380. return 1;
  381. case Attack.Fury:
  382. return swingProbToGoodAnimProb(inputData, 1);
  383. default:
  384. return inputData.goodAnimProb;
  385. }
  386. })();
  387. const badAnimProb = 1 - goodAnimProb;
  388. const [caMod, eleChargeMod] = isSummon(inputData.attack)
  389. ? [1, 1]
  390. : [caModifier(inputData), eleChargeModifier(inputData)];
  391. const eleSus = attackEffectiveEleSus(inputData);
  392. const [minDmgPhysBad, maxDmgPhysGood] = [
  393. (() => {
  394. const goodAnim = goodAnimProb >= 1;
  395. switch (inputData.attack) {
  396. case Attack.BowWhack:
  397. case Attack.PowerKnockBack:
  398. return minDmgBowWhack(inputData);
  399. case Attack.ClawPunch:
  400. return minDmgClawPunch(inputData);
  401. case Attack.Panic:
  402. case Attack.Coma:
  403. return minDmgCaFinisher(inputData, goodAnim);
  404. case Attack.HeavensHammerXiuz:
  405. return minDmgHhXiuz(inputData);
  406. case Attack.HeavensHammerXiuzCorrected:
  407. return minDmgPhys(inputData, true);
  408. case Attack.DragonRoar:
  409. return minDmgDragonRoar(inputData);
  410. // Massive hack to make Arrow Bomb easier to work with...
  411. case Attack.ArrowBombImpact:
  412. return minDmgArrowBombImpact(inputData, false);
  413. case Attack.ArrowBombSplash:
  414. return minDmgArrowBombSplash(inputData, false);
  415. case Attack.Phoenix:
  416. case Attack.Frostprey:
  417. case Attack.Octopus:
  418. case Attack.Gaviota:
  419. case Attack.WrathOfTheOctopi:
  420. return minDmgDexSummon(inputData);
  421. case Attack.LuckySeven:
  422. case Attack.TripleThrow:
  423. return minDmgLuckySeven(inputData);
  424. case Attack.NinjaAmbush:
  425. return dmgNinjaAmbush(inputData);
  426. case Attack.VenomousStar:
  427. case Attack.VenomousStab:
  428. return minDmgVenom(inputData);
  429. default:
  430. return minDmgPhys(inputData, goodAnim);
  431. }
  432. })(),
  433. (() => {
  434. const goodAnim = goodAnimProb > 0;
  435. switch (inputData.attack) {
  436. case Attack.BowWhack:
  437. case Attack.PowerKnockBack:
  438. return maxDmgBowWhack(inputData);
  439. case Attack.ClawPunch:
  440. return maxDmgClawPunch(inputData);
  441. case Attack.Panic:
  442. case Attack.Coma:
  443. return maxDmgCaFinisher(inputData, goodAnim);
  444. case Attack.HeavensHammerXiuz:
  445. case Attack.HeavensHammerXiuzCorrected:
  446. return maxDmgPhys(inputData, true);
  447. case Attack.DragonRoar:
  448. return maxDmgDragonRoar(inputData);
  449. // Massive hack to make Arrow Bomb easier to work with...
  450. case Attack.ArrowBombImpact:
  451. return maxDmgArrowBombImpact(inputData, true);
  452. case Attack.ArrowBombSplash:
  453. return maxDmgArrowBombSplash(inputData, true);
  454. case Attack.Phoenix:
  455. case Attack.Frostprey:
  456. case Attack.Octopus:
  457. case Attack.Gaviota:
  458. case Attack.WrathOfTheOctopi:
  459. return maxDmgDexSummon(inputData);
  460. case Attack.LuckySeven:
  461. case Attack.TripleThrow:
  462. return maxDmgLuckySeven(inputData);
  463. case Attack.NinjaAmbush:
  464. return dmgNinjaAmbush(inputData);
  465. case Attack.VenomousStar:
  466. case Attack.VenomousStab:
  467. return maxDmgVenom(inputData);
  468. default:
  469. return maxDmgPhys(inputData, goodAnim);
  470. }
  471. })(),
  472. ].map(dmg => dmg * eleSus * caMod * eleChargeMod);
  473. const [minDmgPhysGood, maxDmgPhysBad] = [
  474. (() => {
  475. if (goodAnimProb <= 0) {
  476. return minDmgPhysBad;
  477. }
  478. switch (inputData.attack) {
  479. case Attack.BowWhack:
  480. case Attack.PowerKnockBack:
  481. case Attack.ClawPunch:
  482. case Attack.Rush:
  483. case Attack.HeavensHammerXiuz:
  484. case Attack.HeavensHammerXiuzCorrected:
  485. case Attack.CrusherHigh:
  486. case Attack.CrusherLow:
  487. case Attack.Fury:
  488. case Attack.DragonRoar:
  489. case Attack.Phoenix:
  490. case Attack.Frostprey:
  491. case Attack.Octopus:
  492. case Attack.Gaviota:
  493. case Attack.WrathOfTheOctopi:
  494. case Attack.LuckySeven:
  495. case Attack.TripleThrow:
  496. case Attack.NinjaAmbush:
  497. case Attack.VenomousStar:
  498. case Attack.VenomousStab:
  499. case Attack.SomersaultKick:
  500. case Attack.AerialStrike:
  501. return minDmgPhysBad;
  502. case Attack.Panic:
  503. case Attack.Coma:
  504. return minDmgCaFinisher(inputData, true);
  505. // Massive hack to make Arrow Bomb easier to work with...
  506. case Attack.ArrowBombImpact:
  507. return minDmgArrowBombImpact(inputData, true);
  508. case Attack.ArrowBombSplash:
  509. return minDmgArrowBombSplash(inputData, true);
  510. default:
  511. return minDmgPhys(inputData, true);
  512. }
  513. })(),
  514. (() => {
  515. if (goodAnimProb >= 1) {
  516. return maxDmgPhysGood;
  517. }
  518. switch (inputData.attack) {
  519. case Attack.BowWhack:
  520. case Attack.PowerKnockBack:
  521. case Attack.ClawPunch:
  522. case Attack.Rush:
  523. case Attack.HeavensHammerXiuz:
  524. case Attack.HeavensHammerXiuzCorrected:
  525. case Attack.CrusherHigh:
  526. case Attack.CrusherLow:
  527. case Attack.Fury:
  528. case Attack.DragonRoar:
  529. case Attack.Phoenix:
  530. case Attack.Frostprey:
  531. case Attack.Octopus:
  532. case Attack.Gaviota:
  533. case Attack.WrathOfTheOctopi:
  534. case Attack.LuckySeven:
  535. case Attack.TripleThrow:
  536. case Attack.NinjaAmbush:
  537. case Attack.VenomousStar:
  538. case Attack.VenomousStab:
  539. case Attack.SomersaultKick:
  540. case Attack.AerialStrike:
  541. return maxDmgPhysGood;
  542. case Attack.Panic:
  543. case Attack.Coma:
  544. return maxDmgCaFinisher(inputData, false);
  545. // Massive hack to make Arrow Bomb easier to work with...
  546. case Attack.ArrowBombImpact:
  547. return maxDmgArrowBombImpact(inputData, false);
  548. case Attack.ArrowBombSplash:
  549. return maxDmgArrowBombSplash(inputData, false);
  550. default:
  551. return maxDmgPhys(inputData, false);
  552. }
  553. })(),
  554. ].map(dmg => dmg * eleSus * caMod * eleChargeMod);
  555. const [minDmgPhysBadAdjusted, maxDmgPhysGoodAdjusted] = (() => {
  556. switch (inputData.attack) {
  557. case Attack.HeavensHammerXiuz:
  558. case Attack.HeavensHammerXiuzCorrected:
  559. case Attack.Phoenix:
  560. case Attack.Frostprey:
  561. case Attack.Octopus:
  562. case Attack.Gaviota:
  563. case Attack.WrathOfTheOctopi:
  564. return [minDmgPhysBad, maxDmgPhysGood];
  565. case Attack.Assaulter:
  566. return inputData.level >= inputData.enemyLevel
  567. ? [minDmgPhysBad, maxDmgPhysGood]
  568. : adjustRangeForWdef(inputData, [
  569. minDmgPhysBad,
  570. maxDmgPhysGood,
  571. ]);
  572. default:
  573. return adjustRangeForWdef(inputData, [
  574. minDmgPhysBad,
  575. maxDmgPhysGood,
  576. ]);
  577. }
  578. })();
  579. const [minDmgPhysGoodAdjusted, maxDmgPhysBadAdjusted] = (() => {
  580. switch (inputData.attack) {
  581. case Attack.HeavensHammerXiuz:
  582. case Attack.HeavensHammerXiuzCorrected:
  583. case Attack.Phoenix:
  584. case Attack.Frostprey:
  585. case Attack.Octopus:
  586. case Attack.Gaviota:
  587. case Attack.WrathOfTheOctopi:
  588. return [minDmgPhysGood, maxDmgPhysBad];
  589. case Attack.Assaulter:
  590. return inputData.level >= inputData.enemyLevel
  591. ? [minDmgPhysGood, maxDmgPhysBad]
  592. : adjustRangeForWdef(inputData, [
  593. minDmgPhysGood,
  594. maxDmgPhysBad,
  595. ]);
  596. default:
  597. return adjustRangeForWdef(inputData, [
  598. minDmgPhysGood,
  599. maxDmgPhysBad,
  600. ]);
  601. }
  602. })();
  603. const [dmgMultiNoCrit, dmgMultiCrit] = isSummon(inputData.attack)
  604. ? [1, 1]
  605. : [
  606. dmgMulti(inputData, false),
  607. dmgMulti(inputData, inputData.attack !== Attack.HeavensHammerXiuz &&
  608. inputData.attack !==
  609. Attack.HeavensHammerXiuzCorrected &&
  610. inputData.attack !== Attack.VenomousStar &&
  611. inputData.attack !== Attack.VenomousStab),
  612. ];
  613. const afterModifier = isSummon(inputData.attack)
  614. ? 1
  615. : afterModPhys(inputData);
  616. const [minDmgPhysBadNoCrit, maxDmgPhysGoodNoCrit, minDmgPhysGoodNoCrit, maxDmgPhysBadNoCrit,] =
  617. // Massive hack to make Arrow Bomb easier to work with...
  618. inputData.attack === Attack.ArrowBombImpact ||
  619. inputData.attack === Attack.ArrowBombSplash
  620. ? [
  621. minDmgPhysBadAdjusted,
  622. maxDmgPhysBadAdjusted,
  623. minDmgPhysBadAdjusted,
  624. maxDmgPhysBadAdjusted,
  625. ]
  626. : [
  627. minDmgPhysBadAdjusted,
  628. maxDmgPhysGoodAdjusted,
  629. minDmgPhysGoodAdjusted,
  630. maxDmgPhysBadAdjusted,
  631. ].map(x => Math.max(x * dmgMultiNoCrit, 1) * afterModifier);
  632. const [minDmgPhysBadCrit, maxDmgPhysGoodCrit, minDmgPhysGoodCrit, maxDmgPhysBadCrit,] =
  633. // Massive hack to make Arrow Bomb easier to work with...
  634. inputData.attack === Attack.ArrowBombImpact ||
  635. inputData.attack === Attack.ArrowBombSplash
  636. ? [
  637. minDmgPhysGoodAdjusted,
  638. maxDmgPhysGoodAdjusted,
  639. minDmgPhysGoodAdjusted,
  640. maxDmgPhysGoodAdjusted,
  641. ]
  642. : [
  643. minDmgPhysBadAdjusted,
  644. maxDmgPhysGoodAdjusted,
  645. minDmgPhysGoodAdjusted,
  646. maxDmgPhysBadAdjusted,
  647. ].map(x => Math.max(x * dmgMultiCrit, 1) * afterModifier);
  648. // Lots of special-casing for Barrage, the only goddamn attack that
  649. // does this...
  650. const [maxDmgNoCritBarrage, maxDmgCritBarrage] = [
  651. maxDmgPhysGoodNoCrit,
  652. maxDmgPhysGoodCrit,
  653. ].map(x => x *
  654. (inputData.attack === Attack.Barrage
  655. ? afterModBarrage(inputData.skillLines)
  656. : 0));
  657. const range = inputData.attack === Attack.Barrage
  658. ? [
  659. Math.max(Math.trunc(minDmgPhysBadNoCrit), 1),
  660. Math.max(Math.trunc(maxDmgNoCritBarrage), 1),
  661. ]
  662. : [minDmgPhysBadNoCrit, maxDmgPhysGoodNoCrit].map(x => Math.max(Math.trunc(x), 1));
  663. const critRange = inputData.attack === Attack.Barrage
  664. ? [
  665. Math.max(Math.trunc(minDmgPhysBadCrit), 1),
  666. Math.max(Math.trunc(maxDmgCritBarrage), 1),
  667. ]
  668. : [minDmgPhysBadCrit, maxDmgPhysGoodCrit].map(x => Math.max(Math.trunc(x), 1));
  669. rangeOutput.textContent = `${range[0]} ~ ${range[1]}${range[1] ===
  670. (inputData.attack === Attack.Barrage
  671. ? maxDmgNoCritBarrage
  672. : maxDmgPhysGoodNoCrit) && range[1] !== 1
  673. ? "*"
  674. : ""}`;
  675. critRangeOutput.textContent = `${critRange[0]} ~ ${critRange[1]}${critRange[1] ===
  676. (inputData.attack === Attack.Barrage
  677. ? maxDmgCritBarrage
  678. : maxDmgPhysGoodCrit) && critRange[1] !== 1
  679. ? "*"
  680. : ""}`;
  681. const [combinedRangeTop, combinedRangeTopOneLine] = inputData.critProb > 0
  682. ? [critRange[1], maxDmgPhysGoodCrit]
  683. : [range[1], maxDmgPhysGoodNoCrit];
  684. if (inputData.attack === Attack.Barrage) {
  685. let totalRangeBottom = range[0] * Math.min(inputData.skillLines, 4);
  686. for (let i = 5; i <= inputData.skillLines; ++i) {
  687. totalRangeBottom += Math.max(Math.trunc(minDmgPhysBadNoCrit * afterModBarrage(i)), 1);
  688. }
  689. let totalRangeTop = combinedRangeTop * Math.min(inputData.skillLines, 4);
  690. for (let i = 5; i <= inputData.skillLines; ++i) {
  691. totalRangeTop += Math.max(Math.trunc(combinedRangeTopOneLine * afterModBarrage(i)), 1);
  692. }
  693. totalRangeOutput.textContent = `${totalRangeBottom} ~ \
  694. ${totalRangeTop}${combinedRangeTop === combinedRangeTopOneLine &&
  695. combinedRangeTop !== 1
  696. ? "*"
  697. : ""}`;
  698. }
  699. else {
  700. totalRangeOutput.textContent = `${range[0] * inputData.skillLines} ~ ${combinedRangeTop * inputData.skillLines}${combinedRangeTop === combinedRangeTopOneLine &&
  701. combinedRangeTop !== 1
  702. ? "*"
  703. : ""}`;
  704. }
  705. const [expectedPerHitBadNoCrit, expectedPerHitBadCrit] = [
  706. truncClampedExpectation(minDmgPhysBadNoCrit, maxDmgPhysBadNoCrit),
  707. truncClampedExpectation(minDmgPhysBadCrit, maxDmgPhysBadCrit),
  708. ];
  709. const [expectedPerHitGoodNoCrit, expectedPerHitGoodCrit] = [
  710. truncClampedExpectation(minDmgPhysGoodNoCrit, maxDmgPhysGoodNoCrit),
  711. truncClampedExpectation(minDmgPhysGoodCrit, maxDmgPhysGoodCrit),
  712. ];
  713. const expectedPerHitBad = critQ * expectedPerHitBadNoCrit +
  714. inputData.critProb * expectedPerHitBadCrit;
  715. const expectedPerHitGood = critQ * expectedPerHitGoodNoCrit +
  716. inputData.critProb * expectedPerHitGoodCrit;
  717. const expectedPerHit = expectedPerHitBad * badAnimProb +
  718. expectedPerHitGood * goodAnimProb;
  719. const expectedPerHitTotal = inputData.attack === Attack.Barrage
  720. ? (() => {
  721. let accum = expectedPerHit * Math.min(inputData.skillLines, 4);
  722. for (let i = 5; i <= inputData.skillLines; ++i) {
  723. accum += expectedPerHit * afterModBarrage(i);
  724. }
  725. return accum;
  726. })()
  727. : expectedPerHit * inputData.skillLines;
  728. expectedPerHitOutput.textContent = (expectedPerHit *
  729. (inputData.attack === Attack.Barrage
  730. ? barrageEffectiveMulti(inputData.skillLines) /
  731. inputData.skillLines
  732. : 1)).toFixed(3);
  733. expectedPerHitTotalOutput.textContent = expectedPerHitTotal.toFixed(3);
  734. // The "mainVariance" in the following variable names is intended to
  735. // indicate that this is a variance against the expectation across
  736. // _all_ cases (`expectedPerHit`), not against the expected value of
  737. // the particular case in question.
  738. const mainVariancePerHitBadNoCrit = truncClampedVariance(minDmgPhysBadNoCrit, maxDmgPhysBadNoCrit, expectedPerHit);
  739. const mainVariancePerHitGoodNoCrit = truncClampedVariance(minDmgPhysGoodNoCrit, maxDmgPhysGoodNoCrit, expectedPerHit);
  740. const mainVariancePerHitBadCrit = truncClampedVariance(minDmgPhysBadCrit, maxDmgPhysBadCrit, expectedPerHit);
  741. const mainVariancePerHitGoodCrit = truncClampedVariance(minDmgPhysGoodCrit, maxDmgPhysGoodCrit, expectedPerHit);
  742. const variancePerHit = (() => {
  743. if (mainVariancePerHitBadNoCrit !== undefined &&
  744. mainVariancePerHitGoodNoCrit !== undefined &&
  745. mainVariancePerHitBadCrit !== undefined &&
  746. mainVariancePerHitGoodCrit !== undefined) {
  747. const mainVariancePerHitBad = critQ * mainVariancePerHitBadNoCrit +
  748. inputData.critProb * mainVariancePerHitBadCrit;
  749. const mainVariancePerHitGood = critQ * mainVariancePerHitGoodNoCrit +
  750. inputData.critProb * mainVariancePerHitGoodCrit;
  751. return (mainVariancePerHitBad * badAnimProb +
  752. mainVariancePerHitGood * goodAnimProb);
  753. }
  754. return;
  755. })();
  756. let sdPerHitTotal = undefined;
  757. if (variancePerHit !== undefined) {
  758. sdPerHitOutput.classList.remove("error");
  759. cvPerHitOutput.classList.remove("error");
  760. sdPerHitTotalOutput.classList.remove("error");
  761. cvPerHitTotalOutput.classList.remove("error");
  762. const sdPerHit = Math.sqrt(inputData.attack === Attack.Barrage
  763. ? variancePerHit *
  764. (barrageEffectiveMulti(inputData.skillLines) /
  765. inputData.skillLines)
  766. : variancePerHit);
  767. // This is mathematically valid because the damage/outcome of each
  768. // line is independent of the damage of any other line, thus
  769. // implying uncorrelatedness. Furthermore, this implies that the
  770. // variance of the sum of lines is the sum of the variance of said
  771. // lines (see the Bienaymé formula/identity).
  772. sdPerHitTotal = Math.sqrt(variancePerHit *
  773. (inputData.attack === Attack.Barrage
  774. ? barrageEffectiveMulti(inputData.skillLines)
  775. : inputData.skillLines));
  776. sdPerHitOutput.textContent = sdPerHit.toFixed(3);
  777. cvPerHitOutput.textContent = (sdPerHit /
  778. (expectedPerHit *
  779. (inputData.attack === Attack.Barrage
  780. ? barrageEffectiveMulti(inputData.skillLines) /
  781. inputData.skillLines
  782. : 1))).toFixed(5);
  783. sdPerHitTotalOutput.textContent = sdPerHitTotal.toFixed(3);
  784. cvPerHitTotalOutput.textContent = (sdPerHitTotal / expectedPerHitTotal).toFixed(5);
  785. }
  786. else {
  787. sdPerHitOutput.classList.add("error");
  788. cvPerHitOutput.classList.add("error");
  789. sdPerHitTotalOutput.classList.add("error");
  790. cvPerHitTotalOutput.classList.add("error");
  791. sdPerHitOutput.textContent = "[undefined]";
  792. cvPerHitOutput.textContent = "[undefined]";
  793. sdPerHitTotalOutput.textContent = "[undefined]";
  794. cvPerHitTotalOutput.textContent = "[undefined]";
  795. }
  796. const period = attackPeriod(inputData.wepType, inputData.speed, inputData.attack);
  797. if (period !== undefined) {
  798. expectedDpsOutput.classList.remove("error");
  799. const attackHz = 1000 / period;
  800. const expectedDps = attackHz * expectedPerHitTotal;
  801. expectedDpsOutput.textContent = expectedDps.toFixed(3);
  802. if (sdPerHitTotal !== undefined) {
  803. sdDpsOutput.classList.remove("error");
  804. cvDpsOutput.classList.remove("error");
  805. // This is mathematically valid because the damage/outcome of
  806. // each line is independent of the damage of any other line,
  807. // thus implying uncorrelatedness. Furthermore, this implies
  808. // that the variance of the sum of lines is the sum of the
  809. // variance of said lines (see the Bienaymé formula/identity).
  810. const sdDps = Math.sqrt(attackHz) * sdPerHitTotal; /*
  811. = sqrt(attackHz) * sqrt(variancePerHitTotal)
  812. = sqrt(attackHz * variancePerHitTotal)
  813. = sqrt(varianceDps). */
  814. sdDpsOutput.textContent = sdDps.toFixed(3);
  815. cvDpsOutput.textContent = (sdDps / expectedDps).toFixed(5);
  816. }
  817. else {
  818. sdDpsOutput.classList.add("error");
  819. cvDpsOutput.classList.add("error");
  820. sdDpsOutput.textContent = "[undefined]";
  821. cvDpsOutput.textContent = "[undefined]";
  822. }
  823. }
  824. else {
  825. expectedDpsOutput.classList.add("error");
  826. sdDpsOutput.classList.add("error");
  827. cvDpsOutput.classList.add("error");
  828. expectedDpsOutput.textContent = "[unknown attack speed value]";
  829. sdDpsOutput.textContent = "[undefined]";
  830. cvDpsOutput.textContent = "[undefined]";
  831. }
  832. }
  833. function recalculateMagic(inputData, critQ) {
  834. const eleWepBonus = 1 + (isHolySpell(inputData.spell) ? 0 : inputData.eleWep);
  835. const [minDmg, maxDmg] = [
  836. (() => {
  837. switch (inputData.spell) {
  838. case Spell.Heal:
  839. return minDmgHeal(inputData);
  840. default:
  841. return minDmgMagic(inputData);
  842. }
  843. })() *
  844. inputData.eleAmp *
  845. eleWepBonus *
  846. inputData.eleSus,
  847. (() => {
  848. switch (inputData.spell) {
  849. case Spell.Heal:
  850. return maxDmgHeal(inputData);
  851. default:
  852. return maxDmgMagic(inputData);
  853. }
  854. })() *
  855. inputData.eleAmp *
  856. eleWepBonus *
  857. inputData.eleSus,
  858. ];
  859. const [minDmgNoCrit, maxDmgNoCrit] = adjustRangeForMdef(inputData, [
  860. minDmg,
  861. maxDmg,
  862. ]);
  863. const [minDmgCrit, maxDmgCrit] = [minDmgNoCrit, maxDmgNoCrit].map(x => x * inputData.critDmg);
  864. const [minDmgNoCritAfter, maxDmgNoCritAfter, minDmgCritAfter, maxDmgCritAfter,] = [minDmgNoCrit, maxDmgNoCrit, minDmgCrit, maxDmgCrit].map(x => Math.max(x, 1) * afterModMagic(inputData));
  865. const range = [minDmgNoCritAfter, maxDmgNoCritAfter].map(x => Math.max(Math.trunc(x), 1));
  866. const critRange = [minDmgCritAfter, maxDmgCritAfter].map(x => Math.max(Math.trunc(x), 1));
  867. rangeMagicOutput.textContent = `${range[0]} ~ ${range[1]}${range[1] === maxDmgNoCritAfter && range[1] !== 1 ? "*" : ""}`;
  868. critRangeMagicOutput.textContent = `${critRange[0]} ~ ${critRange[1]}${critRange[1] === maxDmgCritAfter && critRange[1] !== 1 ? "*" : ""}`;
  869. const combinedRangeTop = inputData.critProb > 0 ? critRange[1] : range[1];
  870. totalRangeMagicOutput.textContent = `${range[0] * inputData.skillLines} ~ ${combinedRangeTop * inputData.skillLines}${combinedRangeTop ===
  871. (inputData.critProb > 0
  872. ? maxDmgCritAfter
  873. : maxDmgNoCritAfter) && combinedRangeTop !== 1
  874. ? "*"
  875. : ""}`;
  876. const [expectedPerHitNoCrit, expectedPerHitCrit] = [
  877. truncClampedExpectation(minDmgNoCritAfter, maxDmgNoCritAfter),
  878. truncClampedExpectation(minDmgCritAfter, maxDmgCritAfter),
  879. ];
  880. const expectedPerHit = critQ * expectedPerHitNoCrit +
  881. inputData.critProb * expectedPerHitCrit;
  882. const expectedPerHitTotal = expectedPerHit * inputData.skillLines;
  883. expectedPerHitMagicOutput.textContent = expectedPerHit.toFixed(3);
  884. expectedPerHitTotalMagicOutput.textContent =
  885. expectedPerHitTotal.toFixed(3);
  886. // The "mainVariance" in the following variable names is intended to
  887. // indicate that this is a variance against the expectation across
  888. // _all_ cases (`expectedPerHit`), not against the expected value of
  889. // the particular case in question.
  890. const mainVariancePerHitNoCrit = truncClampedVariance(minDmgNoCritAfter, maxDmgNoCritAfter, expectedPerHit);
  891. const mainVariancePerHitCrit = truncClampedVariance(minDmgCritAfter, maxDmgCritAfter, expectedPerHit);
  892. const variancePerHit = mainVariancePerHitNoCrit !== undefined &&
  893. mainVariancePerHitCrit !== undefined
  894. ? critQ * mainVariancePerHitNoCrit +
  895. inputData.critProb * mainVariancePerHitCrit
  896. : undefined;
  897. let sdPerHitTotal = undefined;
  898. if (variancePerHit !== undefined) {
  899. sdPerHitMagicOutput.classList.remove("error");
  900. cvPerHitMagicOutput.classList.remove("error");
  901. sdPerHitTotalMagicOutput.classList.remove("error");
  902. cvPerHitTotalMagicOutput.classList.remove("error");
  903. const sdPerHit = Math.sqrt(variancePerHit);
  904. // This is mathematically valid because the damage/outcome of each
  905. // hit is independent of the damage of any other hit, thus implying
  906. // uncorrelatedness. Furthermore, this implies that the variance
  907. // of the sum of hits is the sum of the variance of said hits (see
  908. // the Bienaymé formula/identity).
  909. sdPerHitTotal = Math.sqrt(variancePerHit * inputData.skillLines);
  910. sdPerHitMagicOutput.textContent = sdPerHit.toFixed(3);
  911. cvPerHitMagicOutput.textContent = (sdPerHit / expectedPerHit).toFixed(5);
  912. sdPerHitTotalMagicOutput.textContent = sdPerHitTotal.toFixed(3);
  913. cvPerHitTotalMagicOutput.textContent = (sdPerHitTotal / expectedPerHitTotal).toFixed(5);
  914. }
  915. else {
  916. sdPerHitMagicOutput.classList.add("error");
  917. cvPerHitMagicOutput.classList.add("error");
  918. sdPerHitTotalMagicOutput.classList.add("error");
  919. cvPerHitTotalMagicOutput.classList.add("error");
  920. sdPerHitMagicOutput.textContent = "[undefined]";
  921. cvPerHitMagicOutput.textContent = "[undefined]";
  922. sdPerHitTotalMagicOutput.textContent = "[undefined]";
  923. cvPerHitTotalMagicOutput.textContent = "[undefined]";
  924. }
  925. const period = magicAttackPeriod(inputData.spellBooster, inputData.spell, inputData.speed);
  926. if (period !== undefined) {
  927. expectedDpsMagicOutput.classList.remove("error");
  928. const attackHz = 1000 / period;
  929. const expectedDps = attackHz * expectedPerHitTotal;
  930. expectedDpsMagicOutput.textContent = expectedDps.toFixed(3);
  931. if (sdPerHitTotal !== undefined) {
  932. sdDpsMagicOutput.classList.remove("error");
  933. cvDpsMagicOutput.classList.remove("error");
  934. // This is mathematically valid because the damage/outcome of
  935. // each hit is independent of the damage of any other hit, thus
  936. // implying uncorrelatedness. Furthermore, this implies that
  937. // the variance of the sum of hits is the sum of the variance
  938. // of said hits.
  939. const sdDps = Math.sqrt(attackHz) * sdPerHitTotal; /*
  940. = sqrt(attackHz) * sqrt(variancePerHitTotal)
  941. = sqrt(attackHz * variancePerHitTotal)
  942. = sqrt(varianceDps). */
  943. sdDpsMagicOutput.textContent = sdDps.toFixed(3);
  944. cvDpsMagicOutput.textContent = (sdDps / expectedDps).toFixed(5);
  945. }
  946. else {
  947. sdDpsMagicOutput.classList.add("error");
  948. cvDpsMagicOutput.classList.add("error");
  949. sdDpsMagicOutput.textContent = "[undefined]";
  950. cvDpsMagicOutput.textContent = "[undefined]";
  951. }
  952. }
  953. else {
  954. expectedDpsMagicOutput.classList.add("error");
  955. sdDpsMagicOutput.classList.add("error");
  956. cvDpsMagicOutput.classList.add("error");
  957. expectedDpsMagicOutput.textContent =
  958. "[unknown attack speed value]";
  959. sdDpsMagicOutput.textContent = "[undefined]";
  960. cvDpsMagicOutput.textContent = "[undefined]";
  961. }
  962. }
  963. function recalculateWarnings(inputData) {
  964. const warnings = [];
  965. /*======== Accumulate warnings ========*/
  966. if (inputData.totalWatk === 0 &&
  967. inputData.wepType !== WeaponType.None) {
  968. warnings.push("Your total WATK is zero, but you have a weapon equipped.");
  969. }
  970. if (inputData.totalMatk < inputData.stats.int) {
  971. warnings.push("Your total MATK is less than your total INT.");
  972. }
  973. switch (inputData.wepType) {
  974. case WeaponType.OneHandedSword:
  975. case WeaponType.OneHandedAxe:
  976. case WeaponType.OneHandedMace:
  977. case WeaponType.TwoHandedSword:
  978. case WeaponType.TwoHandedAxe:
  979. case WeaponType.TwoHandedMace:
  980. case WeaponType.Spear:
  981. case WeaponType.Polearm: {
  982. if (inputData.mastery > 0.1 &&
  983. inputData.clazz !== Class.Warrior) {
  984. warnings.push(`You have >10% mastery with a ${weaponTypeName(inputData.wepType)}, but you\u{2019}re not a warrior.`);
  985. }
  986. break;
  987. }
  988. case WeaponType.Dagger:
  989. case WeaponType.Claw: {
  990. if (inputData.mastery > 0.1 &&
  991. inputData.clazz !== Class.Rogue) {
  992. warnings.push(`You have >10% mastery with a ${weaponTypeName(inputData.wepType)}, but you\u{2019}re not a rogue.`);
  993. }
  994. break;
  995. }
  996. case WeaponType.Bow:
  997. case WeaponType.Crossbow: {
  998. if (inputData.mastery > 0.1 &&
  999. inputData.clazz !== Class.Archer) {
  1000. warnings.push(`You have >10% mastery with a ${weaponTypeName(inputData.wepType)}, but you\u{2019}re not an archer.`);
  1001. }
  1002. break;
  1003. }
  1004. case WeaponType.Knuckler:
  1005. case WeaponType.Gun: {
  1006. if (inputData.mastery > 0.1 &&
  1007. inputData.clazz !== Class.Pirate2nd) {
  1008. warnings.push(`You have >10% mastery with a ${weaponTypeName(inputData.wepType)}, but you\u{2019}re not a \u{2265}2\u{207f}\u{1d48} \
  1009. job pirate.`);
  1010. }
  1011. break;
  1012. }
  1013. default:
  1014. break;
  1015. }
  1016. if (inputData.mastery > 0.6 &&
  1017. inputData.wepType !== WeaponType.Bow &&
  1018. inputData.wepType !== WeaponType.Crossbow &&
  1019. inputData.wepType !== WeaponType.Polearm &&
  1020. inputData.wepType !== WeaponType.Spear &&
  1021. inputData.wepType !== WeaponType.None) {
  1022. warnings.push(`You have >60% mastery with a ${weaponTypeName(inputData.wepType)}.`);
  1023. }
  1024. if (!(inputData.clazz === Class.Rogue &&
  1025. inputData.wepType === WeaponType.Claw) &&
  1026. !(inputData.clazz === Class.Archer &&
  1027. (inputData.wepType === WeaponType.Bow ||
  1028. inputData.wepType === WeaponType.Crossbow)) &&
  1029. !(inputData.clazz === Class.Pirate2nd &&
  1030. (inputData.wepType === WeaponType.None ||
  1031. inputData.wepType === WeaponType.Knuckler))) {
  1032. if (inputData.critProb > 0.15) {
  1033. warnings.push("You have a >15% probability of critting, but you do not \
  1034. have a class & weapon combo that has access to crits. You \
  1035. can only crit due to Sharp Eyes, which normally grants a \
  1036. 15% crit probably at best.");
  1037. }
  1038. if (inputData.critDmg > 1.4) {
  1039. warnings.push("You have a >140% probability of critical multi, but you \
  1040. do not have a class & weapon combo that has access to \
  1041. crits. You can only crit due to Sharp Eyes, which \
  1042. normally grants a 140% critical multi at best.");
  1043. }
  1044. }
  1045. const badWeps = BAD_WEPS.get(inputData.clazz);
  1046. if (badWeps === undefined) {
  1047. console.error(`Logic error: ${inputData.clazz} is not a key in BAD_WEPS`);
  1048. }
  1049. else if (badWeps.has(inputData.wepType)) {
  1050. switch (inputData.wepType) {
  1051. case WeaponType.None: {
  1052. warnings.push(`You\u{2019}re not wielding a weapon, but ${className(inputData.clazz)}s normally cannot attack that way.`);
  1053. break;
  1054. }
  1055. case WeaponType.Staff: {
  1056. warnings.push(`You\u{2019}re wielding a staff, but staves that are \
  1057. equippable by ${className(inputData.clazz)}s don\u{2019}t usually exist.`);
  1058. break;
  1059. }
  1060. case WeaponType.TwoHandedAxe:
  1061. case WeaponType.Bow:
  1062. case WeaponType.Crossbow:
  1063. case WeaponType.Knuckler:
  1064. case WeaponType.Gun: {
  1065. const wepName = weaponTypeName(inputData.wepType);
  1066. warnings.push(`You\u{2019}re wielding a ${wepName}, but \
  1067. ${wepName}s that are equippable by ${className(inputData.clazz)}s don\u{2019}t usually exist.`);
  1068. break;
  1069. }
  1070. default:
  1071. break;
  1072. }
  1073. }
  1074. switch (inputData.clazz) {
  1075. case Class.Beginner: {
  1076. if (inputData.skillDmgMulti !== 1) {
  1077. warnings.push("Your damage multi \u{2260}100%, but you\u{2019}re a \
  1078. beginner.");
  1079. }
  1080. if (inputData.skillLines !== 1) {
  1081. warnings.push("You\u{2019}re attacking with a number of lines \
  1082. \u{2260}1, but you\u{2019}re a beginner.");
  1083. }
  1084. break;
  1085. }
  1086. case Class.Warrior: {
  1087. if (inputData.stats.str < 35) {
  1088. warnings.push("Your total STR <35, but you\u{2019}re a warrior.");
  1089. }
  1090. if (inputData.skillDmgMulti !== 1) {
  1091. if (inputData.wepType === WeaponType.Claw) {
  1092. warnings.push("Your damage multi \u{2260}100%, but \
  1093. you\u{2019}re a warrior using a claw.");
  1094. }
  1095. }
  1096. break;
  1097. }
  1098. case Class.Magician: {
  1099. if (inputData.stats.int < 20) {
  1100. warnings.push("Your total INT <20, but you\u{2019}re a magician.");
  1101. }
  1102. if (inputData.skillDmgMulti !== 1 &&
  1103. inputData.spell !== Spell.Heal) {
  1104. warnings.push("Your damage multi \u{2260}100%, but you\u{2019}re a \
  1105. magician who is not casting Heal.");
  1106. }
  1107. break;
  1108. }
  1109. case Class.Archer: {
  1110. if (inputData.stats.dex < 25) {
  1111. warnings.push("Your total DEX <25, but you\u{2019}re an archer.");
  1112. }
  1113. if (inputData.skillDmgMulti !== 1) {
  1114. if (inputData.wepType !== WeaponType.Bow &&
  1115. inputData.wepType !== WeaponType.Crossbow) {
  1116. warnings.push(`Your damage multi \u{2260}100%, but \
  1117. you\u{2019}re an archer using a ${weaponTypeName(inputData.wepType)}.`);
  1118. }
  1119. }
  1120. break;
  1121. }
  1122. case Class.Rogue: {
  1123. if (inputData.stats.dex < 25) {
  1124. warnings.push("Your total DEX <25, but you\u{2019}re a rogue.");
  1125. }
  1126. break;
  1127. }
  1128. case Class.Pirate:
  1129. case Class.Pirate2nd: {
  1130. if (inputData.stats.dex < 20) {
  1131. warnings.push("Your total DEX <20, but you\u{2019}re a pirate.");
  1132. }
  1133. if (inputData.skillDmgMulti !== 1) {
  1134. if (inputData.wepType === WeaponType.Claw) {
  1135. warnings.push("Your damage multi \u{2260}100%, but \
  1136. you\u{2019}re a pirate using a claw. \
  1137. Claws\u{2019} interaction with Somersault \
  1138. Kick/Aerial Strike is poorly understood.");
  1139. }
  1140. }
  1141. break;
  1142. }
  1143. }
  1144. const attackReqs = ATTACK_REQS.get(inputData.attack);
  1145. if (attackReqs === undefined) {
  1146. console.error(`Logic error: ${inputData.attack} is not a key in ATTACK_REQS`);
  1147. }
  1148. else {
  1149. const [attackReqClasses, attackReqLvl, attackReqWepTypes] = attackReqs;
  1150. if (!attackReqClasses.has(inputData.clazz)) {
  1151. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but you\u{2019}re not ${indefinite(Array.from(attackReqClasses).map(className).join("/"))}.`);
  1152. }
  1153. if (inputData.level < attackReqLvl) {
  1154. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but your level <${attackReqLvl}.`);
  1155. }
  1156. if (!attackReqWepTypes.has(inputData.wepType)) {
  1157. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but you don\u{2019}t have ${indefinite(Array.from(attackReqWepTypes)
  1158. .map(weaponTypeName)
  1159. .join("/"))} equipped.`);
  1160. }
  1161. }
  1162. if (inputData.spell !== Spell.Other &&
  1163. inputData.clazz !== Class.Magician) {
  1164. warnings.push("You have a specific spell selected, but you\u{2019}re not a \
  1165. magician.");
  1166. }
  1167. const spellLvlReq = SPELL_LVL_REQS.get(inputData.spell);
  1168. if (spellLvlReq === undefined) {
  1169. console.error(`Logic error: ${inputData.spell} is not a key in \
  1170. SPELL_LVL_REQS`);
  1171. }
  1172. else if (inputData.level < spellLvlReq) {
  1173. warnings.push(`You\u{2019}re casting ${spellName(inputData.spell)}, but your level <${spellLvlReq}.`);
  1174. }
  1175. if (inputData.speed > 4 /* Speed.Fast4 */ &&
  1176. inputData.wepType === WeaponType.None) {
  1177. warnings.push("You have no weapon equipped, but your speed >4. Bare fists \
  1178. have speed 4 when unbuffed.");
  1179. }
  1180. if (inputData.spellBooster !== 0) {
  1181. if (isHolySpell(inputData.spell)) {
  1182. warnings.push("Your spell booster value is nonzero, but you\u{2019}re \
  1183. casting a cleric/priest/bishop spell.");
  1184. }
  1185. if (inputData.wepType !== WeaponType.Wand &&
  1186. inputData.wepType !== WeaponType.Staff) {
  1187. warnings.push("Your spell booster value is nonzero, but you \
  1188. don\u{2019}t have a wand/staff equipped (Spell Booster \
  1189. doesn\u{2019}t work with swords).");
  1190. }
  1191. if (inputData.clazz !== Class.Magician) {
  1192. warnings.push("Your spell booster value is nonzero, but you\u{2019}re \
  1193. not a magician.");
  1194. }
  1195. if (inputData.spellBooster < -1 && inputData.level < 75) {
  1196. warnings.push("Your spell booster value <\u{2212}1, but your level <75.");
  1197. }
  1198. else if (inputData.level < 71) {
  1199. warnings.push("Your spell booster value is nonzero, but your level <71.");
  1200. }
  1201. }
  1202. if (inputData.eleAmp !== 1) {
  1203. if (inputData.clazz !== Class.Magician) {
  1204. warnings.push("Your element amplification >100%, but you\u{2019}re not \
  1205. a magician.");
  1206. }
  1207. if (inputData.eleAmp > 1.5) {
  1208. warnings.push("Your element amplification >150%, but Element \
  1209. Amplification usually goes up to 150% at best.");
  1210. }
  1211. if (isHolySpell(inputData.spell)) {
  1212. warnings.push("Your element amplification >100%, but you\u{2019}re \
  1213. casting a cleric/priest/bishop spell.");
  1214. }
  1215. if (inputData.level < 70) {
  1216. warnings.push("Your element amplification >100%, but your level <70.");
  1217. }
  1218. }
  1219. const jobLvlReq = JOB_LVL_REQS.get(inputData.clazz);
  1220. if (jobLvlReq === undefined) {
  1221. console.error(`Logic error: ${inputData.clazz} is not a key in JOB_LVL_REQS`);
  1222. }
  1223. else if (inputData.level < jobLvlReq) {
  1224. warnings.push(`You\u{2019}re ${indefinite(className(inputData.clazz))}, but your level <${jobLvlReq}.`);
  1225. }
  1226. const attackLines = ATTACK_LINES.get(inputData.attack);
  1227. if (attackLines === undefined) {
  1228. console.error(`Logic error: ${inputData.attack} is not a key in \
  1229. ATTACK_LINES`);
  1230. }
  1231. else {
  1232. const [minLines, maxLines, maxTargets] = attackLines;
  1233. if (inputData.skillLines < minLines) {
  1234. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but its number of lines <${minLines}.`);
  1235. }
  1236. if (inputData.skillLines > maxLines) {
  1237. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but its number of lines >${maxLines}.`);
  1238. }
  1239. if (inputData.enemyCount > maxTargets) {
  1240. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but your number of targets >${maxTargets}.`);
  1241. }
  1242. }
  1243. const spellLines = SPELL_LINES.get(inputData.spell);
  1244. if (spellLines === undefined) {
  1245. console.error(`Logic error: ${inputData.spell} is not a key in SPELL_LINES`);
  1246. }
  1247. else {
  1248. const [minLines, maxLines, maxTargets] = spellLines;
  1249. if (inputData.skillLines < minLines) {
  1250. warnings.push(`You\u{2019}re casting ${spellName(inputData.spell)}, but its number of lines <${minLines}.`);
  1251. }
  1252. if (inputData.skillLines > maxLines) {
  1253. warnings.push(`You\u{2019}re casting ${spellName(inputData.spell)}, but its number of lines >${maxLines}.`);
  1254. }
  1255. if (inputData.enemyCount > maxTargets) {
  1256. warnings.push(`You\u{2019}re casting ${spellName(inputData.spell)}, but your number of targets >${maxTargets}.`);
  1257. }
  1258. }
  1259. if (inputData.attack === Attack.SomersaultKick ||
  1260. inputData.attack === Attack.AerialStrike) {
  1261. if (inputData.attack === Attack.SomersaultKick &&
  1262. inputData.wepType !== WeaponType.None &&
  1263. inputData.wepType !== WeaponType.Knuckler) {
  1264. warnings.push("You\u{2019}re attacking with Somersault Kick, and have a \
  1265. weapon equipped that isn\u{2019}t a knuckler; the attack \
  1266. period is given on a best-effort basis that may or may \
  1267. not be accurate.");
  1268. }
  1269. switch (inputData.wepType) {
  1270. case WeaponType.Bow:
  1271. case WeaponType.Crossbow:
  1272. case WeaponType.Claw:
  1273. case WeaponType.Gun:
  1274. warnings.push(`You\u{2019}re using ${attackName(inputData.attack)} with ${indefinite(weaponTypeName(inputData.wepType))} equipped; the damage calculation is done on a \
  1275. best-effort basis that may or may not be accurate.`);
  1276. break;
  1277. default:
  1278. break;
  1279. }
  1280. }
  1281. if (inputData.attack === Attack.BowWhack) {
  1282. warnings.push("You\u{2019}re whacking with a (cross)bow; the damage \
  1283. calculation is done on a best-effort basis that may or may \
  1284. not be accurate.");
  1285. if (inputData.skillDmgMulti !== 1) {
  1286. warnings.push("You\u{2019}re whacking with a (cross)bow, but your \
  1287. damage multi \u{2260}100%. Maybe you meant to use Power \
  1288. Knock-Back?");
  1289. }
  1290. }
  1291. if (inputData.attack === Attack.ClawPunch &&
  1292. inputData.skillDmgMulti !== 1) {
  1293. warnings.push("You\u{2019}re punching with a claw, but your damage multi \
  1294. \u{2260}100%.");
  1295. }
  1296. if (inputData.attack === Attack.Gaviota) {
  1297. warnings.push("Gaviota\u{2019}s attack period is based on an idealization; \
  1298. actual usage will almost certainly have a larger attack \
  1299. period and thus lower DPS.");
  1300. }
  1301. if (inputData.attack === Attack.WrathOfTheOctopi) {
  1302. warnings.push("The attack period for Wrath of the Octopi is unmeasured, so \
  1303. the slower attack period of Octopus (the skill which it \
  1304. upgrades) is used instead.");
  1305. }
  1306. if (inputData.caActive) {
  1307. if (inputData.clazz !== Class.Warrior) {
  1308. warnings.push("You have (Advanced) Combo Attack active, but \
  1309. you\u{2019}re not a warrior.");
  1310. }
  1311. if (inputData.level < 70) {
  1312. warnings.push("You have (Advanced) Combo Attack active, but your level \
  1313. <70.");
  1314. }
  1315. if (1 + (inputData.level - 70) * 3 < inputData.caLevel) {
  1316. warnings.push(`You have ${inputData.caLevel} SP in the Combo Attack \
  1317. skill, but you\u{2019}re not high enough level to have \
  1318. that many third job SP.`);
  1319. }
  1320. if (inputData.caOrbs > 5 && inputData.level < 120) {
  1321. warnings.push("You have >5 (Advanced) Combo Attack orbs, but your level \
  1322. <120.");
  1323. }
  1324. }
  1325. else {
  1326. if (inputData.attack === Attack.Panic ||
  1327. inputData.attack === Attack.Coma) {
  1328. warnings.push(`You\u{2019}re attacking with ${attackName(inputData.attack)}, but (Advanced) Combo Attack is inactive.`);
  1329. }
  1330. }
  1331. if (inputData.zerkActive) {
  1332. if (inputData.clazz !== Class.Warrior) {
  1333. warnings.push("You have Berserk active, but you\u{2019}re not a \
  1334. warrior.");
  1335. }
  1336. if (inputData.level < 120) {
  1337. warnings.push("You have Berserk active, but you\u{2019}re not high \
  1338. enough level to have access to that skill.");
  1339. }
  1340. }
  1341. if (inputData.hitOrd > inputData.enemyCount) {
  1342. warnings.push("The ordinal # of your hit is greater than the total number \
  1343. of enemies being targeted.");
  1344. }
  1345. function delayWarn(jobName) {
  1346. const atkName = attackName(inputData.attack);
  1347. warnings.push(`You\u{2019}re attacking with ${atkName}; the attack period \
  1348. (and thus DPS value) is based on the spamming of solely \
  1349. ${atkName}. The projected DPS will thus be less than that of \
  1350. a hypothetical ${jobName}, who would be attacking in between \
  1351. ${atkName}s.`);
  1352. }
  1353. switch (inputData.attack) {
  1354. case Attack.NinjaStorm:
  1355. delayWarn("nightlord");
  1356. break;
  1357. case Attack.BoomerangStep:
  1358. delayWarn("shadower");
  1359. break;
  1360. case Attack.BackspinBlow:
  1361. case Attack.DoubleUppercut:
  1362. delayWarn("brawler");
  1363. break;
  1364. case Attack.EnergyBlast:
  1365. delayWarn("marauder");
  1366. break;
  1367. case Attack.DragonStrike:
  1368. case Attack.Snatch:
  1369. case Attack.Barrage:
  1370. delayWarn("buccaneer");
  1371. break;
  1372. case Attack.RecoilShot:
  1373. delayWarn("gunslinger");
  1374. break;
  1375. case Attack.Flamethrower:
  1376. case Attack.IceSplitter:
  1377. delayWarn("outlaw");
  1378. break;
  1379. case Attack.AerialStrike:
  1380. delayWarn("corsair");
  1381. break;
  1382. }
  1383. if (inputData.attack === Attack.Flamethrower ||
  1384. inputData.attack === Attack.Inferno) {
  1385. warnings.push(`The damage calculation used here for ${inputData.attack} \
  1386. does not take into account the flaming/burning damage over \
  1387. time.`);
  1388. }
  1389. if (inputData.echo !== 0 && inputData.echo !== 4 / 100) {
  1390. warnings.push("You have specified a nonzero value for Echo of Hero that is \
  1391. not exactly 4%.");
  1392. }
  1393. switch (inputData.eleChargeType) {
  1394. case 0 /* ChargeType.None */: {
  1395. if (inputData.eleChargeDmg !== 1) {
  1396. warnings.push("You have no elemental charge, but your elemental \
  1397. charge damage \u{2260}100%.");
  1398. }
  1399. break;
  1400. }
  1401. case 1 /* ChargeType.Holy */: {
  1402. if (1 + (inputData.level - 120) * 3 <
  1403. inputData.eleChargeLevel) {
  1404. warnings.push(`You have level ${inputData.eleChargeLevel} \
  1405. Holy/Divine Charge selected, but you aren\u{2019}t a \
  1406. high enough level to have that much fourth job SP.`);
  1407. }
  1408. if (inputData.eleChargeLevel > 20) {
  1409. warnings.push("You have a Holy/Divine Charge level >20.");
  1410. }
  1411. break;
  1412. }
  1413. case 2 /* ChargeType.Other */: {
  1414. if (1 + (inputData.level - 70) * 3 <
  1415. inputData.eleChargeLevel) {
  1416. warnings.push(`You have a level ${inputData.eleChargeLevel} \
  1417. non-Holy/Divine Charge selected, but you \
  1418. aren\u{2019}t a high enough level to have that much \
  1419. third job SP.`);
  1420. }
  1421. break;
  1422. }
  1423. }
  1424. if (inputData.eleChargeType !== 0 /* ChargeType.None */) {
  1425. if (inputData.clazz !== Class.Warrior) {
  1426. warnings.push("You have an elemental charge selected, but you\u{2019}re \
  1427. not a warrior.");
  1428. }
  1429. switch (inputData.wepType) {
  1430. case WeaponType.OneHandedSword:
  1431. case WeaponType.OneHandedMace:
  1432. case WeaponType.TwoHandedSword:
  1433. case WeaponType.TwoHandedMace:
  1434. break;
  1435. default:
  1436. warnings.push("You have an elemental charge selected, but \
  1437. you\u{2019}re not using a sword nor a blunt weapon.");
  1438. break;
  1439. }
  1440. }
  1441. if (inputData.eleBoost !== 0) {
  1442. if (inputData.clazz !== Class.Pirate2nd) {
  1443. warnings.push("Your Elemental Boost \u{2260}0%, but you\u{2019}re not a \
  1444. \u{2265}2\u{207f}\u{1d48} job pirate.");
  1445. }
  1446. if (inputData.level < 120) {
  1447. warnings.push("Your Elemental Boost \u{2260}0%, but your level <120.");
  1448. }
  1449. }
  1450. if (inputData.eleWep !== 0) {
  1451. if (inputData.wepType !== WeaponType.Wand &&
  1452. inputData.wepType !== WeaponType.Staff) {
  1453. warnings.push("You are getting a nonzero elemental bonus from your \
  1454. weapon, but you aren\u{2019}t wielding a wand nor a \
  1455. staff.");
  1456. }
  1457. else if (inputData.clazz !== Class.Magician) {
  1458. warnings.push(`You\u{2019}re using an Elemental ${inputData.wepType === WeaponType.Wand
  1459. ? "Wand"
  1460. : "Staff"}, but you\u{2019}re not a magician.`);
  1461. }
  1462. if (inputData.eleWep !== 25 / 100) {
  1463. warnings.push("Your Elemental Wand/Staff is giving an elemental bonus \
  1464. \u{2260}25%.");
  1465. }
  1466. }
  1467. switch (inputData.wepType) {
  1468. case WeaponType.OneHandedAxe:
  1469. case WeaponType.TwoHandedAxe:
  1470. case WeaponType.OneHandedMace:
  1471. case WeaponType.TwoHandedMace:
  1472. case WeaponType.Polearm: {
  1473. if (inputData.goodAnimProb !== 60 / 100) {
  1474. warnings.push(`You\u{2019}re using ${indefinite(weaponTypeName(inputData.wepType))}, but your good animation probability \u{2260}60%. \
  1475. It is currently suspected that the actual probability \
  1476. is 60% in pre-BB GMS.`);
  1477. }
  1478. break;
  1479. }
  1480. case WeaponType.Spear: {
  1481. if (inputData.goodAnimProb !== 40 / 100) {
  1482. warnings.push("You\u{2019}re using a spear, but your good \
  1483. animation probability \u{2260}40%. It is currently \
  1484. suspected that the actual probability is 40% in \
  1485. pre-BB GMS.");
  1486. }
  1487. break;
  1488. }
  1489. case WeaponType.Wand:
  1490. case WeaponType.Staff: {
  1491. if (inputData.goodAnimProb !== 1) {
  1492. warnings.push(`You\u{2019}re using ${indefinite(weaponTypeName(inputData.wepType))}, but your good animation probability \u{2260}100%.
  1493. It is currently suspected that the actual probability
  1494. is 100% in pre-BB GMS.`);
  1495. }
  1496. break;
  1497. }
  1498. default:
  1499. break;
  1500. }
  1501. /*======== Remove old warnings display ========*/
  1502. {
  1503. const warningsElem = document.getElementById("warnings");
  1504. if (warningsElem) {
  1505. warningsDiv.removeChild(warningsElem);
  1506. }
  1507. }
  1508. /*======== Display warnings ========*/
  1509. if (warnings.length === 0) {
  1510. const warningsSpan = document.createElement("span");
  1511. warningsSpan.id = "warnings";
  1512. warningsSpan.classList.add("success");
  1513. const warningsTextNode = document.createTextNode("No warnings.");
  1514. warningsSpan.appendChild(warningsTextNode);
  1515. warningsDiv.appendChild(warningsSpan);
  1516. }
  1517. else {
  1518. const warningsUl = document.createElement("ul");
  1519. warningsUl.id = "warnings";
  1520. warningsUl.classList.add("warning");
  1521. for (const warningText of warnings) {
  1522. const warningLi = document.createElement("li");
  1523. const warningTextNode = document.createTextNode(warningText);
  1524. warningLi.appendChild(warningTextNode);
  1525. warningsUl.appendChild(warningLi);
  1526. }
  1527. warningsDiv.appendChild(warningsUl);
  1528. }
  1529. }
  1530. saveAsButton.addEventListener("click", () => {
  1531. saveLoadDialog.replaceChildren();
  1532. const saveAsInputLabel = document.createElement("label");
  1533. saveAsInputLabel.htmlFor = "save-as-input";
  1534. saveAsInputLabel.append(document.createTextNode("New name: "));
  1535. const saveAsInput = document.createElement("input");
  1536. saveAsInput.id = "save-as-input";
  1537. saveAsInput.name = "save-as-input";
  1538. saveAsInput.type = "text";
  1539. saveAsInputLabel.append(saveAsInput);
  1540. saveLoadDialog.append(saveAsInputLabel);
  1541. const saveAsSaveButton = document.createElement("button");
  1542. saveAsSaveButton.type = "button";
  1543. saveAsSaveButton.append(document.createTextNode("Save"));
  1544. saveLoadDialog.append(saveAsSaveButton);
  1545. saveLoadDialog.append(document.createTextNode(" "));
  1546. const saveAsCancelButton = document.createElement("button");
  1547. saveAsCancelButton.type = "button";
  1548. saveAsCancelButton.append(document.createTextNode("Cancel"));
  1549. saveLoadDialog.append(saveAsCancelButton);
  1550. saveAsSaveButton.addEventListener("click", () => {
  1551. if (!saveAsInput.value.trim()) {
  1552. return;
  1553. }
  1554. const keyName = `dmg-calc.saved.${saveAsInput.value.trim()}`;
  1555. if (window.localStorage.getItem(keyName) &&
  1556. !window.confirm("An entry is already saved with this name. Are you sure that you want to overwrite it?")) {
  1557. return;
  1558. }
  1559. window.localStorage.setItem(keyName, JSON.stringify(readInputData()));
  1560. currentlyLoaded.replaceChildren();
  1561. currentlyLoaded.append(document.createTextNode(saveAsInput.value.trim()));
  1562. saveLoadDialog.replaceChildren();
  1563. });
  1564. saveAsCancelButton.addEventListener("click", () => {
  1565. saveLoadDialog.replaceChildren();
  1566. });
  1567. saveAsInput.focus();
  1568. });
  1569. saveButton.addEventListener("click", () => {
  1570. const cln = currentlyLoadedName();
  1571. if (cln) {
  1572. window.localStorage.setItem(`dmg-calc.saved.${cln}`, JSON.stringify(readInputData()));
  1573. }
  1574. else {
  1575. window.alert("Nothing was saved. Try using \u{201c}Save as\u{201d}, instead.");
  1576. }
  1577. });
  1578. loadButton.addEventListener("click", () => {
  1579. saveLoadDialog.replaceChildren();
  1580. const loadSelectLabel = document.createElement("label");
  1581. loadSelectLabel.htmlFor = "load-select";
  1582. loadSelectLabel.append(document.createTextNode("Name: "));
  1583. const loadSelect = document.createElement("select");
  1584. loadSelect.id = "load-select";
  1585. loadSelect.name = "load-select";
  1586. for (let i = 0; i < window.localStorage.length; ++i) {
  1587. const key = window.localStorage.key(i);
  1588. if (key === null || !key.startsWith("dmg-calc.saved.")) {
  1589. continue;
  1590. }
  1591. const name = key.slice("dmg-calc.saved.".length);
  1592. const opt = document.createElement("option");
  1593. opt.value = key;
  1594. opt.append(document.createTextNode(name));
  1595. loadSelect.append(opt);
  1596. }
  1597. loadSelectLabel.append(loadSelect);
  1598. saveLoadDialog.append(loadSelectLabel);
  1599. const loadLoadButton = document.createElement("button");
  1600. loadLoadButton.type = "button";
  1601. loadLoadButton.append(document.createTextNode("Load"));
  1602. saveLoadDialog.append(loadLoadButton);
  1603. saveLoadDialog.append(document.createTextNode(" "));
  1604. const loadCancelButton = document.createElement("button");
  1605. loadCancelButton.type = "button";
  1606. loadCancelButton.append(document.createTextNode("Cancel"));
  1607. saveLoadDialog.append(loadCancelButton);
  1608. loadLoadButton.addEventListener("click", () => {
  1609. const val = window.localStorage.getItem(loadSelect.value);
  1610. if (!loadSelect.value ||
  1611. !val ||
  1612. !window.confirm("Loading will overwrite the current working state, causing you to lose any unsaved changes. Are you sure that you still want to load?")) {
  1613. return;
  1614. }
  1615. let inputData = JSON.parse(val);
  1616. writeInputData(inputData);
  1617. currentlyLoaded.replaceChildren();
  1618. currentlyLoaded.append(document.createTextNode(loadSelect.value.slice("dmg-calc.saved.".length)));
  1619. saveLoadDialog.replaceChildren();
  1620. recalculate();
  1621. });
  1622. loadCancelButton.addEventListener("click", () => {
  1623. saveLoadDialog.replaceChildren();
  1624. });
  1625. loadSelect.focus();
  1626. });
  1627. deleteButton.addEventListener("click", () => {
  1628. saveLoadDialog.replaceChildren();
  1629. const deleteSelectLabel = document.createElement("label");
  1630. deleteSelectLabel.htmlFor = "delete-select";
  1631. deleteSelectLabel.append(document.createTextNode("Name: "));
  1632. const deleteSelect = document.createElement("select");
  1633. deleteSelect.id = "delete-select";
  1634. deleteSelect.name = "delete-select";
  1635. for (let i = 0; i < window.localStorage.length; ++i) {
  1636. const key = window.localStorage.key(i);
  1637. if (key === null || !key.startsWith("dmg-calc.saved.")) {
  1638. continue;
  1639. }
  1640. const name = key.slice("dmg-calc.saved.".length);
  1641. const opt = document.createElement("option");
  1642. opt.value = key;
  1643. opt.append(document.createTextNode(name));
  1644. deleteSelect.append(opt);
  1645. }
  1646. deleteSelectLabel.append(deleteSelect);
  1647. saveLoadDialog.append(deleteSelectLabel);
  1648. const deleteDeleteButton = document.createElement("button");
  1649. deleteDeleteButton.type = "button";
  1650. deleteDeleteButton.append(document.createTextNode("Delete"));
  1651. saveLoadDialog.append(deleteDeleteButton);
  1652. saveLoadDialog.append(document.createTextNode(" "));
  1653. const deleteCancelButton = document.createElement("button");
  1654. deleteCancelButton.type = "button";
  1655. deleteCancelButton.append(document.createTextNode("Cancel"));
  1656. saveLoadDialog.append(deleteCancelButton);
  1657. deleteDeleteButton.addEventListener("click", () => {
  1658. const val = window.localStorage.getItem(deleteSelect.value);
  1659. const name = deleteSelect.value.slice("dmg-calc.saved.".length);
  1660. if (!deleteSelect.value ||
  1661. !val ||
  1662. !window.confirm(`Are you sure that you want to delete \u{201c}${name}\u{201d} permanently?`)) {
  1663. return;
  1664. }
  1665. window.localStorage.removeItem(deleteSelect.value);
  1666. const cln = currentlyLoadedName();
  1667. if (cln === name) {
  1668. currentlyLoaded.replaceChildren();
  1669. }
  1670. saveLoadDialog.replaceChildren();
  1671. });
  1672. deleteCancelButton.addEventListener("click", () => {
  1673. saveLoadDialog.replaceChildren();
  1674. });
  1675. deleteSelect.focus();
  1676. });
  1677. exportButton.addEventListener("click", () => {
  1678. const anchor = document.createElement("a");
  1679. anchor.href = URL.createObjectURL(new Blob([JSON.stringify(readInputData()) + "\n"], {
  1680. type: "application/json",
  1681. }));
  1682. const cln = currentlyLoadedName();
  1683. anchor.download = cln
  1684. ? cln.replaceAll(/\s/gi, "_").replaceAll(/[^a-z0-9_\.\-]/gi, "-") +
  1685. ".json"
  1686. : "dmg-calc-input.json";
  1687. anchor.click();
  1688. });
  1689. importButton.addEventListener("click", () => {
  1690. saveLoadDialog.replaceChildren();
  1691. const importInputLabel = document.createElement("label");
  1692. importInputLabel.htmlFor = "import-input";
  1693. importInputLabel.append(document.createTextNode("Choose a JSON file: "));
  1694. const importInput = document.createElement("input");
  1695. importInput.type = "file";
  1696. importInput.id = "import-input";
  1697. importInput.name = "import-input";
  1698. importInput.accept = ".json,application/json,text/plain,text/json";
  1699. importInputLabel.append(importInput);
  1700. saveLoadDialog.append(importInputLabel);
  1701. const importImportButton = document.createElement("button");
  1702. importImportButton.type = "button";
  1703. importImportButton.append(document.createTextNode("Import"));
  1704. saveLoadDialog.append(importImportButton);
  1705. saveLoadDialog.append(document.createTextNode(" "));
  1706. const importCancelButton = document.createElement("button");
  1707. importCancelButton.type = "button";
  1708. importCancelButton.append(document.createTextNode("Cancel"));
  1709. saveLoadDialog.append(importCancelButton);
  1710. importImportButton.addEventListener("click", () => {
  1711. const files = importInput.files;
  1712. if (files === null) {
  1713. return;
  1714. }
  1715. const file = files[0];
  1716. if (!file ||
  1717. !window.confirm("Importing will overwrite the current working state, causing you to lose any unsaved changes. Are you sure that you still want to import?")) {
  1718. return;
  1719. }
  1720. file.text().then(s => {
  1721. let inputData = JSON.parse(s);
  1722. writeInputData(inputData);
  1723. currentlyLoaded.replaceChildren();
  1724. });
  1725. saveLoadDialog.replaceChildren();
  1726. recalculate();
  1727. });
  1728. importCancelButton.addEventListener("click", () => {
  1729. saveLoadDialog.replaceChildren();
  1730. });
  1731. importInput.focus();
  1732. });
  1733. for (const input of [
  1734. strInput,
  1735. dexInput,
  1736. intInput,
  1737. lukInput,
  1738. totalWatkInput,
  1739. totalMatkInput,
  1740. echoInput,
  1741. masteryInput,
  1742. skillDmgMultiInput,
  1743. skillBasicAtkInput,
  1744. skillLinesInput,
  1745. critProbInput,
  1746. critDmgInput,
  1747. classInput,
  1748. levelInput,
  1749. weaponTypeInput,
  1750. goodAnimProbInput,
  1751. attackInput,
  1752. spellInput,
  1753. speedInput,
  1754. spellBoosterInput,
  1755. eleAmpInput,
  1756. eleBoostInput,
  1757. eleWepInput,
  1758. eleChargeDmgInput,
  1759. eleChargeLevelInput,
  1760. caActiveInput,
  1761. caDmgInput,
  1762. caLevelInput,
  1763. caOrbsInput,
  1764. zerkActiveInput,
  1765. zerkDmgInput,
  1766. enemyWdefInput,
  1767. enemyMdefInput,
  1768. eleSusInput,
  1769. enemyLevelInput,
  1770. enemyCountInput,
  1771. hitOrdInput,
  1772. ]) {
  1773. input.addEventListener("change", recalculate);
  1774. }
  1775. for (const eleChargeInput of eleChargeInputs) {
  1776. eleChargeInput.addEventListener("change", () => eleChargeInput.checked ? recalculate() : undefined);
  1777. }
  1778. recalculate();
  1779. }
  1780. function dmgMulti(inputData, crit) {
  1781. return (inputData.skillDmgMulti +
  1782. (crit ? inputData.critDmg : 0) +
  1783. (inputData.attack === Attack.Flamethrower ||
  1784. inputData.attack === Attack.IceSplitter
  1785. ? inputData.eleBoost
  1786. : 0));
  1787. }
  1788. function attackEffectiveEleSus(inputData) {
  1789. if (!attackIsElemental(inputData.attack)) {
  1790. return 1;
  1791. }
  1792. return inputData.eleSus;
  1793. }
  1794. function maxDmgPhys(inputData, goodAnim) {
  1795. return (((primaryStat(inputData.stats, inputData.wepType, goodAnim, inputData.clazz) +
  1796. secondaryStat(inputData.stats, inputData.wepType, inputData.clazz)) *
  1797. effectiveWatk(inputData)) /
  1798. 100);
  1799. }
  1800. function minDmgPhys(inputData, goodAnim) {
  1801. return (((primaryStat(inputData.stats, inputData.wepType, goodAnim, inputData.clazz) *
  1802. 0.9 *
  1803. effectiveMastery(inputData) +
  1804. secondaryStat(inputData.stats, inputData.wepType, inputData.clazz)) *
  1805. effectiveWatk(inputData)) /
  1806. 100);
  1807. }
  1808. function afterModPhys(inputData) {
  1809. const hitOrdAfterMod = (() => {
  1810. switch (inputData.attack) {
  1811. case Attack.IronArrow:
  1812. return 0.9 ** (inputData.hitOrd - 1);
  1813. case Attack.PiercingArrow:
  1814. return 1.2 ** (inputData.hitOrd - 1);
  1815. case Attack.EnergyOrb:
  1816. return (2 / 3) ** (inputData.hitOrd - 1);
  1817. default:
  1818. return 1;
  1819. }
  1820. })();
  1821. return inputData.zerkActive
  1822. ? hitOrdAfterMod * inputData.zerkDmg
  1823. : hitOrdAfterMod;
  1824. }
  1825. function afterModBarrage(ord) {
  1826. return 2 ** Math.max(ord - 4, 0);
  1827. }
  1828. function barrageEffectiveMulti(lines) {
  1829. return Math.min(lines + 1, 4) + 2 ** Math.max(lines - 3, 0) - 2;
  1830. }
  1831. function caModifier(inputData) {
  1832. if (!inputData.caActive) {
  1833. return 1;
  1834. }
  1835. if (inputData.caOrbs < 6) {
  1836. return ((inputData.caDmg +
  1837. Math.floor((inputData.caOrbs - 1) * (inputData.caLevel / 6))) /
  1838. 100);
  1839. }
  1840. return (inputData.caDmg + 20 + (inputData.caOrbs - 5) * 4) / 100;
  1841. }
  1842. function eleChargeModifier(inputData) {
  1843. switch (inputData.eleChargeType) {
  1844. case 0 /* ChargeType.None */:
  1845. return 1;
  1846. case 1 /* ChargeType.Holy */: {
  1847. if (inputData.eleSus === 1) {
  1848. return inputData.eleChargeDmg;
  1849. }
  1850. else if (inputData.eleSus === 0) {
  1851. return 0;
  1852. }
  1853. else if (inputData.eleSus < 1) {
  1854. return ((inputData.eleChargeDmg *
  1855. (80 - inputData.eleChargeLevel * 1.5)) /
  1856. 100);
  1857. }
  1858. return ((inputData.eleChargeDmg *
  1859. (120 + inputData.eleChargeLevel * 1.5)) /
  1860. 100);
  1861. }
  1862. case 2 /* ChargeType.Other */: {
  1863. if (inputData.eleSus === 1) {
  1864. return inputData.eleChargeDmg;
  1865. }
  1866. else if (inputData.eleSus === 0) {
  1867. return 0;
  1868. }
  1869. else if (inputData.eleSus < 1) {
  1870. return ((inputData.eleChargeDmg *
  1871. (95 - inputData.eleChargeLevel * 1.5)) /
  1872. 100);
  1873. }
  1874. return ((inputData.eleChargeDmg *
  1875. (105 + inputData.eleChargeLevel * 1.5)) /
  1876. 100);
  1877. }
  1878. }
  1879. }
  1880. function maxDmgBowWhack(inputData) {
  1881. return (((inputData.stats.dex * 3.4 + inputData.stats.str) *
  1882. effectiveWatk(inputData)) /
  1883. 150);
  1884. }
  1885. function minDmgBowWhack(inputData) {
  1886. return (((inputData.stats.dex * 3.4 * 0.1 * 0.9 + inputData.stats.str) *
  1887. effectiveWatk(inputData)) /
  1888. 150);
  1889. }
  1890. function maxDmgClawPunch(inputData) {
  1891. return (((inputData.stats.luk + inputData.stats.str + inputData.stats.dex) *
  1892. effectiveWatk(inputData)) /
  1893. 150);
  1894. }
  1895. function minDmgClawPunch(inputData) {
  1896. return (((inputData.stats.luk * 0.1 +
  1897. inputData.stats.str +
  1898. inputData.stats.dex) *
  1899. effectiveWatk(inputData)) /
  1900. 150);
  1901. }
  1902. function orbMulti(inputData) {
  1903. switch (inputData.caOrbs) {
  1904. case 1:
  1905. return 1;
  1906. case 2:
  1907. return 1.2;
  1908. case 3:
  1909. return 1.54;
  1910. case 4:
  1911. return 2;
  1912. default:
  1913. return 2.5;
  1914. }
  1915. }
  1916. function maxDmgCaFinisher(inputData, goodAnim) {
  1917. return maxDmgPhys(inputData, goodAnim) * orbMulti(inputData);
  1918. }
  1919. function minDmgCaFinisher(inputData, goodAnim) {
  1920. return minDmgPhys(inputData, goodAnim) * orbMulti(inputData);
  1921. }
  1922. function minDmgHhXiuz(inputData) {
  1923. return maxDmgPhys(inputData, true) * 0.8;
  1924. }
  1925. function maxDmgDragonRoar(inputData) {
  1926. return (((inputData.stats.str * 4 + inputData.stats.dex) *
  1927. effectiveWatk(inputData)) /
  1928. 100);
  1929. }
  1930. function minDmgDragonRoar(inputData) {
  1931. return (((inputData.stats.str * 4 * inputData.mastery * 0.9 +
  1932. inputData.stats.dex) *
  1933. effectiveWatk(inputData)) /
  1934. 100);
  1935. }
  1936. function maxDmgArrowBombImpact(inputData, crit) {
  1937. return 0.5 * maxDmgPhys(inputData, true) * (crit ? inputData.critDmg : 1);
  1938. }
  1939. function minDmgArrowBombImpact(inputData, crit) {
  1940. return 0.5 * minDmgPhys(inputData, true) * (crit ? inputData.critDmg : 1);
  1941. }
  1942. function maxDmgArrowBombSplash(inputData, crit) {
  1943. return (inputData.skillDmgMulti *
  1944. maxDmgPhys(inputData, true) *
  1945. (crit ? inputData.critDmg : 1));
  1946. }
  1947. function minDmgArrowBombSplash(inputData, crit) {
  1948. return (inputData.skillDmgMulti *
  1949. minDmgPhys(inputData, true) *
  1950. (crit ? inputData.critDmg : 1));
  1951. }
  1952. function maxDmgDexSummon(inputData) {
  1953. return (((inputData.stats.dex * 2.5 + inputData.stats.str) *
  1954. inputData.skillBasicAtk) /
  1955. 100);
  1956. }
  1957. function minDmgDexSummon(inputData) {
  1958. return (((inputData.stats.dex * 2.5 * 0.7 + inputData.stats.str) *
  1959. inputData.skillBasicAtk) /
  1960. 100);
  1961. }
  1962. function maxDmgLuckySeven(inputData) {
  1963. return (inputData.stats.luk * 5 * effectiveWatk(inputData)) / 100;
  1964. }
  1965. function minDmgLuckySeven(inputData) {
  1966. return (inputData.stats.luk * 2.5 * effectiveWatk(inputData)) / 100;
  1967. }
  1968. function dmgNinjaAmbush(inputData) {
  1969. return (2 *
  1970. (inputData.stats.str + inputData.stats.luk) *
  1971. inputData.skillDmgMulti);
  1972. }
  1973. function maxDmgVenom(inputData) {
  1974. return (((18.5 * (inputData.stats.str + inputData.stats.luk) +
  1975. inputData.stats.dex * 2) /
  1976. 100) *
  1977. inputData.skillBasicAtk);
  1978. }
  1979. function minDmgVenom(inputData) {
  1980. return (((8 * (inputData.stats.str + inputData.stats.luk) +
  1981. inputData.stats.dex * 2) /
  1982. 100) *
  1983. inputData.skillBasicAtk);
  1984. }
  1985. function swingProbToGoodAnimProb(inputData, swingProb) {
  1986. switch (inputData.wepType) {
  1987. case WeaponType.OneHandedAxe:
  1988. case WeaponType.OneHandedMace:
  1989. case WeaponType.Wand:
  1990. case WeaponType.Staff:
  1991. case WeaponType.TwoHandedAxe:
  1992. case WeaponType.TwoHandedMace:
  1993. case WeaponType.Polearm:
  1994. return swingProb;
  1995. default:
  1996. return 1 - swingProb;
  1997. }
  1998. }
  1999. function maxDmgMagic(inputData) {
  2000. const matk = effectiveMatk(inputData);
  2001. return (((matk ** 2 / 1000 + matk) / 30 + inputData.stats.int / 200) *
  2002. inputData.skillBasicAtk);
  2003. }
  2004. function minDmgMagic(inputData) {
  2005. const matk = effectiveMatk(inputData);
  2006. return (((matk ** 2 / 1000 + matk * inputData.mastery * 0.9) / 30 +
  2007. inputData.stats.int / 200) *
  2008. inputData.skillBasicAtk);
  2009. }
  2010. function afterModMagic(inputData) {
  2011. switch (inputData.spell) {
  2012. case Spell.ChainLightning:
  2013. return 0.7 ** (inputData.hitOrd - 1);
  2014. default:
  2015. return 1;
  2016. }
  2017. }
  2018. function healTargetMulti(enemyCount) {
  2019. return 1.5 + 5 / (enemyCount + 1);
  2020. }
  2021. function maxDmgHeal(inputData) {
  2022. return ((((inputData.stats.int * 1.2 + inputData.stats.luk) *
  2023. effectiveMatk(inputData)) /
  2024. 1000) *
  2025. healTargetMulti(inputData.enemyCount) *
  2026. inputData.skillDmgMulti);
  2027. }
  2028. function minDmgHeal(inputData) {
  2029. return ((((inputData.stats.int * 0.3 + inputData.stats.luk) *
  2030. effectiveMatk(inputData)) /
  2031. 1000) *
  2032. healTargetMulti(inputData.enemyCount) *
  2033. inputData.skillDmgMulti);
  2034. }
  2035. function adjustRangeForWdef(inputData, range) {
  2036. const [min, max] = range;
  2037. const levelDelta = Math.max(inputData.enemyLevel - inputData.level, 0);
  2038. const levelDeltaSlope = 1 - 0.01 * levelDelta;
  2039. return [
  2040. min * levelDeltaSlope - inputData.enemyWdef * 0.6,
  2041. max * levelDeltaSlope - inputData.enemyWdef * 0.5,
  2042. ];
  2043. }
  2044. function adjustRangeForMdef(inputData, range) {
  2045. const [min, max] = range;
  2046. const levelDelta = Math.max(inputData.enemyLevel - inputData.level, 0);
  2047. const levelDeltaSlope = 1 + 0.01 * levelDelta;
  2048. return [
  2049. min - inputData.enemyMdef * 0.6 * levelDeltaSlope,
  2050. max - inputData.enemyMdef * 0.5 * levelDeltaSlope,
  2051. ];
  2052. }
  2053. function effectiveMastery(inputData) {
  2054. if (inputData.wepType === WeaponType.None) {
  2055. return 0.1;
  2056. }
  2057. return inputData.mastery;
  2058. }
  2059. function effectiveWatk(inputData) {
  2060. if (inputData.wepType === WeaponType.None) {
  2061. switch (inputData.clazz) {
  2062. case Class.Pirate:
  2063. case Class.Pirate2nd: {
  2064. const totalWatk = inputData.totalWatk +
  2065. Math.min(Math.trunc((2 * inputData.level + 31) / 3), 31);
  2066. return totalWatk + totalWatk * inputData.echo;
  2067. }
  2068. default:
  2069. return 0;
  2070. }
  2071. }
  2072. return inputData.totalWatk + inputData.totalWatk * inputData.echo;
  2073. }
  2074. function effectiveMatk(inputData) {
  2075. return inputData.totalMatk + inputData.totalMatk * inputData.echo;
  2076. }