main.ts 106 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045
  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 {
  24. ATTACK_LINES,
  25. ATTACK_REQS,
  26. attackIsElemental,
  27. attackName,
  28. attackPeriod,
  29. BAD_WEPS,
  30. chargeTypeFromValue,
  31. className,
  32. isHolySpell,
  33. isSummon,
  34. JOB_LVL_REQS,
  35. magicAttackPeriod,
  36. primaryStat,
  37. secondaryStat,
  38. SPELL_LINES,
  39. SPELL_LVL_REQS,
  40. spellName,
  41. weaponTypeName,
  42. chargeTypeToValue,
  43. } from "./data.js";
  44. import { truncClampedExpectation, truncClampedVariance } from "./math.js";
  45. import {
  46. Attack,
  47. ChargeType,
  48. Class,
  49. InputData,
  50. Speed,
  51. Spell,
  52. Stats,
  53. WeaponType,
  54. } from "./types.js";
  55. import { indefinite } from "./util.js";
  56. document.addEventListener("readystatechange", () => {
  57. if (document.readyState === "complete") {
  58. main();
  59. }
  60. });
  61. function main(): void {
  62. const saveAsButton = document.getElementById(
  63. "save-as",
  64. ) as HTMLButtonElement;
  65. const saveButton = document.getElementById("save") as HTMLButtonElement;
  66. const loadButton = document.getElementById("load") as HTMLButtonElement;
  67. const deleteButton = document.getElementById(
  68. "delete",
  69. ) as HTMLButtonElement;
  70. const exportButton = document.getElementById(
  71. "export",
  72. ) as HTMLButtonElement;
  73. const importButton = document.getElementById(
  74. "import",
  75. ) as HTMLButtonElement;
  76. const saveLoadDialog = document.getElementById(
  77. "save-load-dialog",
  78. ) as HTMLDivElement;
  79. const currentlyLoaded = document.getElementById(
  80. "currently-loaded",
  81. ) as HTMLDivElement;
  82. function currentlyLoadedName(): string | undefined {
  83. const cln = currentlyLoaded.childNodes[0];
  84. if (cln && cln.textContent && cln.textContent.trim()) {
  85. return cln.textContent.trim();
  86. }
  87. return undefined;
  88. }
  89. const strInput = document.getElementById("str") as HTMLInputElement;
  90. const dexInput = document.getElementById("dex") as HTMLInputElement;
  91. const intInput = document.getElementById("int") as HTMLInputElement;
  92. const lukInput = document.getElementById("luk") as HTMLInputElement;
  93. const totalWatkInput = document.getElementById(
  94. "total-watk",
  95. ) as HTMLInputElement;
  96. const totalMatkInput = document.getElementById(
  97. "total-matk",
  98. ) as HTMLInputElement;
  99. const echoInput = document.getElementById("echo") as HTMLInputElement;
  100. const masteryInput = document.getElementById(
  101. "mastery",
  102. ) as HTMLInputElement;
  103. const skillDmgMultiInput = document.getElementById(
  104. "skill-dmg-multi",
  105. ) as HTMLInputElement;
  106. const skillBasicAtkInput = document.getElementById(
  107. "skill-basic-atk",
  108. ) as HTMLInputElement;
  109. const skillLinesInput = document.getElementById(
  110. "skill-lines",
  111. ) as HTMLInputElement;
  112. const critProbInput = document.getElementById(
  113. "crit-prob",
  114. ) as HTMLInputElement;
  115. const critDmgInput = document.getElementById(
  116. "crit-dmg",
  117. ) as HTMLInputElement;
  118. const classInput = document.getElementById("class") as HTMLSelectElement;
  119. const levelInput = document.getElementById("level") as HTMLInputElement;
  120. const weaponTypeInput = document.getElementById(
  121. "weapon-type",
  122. ) as HTMLSelectElement;
  123. const goodAnimProbInput = document.getElementById(
  124. "good-anim-prob",
  125. ) as HTMLInputElement;
  126. const attackInput = document.getElementById("attack") as HTMLSelectElement;
  127. const spellInput = document.getElementById("spell") as HTMLSelectElement;
  128. const speedInput = document.getElementById("speed") as HTMLSelectElement;
  129. const spellBoosterInput = document.getElementById(
  130. "spell-booster",
  131. ) as HTMLInputElement;
  132. const eleAmpInput = document.getElementById("ele-amp") as HTMLInputElement;
  133. const eleBoostInput = document.getElementById(
  134. "ele-boost",
  135. ) as HTMLInputElement;
  136. const eleWepInput = document.getElementById("ele-wep") as HTMLInputElement;
  137. const eleChargeInputs = Array.from(
  138. document.getElementsByName(
  139. "ele-charge",
  140. ) as NodeListOf<HTMLInputElement>,
  141. );
  142. const eleChargeDmgInput = document.getElementById(
  143. "ele-charge-dmg",
  144. ) as HTMLInputElement;
  145. const eleChargeLevelInput = document.getElementById(
  146. "ele-charge-level",
  147. ) as HTMLInputElement;
  148. const caActiveInput = document.getElementById(
  149. "ca-active",
  150. ) as HTMLInputElement;
  151. const caDmgInput = document.getElementById("ca-dmg") as HTMLInputElement;
  152. const caLevelInput = document.getElementById(
  153. "ca-level",
  154. ) as HTMLInputElement;
  155. const caOrbsInput = document.getElementById("ca-orbs") as HTMLInputElement;
  156. const zerkActiveInput = document.getElementById(
  157. "zerk-active",
  158. ) as HTMLInputElement;
  159. const zerkDmgInput = document.getElementById(
  160. "zerk-dmg",
  161. ) as HTMLInputElement;
  162. const enemyWdefInput = document.getElementById(
  163. "enemy-wdef",
  164. ) as HTMLInputElement;
  165. const enemyMdefInput = document.getElementById(
  166. "enemy-mdef",
  167. ) as HTMLInputElement;
  168. const eleSusInput = document.getElementById(
  169. "ele-sus",
  170. ) as HTMLSelectElement;
  171. const enemyLevelInput = document.getElementById(
  172. "enemy-level",
  173. ) as HTMLInputElement;
  174. const enemyCountInput = document.getElementById(
  175. "enemy-count",
  176. ) as HTMLInputElement;
  177. const hitOrdInput = document.getElementById("hit-ord") as HTMLInputElement;
  178. const rangeOutput = document.getElementById("range") as HTMLSpanElement;
  179. const critRangeOutput = document.getElementById(
  180. "crit-range",
  181. ) as HTMLSpanElement;
  182. const expectedPerHitOutput = document.getElementById(
  183. "expected-per-hit",
  184. ) as HTMLSpanElement;
  185. const sdPerHitOutput = document.getElementById(
  186. "sd-per-hit",
  187. ) as HTMLSpanElement;
  188. const cvPerHitOutput = document.getElementById(
  189. "cv-per-hit",
  190. ) as HTMLSpanElement;
  191. const totalRangeOutput = document.getElementById(
  192. "total-range",
  193. ) as HTMLSpanElement;
  194. const expectedPerHitTotalOutput = document.getElementById(
  195. "expected-per-hit-total",
  196. ) as HTMLSpanElement;
  197. const sdPerHitTotalOutput = document.getElementById(
  198. "sd-per-hit-total",
  199. ) as HTMLSpanElement;
  200. const cvPerHitTotalOutput = document.getElementById(
  201. "cv-per-hit-total",
  202. ) as HTMLSpanElement;
  203. const expectedDpsOutput = document.getElementById(
  204. "expected-dps",
  205. ) as HTMLSpanElement;
  206. const sdDpsOutput = document.getElementById("sd-dps") as HTMLSpanElement;
  207. const cvDpsOutput = document.getElementById("cv-dps") as HTMLSpanElement;
  208. const rangeMagicOutput = document.getElementById(
  209. "range-magic",
  210. ) as HTMLSpanElement;
  211. const critRangeMagicOutput = document.getElementById(
  212. "crit-range-magic",
  213. ) as HTMLSpanElement;
  214. const expectedPerHitMagicOutput = document.getElementById(
  215. "expected-per-hit-magic",
  216. ) as HTMLSpanElement;
  217. const sdPerHitMagicOutput = document.getElementById(
  218. "sd-per-hit-magic",
  219. ) as HTMLSpanElement;
  220. const cvPerHitMagicOutput = document.getElementById(
  221. "cv-per-hit-magic",
  222. ) as HTMLSpanElement;
  223. const totalRangeMagicOutput = document.getElementById(
  224. "total-range-magic",
  225. ) as HTMLSpanElement;
  226. const expectedPerHitTotalMagicOutput = document.getElementById(
  227. "expected-per-hit-total-magic",
  228. ) as HTMLSpanElement;
  229. const sdPerHitTotalMagicOutput = document.getElementById(
  230. "sd-per-hit-total-magic",
  231. ) as HTMLSpanElement;
  232. const cvPerHitTotalMagicOutput = document.getElementById(
  233. "cv-per-hit-total-magic",
  234. ) as HTMLSpanElement;
  235. const expectedDpsMagicOutput = document.getElementById(
  236. "expected-dps-magic",
  237. ) as HTMLSpanElement;
  238. const sdDpsMagicOutput = document.getElementById(
  239. "sd-dps-magic",
  240. ) as HTMLSpanElement;
  241. const cvDpsMagicOutput = document.getElementById(
  242. "cv-dps-magic",
  243. ) as HTMLSpanElement;
  244. const warningsDiv = document.getElementById(
  245. "warnings-div",
  246. ) as HTMLDivElement;
  247. function readInputData(): InputData {
  248. let str = Math.max(parseInt(strInput.value, 10), 4);
  249. if (!Number.isFinite(str)) {
  250. str = 4;
  251. }
  252. strInput.value = "" + str;
  253. let dex = Math.max(parseInt(dexInput.value, 10), 4);
  254. if (!Number.isFinite(dex)) {
  255. dex = 4;
  256. }
  257. dexInput.value = "" + dex;
  258. let int = Math.max(parseInt(intInput.value, 10), 4);
  259. if (!Number.isFinite(int)) {
  260. int = 4;
  261. }
  262. intInput.value = "" + int;
  263. let luk = Math.max(parseInt(lukInput.value, 10), 4);
  264. if (!Number.isFinite(luk)) {
  265. luk = 4;
  266. }
  267. lukInput.value = "" + luk;
  268. let totalWatk = Math.max(parseInt(totalWatkInput.value, 10), 0);
  269. if (!Number.isFinite(totalWatk)) {
  270. totalWatk = 0;
  271. }
  272. totalWatkInput.value = "" + totalWatk;
  273. let totalMatk = Math.max(parseInt(totalMatkInput.value, 10), 0);
  274. if (!Number.isFinite(totalMatk)) {
  275. totalMatk = 0;
  276. }
  277. totalMatkInput.value = "" + totalMatk;
  278. let echo = Math.max(parseInt(echoInput.value, 10), 0);
  279. if (!Number.isFinite(echo)) {
  280. echo = 0;
  281. }
  282. echoInput.value = "" + echo;
  283. let mastery = Math.min(
  284. Math.max(parseInt(masteryInput.value, 10), 10),
  285. 90,
  286. );
  287. if (!Number.isFinite(mastery)) {
  288. mastery = 10;
  289. }
  290. mastery -= mastery % 5;
  291. masteryInput.value = "" + mastery;
  292. let skillDmgMulti = Math.max(
  293. parseInt(skillDmgMultiInput.value, 10),
  294. 0,
  295. );
  296. if (!Number.isFinite(skillDmgMulti)) {
  297. skillDmgMulti = 100;
  298. }
  299. skillDmgMultiInput.value = "" + skillDmgMulti;
  300. let skillBasicAtk = Math.max(
  301. parseInt(skillBasicAtkInput.value, 10),
  302. 0,
  303. );
  304. if (!Number.isFinite(skillBasicAtk)) {
  305. skillBasicAtk = 10;
  306. }
  307. skillBasicAtkInput.value = "" + skillBasicAtk;
  308. let skillLines = Math.max(parseInt(skillLinesInput.value, 10), 1);
  309. if (!Number.isFinite(skillLines)) {
  310. skillLines = 1;
  311. }
  312. skillLinesInput.value = "" + skillLines;
  313. let critProb = Math.min(
  314. Math.max(parseInt(critProbInput.value, 10), 0),
  315. 100,
  316. );
  317. if (!Number.isFinite(critProb)) {
  318. critProb = 0;
  319. }
  320. critProbInput.value = "" + critProb;
  321. let critDmg = Math.max(parseInt(critDmgInput.value, 10), 0);
  322. if (!Number.isFinite(critDmg)) {
  323. critDmg = 0;
  324. }
  325. critDmgInput.value = "" + critDmg;
  326. let clazz = parseInt(classInput.value, 10);
  327. if (!Number.isFinite(clazz) || !(clazz in Class)) {
  328. clazz = 0;
  329. }
  330. classInput.value = "" + clazz;
  331. let level = Math.min(Math.max(parseInt(levelInput.value, 10), 1), 200);
  332. if (!Number.isFinite(level)) {
  333. level = 30;
  334. }
  335. levelInput.value = "" + level;
  336. let wepType = parseInt(weaponTypeInput.value, 10);
  337. if (!Number.isFinite(wepType) || !(wepType in WeaponType)) {
  338. wepType = 30;
  339. }
  340. weaponTypeInput.value = "" + wepType;
  341. let goodAnimProb = Math.min(
  342. Math.max(parseFloat(goodAnimProbInput.value), 0),
  343. 100,
  344. );
  345. if (!Number.isFinite(goodAnimProb)) {
  346. goodAnimProb = 60;
  347. }
  348. goodAnimProbInput.value = "" + goodAnimProb;
  349. let attack = parseInt(attackInput.value, 10);
  350. if (!Number.isFinite(attack) || !(attack in Attack)) {
  351. attack = 0;
  352. }
  353. attackInput.value = "" + attack;
  354. let spell = parseInt(spellInput.value, 10);
  355. if (!Number.isFinite(spell) || !(spell in Spell)) {
  356. spell = 0;
  357. }
  358. spellInput.value = "" + spell;
  359. let speed = Math.min(Math.max(parseInt(speedInput.value, 10), 2), 9);
  360. if (!Number.isFinite(speed)) {
  361. speed = 6;
  362. }
  363. speedInput.value = "" + speed;
  364. let spellBooster = Math.min(
  365. Math.max(parseInt(spellBoosterInput.value, 10), -2),
  366. 0,
  367. );
  368. if (!Number.isFinite(spellBooster)) {
  369. spellBooster = 0;
  370. }
  371. spellBoosterInput.value = "" + spellBooster;
  372. let eleAmp = Math.max(parseInt(eleAmpInput.value, 10), 100);
  373. if (!Number.isFinite(eleAmp)) {
  374. eleAmp = 100;
  375. }
  376. eleAmpInput.value = "" + eleAmp;
  377. let eleBoost = Math.max(parseInt(eleBoostInput.value, 10), 0);
  378. if (!Number.isFinite(eleBoost)) {
  379. eleBoost = 0;
  380. }
  381. eleBoostInput.value = "" + eleBoost;
  382. let eleWep = Math.max(parseInt(eleWepInput.value, 10), 0);
  383. if (!Number.isFinite(eleWep)) {
  384. eleWep = 0;
  385. }
  386. eleWepInput.value = "" + eleWep;
  387. const eleChargeType: ChargeType = (() => {
  388. let eleChargeType: ChargeType | undefined = undefined;
  389. for (const eleChargeInput of eleChargeInputs) {
  390. if (eleChargeInput.checked) {
  391. eleChargeType = chargeTypeFromValue(eleChargeInput.value);
  392. break;
  393. }
  394. }
  395. if (eleChargeType === undefined) {
  396. eleChargeInputs.forEach(inp => (inp.checked = false));
  397. const noEleChargeInput = document.getElementById(
  398. "no-ele-charge",
  399. ) as HTMLInputElement;
  400. noEleChargeInput.checked = true;
  401. return ChargeType.None;
  402. }
  403. return eleChargeType;
  404. })();
  405. let eleChargeDmg = Math.max(
  406. parseInt(eleChargeDmgInput.value, 10),
  407. 100,
  408. );
  409. if (!Number.isFinite(eleChargeDmg)) {
  410. eleChargeDmg = 100;
  411. }
  412. eleChargeDmgInput.value = "" + eleChargeDmg;
  413. let eleChargeLevel = Math.min(
  414. Math.max(parseInt(eleChargeLevelInput.value, 10), 1),
  415. 30,
  416. );
  417. if (!Number.isFinite(eleChargeLevel)) {
  418. eleChargeLevel = 1;
  419. }
  420. eleChargeLevelInput.value = "" + eleChargeLevel;
  421. const caActive = caActiveInput.checked;
  422. let caDmg = Math.max(parseInt(caDmgInput.value, 10), 100);
  423. if (!Number.isFinite(caDmg)) {
  424. caDmg = 104;
  425. }
  426. caDmgInput.value = "" + caDmg;
  427. let caLevel = Math.min(
  428. Math.max(parseInt(caLevelInput.value, 10), 1),
  429. 30,
  430. );
  431. if (!Number.isFinite(caLevel)) {
  432. caLevel = 1;
  433. }
  434. caLevelInput.value = "" + caLevel;
  435. let caOrbs = Math.min(
  436. Math.max(parseInt(caOrbsInput.value, 10), 1),
  437. 10,
  438. );
  439. if (!Number.isFinite(caOrbs)) {
  440. caOrbs = 1;
  441. }
  442. caOrbsInput.value = "" + caOrbs;
  443. const zerkActive = zerkActiveInput.checked;
  444. let zerkDmg = Math.max(parseInt(zerkDmgInput.value, 10), 100);
  445. if (!Number.isFinite(zerkDmg)) {
  446. zerkDmg = 100;
  447. }
  448. zerkDmgInput.value = "" + zerkDmg;
  449. let enemyWdef = parseInt(enemyWdefInput.value, 10);
  450. if (!Number.isFinite(enemyWdef)) {
  451. enemyWdef = 0;
  452. }
  453. enemyWdefInput.value = "" + enemyWdef;
  454. let enemyMdef = parseInt(enemyMdefInput.value, 10);
  455. if (!Number.isFinite(enemyMdef)) {
  456. enemyMdef = 0;
  457. }
  458. enemyMdefInput.value = "" + enemyMdef;
  459. let eleSus = Math.min(Math.max(parseFloat(eleSusInput.value), 0), 1.5);
  460. if (
  461. !Number.isFinite(eleSus) ||
  462. !(eleSus === 0 || eleSus === 0.5 || eleSus === 1 || eleSus === 1.5)
  463. ) {
  464. eleSus = 1;
  465. }
  466. eleSusInput.value = "" + eleSus;
  467. let enemyLevel = Math.max(parseInt(enemyLevelInput.value, 10), 1);
  468. if (!Number.isFinite(enemyLevel)) {
  469. enemyLevel = 1;
  470. }
  471. enemyLevelInput.value = "" + enemyLevel;
  472. let enemyCount = Math.min(
  473. Math.max(parseInt(enemyCountInput.value, 10), 1),
  474. 15,
  475. );
  476. if (!Number.isFinite(enemyCount)) {
  477. enemyCount = 1;
  478. }
  479. enemyCountInput.value = "" + enemyCount;
  480. let hitOrd = Math.min(Math.max(parseInt(hitOrdInput.value, 10), 1), 6);
  481. if (!Number.isFinite(hitOrd)) {
  482. hitOrd = 1;
  483. }
  484. hitOrdInput.value = "" + hitOrd;
  485. return new InputData(
  486. new Stats(str, dex, int, luk),
  487. totalWatk,
  488. totalMatk,
  489. echo / 100,
  490. mastery / 100,
  491. skillDmgMulti / 100,
  492. skillBasicAtk,
  493. skillLines,
  494. critProb / 100,
  495. critDmg / 100,
  496. clazz,
  497. level,
  498. wepType,
  499. goodAnimProb / 100,
  500. attack,
  501. spell,
  502. speed,
  503. spellBooster,
  504. eleAmp / 100,
  505. eleBoost / 100,
  506. eleWep / 100,
  507. eleChargeType,
  508. eleChargeDmg / 100,
  509. eleChargeLevel,
  510. caActive,
  511. caDmg,
  512. caLevel,
  513. caOrbs,
  514. zerkActive,
  515. zerkDmg / 100,
  516. enemyWdef,
  517. enemyMdef,
  518. eleSus,
  519. enemyLevel,
  520. enemyCount,
  521. hitOrd,
  522. );
  523. }
  524. function writeInputData(inputData: InputData): void {
  525. strInput.value = "" + inputData.stats.str;
  526. dexInput.value = "" + inputData.stats.dex;
  527. intInput.value = "" + inputData.stats.int;
  528. lukInput.value = "" + inputData.stats.luk;
  529. totalWatkInput.value = "" + inputData.totalWatk;
  530. totalMatkInput.value = "" + inputData.totalMatk;
  531. echoInput.value = "" + inputData.echo * 100;
  532. masteryInput.value = "" + inputData.mastery * 100;
  533. skillDmgMultiInput.value = "" + inputData.skillDmgMulti * 100;
  534. skillBasicAtkInput.value = "" + inputData.skillBasicAtk;
  535. skillLinesInput.value = "" + inputData.skillLines;
  536. critProbInput.value = "" + inputData.critProb * 100;
  537. critDmgInput.value = "" + inputData.critDmg * 100;
  538. classInput.value = "" + inputData.clazz;
  539. levelInput.value = "" + inputData.level;
  540. weaponTypeInput.value = "" + inputData.wepType;
  541. goodAnimProbInput.value = "" + inputData.goodAnimProb * 100;
  542. attackInput.value = "" + inputData.attack;
  543. spellInput.value = "" + inputData.spell;
  544. speedInput.value = "" + inputData.speed;
  545. spellBoosterInput.value = "" + inputData.spellBooster;
  546. eleAmpInput.value = "" + inputData.eleAmp * 100;
  547. eleBoostInput.value = "" + inputData.eleBoost * 100;
  548. eleWepInput.value = "" + inputData.eleWep * 100;
  549. eleChargeInputs.forEach(inp => (inp.checked = false));
  550. const chargeInput = document.getElementById(
  551. chargeTypeToValue(inputData.eleChargeType),
  552. ) as HTMLInputElement;
  553. chargeInput.checked = true;
  554. eleChargeDmgInput.value = "" + inputData.eleChargeDmg * 100;
  555. eleChargeLevelInput.value = "" + inputData.eleChargeLevel;
  556. caActiveInput.checked = inputData.caActive;
  557. caDmgInput.value = "" + inputData.caDmg;
  558. caLevelInput.value = "" + inputData.caLevel;
  559. caOrbsInput.value = "" + inputData.caOrbs;
  560. zerkActiveInput.checked = inputData.zerkActive;
  561. zerkDmgInput.value = "" + inputData.zerkDmg * 100;
  562. enemyWdefInput.value = "" + inputData.enemyWdef;
  563. enemyMdefInput.value = "" + inputData.enemyMdef;
  564. eleSusInput.value = "" + inputData.eleSus;
  565. enemyLevelInput.value = "" + inputData.enemyLevel;
  566. enemyCountInput.value = "" + inputData.enemyCount;
  567. hitOrdInput.value = "" + inputData.hitOrd;
  568. }
  569. function recalculate(): void {
  570. const inputData = readInputData();
  571. const critQ = 1 - inputData.critProb;
  572. recalculatePhys(inputData, critQ);
  573. recalculateMagic(inputData, critQ);
  574. recalculateWarnings(inputData);
  575. }
  576. function recalculatePhys(inputData: InputData, critQ: number): void {
  577. const goodAnimProb = (() => {
  578. switch (inputData.attack) {
  579. case Attack.Rush:
  580. case Attack.CrusherLow:
  581. case Attack.CrusherHigh:
  582. case Attack.SomersaultKick:
  583. case Attack.AerialStrike:
  584. return swingProbToGoodAnimProb(inputData, 0);
  585. case Attack.Brandish:
  586. return 0.5;
  587. case Attack.Blast:
  588. return swingProbToGoodAnimProb(inputData, 0.6);
  589. case Attack.HeavensHammerXiuz:
  590. case Attack.HeavensHammerXiuzCorrected:
  591. return 1;
  592. case Attack.Fury:
  593. return swingProbToGoodAnimProb(inputData, 1);
  594. default:
  595. return inputData.goodAnimProb;
  596. }
  597. })();
  598. const badAnimProb = 1 - goodAnimProb;
  599. const [caMod, eleChargeMod] = isSummon(inputData.attack)
  600. ? [1, 1]
  601. : [caModifier(inputData), eleChargeModifier(inputData)];
  602. const eleSus = attackEffectiveEleSus(inputData);
  603. const [minDmgPhysBad, maxDmgPhysGood] = [
  604. (() => {
  605. const goodAnim = goodAnimProb >= 1;
  606. switch (inputData.attack) {
  607. case Attack.BowWhack:
  608. case Attack.PowerKnockBack:
  609. return minDmgBowWhack(inputData);
  610. case Attack.ClawPunch:
  611. return minDmgClawPunch(inputData);
  612. case Attack.Panic:
  613. case Attack.Coma:
  614. return minDmgCaFinisher(inputData, goodAnim);
  615. case Attack.HeavensHammerXiuz:
  616. return minDmgHhXiuz(inputData);
  617. case Attack.HeavensHammerXiuzCorrected:
  618. return minDmgPhys(inputData, true);
  619. case Attack.DragonRoar:
  620. return minDmgDragonRoar(inputData);
  621. // Massive hack to make Arrow Bomb easier to work with...
  622. case Attack.ArrowBombImpact:
  623. return minDmgArrowBombImpact(inputData, false);
  624. case Attack.ArrowBombSplash:
  625. return minDmgArrowBombSplash(inputData, false);
  626. case Attack.Phoenix:
  627. case Attack.Frostprey:
  628. case Attack.Octopus:
  629. case Attack.Gaviota:
  630. case Attack.WrathOfTheOctopi:
  631. return minDmgDexSummon(inputData);
  632. case Attack.LuckySeven:
  633. case Attack.TripleThrow:
  634. return minDmgLuckySeven(inputData);
  635. case Attack.NinjaAmbush:
  636. return dmgNinjaAmbush(inputData);
  637. case Attack.VenomousStar:
  638. case Attack.VenomousStab:
  639. return minDmgVenom(inputData);
  640. default:
  641. return minDmgPhys(inputData, goodAnim);
  642. }
  643. })(),
  644. (() => {
  645. const goodAnim = goodAnimProb > 0;
  646. switch (inputData.attack) {
  647. case Attack.BowWhack:
  648. case Attack.PowerKnockBack:
  649. return maxDmgBowWhack(inputData);
  650. case Attack.ClawPunch:
  651. return maxDmgClawPunch(inputData);
  652. case Attack.Panic:
  653. case Attack.Coma:
  654. return maxDmgCaFinisher(inputData, goodAnim);
  655. case Attack.HeavensHammerXiuz:
  656. case Attack.HeavensHammerXiuzCorrected:
  657. return maxDmgPhys(inputData, true);
  658. case Attack.DragonRoar:
  659. return maxDmgDragonRoar(inputData);
  660. // Massive hack to make Arrow Bomb easier to work with...
  661. case Attack.ArrowBombImpact:
  662. return maxDmgArrowBombImpact(inputData, true);
  663. case Attack.ArrowBombSplash:
  664. return maxDmgArrowBombSplash(inputData, true);
  665. case Attack.Phoenix:
  666. case Attack.Frostprey:
  667. case Attack.Octopus:
  668. case Attack.Gaviota:
  669. case Attack.WrathOfTheOctopi:
  670. return maxDmgDexSummon(inputData);
  671. case Attack.LuckySeven:
  672. case Attack.TripleThrow:
  673. return maxDmgLuckySeven(inputData);
  674. case Attack.NinjaAmbush:
  675. return dmgNinjaAmbush(inputData);
  676. case Attack.VenomousStar:
  677. case Attack.VenomousStab:
  678. return maxDmgVenom(inputData);
  679. default:
  680. return maxDmgPhys(inputData, goodAnim);
  681. }
  682. })(),
  683. ].map(dmg => dmg * eleSus * caMod * eleChargeMod);
  684. const [minDmgPhysGood, maxDmgPhysBad] = [
  685. (() => {
  686. if (goodAnimProb <= 0) {
  687. return minDmgPhysBad;
  688. }
  689. switch (inputData.attack) {
  690. case Attack.BowWhack:
  691. case Attack.PowerKnockBack:
  692. case Attack.ClawPunch:
  693. case Attack.Rush:
  694. case Attack.HeavensHammerXiuz:
  695. case Attack.HeavensHammerXiuzCorrected:
  696. case Attack.CrusherHigh:
  697. case Attack.CrusherLow:
  698. case Attack.Fury:
  699. case Attack.DragonRoar:
  700. case Attack.Phoenix:
  701. case Attack.Frostprey:
  702. case Attack.Octopus:
  703. case Attack.Gaviota:
  704. case Attack.WrathOfTheOctopi:
  705. case Attack.LuckySeven:
  706. case Attack.TripleThrow:
  707. case Attack.NinjaAmbush:
  708. case Attack.VenomousStar:
  709. case Attack.VenomousStab:
  710. case Attack.SomersaultKick:
  711. case Attack.AerialStrike:
  712. return minDmgPhysBad;
  713. case Attack.Panic:
  714. case Attack.Coma:
  715. return minDmgCaFinisher(inputData, true);
  716. // Massive hack to make Arrow Bomb easier to work with...
  717. case Attack.ArrowBombImpact:
  718. return minDmgArrowBombImpact(inputData, true);
  719. case Attack.ArrowBombSplash:
  720. return minDmgArrowBombSplash(inputData, true);
  721. default:
  722. return minDmgPhys(inputData, true);
  723. }
  724. })(),
  725. (() => {
  726. if (goodAnimProb >= 1) {
  727. return maxDmgPhysGood;
  728. }
  729. switch (inputData.attack) {
  730. case Attack.BowWhack:
  731. case Attack.PowerKnockBack:
  732. case Attack.ClawPunch:
  733. case Attack.Rush:
  734. case Attack.HeavensHammerXiuz:
  735. case Attack.HeavensHammerXiuzCorrected:
  736. case Attack.CrusherHigh:
  737. case Attack.CrusherLow:
  738. case Attack.Fury:
  739. case Attack.DragonRoar:
  740. case Attack.Phoenix:
  741. case Attack.Frostprey:
  742. case Attack.Octopus:
  743. case Attack.Gaviota:
  744. case Attack.WrathOfTheOctopi:
  745. case Attack.LuckySeven:
  746. case Attack.TripleThrow:
  747. case Attack.NinjaAmbush:
  748. case Attack.VenomousStar:
  749. case Attack.VenomousStab:
  750. case Attack.SomersaultKick:
  751. case Attack.AerialStrike:
  752. return maxDmgPhysGood;
  753. case Attack.Panic:
  754. case Attack.Coma:
  755. return maxDmgCaFinisher(inputData, false);
  756. // Massive hack to make Arrow Bomb easier to work with...
  757. case Attack.ArrowBombImpact:
  758. return maxDmgArrowBombImpact(inputData, false);
  759. case Attack.ArrowBombSplash:
  760. return maxDmgArrowBombSplash(inputData, false);
  761. default:
  762. return maxDmgPhys(inputData, false);
  763. }
  764. })(),
  765. ].map(dmg => dmg * eleSus * caMod * eleChargeMod);
  766. const [minDmgPhysBadAdjusted, maxDmgPhysGoodAdjusted] = (() => {
  767. switch (inputData.attack) {
  768. case Attack.HeavensHammerXiuz:
  769. case Attack.HeavensHammerXiuzCorrected:
  770. case Attack.Phoenix:
  771. case Attack.Frostprey:
  772. case Attack.Octopus:
  773. case Attack.Gaviota:
  774. case Attack.WrathOfTheOctopi:
  775. return [minDmgPhysBad, maxDmgPhysGood];
  776. case Attack.Assaulter:
  777. return inputData.level >= inputData.enemyLevel
  778. ? [minDmgPhysBad, maxDmgPhysGood]
  779. : adjustRangeForWdef(inputData, [
  780. minDmgPhysBad,
  781. maxDmgPhysGood,
  782. ]);
  783. default:
  784. return adjustRangeForWdef(inputData, [
  785. minDmgPhysBad,
  786. maxDmgPhysGood,
  787. ]);
  788. }
  789. })();
  790. const [minDmgPhysGoodAdjusted, maxDmgPhysBadAdjusted] = (() => {
  791. switch (inputData.attack) {
  792. case Attack.HeavensHammerXiuz:
  793. case Attack.HeavensHammerXiuzCorrected:
  794. case Attack.Phoenix:
  795. case Attack.Frostprey:
  796. case Attack.Octopus:
  797. case Attack.Gaviota:
  798. case Attack.WrathOfTheOctopi:
  799. return [minDmgPhysGood, maxDmgPhysBad];
  800. case Attack.Assaulter:
  801. return inputData.level >= inputData.enemyLevel
  802. ? [minDmgPhysGood, maxDmgPhysBad]
  803. : adjustRangeForWdef(inputData, [
  804. minDmgPhysGood,
  805. maxDmgPhysBad,
  806. ]);
  807. default:
  808. return adjustRangeForWdef(inputData, [
  809. minDmgPhysGood,
  810. maxDmgPhysBad,
  811. ]);
  812. }
  813. })();
  814. const [dmgMultiNoCrit, dmgMultiCrit] = isSummon(inputData.attack)
  815. ? [1, 1]
  816. : [
  817. dmgMulti(inputData, false),
  818. dmgMulti(
  819. inputData,
  820. inputData.attack !== Attack.HeavensHammerXiuz &&
  821. inputData.attack !==
  822. Attack.HeavensHammerXiuzCorrected &&
  823. inputData.attack !== Attack.VenomousStar &&
  824. inputData.attack !== Attack.VenomousStab,
  825. ),
  826. ];
  827. const afterModifier = isSummon(inputData.attack)
  828. ? 1
  829. : afterModPhys(inputData);
  830. const [
  831. minDmgPhysBadNoCrit,
  832. maxDmgPhysGoodNoCrit,
  833. minDmgPhysGoodNoCrit,
  834. maxDmgPhysBadNoCrit,
  835. ] =
  836. // Massive hack to make Arrow Bomb easier to work with...
  837. inputData.attack === Attack.ArrowBombImpact ||
  838. inputData.attack === Attack.ArrowBombSplash
  839. ? [
  840. minDmgPhysBadAdjusted,
  841. maxDmgPhysBadAdjusted,
  842. minDmgPhysBadAdjusted,
  843. maxDmgPhysBadAdjusted,
  844. ]
  845. : [
  846. minDmgPhysBadAdjusted,
  847. maxDmgPhysGoodAdjusted,
  848. minDmgPhysGoodAdjusted,
  849. maxDmgPhysBadAdjusted,
  850. ].map(x => Math.max(x * dmgMultiNoCrit, 1) * afterModifier);
  851. const [
  852. minDmgPhysBadCrit,
  853. maxDmgPhysGoodCrit,
  854. minDmgPhysGoodCrit,
  855. maxDmgPhysBadCrit,
  856. ] =
  857. // Massive hack to make Arrow Bomb easier to work with...
  858. inputData.attack === Attack.ArrowBombImpact ||
  859. inputData.attack === Attack.ArrowBombSplash
  860. ? [
  861. minDmgPhysGoodAdjusted,
  862. maxDmgPhysGoodAdjusted,
  863. minDmgPhysGoodAdjusted,
  864. maxDmgPhysGoodAdjusted,
  865. ]
  866. : [
  867. minDmgPhysBadAdjusted,
  868. maxDmgPhysGoodAdjusted,
  869. minDmgPhysGoodAdjusted,
  870. maxDmgPhysBadAdjusted,
  871. ].map(x => Math.max(x * dmgMultiCrit, 1) * afterModifier);
  872. // Lots of special-casing for Barrage, the only goddamn attack that
  873. // does this...
  874. const [maxDmgNoCritBarrage, maxDmgCritBarrage] = [
  875. maxDmgPhysGoodNoCrit,
  876. maxDmgPhysGoodCrit,
  877. ].map(
  878. x =>
  879. x *
  880. (inputData.attack === Attack.Barrage
  881. ? afterModBarrage(inputData.skillLines)
  882. : 0),
  883. );
  884. const range =
  885. inputData.attack === Attack.Barrage
  886. ? [
  887. Math.max(Math.trunc(minDmgPhysBadNoCrit), 1),
  888. Math.max(Math.trunc(maxDmgNoCritBarrage), 1),
  889. ]
  890. : [minDmgPhysBadNoCrit, maxDmgPhysGoodNoCrit].map(x =>
  891. Math.max(Math.trunc(x), 1),
  892. );
  893. const critRange =
  894. inputData.attack === Attack.Barrage
  895. ? [
  896. Math.max(Math.trunc(minDmgPhysBadCrit), 1),
  897. Math.max(Math.trunc(maxDmgCritBarrage), 1),
  898. ]
  899. : [minDmgPhysBadCrit, maxDmgPhysGoodCrit].map(x =>
  900. Math.max(Math.trunc(x), 1),
  901. );
  902. rangeOutput.textContent = `${range[0]} ~ ${range[1]}${
  903. range[1] ===
  904. (inputData.attack === Attack.Barrage
  905. ? maxDmgNoCritBarrage
  906. : maxDmgPhysGoodNoCrit) && range[1] !== 1
  907. ? "*"
  908. : ""
  909. }`;
  910. critRangeOutput.textContent = `${critRange[0]} ~ ${critRange[1]}${
  911. critRange[1] ===
  912. (inputData.attack === Attack.Barrage
  913. ? maxDmgCritBarrage
  914. : maxDmgPhysGoodCrit) && critRange[1] !== 1
  915. ? "*"
  916. : ""
  917. }`;
  918. const [combinedRangeTop, combinedRangeTopOneLine] =
  919. inputData.critProb > 0
  920. ? [critRange[1], maxDmgPhysGoodCrit]
  921. : [range[1], maxDmgPhysGoodNoCrit];
  922. if (inputData.attack === Attack.Barrage) {
  923. let totalRangeBottom =
  924. range[0] * Math.min(inputData.skillLines, 4);
  925. for (let i = 5; i <= inputData.skillLines; ++i) {
  926. totalRangeBottom += Math.max(
  927. Math.trunc(minDmgPhysBadNoCrit * afterModBarrage(i)),
  928. 1,
  929. );
  930. }
  931. let totalRangeTop =
  932. combinedRangeTop * Math.min(inputData.skillLines, 4);
  933. for (let i = 5; i <= inputData.skillLines; ++i) {
  934. totalRangeTop += Math.max(
  935. Math.trunc(combinedRangeTopOneLine * afterModBarrage(i)),
  936. 1,
  937. );
  938. }
  939. totalRangeOutput.textContent = `${totalRangeBottom} ~ \
  940. ${totalRangeTop}${
  941. combinedRangeTop === combinedRangeTopOneLine &&
  942. combinedRangeTop !== 1
  943. ? "*"
  944. : ""
  945. }`;
  946. } else {
  947. totalRangeOutput.textContent = `${
  948. range[0] * inputData.skillLines
  949. } ~ ${combinedRangeTop * inputData.skillLines}${
  950. combinedRangeTop === combinedRangeTopOneLine &&
  951. combinedRangeTop !== 1
  952. ? "*"
  953. : ""
  954. }`;
  955. }
  956. const [expectedPerHitBadNoCrit, expectedPerHitBadCrit] = [
  957. truncClampedExpectation(minDmgPhysBadNoCrit, maxDmgPhysBadNoCrit),
  958. truncClampedExpectation(minDmgPhysBadCrit, maxDmgPhysBadCrit),
  959. ];
  960. const [expectedPerHitGoodNoCrit, expectedPerHitGoodCrit] = [
  961. truncClampedExpectation(
  962. minDmgPhysGoodNoCrit,
  963. maxDmgPhysGoodNoCrit,
  964. ),
  965. truncClampedExpectation(minDmgPhysGoodCrit, maxDmgPhysGoodCrit),
  966. ];
  967. const expectedPerHitBad =
  968. critQ * expectedPerHitBadNoCrit +
  969. inputData.critProb * expectedPerHitBadCrit;
  970. const expectedPerHitGood =
  971. critQ * expectedPerHitGoodNoCrit +
  972. inputData.critProb * expectedPerHitGoodCrit;
  973. const expectedPerHit =
  974. expectedPerHitBad * badAnimProb +
  975. expectedPerHitGood * goodAnimProb;
  976. const expectedPerHitTotal =
  977. inputData.attack === Attack.Barrage
  978. ? (() => {
  979. let accum =
  980. expectedPerHit * Math.min(inputData.skillLines, 4);
  981. for (let i = 5; i <= inputData.skillLines; ++i) {
  982. accum += expectedPerHit * afterModBarrage(i);
  983. }
  984. return accum;
  985. })()
  986. : expectedPerHit * inputData.skillLines;
  987. expectedPerHitOutput.textContent = (
  988. expectedPerHit *
  989. (inputData.attack === Attack.Barrage
  990. ? barrageEffectiveMulti(inputData.skillLines) /
  991. inputData.skillLines
  992. : 1)
  993. ).toFixed(3);
  994. expectedPerHitTotalOutput.textContent = expectedPerHitTotal.toFixed(3);
  995. // The "mainVariance" in the following variable names is intended to
  996. // indicate that this is a variance against the expectation across
  997. // _all_ cases (`expectedPerHit`), not against the expected value of
  998. // the particular case in question.
  999. const mainVariancePerHitBadNoCrit = truncClampedVariance(
  1000. minDmgPhysBadNoCrit,
  1001. maxDmgPhysBadNoCrit,
  1002. expectedPerHit,
  1003. );
  1004. const mainVariancePerHitGoodNoCrit = truncClampedVariance(
  1005. minDmgPhysGoodNoCrit,
  1006. maxDmgPhysGoodNoCrit,
  1007. expectedPerHit,
  1008. );
  1009. const mainVariancePerHitBadCrit = truncClampedVariance(
  1010. minDmgPhysBadCrit,
  1011. maxDmgPhysBadCrit,
  1012. expectedPerHit,
  1013. );
  1014. const mainVariancePerHitGoodCrit = truncClampedVariance(
  1015. minDmgPhysGoodCrit,
  1016. maxDmgPhysGoodCrit,
  1017. expectedPerHit,
  1018. );
  1019. const variancePerHit = (() => {
  1020. if (
  1021. mainVariancePerHitBadNoCrit !== undefined &&
  1022. mainVariancePerHitGoodNoCrit !== undefined &&
  1023. mainVariancePerHitBadCrit !== undefined &&
  1024. mainVariancePerHitGoodCrit !== undefined
  1025. ) {
  1026. const mainVariancePerHitBad =
  1027. critQ * mainVariancePerHitBadNoCrit +
  1028. inputData.critProb * mainVariancePerHitBadCrit;
  1029. const mainVariancePerHitGood =
  1030. critQ * mainVariancePerHitGoodNoCrit +
  1031. inputData.critProb * mainVariancePerHitGoodCrit;
  1032. return (
  1033. mainVariancePerHitBad * badAnimProb +
  1034. mainVariancePerHitGood * goodAnimProb
  1035. );
  1036. }
  1037. return;
  1038. })();
  1039. let sdPerHitTotal: number | undefined = undefined;
  1040. if (variancePerHit !== undefined) {
  1041. sdPerHitOutput.classList.remove("error");
  1042. cvPerHitOutput.classList.remove("error");
  1043. sdPerHitTotalOutput.classList.remove("error");
  1044. cvPerHitTotalOutput.classList.remove("error");
  1045. const sdPerHit = Math.sqrt(
  1046. inputData.attack === Attack.Barrage
  1047. ? variancePerHit *
  1048. (barrageEffectiveMulti(inputData.skillLines) /
  1049. inputData.skillLines)
  1050. : variancePerHit,
  1051. );
  1052. // This is mathematically valid because the damage/outcome of each
  1053. // line is independent of the damage of any other line, thus
  1054. // implying uncorrelatedness. Furthermore, this implies that the
  1055. // variance of the sum of lines is the sum of the variance of said
  1056. // lines (see the Bienaymé formula/identity).
  1057. sdPerHitTotal = Math.sqrt(
  1058. variancePerHit *
  1059. (inputData.attack === Attack.Barrage
  1060. ? barrageEffectiveMulti(inputData.skillLines)
  1061. : inputData.skillLines),
  1062. );
  1063. sdPerHitOutput.textContent = sdPerHit.toFixed(3);
  1064. cvPerHitOutput.textContent = (
  1065. sdPerHit /
  1066. (expectedPerHit *
  1067. (inputData.attack === Attack.Barrage
  1068. ? barrageEffectiveMulti(inputData.skillLines) /
  1069. inputData.skillLines
  1070. : 1))
  1071. ).toFixed(5);
  1072. sdPerHitTotalOutput.textContent = sdPerHitTotal.toFixed(3);
  1073. cvPerHitTotalOutput.textContent = (
  1074. sdPerHitTotal / expectedPerHitTotal
  1075. ).toFixed(5);
  1076. } else {
  1077. sdPerHitOutput.classList.add("error");
  1078. cvPerHitOutput.classList.add("error");
  1079. sdPerHitTotalOutput.classList.add("error");
  1080. cvPerHitTotalOutput.classList.add("error");
  1081. sdPerHitOutput.textContent = "[undefined]";
  1082. cvPerHitOutput.textContent = "[undefined]";
  1083. sdPerHitTotalOutput.textContent = "[undefined]";
  1084. cvPerHitTotalOutput.textContent = "[undefined]";
  1085. }
  1086. const period = attackPeriod(
  1087. inputData.wepType,
  1088. inputData.speed,
  1089. inputData.attack,
  1090. );
  1091. if (period !== undefined) {
  1092. expectedDpsOutput.classList.remove("error");
  1093. const attackHz = 1000 / period;
  1094. const expectedDps = attackHz * expectedPerHitTotal;
  1095. expectedDpsOutput.textContent = expectedDps.toFixed(3);
  1096. if (sdPerHitTotal !== undefined) {
  1097. sdDpsOutput.classList.remove("error");
  1098. cvDpsOutput.classList.remove("error");
  1099. // This is mathematically valid because the damage/outcome of
  1100. // each line is independent of the damage of any other line,
  1101. // thus implying uncorrelatedness. Furthermore, this implies
  1102. // that the variance of the sum of lines is the sum of the
  1103. // variance of said lines (see the Bienaymé formula/identity).
  1104. const sdDps = Math.sqrt(attackHz) * sdPerHitTotal; /*
  1105. = sqrt(attackHz) * sqrt(variancePerHitTotal)
  1106. = sqrt(attackHz * variancePerHitTotal)
  1107. = sqrt(varianceDps). */
  1108. sdDpsOutput.textContent = sdDps.toFixed(3);
  1109. cvDpsOutput.textContent = (sdDps / expectedDps).toFixed(5);
  1110. } else {
  1111. sdDpsOutput.classList.add("error");
  1112. cvDpsOutput.classList.add("error");
  1113. sdDpsOutput.textContent = "[undefined]";
  1114. cvDpsOutput.textContent = "[undefined]";
  1115. }
  1116. } else {
  1117. expectedDpsOutput.classList.add("error");
  1118. sdDpsOutput.classList.add("error");
  1119. cvDpsOutput.classList.add("error");
  1120. expectedDpsOutput.textContent = "[unknown attack speed value]";
  1121. sdDpsOutput.textContent = "[undefined]";
  1122. cvDpsOutput.textContent = "[undefined]";
  1123. }
  1124. }
  1125. function recalculateMagic(inputData: InputData, critQ: number): void {
  1126. const eleWepBonus =
  1127. 1 + (isHolySpell(inputData.spell) ? 0 : inputData.eleWep);
  1128. const [minDmg, maxDmg] = [
  1129. (() => {
  1130. switch (inputData.spell) {
  1131. case Spell.Heal:
  1132. return minDmgHeal(inputData);
  1133. default:
  1134. return minDmgMagic(inputData);
  1135. }
  1136. })() *
  1137. inputData.eleAmp *
  1138. eleWepBonus *
  1139. inputData.eleSus,
  1140. (() => {
  1141. switch (inputData.spell) {
  1142. case Spell.Heal:
  1143. return maxDmgHeal(inputData);
  1144. default:
  1145. return maxDmgMagic(inputData);
  1146. }
  1147. })() *
  1148. inputData.eleAmp *
  1149. eleWepBonus *
  1150. inputData.eleSus,
  1151. ];
  1152. const [minDmgNoCrit, maxDmgNoCrit] = adjustRangeForMdef(inputData, [
  1153. minDmg,
  1154. maxDmg,
  1155. ]);
  1156. const [minDmgCrit, maxDmgCrit] = [minDmgNoCrit, maxDmgNoCrit].map(
  1157. x => x * inputData.critDmg,
  1158. );
  1159. const [
  1160. minDmgNoCritAfter,
  1161. maxDmgNoCritAfter,
  1162. minDmgCritAfter,
  1163. maxDmgCritAfter,
  1164. ] = [minDmgNoCrit, maxDmgNoCrit, minDmgCrit, maxDmgCrit].map(
  1165. x => Math.max(x, 1) * afterModMagic(inputData),
  1166. );
  1167. const range = [minDmgNoCritAfter, maxDmgNoCritAfter].map(x =>
  1168. Math.max(Math.trunc(x), 1),
  1169. );
  1170. const critRange = [minDmgCritAfter, maxDmgCritAfter].map(x =>
  1171. Math.max(Math.trunc(x), 1),
  1172. );
  1173. rangeMagicOutput.textContent = `${range[0]} ~ ${range[1]}${
  1174. range[1] === maxDmgNoCritAfter && range[1] !== 1 ? "*" : ""
  1175. }`;
  1176. critRangeMagicOutput.textContent = `${critRange[0]} ~ ${critRange[1]}${
  1177. critRange[1] === maxDmgCritAfter && critRange[1] !== 1 ? "*" : ""
  1178. }`;
  1179. const combinedRangeTop =
  1180. inputData.critProb > 0 ? critRange[1] : range[1];
  1181. totalRangeMagicOutput.textContent = `${
  1182. range[0] * inputData.skillLines
  1183. } ~ ${combinedRangeTop * inputData.skillLines}${
  1184. combinedRangeTop ===
  1185. (inputData.critProb > 0
  1186. ? maxDmgCritAfter
  1187. : maxDmgNoCritAfter) && combinedRangeTop !== 1
  1188. ? "*"
  1189. : ""
  1190. }`;
  1191. const [expectedPerHitNoCrit, expectedPerHitCrit] = [
  1192. truncClampedExpectation(minDmgNoCritAfter, maxDmgNoCritAfter),
  1193. truncClampedExpectation(minDmgCritAfter, maxDmgCritAfter),
  1194. ];
  1195. const expectedPerHit =
  1196. critQ * expectedPerHitNoCrit +
  1197. inputData.critProb * expectedPerHitCrit;
  1198. const expectedPerHitTotal = expectedPerHit * inputData.skillLines;
  1199. expectedPerHitMagicOutput.textContent = expectedPerHit.toFixed(3);
  1200. expectedPerHitTotalMagicOutput.textContent =
  1201. expectedPerHitTotal.toFixed(3);
  1202. // The "mainVariance" in the following variable names is intended to
  1203. // indicate that this is a variance against the expectation across
  1204. // _all_ cases (`expectedPerHit`), not against the expected value of
  1205. // the particular case in question.
  1206. const mainVariancePerHitNoCrit = truncClampedVariance(
  1207. minDmgNoCritAfter,
  1208. maxDmgNoCritAfter,
  1209. expectedPerHit,
  1210. );
  1211. const mainVariancePerHitCrit = truncClampedVariance(
  1212. minDmgCritAfter,
  1213. maxDmgCritAfter,
  1214. expectedPerHit,
  1215. );
  1216. const variancePerHit =
  1217. mainVariancePerHitNoCrit !== undefined &&
  1218. mainVariancePerHitCrit !== undefined
  1219. ? critQ * mainVariancePerHitNoCrit +
  1220. inputData.critProb * mainVariancePerHitCrit
  1221. : undefined;
  1222. let sdPerHitTotal: number | undefined = undefined;
  1223. if (variancePerHit !== undefined) {
  1224. sdPerHitMagicOutput.classList.remove("error");
  1225. cvPerHitMagicOutput.classList.remove("error");
  1226. sdPerHitTotalMagicOutput.classList.remove("error");
  1227. cvPerHitTotalMagicOutput.classList.remove("error");
  1228. const sdPerHit = Math.sqrt(variancePerHit);
  1229. // This is mathematically valid because the damage/outcome of each
  1230. // hit is independent of the damage of any other hit, thus implying
  1231. // uncorrelatedness. Furthermore, this implies that the variance
  1232. // of the sum of hits is the sum of the variance of said hits (see
  1233. // the Bienaymé formula/identity).
  1234. sdPerHitTotal = Math.sqrt(variancePerHit * inputData.skillLines);
  1235. sdPerHitMagicOutput.textContent = sdPerHit.toFixed(3);
  1236. cvPerHitMagicOutput.textContent = (
  1237. sdPerHit / expectedPerHit
  1238. ).toFixed(5);
  1239. sdPerHitTotalMagicOutput.textContent = sdPerHitTotal.toFixed(3);
  1240. cvPerHitTotalMagicOutput.textContent = (
  1241. sdPerHitTotal / expectedPerHitTotal
  1242. ).toFixed(5);
  1243. } else {
  1244. sdPerHitMagicOutput.classList.add("error");
  1245. cvPerHitMagicOutput.classList.add("error");
  1246. sdPerHitTotalMagicOutput.classList.add("error");
  1247. cvPerHitTotalMagicOutput.classList.add("error");
  1248. sdPerHitMagicOutput.textContent = "[undefined]";
  1249. cvPerHitMagicOutput.textContent = "[undefined]";
  1250. sdPerHitTotalMagicOutput.textContent = "[undefined]";
  1251. cvPerHitTotalMagicOutput.textContent = "[undefined]";
  1252. }
  1253. const period = magicAttackPeriod(
  1254. inputData.spellBooster,
  1255. inputData.spell,
  1256. inputData.speed,
  1257. );
  1258. if (period !== undefined) {
  1259. expectedDpsMagicOutput.classList.remove("error");
  1260. const attackHz = 1000 / period;
  1261. const expectedDps = attackHz * expectedPerHitTotal;
  1262. expectedDpsMagicOutput.textContent = expectedDps.toFixed(3);
  1263. if (sdPerHitTotal !== undefined) {
  1264. sdDpsMagicOutput.classList.remove("error");
  1265. cvDpsMagicOutput.classList.remove("error");
  1266. // This is mathematically valid because the damage/outcome of
  1267. // each hit is independent of the damage of any other hit, thus
  1268. // implying uncorrelatedness. Furthermore, this implies that
  1269. // the variance of the sum of hits is the sum of the variance
  1270. // of said hits.
  1271. const sdDps = Math.sqrt(attackHz) * sdPerHitTotal; /*
  1272. = sqrt(attackHz) * sqrt(variancePerHitTotal)
  1273. = sqrt(attackHz * variancePerHitTotal)
  1274. = sqrt(varianceDps). */
  1275. sdDpsMagicOutput.textContent = sdDps.toFixed(3);
  1276. cvDpsMagicOutput.textContent = (sdDps / expectedDps).toFixed(
  1277. 5,
  1278. );
  1279. } else {
  1280. sdDpsMagicOutput.classList.add("error");
  1281. cvDpsMagicOutput.classList.add("error");
  1282. sdDpsMagicOutput.textContent = "[undefined]";
  1283. cvDpsMagicOutput.textContent = "[undefined]";
  1284. }
  1285. } else {
  1286. expectedDpsMagicOutput.classList.add("error");
  1287. sdDpsMagicOutput.classList.add("error");
  1288. cvDpsMagicOutput.classList.add("error");
  1289. expectedDpsMagicOutput.textContent =
  1290. "[unknown attack speed value]";
  1291. sdDpsMagicOutput.textContent = "[undefined]";
  1292. cvDpsMagicOutput.textContent = "[undefined]";
  1293. }
  1294. }
  1295. function recalculateWarnings(inputData: InputData): void {
  1296. const warnings: string[] = [];
  1297. /*======== Accumulate warnings ========*/
  1298. if (
  1299. inputData.totalWatk === 0 &&
  1300. inputData.wepType !== WeaponType.None
  1301. ) {
  1302. warnings.push(
  1303. "Your total WATK is zero, but you have a weapon equipped.",
  1304. );
  1305. }
  1306. if (inputData.totalMatk < inputData.stats.int) {
  1307. warnings.push("Your total MATK is less than your total INT.");
  1308. }
  1309. switch (inputData.wepType) {
  1310. case WeaponType.OneHandedSword:
  1311. case WeaponType.OneHandedAxe:
  1312. case WeaponType.OneHandedMace:
  1313. case WeaponType.TwoHandedSword:
  1314. case WeaponType.TwoHandedAxe:
  1315. case WeaponType.TwoHandedMace:
  1316. case WeaponType.Spear:
  1317. case WeaponType.Polearm: {
  1318. if (
  1319. inputData.mastery > 0.1 &&
  1320. inputData.clazz !== Class.Warrior
  1321. ) {
  1322. warnings.push(
  1323. `You have >10% mastery with a ${weaponTypeName(
  1324. inputData.wepType,
  1325. )}, but you\u{2019}re not a warrior.`,
  1326. );
  1327. }
  1328. break;
  1329. }
  1330. case WeaponType.Dagger:
  1331. case WeaponType.Claw: {
  1332. if (
  1333. inputData.mastery > 0.1 &&
  1334. inputData.clazz !== Class.Rogue
  1335. ) {
  1336. warnings.push(
  1337. `You have >10% mastery with a ${weaponTypeName(
  1338. inputData.wepType,
  1339. )}, but you\u{2019}re not a rogue.`,
  1340. );
  1341. }
  1342. break;
  1343. }
  1344. case WeaponType.Bow:
  1345. case WeaponType.Crossbow: {
  1346. if (
  1347. inputData.mastery > 0.1 &&
  1348. inputData.clazz !== Class.Archer
  1349. ) {
  1350. warnings.push(
  1351. `You have >10% mastery with a ${weaponTypeName(
  1352. inputData.wepType,
  1353. )}, but you\u{2019}re not an archer.`,
  1354. );
  1355. }
  1356. break;
  1357. }
  1358. case WeaponType.Knuckler:
  1359. case WeaponType.Gun: {
  1360. if (
  1361. inputData.mastery > 0.1 &&
  1362. inputData.clazz !== Class.Pirate2nd
  1363. ) {
  1364. warnings.push(
  1365. `You have >10% mastery with a ${weaponTypeName(
  1366. inputData.wepType,
  1367. )}, but you\u{2019}re not a \u{2265}2\u{207f}\u{1d48} \
  1368. job pirate.`,
  1369. );
  1370. }
  1371. break;
  1372. }
  1373. default:
  1374. break;
  1375. }
  1376. if (
  1377. inputData.mastery > 0.6 &&
  1378. inputData.wepType !== WeaponType.Bow &&
  1379. inputData.wepType !== WeaponType.Crossbow &&
  1380. inputData.wepType !== WeaponType.Polearm &&
  1381. inputData.wepType !== WeaponType.Spear &&
  1382. inputData.wepType !== WeaponType.None
  1383. ) {
  1384. warnings.push(
  1385. `You have >60% mastery with a ${weaponTypeName(
  1386. inputData.wepType,
  1387. )}.`,
  1388. );
  1389. }
  1390. if (
  1391. !(
  1392. inputData.clazz === Class.Rogue &&
  1393. inputData.wepType === WeaponType.Claw
  1394. ) &&
  1395. !(
  1396. inputData.clazz === Class.Archer &&
  1397. (inputData.wepType === WeaponType.Bow ||
  1398. inputData.wepType === WeaponType.Crossbow)
  1399. ) &&
  1400. !(
  1401. inputData.clazz === Class.Pirate2nd &&
  1402. (inputData.wepType === WeaponType.None ||
  1403. inputData.wepType === WeaponType.Knuckler)
  1404. )
  1405. ) {
  1406. if (inputData.critProb > 0.15) {
  1407. warnings.push(
  1408. "You have a >15% probability of critting, but you do not \
  1409. have a class & weapon combo that has access to crits. You \
  1410. can only crit due to Sharp Eyes, which normally grants a \
  1411. 15% crit probably at best.",
  1412. );
  1413. }
  1414. if (inputData.critDmg > 1.4) {
  1415. warnings.push(
  1416. "You have a >140% probability of critical multi, but you \
  1417. do not have a class & weapon combo that has access to \
  1418. crits. You can only crit due to Sharp Eyes, which \
  1419. normally grants a 140% critical multi at best.",
  1420. );
  1421. }
  1422. }
  1423. const badWeps = BAD_WEPS.get(inputData.clazz);
  1424. if (badWeps === undefined) {
  1425. console.error(
  1426. `Logic error: ${inputData.clazz} is not a key in BAD_WEPS`,
  1427. );
  1428. } else if (badWeps.has(inputData.wepType)) {
  1429. switch (inputData.wepType) {
  1430. case WeaponType.None: {
  1431. warnings.push(
  1432. `You\u{2019}re not wielding a weapon, but ${className(
  1433. inputData.clazz,
  1434. )}s normally cannot attack that way.`,
  1435. );
  1436. break;
  1437. }
  1438. case WeaponType.Staff: {
  1439. warnings.push(
  1440. `You\u{2019}re wielding a staff, but staves that are \
  1441. equippable by ${className(
  1442. inputData.clazz,
  1443. )}s don\u{2019}t usually exist.`,
  1444. );
  1445. break;
  1446. }
  1447. case WeaponType.TwoHandedAxe:
  1448. case WeaponType.Bow:
  1449. case WeaponType.Crossbow:
  1450. case WeaponType.Knuckler:
  1451. case WeaponType.Gun: {
  1452. const wepName = weaponTypeName(inputData.wepType);
  1453. warnings.push(
  1454. `You\u{2019}re wielding a ${wepName}, but \
  1455. ${wepName}s that are equippable by ${className(
  1456. inputData.clazz,
  1457. )}s don\u{2019}t usually exist.`,
  1458. );
  1459. break;
  1460. }
  1461. default:
  1462. break;
  1463. }
  1464. }
  1465. switch (inputData.clazz) {
  1466. case Class.Beginner: {
  1467. if (inputData.skillDmgMulti !== 1) {
  1468. warnings.push(
  1469. "Your damage multi \u{2260}100%, but you\u{2019}re a \
  1470. beginner.",
  1471. );
  1472. }
  1473. if (inputData.skillLines !== 1) {
  1474. warnings.push(
  1475. "You\u{2019}re attacking with a number of lines \
  1476. \u{2260}1, but you\u{2019}re a beginner.",
  1477. );
  1478. }
  1479. break;
  1480. }
  1481. case Class.Warrior: {
  1482. if (inputData.stats.str < 35) {
  1483. warnings.push(
  1484. "Your total STR <35, but you\u{2019}re a warrior.",
  1485. );
  1486. }
  1487. if (inputData.skillDmgMulti !== 1) {
  1488. if (inputData.wepType === WeaponType.Claw) {
  1489. warnings.push(
  1490. "Your damage multi \u{2260}100%, but \
  1491. you\u{2019}re a warrior using a claw.",
  1492. );
  1493. }
  1494. }
  1495. break;
  1496. }
  1497. case Class.Magician: {
  1498. if (inputData.stats.int < 20) {
  1499. warnings.push(
  1500. "Your total INT <20, but you\u{2019}re a magician.",
  1501. );
  1502. }
  1503. if (
  1504. inputData.skillDmgMulti !== 1 &&
  1505. inputData.spell !== Spell.Heal
  1506. ) {
  1507. warnings.push(
  1508. "Your damage multi \u{2260}100%, but you\u{2019}re a \
  1509. magician who is not casting Heal.",
  1510. );
  1511. }
  1512. break;
  1513. }
  1514. case Class.Archer: {
  1515. if (inputData.stats.dex < 25) {
  1516. warnings.push(
  1517. "Your total DEX <25, but you\u{2019}re an archer.",
  1518. );
  1519. }
  1520. if (inputData.skillDmgMulti !== 1) {
  1521. if (
  1522. inputData.wepType !== WeaponType.Bow &&
  1523. inputData.wepType !== WeaponType.Crossbow
  1524. ) {
  1525. warnings.push(
  1526. `Your damage multi \u{2260}100%, but \
  1527. you\u{2019}re an archer using a ${weaponTypeName(
  1528. inputData.wepType,
  1529. )}.`,
  1530. );
  1531. }
  1532. }
  1533. break;
  1534. }
  1535. case Class.Rogue: {
  1536. if (inputData.stats.dex < 25) {
  1537. warnings.push(
  1538. "Your total DEX <25, but you\u{2019}re a rogue.",
  1539. );
  1540. }
  1541. break;
  1542. }
  1543. case Class.Pirate:
  1544. case Class.Pirate2nd: {
  1545. if (inputData.stats.dex < 20) {
  1546. warnings.push(
  1547. "Your total DEX <20, but you\u{2019}re a pirate.",
  1548. );
  1549. }
  1550. if (inputData.skillDmgMulti !== 1) {
  1551. if (inputData.wepType === WeaponType.Claw) {
  1552. warnings.push(
  1553. "Your damage multi \u{2260}100%, but \
  1554. you\u{2019}re a pirate using a claw. \
  1555. Claws\u{2019} interaction with Somersault \
  1556. Kick/Aerial Strike is poorly understood.",
  1557. );
  1558. }
  1559. }
  1560. break;
  1561. }
  1562. }
  1563. const attackReqs = ATTACK_REQS.get(inputData.attack);
  1564. if (attackReqs === undefined) {
  1565. console.error(
  1566. `Logic error: ${inputData.attack} is not a key in ATTACK_REQS`,
  1567. );
  1568. } else {
  1569. const [attackReqClasses, attackReqLvl, attackReqWepTypes] =
  1570. attackReqs;
  1571. if (!attackReqClasses.has(inputData.clazz)) {
  1572. warnings.push(
  1573. `You\u{2019}re attacking with ${attackName(
  1574. inputData.attack,
  1575. )}, but you\u{2019}re not ${indefinite(
  1576. Array.from(attackReqClasses).map(className).join("/"),
  1577. )}.`,
  1578. );
  1579. }
  1580. if (inputData.level < attackReqLvl) {
  1581. warnings.push(
  1582. `You\u{2019}re attacking with ${attackName(
  1583. inputData.attack,
  1584. )}, but your level <${attackReqLvl}.`,
  1585. );
  1586. }
  1587. if (!attackReqWepTypes.has(inputData.wepType)) {
  1588. warnings.push(
  1589. `You\u{2019}re attacking with ${attackName(
  1590. inputData.attack,
  1591. )}, but you don\u{2019}t have ${indefinite(
  1592. Array.from(attackReqWepTypes)
  1593. .map(weaponTypeName)
  1594. .join("/"),
  1595. )} equipped.`,
  1596. );
  1597. }
  1598. }
  1599. if (
  1600. inputData.spell !== Spell.Other &&
  1601. inputData.clazz !== Class.Magician
  1602. ) {
  1603. warnings.push(
  1604. "You have a specific spell selected, but you\u{2019}re not a \
  1605. magician.",
  1606. );
  1607. }
  1608. const spellLvlReq = SPELL_LVL_REQS.get(inputData.spell);
  1609. if (spellLvlReq === undefined) {
  1610. console.error(
  1611. `Logic error: ${inputData.spell} is not a key in \
  1612. SPELL_LVL_REQS`,
  1613. );
  1614. } else if (inputData.level < spellLvlReq) {
  1615. warnings.push(
  1616. `You\u{2019}re casting ${spellName(
  1617. inputData.spell,
  1618. )}, but your level <${spellLvlReq}.`,
  1619. );
  1620. }
  1621. if (
  1622. inputData.speed > Speed.Fast4 &&
  1623. inputData.wepType === WeaponType.None
  1624. ) {
  1625. warnings.push(
  1626. "You have no weapon equipped, but your speed >4. Bare fists \
  1627. have speed 4 when unbuffed.",
  1628. );
  1629. }
  1630. if (inputData.spellBooster !== 0) {
  1631. if (isHolySpell(inputData.spell)) {
  1632. warnings.push(
  1633. "Your spell booster value is nonzero, but you\u{2019}re \
  1634. casting a cleric/priest/bishop spell.",
  1635. );
  1636. }
  1637. if (
  1638. inputData.wepType !== WeaponType.Wand &&
  1639. inputData.wepType !== WeaponType.Staff
  1640. ) {
  1641. warnings.push(
  1642. "Your spell booster value is nonzero, but you \
  1643. don\u{2019}t have a wand/staff equipped (Spell Booster \
  1644. doesn\u{2019}t work with swords).",
  1645. );
  1646. }
  1647. if (inputData.clazz !== Class.Magician) {
  1648. warnings.push(
  1649. "Your spell booster value is nonzero, but you\u{2019}re \
  1650. not a magician.",
  1651. );
  1652. }
  1653. if (inputData.spellBooster < -1 && inputData.level < 75) {
  1654. warnings.push(
  1655. "Your spell booster value <\u{2212}1, but your level <75.",
  1656. );
  1657. } else if (inputData.level < 71) {
  1658. warnings.push(
  1659. "Your spell booster value is nonzero, but your level <71.",
  1660. );
  1661. }
  1662. }
  1663. if (inputData.eleAmp !== 1) {
  1664. if (inputData.clazz !== Class.Magician) {
  1665. warnings.push(
  1666. "Your element amplification >100%, but you\u{2019}re not \
  1667. a magician.",
  1668. );
  1669. }
  1670. if (inputData.eleAmp > 1.5) {
  1671. warnings.push(
  1672. "Your element amplification >150%, but Element \
  1673. Amplification usually goes up to 150% at best.",
  1674. );
  1675. }
  1676. if (isHolySpell(inputData.spell)) {
  1677. warnings.push(
  1678. "Your element amplification >100%, but you\u{2019}re \
  1679. casting a cleric/priest/bishop spell.",
  1680. );
  1681. }
  1682. if (inputData.level < 70) {
  1683. warnings.push(
  1684. "Your element amplification >100%, but your level <70.",
  1685. );
  1686. }
  1687. }
  1688. const jobLvlReq = JOB_LVL_REQS.get(inputData.clazz);
  1689. if (jobLvlReq === undefined) {
  1690. console.error(
  1691. `Logic error: ${inputData.clazz} is not a key in JOB_LVL_REQS`,
  1692. );
  1693. } else if (inputData.level < jobLvlReq) {
  1694. warnings.push(
  1695. `You\u{2019}re ${indefinite(
  1696. className(inputData.clazz),
  1697. )}, but your level <${jobLvlReq}.`,
  1698. );
  1699. }
  1700. const attackLines = ATTACK_LINES.get(inputData.attack);
  1701. if (attackLines === undefined) {
  1702. console.error(
  1703. `Logic error: ${inputData.attack} is not a key in \
  1704. ATTACK_LINES`,
  1705. );
  1706. } else {
  1707. const [minLines, maxLines, maxTargets] = attackLines;
  1708. if (inputData.skillLines < minLines) {
  1709. warnings.push(
  1710. `You\u{2019}re attacking with ${attackName(
  1711. inputData.attack,
  1712. )}, but its number of lines <${minLines}.`,
  1713. );
  1714. }
  1715. if (inputData.skillLines > maxLines) {
  1716. warnings.push(
  1717. `You\u{2019}re attacking with ${attackName(
  1718. inputData.attack,
  1719. )}, but its number of lines >${maxLines}.`,
  1720. );
  1721. }
  1722. if (inputData.enemyCount > maxTargets) {
  1723. warnings.push(
  1724. `You\u{2019}re attacking with ${attackName(
  1725. inputData.attack,
  1726. )}, but your number of targets >${maxTargets}.`,
  1727. );
  1728. }
  1729. }
  1730. const spellLines = SPELL_LINES.get(inputData.spell);
  1731. if (spellLines === undefined) {
  1732. console.error(
  1733. `Logic error: ${inputData.spell} is not a key in SPELL_LINES`,
  1734. );
  1735. } else {
  1736. const [minLines, maxLines, maxTargets] = spellLines;
  1737. if (inputData.skillLines < minLines) {
  1738. warnings.push(
  1739. `You\u{2019}re casting ${spellName(
  1740. inputData.spell,
  1741. )}, but its number of lines <${minLines}.`,
  1742. );
  1743. }
  1744. if (inputData.skillLines > maxLines) {
  1745. warnings.push(
  1746. `You\u{2019}re casting ${spellName(
  1747. inputData.spell,
  1748. )}, but its number of lines >${maxLines}.`,
  1749. );
  1750. }
  1751. if (inputData.enemyCount > maxTargets) {
  1752. warnings.push(
  1753. `You\u{2019}re casting ${spellName(
  1754. inputData.spell,
  1755. )}, but your number of targets >${maxTargets}.`,
  1756. );
  1757. }
  1758. }
  1759. if (
  1760. inputData.attack === Attack.SomersaultKick ||
  1761. inputData.attack === Attack.AerialStrike
  1762. ) {
  1763. if (
  1764. inputData.attack === Attack.SomersaultKick &&
  1765. inputData.wepType !== WeaponType.None &&
  1766. inputData.wepType !== WeaponType.Knuckler
  1767. ) {
  1768. warnings.push(
  1769. "You\u{2019}re attacking with Somersault Kick, and have a \
  1770. weapon equipped that isn\u{2019}t a knuckler; the attack \
  1771. period is given on a best-effort basis that may or may \
  1772. not be accurate.",
  1773. );
  1774. }
  1775. switch (inputData.wepType) {
  1776. case WeaponType.Bow:
  1777. case WeaponType.Crossbow:
  1778. case WeaponType.Claw:
  1779. case WeaponType.Gun:
  1780. warnings.push(
  1781. `You\u{2019}re using ${attackName(
  1782. inputData.attack,
  1783. )} with ${indefinite(
  1784. weaponTypeName(inputData.wepType),
  1785. )} equipped; the damage calculation is done on a \
  1786. best-effort basis that may or may not be accurate.`,
  1787. );
  1788. break;
  1789. default:
  1790. break;
  1791. }
  1792. }
  1793. if (inputData.attack === Attack.BowWhack) {
  1794. warnings.push(
  1795. "You\u{2019}re whacking with a (cross)bow; the damage \
  1796. calculation is done on a best-effort basis that may or may \
  1797. not be accurate.",
  1798. );
  1799. if (inputData.skillDmgMulti !== 1) {
  1800. warnings.push(
  1801. "You\u{2019}re whacking with a (cross)bow, but your \
  1802. damage multi \u{2260}100%. Maybe you meant to use Power \
  1803. Knock-Back?",
  1804. );
  1805. }
  1806. }
  1807. if (
  1808. inputData.attack === Attack.ClawPunch &&
  1809. inputData.skillDmgMulti !== 1
  1810. ) {
  1811. warnings.push(
  1812. "You\u{2019}re punching with a claw, but your damage multi \
  1813. \u{2260}100%.",
  1814. );
  1815. }
  1816. if (inputData.attack === Attack.Gaviota) {
  1817. warnings.push(
  1818. "Gaviota\u{2019}s attack period is based on an idealization; \
  1819. actual usage will almost certainly have a larger attack \
  1820. period and thus lower DPS.",
  1821. );
  1822. }
  1823. if (inputData.attack === Attack.WrathOfTheOctopi) {
  1824. warnings.push(
  1825. "The attack period for Wrath of the Octopi is unmeasured, so \
  1826. the slower attack period of Octopus (the skill which it \
  1827. upgrades) is used instead.",
  1828. );
  1829. }
  1830. if (inputData.caActive) {
  1831. if (inputData.clazz !== Class.Warrior) {
  1832. warnings.push(
  1833. "You have (Advanced) Combo Attack active, but \
  1834. you\u{2019}re not a warrior.",
  1835. );
  1836. }
  1837. if (inputData.level < 70) {
  1838. warnings.push(
  1839. "You have (Advanced) Combo Attack active, but your level \
  1840. <70.",
  1841. );
  1842. }
  1843. if (1 + (inputData.level - 70) * 3 < inputData.caLevel) {
  1844. warnings.push(
  1845. `You have ${inputData.caLevel} SP in the Combo Attack \
  1846. skill, but you\u{2019}re not high enough level to have \
  1847. that many third job SP.`,
  1848. );
  1849. }
  1850. if (inputData.caOrbs > 5 && inputData.level < 120) {
  1851. warnings.push(
  1852. "You have >5 (Advanced) Combo Attack orbs, but your level \
  1853. <120.",
  1854. );
  1855. }
  1856. } else {
  1857. if (
  1858. inputData.attack === Attack.Panic ||
  1859. inputData.attack === Attack.Coma
  1860. ) {
  1861. warnings.push(
  1862. `You\u{2019}re attacking with ${attackName(
  1863. inputData.attack,
  1864. )}, but (Advanced) Combo Attack is inactive.`,
  1865. );
  1866. }
  1867. }
  1868. if (inputData.zerkActive) {
  1869. if (inputData.clazz !== Class.Warrior) {
  1870. warnings.push(
  1871. "You have Berserk active, but you\u{2019}re not a \
  1872. warrior.",
  1873. );
  1874. }
  1875. if (inputData.level < 120) {
  1876. warnings.push(
  1877. "You have Berserk active, but you\u{2019}re not high \
  1878. enough level to have access to that skill.",
  1879. );
  1880. }
  1881. }
  1882. if (inputData.hitOrd > inputData.enemyCount) {
  1883. warnings.push(
  1884. "The ordinal # of your hit is greater than the total number \
  1885. of enemies being targeted.",
  1886. );
  1887. }
  1888. function delayWarn(jobName: string) {
  1889. const atkName = attackName(inputData.attack);
  1890. warnings.push(
  1891. `You\u{2019}re attacking with ${atkName}; the attack period \
  1892. (and thus DPS value) is based on the spamming of solely \
  1893. ${atkName}. The projected DPS will thus be less than that of \
  1894. a hypothetical ${jobName}, who would be attacking in between \
  1895. ${atkName}s.`,
  1896. );
  1897. }
  1898. switch (inputData.attack) {
  1899. case Attack.NinjaStorm:
  1900. delayWarn("nightlord");
  1901. break;
  1902. case Attack.BoomerangStep:
  1903. delayWarn("shadower");
  1904. break;
  1905. case Attack.BackspinBlow:
  1906. case Attack.DoubleUppercut:
  1907. delayWarn("brawler");
  1908. break;
  1909. case Attack.EnergyBlast:
  1910. delayWarn("marauder");
  1911. break;
  1912. case Attack.DragonStrike:
  1913. case Attack.Snatch:
  1914. case Attack.Barrage:
  1915. delayWarn("buccaneer");
  1916. break;
  1917. case Attack.RecoilShot:
  1918. delayWarn("gunslinger");
  1919. break;
  1920. case Attack.Flamethrower:
  1921. case Attack.IceSplitter:
  1922. delayWarn("outlaw");
  1923. break;
  1924. case Attack.AerialStrike:
  1925. delayWarn("corsair");
  1926. break;
  1927. }
  1928. if (
  1929. inputData.attack === Attack.Flamethrower ||
  1930. inputData.attack === Attack.Inferno
  1931. ) {
  1932. warnings.push(
  1933. `The damage calculation used here for ${inputData.attack} \
  1934. does not take into account the flaming/burning damage over \
  1935. time.`,
  1936. );
  1937. }
  1938. if (inputData.echo !== 0 && inputData.echo !== 4 / 100) {
  1939. warnings.push(
  1940. "You have specified a nonzero value for Echo of Hero that is \
  1941. not exactly 4%.",
  1942. );
  1943. }
  1944. switch (inputData.eleChargeType) {
  1945. case ChargeType.None: {
  1946. if (inputData.eleChargeDmg !== 1) {
  1947. warnings.push(
  1948. "You have no elemental charge, but your elemental \
  1949. charge damage \u{2260}100%.",
  1950. );
  1951. }
  1952. break;
  1953. }
  1954. case ChargeType.Holy: {
  1955. if (
  1956. 1 + (inputData.level - 120) * 3 <
  1957. inputData.eleChargeLevel
  1958. ) {
  1959. warnings.push(
  1960. `You have level ${inputData.eleChargeLevel} \
  1961. Holy/Divine Charge selected, but you aren\u{2019}t a \
  1962. high enough level to have that much fourth job SP.`,
  1963. );
  1964. }
  1965. if (inputData.eleChargeLevel > 20) {
  1966. warnings.push("You have a Holy/Divine Charge level >20.");
  1967. }
  1968. break;
  1969. }
  1970. case ChargeType.Other: {
  1971. if (
  1972. 1 + (inputData.level - 70) * 3 <
  1973. inputData.eleChargeLevel
  1974. ) {
  1975. warnings.push(
  1976. `You have a level ${inputData.eleChargeLevel} \
  1977. non-Holy/Divine Charge selected, but you \
  1978. aren\u{2019}t a high enough level to have that much \
  1979. third job SP.`,
  1980. );
  1981. }
  1982. break;
  1983. }
  1984. }
  1985. if (inputData.eleChargeType !== ChargeType.None) {
  1986. if (inputData.clazz !== Class.Warrior) {
  1987. warnings.push(
  1988. "You have an elemental charge selected, but you\u{2019}re \
  1989. not a warrior.",
  1990. );
  1991. }
  1992. switch (inputData.wepType) {
  1993. case WeaponType.OneHandedSword:
  1994. case WeaponType.OneHandedMace:
  1995. case WeaponType.TwoHandedSword:
  1996. case WeaponType.TwoHandedMace:
  1997. break;
  1998. default:
  1999. warnings.push(
  2000. "You have an elemental charge selected, but \
  2001. you\u{2019}re not using a sword nor a blunt weapon.",
  2002. );
  2003. break;
  2004. }
  2005. }
  2006. if (inputData.eleBoost !== 0) {
  2007. if (inputData.clazz !== Class.Pirate2nd) {
  2008. warnings.push(
  2009. "Your Elemental Boost \u{2260}0%, but you\u{2019}re not a \
  2010. \u{2265}2\u{207f}\u{1d48} job pirate.",
  2011. );
  2012. }
  2013. if (inputData.level < 120) {
  2014. warnings.push(
  2015. "Your Elemental Boost \u{2260}0%, but your level <120.",
  2016. );
  2017. }
  2018. }
  2019. if (inputData.eleWep !== 0) {
  2020. if (
  2021. inputData.wepType !== WeaponType.Wand &&
  2022. inputData.wepType !== WeaponType.Staff
  2023. ) {
  2024. warnings.push(
  2025. "You are getting a nonzero elemental bonus from your \
  2026. weapon, but you aren\u{2019}t wielding a wand nor a \
  2027. staff.",
  2028. );
  2029. } else if (inputData.clazz !== Class.Magician) {
  2030. warnings.push(
  2031. `You\u{2019}re using an Elemental ${
  2032. inputData.wepType === WeaponType.Wand
  2033. ? "Wand"
  2034. : "Staff"
  2035. }, but you\u{2019}re not a magician.`,
  2036. );
  2037. }
  2038. if (inputData.eleWep !== 25 / 100) {
  2039. warnings.push(
  2040. "Your Elemental Wand/Staff is giving an elemental bonus \
  2041. \u{2260}25%.",
  2042. );
  2043. }
  2044. }
  2045. switch (inputData.wepType) {
  2046. case WeaponType.OneHandedAxe:
  2047. case WeaponType.TwoHandedAxe:
  2048. case WeaponType.OneHandedMace:
  2049. case WeaponType.TwoHandedMace:
  2050. case WeaponType.Polearm: {
  2051. if (inputData.goodAnimProb !== 60 / 100) {
  2052. warnings.push(
  2053. `You\u{2019}re using ${indefinite(
  2054. weaponTypeName(inputData.wepType),
  2055. )}, but your good animation probability \u{2260}60%. \
  2056. It is currently suspected that the actual probability \
  2057. is 60% in pre-BB GMS.`,
  2058. );
  2059. }
  2060. break;
  2061. }
  2062. case WeaponType.Spear: {
  2063. if (inputData.goodAnimProb !== 40 / 100) {
  2064. warnings.push(
  2065. "You\u{2019}re using a spear, but your good \
  2066. animation probability \u{2260}40%. It is currently \
  2067. suspected that the actual probability is 40% in \
  2068. pre-BB GMS.",
  2069. );
  2070. }
  2071. break;
  2072. }
  2073. case WeaponType.Wand:
  2074. case WeaponType.Staff: {
  2075. if (inputData.goodAnimProb !== 1) {
  2076. warnings.push(
  2077. `You\u{2019}re using ${indefinite(
  2078. weaponTypeName(inputData.wepType),
  2079. )}, but your good animation probability \u{2260}100%.
  2080. It is currently suspected that the actual probability
  2081. is 100% in pre-BB GMS.`,
  2082. );
  2083. }
  2084. break;
  2085. }
  2086. default:
  2087. break;
  2088. }
  2089. /*======== Remove old warnings display ========*/
  2090. {
  2091. const warningsElem = document.getElementById("warnings");
  2092. if (warningsElem) {
  2093. warningsDiv.removeChild(warningsElem);
  2094. }
  2095. }
  2096. /*======== Display warnings ========*/
  2097. if (warnings.length === 0) {
  2098. const warningsSpan = document.createElement("span");
  2099. warningsSpan.id = "warnings";
  2100. warningsSpan.classList.add("success");
  2101. const warningsTextNode = document.createTextNode("No warnings.");
  2102. warningsSpan.appendChild(warningsTextNode);
  2103. warningsDiv.appendChild(warningsSpan);
  2104. } else {
  2105. const warningsUl = document.createElement("ul");
  2106. warningsUl.id = "warnings";
  2107. warningsUl.classList.add("warning");
  2108. for (const warningText of warnings) {
  2109. const warningLi = document.createElement("li");
  2110. const warningTextNode = document.createTextNode(warningText);
  2111. warningLi.appendChild(warningTextNode);
  2112. warningsUl.appendChild(warningLi);
  2113. }
  2114. warningsDiv.appendChild(warningsUl);
  2115. }
  2116. }
  2117. saveAsButton.addEventListener("click", () => {
  2118. saveLoadDialog.replaceChildren();
  2119. const saveAsInputLabel = document.createElement("label");
  2120. saveAsInputLabel.htmlFor = "save-as-input";
  2121. saveAsInputLabel.append(document.createTextNode("New name: "));
  2122. const saveAsInput = document.createElement("input");
  2123. saveAsInput.id = "save-as-input";
  2124. saveAsInput.name = "save-as-input";
  2125. saveAsInput.type = "text";
  2126. saveAsInputLabel.append(saveAsInput);
  2127. saveLoadDialog.append(saveAsInputLabel);
  2128. const saveAsSaveButton = document.createElement("button");
  2129. saveAsSaveButton.type = "button";
  2130. saveAsSaveButton.append(document.createTextNode("Save"));
  2131. saveLoadDialog.append(saveAsSaveButton);
  2132. saveLoadDialog.append(document.createTextNode(" "));
  2133. const saveAsCancelButton = document.createElement("button");
  2134. saveAsCancelButton.type = "button";
  2135. saveAsCancelButton.append(document.createTextNode("Cancel"));
  2136. saveLoadDialog.append(saveAsCancelButton);
  2137. saveAsSaveButton.addEventListener("click", () => {
  2138. if (!saveAsInput.value.trim()) {
  2139. return;
  2140. }
  2141. const keyName = `dmg-calc.saved.${saveAsInput.value.trim()}`;
  2142. if (
  2143. window.localStorage.getItem(keyName) &&
  2144. !window.confirm(
  2145. "An entry is already saved with this name. Are you sure that you want to overwrite it?",
  2146. )
  2147. ) {
  2148. return;
  2149. }
  2150. window.localStorage.setItem(
  2151. keyName,
  2152. JSON.stringify(readInputData()),
  2153. );
  2154. currentlyLoaded.replaceChildren();
  2155. currentlyLoaded.append(
  2156. document.createTextNode(saveAsInput.value.trim()),
  2157. );
  2158. saveLoadDialog.replaceChildren();
  2159. });
  2160. saveAsCancelButton.addEventListener("click", () => {
  2161. saveLoadDialog.replaceChildren();
  2162. });
  2163. saveAsInput.focus();
  2164. });
  2165. saveButton.addEventListener("click", () => {
  2166. const cln = currentlyLoadedName();
  2167. if (cln) {
  2168. window.localStorage.setItem(
  2169. `dmg-calc.saved.${cln}`,
  2170. JSON.stringify(readInputData()),
  2171. );
  2172. } else {
  2173. window.alert(
  2174. "Nothing was saved. Try using \u{201c}Save as\u{201d}, instead.",
  2175. );
  2176. }
  2177. });
  2178. loadButton.addEventListener("click", () => {
  2179. saveLoadDialog.replaceChildren();
  2180. const loadSelectLabel = document.createElement("label");
  2181. loadSelectLabel.htmlFor = "load-select";
  2182. loadSelectLabel.append(document.createTextNode("Name: "));
  2183. const loadSelect = document.createElement("select");
  2184. loadSelect.id = "load-select";
  2185. loadSelect.name = "load-select";
  2186. for (let i = 0; i < window.localStorage.length; ++i) {
  2187. const key = window.localStorage.key(i);
  2188. if (key === null || !key.startsWith("dmg-calc.saved.")) {
  2189. continue;
  2190. }
  2191. const name = key.slice("dmg-calc.saved.".length);
  2192. const opt = document.createElement("option");
  2193. opt.value = key;
  2194. opt.append(document.createTextNode(name));
  2195. loadSelect.append(opt);
  2196. }
  2197. loadSelectLabel.append(loadSelect);
  2198. saveLoadDialog.append(loadSelectLabel);
  2199. const loadLoadButton = document.createElement("button");
  2200. loadLoadButton.type = "button";
  2201. loadLoadButton.append(document.createTextNode("Load"));
  2202. saveLoadDialog.append(loadLoadButton);
  2203. saveLoadDialog.append(document.createTextNode(" "));
  2204. const loadCancelButton = document.createElement("button");
  2205. loadCancelButton.type = "button";
  2206. loadCancelButton.append(document.createTextNode("Cancel"));
  2207. saveLoadDialog.append(loadCancelButton);
  2208. loadLoadButton.addEventListener("click", () => {
  2209. const val = window.localStorage.getItem(loadSelect.value);
  2210. if (
  2211. !loadSelect.value ||
  2212. !val ||
  2213. !window.confirm(
  2214. "Loading will overwrite the current working state, causing you to lose any unsaved changes. Are you sure that you still want to load?",
  2215. )
  2216. ) {
  2217. return;
  2218. }
  2219. let inputData: InputData = JSON.parse(val);
  2220. writeInputData(inputData);
  2221. currentlyLoaded.replaceChildren();
  2222. currentlyLoaded.append(
  2223. document.createTextNode(
  2224. loadSelect.value.slice("dmg-calc.saved.".length),
  2225. ),
  2226. );
  2227. saveLoadDialog.replaceChildren();
  2228. recalculate();
  2229. });
  2230. loadCancelButton.addEventListener("click", () => {
  2231. saveLoadDialog.replaceChildren();
  2232. });
  2233. loadSelect.focus();
  2234. });
  2235. deleteButton.addEventListener("click", () => {
  2236. saveLoadDialog.replaceChildren();
  2237. const deleteSelectLabel = document.createElement("label");
  2238. deleteSelectLabel.htmlFor = "delete-select";
  2239. deleteSelectLabel.append(document.createTextNode("Name: "));
  2240. const deleteSelect = document.createElement("select");
  2241. deleteSelect.id = "delete-select";
  2242. deleteSelect.name = "delete-select";
  2243. for (let i = 0; i < window.localStorage.length; ++i) {
  2244. const key = window.localStorage.key(i);
  2245. if (key === null || !key.startsWith("dmg-calc.saved.")) {
  2246. continue;
  2247. }
  2248. const name = key.slice("dmg-calc.saved.".length);
  2249. const opt = document.createElement("option");
  2250. opt.value = key;
  2251. opt.append(document.createTextNode(name));
  2252. deleteSelect.append(opt);
  2253. }
  2254. deleteSelectLabel.append(deleteSelect);
  2255. saveLoadDialog.append(deleteSelectLabel);
  2256. const deleteDeleteButton = document.createElement("button");
  2257. deleteDeleteButton.type = "button";
  2258. deleteDeleteButton.append(document.createTextNode("Delete"));
  2259. saveLoadDialog.append(deleteDeleteButton);
  2260. saveLoadDialog.append(document.createTextNode(" "));
  2261. const deleteCancelButton = document.createElement("button");
  2262. deleteCancelButton.type = "button";
  2263. deleteCancelButton.append(document.createTextNode("Cancel"));
  2264. saveLoadDialog.append(deleteCancelButton);
  2265. deleteDeleteButton.addEventListener("click", () => {
  2266. const val = window.localStorage.getItem(deleteSelect.value);
  2267. const name = deleteSelect.value.slice("dmg-calc.saved.".length);
  2268. if (
  2269. !deleteSelect.value ||
  2270. !val ||
  2271. !window.confirm(
  2272. `Are you sure that you want to delete \u{201c}${name}\u{201d} permanently?`,
  2273. )
  2274. ) {
  2275. return;
  2276. }
  2277. window.localStorage.removeItem(deleteSelect.value);
  2278. const cln = currentlyLoadedName();
  2279. if (cln === name) {
  2280. currentlyLoaded.replaceChildren();
  2281. }
  2282. saveLoadDialog.replaceChildren();
  2283. });
  2284. deleteCancelButton.addEventListener("click", () => {
  2285. saveLoadDialog.replaceChildren();
  2286. });
  2287. deleteSelect.focus();
  2288. });
  2289. exportButton.addEventListener("click", () => {
  2290. const anchor = document.createElement("a");
  2291. anchor.href = URL.createObjectURL(
  2292. new Blob([JSON.stringify(readInputData()) + "\n"], {
  2293. type: "application/json",
  2294. }),
  2295. );
  2296. const cln = currentlyLoadedName();
  2297. anchor.download = cln
  2298. ? cln.replaceAll(/\s/gi, "_").replaceAll(/[^a-z0-9_\.\-]/gi, "-") +
  2299. ".json"
  2300. : "dmg-calc-input.json";
  2301. anchor.click();
  2302. });
  2303. importButton.addEventListener("click", () => {
  2304. saveLoadDialog.replaceChildren();
  2305. const importInputLabel = document.createElement("label");
  2306. importInputLabel.htmlFor = "import-input";
  2307. importInputLabel.append(
  2308. document.createTextNode("Choose a JSON file: "),
  2309. );
  2310. const importInput = document.createElement("input");
  2311. importInput.type = "file";
  2312. importInput.id = "import-input";
  2313. importInput.name = "import-input";
  2314. importInput.accept = ".json,application/json,text/plain,text/json";
  2315. importInputLabel.append(importInput);
  2316. saveLoadDialog.append(importInputLabel);
  2317. const importImportButton = document.createElement("button");
  2318. importImportButton.type = "button";
  2319. importImportButton.append(document.createTextNode("Import"));
  2320. saveLoadDialog.append(importImportButton);
  2321. saveLoadDialog.append(document.createTextNode(" "));
  2322. const importCancelButton = document.createElement("button");
  2323. importCancelButton.type = "button";
  2324. importCancelButton.append(document.createTextNode("Cancel"));
  2325. saveLoadDialog.append(importCancelButton);
  2326. importImportButton.addEventListener("click", () => {
  2327. const files = importInput.files;
  2328. if (files === null) {
  2329. return;
  2330. }
  2331. const file = files[0];
  2332. if (
  2333. !file ||
  2334. !window.confirm(
  2335. "Importing will overwrite the current working state, causing you to lose any unsaved changes. Are you sure that you still want to import?",
  2336. )
  2337. ) {
  2338. return;
  2339. }
  2340. file.text().then(s => {
  2341. let inputData: InputData = JSON.parse(s);
  2342. writeInputData(inputData);
  2343. currentlyLoaded.replaceChildren();
  2344. });
  2345. saveLoadDialog.replaceChildren();
  2346. recalculate();
  2347. });
  2348. importCancelButton.addEventListener("click", () => {
  2349. saveLoadDialog.replaceChildren();
  2350. });
  2351. importInput.focus();
  2352. });
  2353. for (const input of [
  2354. strInput,
  2355. dexInput,
  2356. intInput,
  2357. lukInput,
  2358. totalWatkInput,
  2359. totalMatkInput,
  2360. echoInput,
  2361. masteryInput,
  2362. skillDmgMultiInput,
  2363. skillBasicAtkInput,
  2364. skillLinesInput,
  2365. critProbInput,
  2366. critDmgInput,
  2367. classInput,
  2368. levelInput,
  2369. weaponTypeInput,
  2370. goodAnimProbInput,
  2371. attackInput,
  2372. spellInput,
  2373. speedInput,
  2374. spellBoosterInput,
  2375. eleAmpInput,
  2376. eleBoostInput,
  2377. eleWepInput,
  2378. eleChargeDmgInput,
  2379. eleChargeLevelInput,
  2380. caActiveInput,
  2381. caDmgInput,
  2382. caLevelInput,
  2383. caOrbsInput,
  2384. zerkActiveInput,
  2385. zerkDmgInput,
  2386. enemyWdefInput,
  2387. enemyMdefInput,
  2388. eleSusInput,
  2389. enemyLevelInput,
  2390. enemyCountInput,
  2391. hitOrdInput,
  2392. ]) {
  2393. input.addEventListener("change", recalculate);
  2394. }
  2395. for (const eleChargeInput of eleChargeInputs) {
  2396. eleChargeInput.addEventListener("change", () =>
  2397. eleChargeInput.checked ? recalculate() : undefined,
  2398. );
  2399. }
  2400. recalculate();
  2401. }
  2402. function dmgMulti(inputData: InputData, crit: boolean): number {
  2403. return (
  2404. inputData.skillDmgMulti +
  2405. (crit ? inputData.critDmg : 0) +
  2406. (inputData.attack === Attack.Flamethrower ||
  2407. inputData.attack === Attack.IceSplitter
  2408. ? inputData.eleBoost
  2409. : 0)
  2410. );
  2411. }
  2412. function attackEffectiveEleSus(inputData: InputData): number {
  2413. if (!attackIsElemental(inputData.attack)) {
  2414. return 1;
  2415. }
  2416. return inputData.eleSus;
  2417. }
  2418. function maxDmgPhys(inputData: InputData, goodAnim: boolean): number {
  2419. return (
  2420. ((primaryStat(
  2421. inputData.stats,
  2422. inputData.wepType,
  2423. goodAnim,
  2424. inputData.clazz,
  2425. ) +
  2426. secondaryStat(
  2427. inputData.stats,
  2428. inputData.wepType,
  2429. inputData.clazz,
  2430. )) *
  2431. effectiveWatk(inputData)) /
  2432. 100
  2433. );
  2434. }
  2435. function minDmgPhys(inputData: InputData, goodAnim: boolean): number {
  2436. return (
  2437. ((primaryStat(
  2438. inputData.stats,
  2439. inputData.wepType,
  2440. goodAnim,
  2441. inputData.clazz,
  2442. ) *
  2443. 0.9 *
  2444. effectiveMastery(inputData) +
  2445. secondaryStat(
  2446. inputData.stats,
  2447. inputData.wepType,
  2448. inputData.clazz,
  2449. )) *
  2450. effectiveWatk(inputData)) /
  2451. 100
  2452. );
  2453. }
  2454. function afterModPhys(inputData: InputData): number {
  2455. const hitOrdAfterMod = (() => {
  2456. switch (inputData.attack) {
  2457. case Attack.IronArrow:
  2458. return 0.9 ** (inputData.hitOrd - 1);
  2459. case Attack.PiercingArrow:
  2460. return 1.2 ** (inputData.hitOrd - 1);
  2461. case Attack.EnergyOrb:
  2462. return (2 / 3) ** (inputData.hitOrd - 1);
  2463. default:
  2464. return 1;
  2465. }
  2466. })();
  2467. return inputData.zerkActive
  2468. ? hitOrdAfterMod * inputData.zerkDmg
  2469. : hitOrdAfterMod;
  2470. }
  2471. function afterModBarrage(ord: number): number {
  2472. return 2 ** Math.max(ord - 4, 0);
  2473. }
  2474. function barrageEffectiveMulti(lines: number): number {
  2475. return Math.min(lines + 1, 4) + 2 ** Math.max(lines - 3, 0) - 2;
  2476. }
  2477. function caModifier(inputData: InputData): number {
  2478. if (!inputData.caActive) {
  2479. return 1;
  2480. }
  2481. if (inputData.caOrbs < 6) {
  2482. return (
  2483. (inputData.caDmg +
  2484. Math.floor((inputData.caOrbs - 1) * (inputData.caLevel / 6))) /
  2485. 100
  2486. );
  2487. }
  2488. return (inputData.caDmg + 20 + (inputData.caOrbs - 5) * 4) / 100;
  2489. }
  2490. function eleChargeModifier(inputData: InputData): number {
  2491. switch (inputData.eleChargeType) {
  2492. case ChargeType.None:
  2493. return 1;
  2494. case ChargeType.Holy: {
  2495. if (inputData.eleSus === 1) {
  2496. return inputData.eleChargeDmg;
  2497. } else if (inputData.eleSus === 0) {
  2498. return 0;
  2499. } else if (inputData.eleSus < 1) {
  2500. return (
  2501. (inputData.eleChargeDmg *
  2502. (80 - inputData.eleChargeLevel * 1.5)) /
  2503. 100
  2504. );
  2505. }
  2506. return (
  2507. (inputData.eleChargeDmg *
  2508. (120 + inputData.eleChargeLevel * 1.5)) /
  2509. 100
  2510. );
  2511. }
  2512. case ChargeType.Other: {
  2513. if (inputData.eleSus === 1) {
  2514. return inputData.eleChargeDmg;
  2515. } else if (inputData.eleSus === 0) {
  2516. return 0;
  2517. } else if (inputData.eleSus < 1) {
  2518. return (
  2519. (inputData.eleChargeDmg *
  2520. (95 - inputData.eleChargeLevel * 1.5)) /
  2521. 100
  2522. );
  2523. }
  2524. return (
  2525. (inputData.eleChargeDmg *
  2526. (105 + inputData.eleChargeLevel * 1.5)) /
  2527. 100
  2528. );
  2529. }
  2530. }
  2531. }
  2532. function maxDmgBowWhack(inputData: InputData): number {
  2533. return (
  2534. ((inputData.stats.dex * 3.4 + inputData.stats.str) *
  2535. effectiveWatk(inputData)) /
  2536. 150
  2537. );
  2538. }
  2539. function minDmgBowWhack(inputData: InputData): number {
  2540. return (
  2541. ((inputData.stats.dex * 3.4 * 0.1 * 0.9 + inputData.stats.str) *
  2542. effectiveWatk(inputData)) /
  2543. 150
  2544. );
  2545. }
  2546. function maxDmgClawPunch(inputData: InputData): number {
  2547. return (
  2548. ((inputData.stats.luk + inputData.stats.str + inputData.stats.dex) *
  2549. effectiveWatk(inputData)) /
  2550. 150
  2551. );
  2552. }
  2553. function minDmgClawPunch(inputData: InputData): number {
  2554. return (
  2555. ((inputData.stats.luk * 0.1 +
  2556. inputData.stats.str +
  2557. inputData.stats.dex) *
  2558. effectiveWatk(inputData)) /
  2559. 150
  2560. );
  2561. }
  2562. function orbMulti(inputData: InputData): number {
  2563. switch (inputData.caOrbs) {
  2564. case 1:
  2565. return 1;
  2566. case 2:
  2567. return 1.2;
  2568. case 3:
  2569. return 1.54;
  2570. case 4:
  2571. return 2;
  2572. default:
  2573. return 2.5;
  2574. }
  2575. }
  2576. function maxDmgCaFinisher(inputData: InputData, goodAnim: boolean): number {
  2577. return maxDmgPhys(inputData, goodAnim) * orbMulti(inputData);
  2578. }
  2579. function minDmgCaFinisher(inputData: InputData, goodAnim: boolean): number {
  2580. return minDmgPhys(inputData, goodAnim) * orbMulti(inputData);
  2581. }
  2582. function minDmgHhXiuz(inputData: InputData): number {
  2583. return maxDmgPhys(inputData, true) * 0.8;
  2584. }
  2585. function maxDmgDragonRoar(inputData: InputData): number {
  2586. return (
  2587. ((inputData.stats.str * 4 + inputData.stats.dex) *
  2588. effectiveWatk(inputData)) /
  2589. 100
  2590. );
  2591. }
  2592. function minDmgDragonRoar(inputData: InputData): number {
  2593. return (
  2594. ((inputData.stats.str * 4 * inputData.mastery * 0.9 +
  2595. inputData.stats.dex) *
  2596. effectiveWatk(inputData)) /
  2597. 100
  2598. );
  2599. }
  2600. function maxDmgArrowBombImpact(inputData: InputData, crit: boolean): number {
  2601. return 0.5 * maxDmgPhys(inputData, true) * (crit ? inputData.critDmg : 1);
  2602. }
  2603. function minDmgArrowBombImpact(inputData: InputData, crit: boolean): number {
  2604. return 0.5 * minDmgPhys(inputData, true) * (crit ? inputData.critDmg : 1);
  2605. }
  2606. function maxDmgArrowBombSplash(inputData: InputData, crit: boolean): number {
  2607. return (
  2608. inputData.skillDmgMulti *
  2609. maxDmgPhys(inputData, true) *
  2610. (crit ? inputData.critDmg : 1)
  2611. );
  2612. }
  2613. function minDmgArrowBombSplash(inputData: InputData, crit: boolean): number {
  2614. return (
  2615. inputData.skillDmgMulti *
  2616. minDmgPhys(inputData, true) *
  2617. (crit ? inputData.critDmg : 1)
  2618. );
  2619. }
  2620. function maxDmgDexSummon(inputData: InputData): number {
  2621. return (
  2622. ((inputData.stats.dex * 2.5 + inputData.stats.str) *
  2623. inputData.skillBasicAtk) /
  2624. 100
  2625. );
  2626. }
  2627. function minDmgDexSummon(inputData: InputData): number {
  2628. return (
  2629. ((inputData.stats.dex * 2.5 * 0.7 + inputData.stats.str) *
  2630. inputData.skillBasicAtk) /
  2631. 100
  2632. );
  2633. }
  2634. function maxDmgLuckySeven(inputData: InputData): number {
  2635. return (inputData.stats.luk * 5 * effectiveWatk(inputData)) / 100;
  2636. }
  2637. function minDmgLuckySeven(inputData: InputData): number {
  2638. return (inputData.stats.luk * 2.5 * effectiveWatk(inputData)) / 100;
  2639. }
  2640. function dmgNinjaAmbush(inputData: InputData): number {
  2641. return (
  2642. 2 *
  2643. (inputData.stats.str + inputData.stats.luk) *
  2644. inputData.skillDmgMulti
  2645. );
  2646. }
  2647. function maxDmgVenom(inputData: InputData): number {
  2648. return (
  2649. ((18.5 * (inputData.stats.str + inputData.stats.luk) +
  2650. inputData.stats.dex * 2) /
  2651. 100) *
  2652. inputData.skillBasicAtk
  2653. );
  2654. }
  2655. function minDmgVenom(inputData: InputData): number {
  2656. return (
  2657. ((8 * (inputData.stats.str + inputData.stats.luk) +
  2658. inputData.stats.dex * 2) /
  2659. 100) *
  2660. inputData.skillBasicAtk
  2661. );
  2662. }
  2663. function swingProbToGoodAnimProb(
  2664. inputData: InputData,
  2665. swingProb: number,
  2666. ): number {
  2667. switch (inputData.wepType) {
  2668. case WeaponType.OneHandedAxe:
  2669. case WeaponType.OneHandedMace:
  2670. case WeaponType.Wand:
  2671. case WeaponType.Staff:
  2672. case WeaponType.TwoHandedAxe:
  2673. case WeaponType.TwoHandedMace:
  2674. case WeaponType.Polearm:
  2675. return swingProb;
  2676. default:
  2677. return 1 - swingProb;
  2678. }
  2679. }
  2680. function maxDmgMagic(inputData: InputData): number {
  2681. const matk = effectiveMatk(inputData);
  2682. return (
  2683. ((matk ** 2 / 1000 + matk) / 30 + inputData.stats.int / 200) *
  2684. inputData.skillBasicAtk
  2685. );
  2686. }
  2687. function minDmgMagic(inputData: InputData): number {
  2688. const matk = effectiveMatk(inputData);
  2689. return (
  2690. ((matk ** 2 / 1000 + matk * inputData.mastery * 0.9) / 30 +
  2691. inputData.stats.int / 200) *
  2692. inputData.skillBasicAtk
  2693. );
  2694. }
  2695. function afterModMagic(inputData: InputData): number {
  2696. switch (inputData.spell) {
  2697. case Spell.ChainLightning:
  2698. return 0.7 ** (inputData.hitOrd - 1);
  2699. default:
  2700. return 1;
  2701. }
  2702. }
  2703. function healTargetMulti(enemyCount: number): number {
  2704. return 1.5 + 5 / (enemyCount + 1);
  2705. }
  2706. function maxDmgHeal(inputData: InputData): number {
  2707. return (
  2708. (((inputData.stats.int * 1.2 + inputData.stats.luk) *
  2709. effectiveMatk(inputData)) /
  2710. 1000) *
  2711. healTargetMulti(inputData.enemyCount) *
  2712. inputData.skillDmgMulti
  2713. );
  2714. }
  2715. function minDmgHeal(inputData: InputData): number {
  2716. return (
  2717. (((inputData.stats.int * 0.3 + inputData.stats.luk) *
  2718. effectiveMatk(inputData)) /
  2719. 1000) *
  2720. healTargetMulti(inputData.enemyCount) *
  2721. inputData.skillDmgMulti
  2722. );
  2723. }
  2724. function adjustRangeForWdef(
  2725. inputData: InputData,
  2726. range: [number, number],
  2727. ): [number, number] {
  2728. const [min, max] = range;
  2729. const levelDelta = Math.max(inputData.enemyLevel - inputData.level, 0);
  2730. const levelDeltaSlope = 1 - 0.01 * levelDelta;
  2731. return [
  2732. min * levelDeltaSlope - inputData.enemyWdef * 0.6,
  2733. max * levelDeltaSlope - inputData.enemyWdef * 0.5,
  2734. ];
  2735. }
  2736. function adjustRangeForMdef(
  2737. inputData: InputData,
  2738. range: [number, number],
  2739. ): [number, number] {
  2740. const [min, max] = range;
  2741. const levelDelta = Math.max(inputData.enemyLevel - inputData.level, 0);
  2742. const levelDeltaSlope = 1 + 0.01 * levelDelta;
  2743. return [
  2744. min - inputData.enemyMdef * 0.6 * levelDeltaSlope,
  2745. max - inputData.enemyMdef * 0.5 * levelDeltaSlope,
  2746. ];
  2747. }
  2748. function effectiveMastery(inputData: InputData): number {
  2749. if (inputData.wepType === WeaponType.None) {
  2750. return 0.1;
  2751. }
  2752. return inputData.mastery;
  2753. }
  2754. function effectiveWatk(inputData: InputData): number {
  2755. if (inputData.wepType === WeaponType.None) {
  2756. switch (inputData.clazz) {
  2757. case Class.Pirate:
  2758. case Class.Pirate2nd: {
  2759. const totalWatk =
  2760. inputData.totalWatk +
  2761. Math.min(Math.trunc((2 * inputData.level + 31) / 3), 31);
  2762. return totalWatk + totalWatk * inputData.echo;
  2763. }
  2764. default:
  2765. return 0;
  2766. }
  2767. }
  2768. return inputData.totalWatk + inputData.totalWatk * inputData.echo;
  2769. }
  2770. function effectiveMatk(inputData: InputData): number {
  2771. return inputData.totalMatk + inputData.totalMatk * inputData.echo;
  2772. }