123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045 |
- /*
- * @licstart The following is the entire license notice for the JavaScript
- * code in this page.
- *
- * This file is part of oddjobs-dmg-calc.
- *
- * oddjobs-dmg-calc is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License as published by the
- * Free Software Foundation, either version 3 of the License, or (at your
- * option) any later version.
- *
- * oddjobs-dmg-calc is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
- * License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with oddjobs-dmg-calc. If not, see <https://www.gnu.org/licenses/>.
- *
- * @licend The above is the entire license notice for the JavaScript code in
- * this page.
- */
- import {
- ATTACK_LINES,
- ATTACK_REQS,
- attackIsElemental,
- attackName,
- attackPeriod,
- BAD_WEPS,
- chargeTypeFromValue,
- className,
- isHolySpell,
- isSummon,
- JOB_LVL_REQS,
- magicAttackPeriod,
- primaryStat,
- secondaryStat,
- SPELL_LINES,
- SPELL_LVL_REQS,
- spellName,
- weaponTypeName,
- chargeTypeToValue,
- } from "./data.js";
- import { truncClampedExpectation, truncClampedVariance } from "./math.js";
- import {
- Attack,
- ChargeType,
- Class,
- InputData,
- Speed,
- Spell,
- Stats,
- WeaponType,
- } from "./types.js";
- import { indefinite } from "./util.js";
- document.addEventListener("readystatechange", () => {
- if (document.readyState === "complete") {
- main();
- }
- });
- function main(): void {
- const saveAsButton = document.getElementById(
- "save-as",
- ) as HTMLButtonElement;
- const saveButton = document.getElementById("save") as HTMLButtonElement;
- const loadButton = document.getElementById("load") as HTMLButtonElement;
- const deleteButton = document.getElementById(
- "delete",
- ) as HTMLButtonElement;
- const exportButton = document.getElementById(
- "export",
- ) as HTMLButtonElement;
- const importButton = document.getElementById(
- "import",
- ) as HTMLButtonElement;
- const saveLoadDialog = document.getElementById(
- "save-load-dialog",
- ) as HTMLDivElement;
- const currentlyLoaded = document.getElementById(
- "currently-loaded",
- ) as HTMLDivElement;
- function currentlyLoadedName(): string | undefined {
- const cln = currentlyLoaded.childNodes[0];
- if (cln && cln.textContent && cln.textContent.trim()) {
- return cln.textContent.trim();
- }
- return undefined;
- }
- const strInput = document.getElementById("str") as HTMLInputElement;
- const dexInput = document.getElementById("dex") as HTMLInputElement;
- const intInput = document.getElementById("int") as HTMLInputElement;
- const lukInput = document.getElementById("luk") as HTMLInputElement;
- const totalWatkInput = document.getElementById(
- "total-watk",
- ) as HTMLInputElement;
- const totalMatkInput = document.getElementById(
- "total-matk",
- ) as HTMLInputElement;
- const echoInput = document.getElementById("echo") as HTMLInputElement;
- const masteryInput = document.getElementById(
- "mastery",
- ) as HTMLInputElement;
- const skillDmgMultiInput = document.getElementById(
- "skill-dmg-multi",
- ) as HTMLInputElement;
- const skillBasicAtkInput = document.getElementById(
- "skill-basic-atk",
- ) as HTMLInputElement;
- const skillLinesInput = document.getElementById(
- "skill-lines",
- ) as HTMLInputElement;
- const critProbInput = document.getElementById(
- "crit-prob",
- ) as HTMLInputElement;
- const critDmgInput = document.getElementById(
- "crit-dmg",
- ) as HTMLInputElement;
- const classInput = document.getElementById("class") as HTMLSelectElement;
- const levelInput = document.getElementById("level") as HTMLInputElement;
- const weaponTypeInput = document.getElementById(
- "weapon-type",
- ) as HTMLSelectElement;
- const goodAnimProbInput = document.getElementById(
- "good-anim-prob",
- ) as HTMLInputElement;
- const attackInput = document.getElementById("attack") as HTMLSelectElement;
- const spellInput = document.getElementById("spell") as HTMLSelectElement;
- const speedInput = document.getElementById("speed") as HTMLSelectElement;
- const spellBoosterInput = document.getElementById(
- "spell-booster",
- ) as HTMLInputElement;
- const eleAmpInput = document.getElementById("ele-amp") as HTMLInputElement;
- const eleBoostInput = document.getElementById(
- "ele-boost",
- ) as HTMLInputElement;
- const eleWepInput = document.getElementById("ele-wep") as HTMLInputElement;
- const eleChargeInputs = Array.from(
- document.getElementsByName(
- "ele-charge",
- ) as NodeListOf<HTMLInputElement>,
- );
- const eleChargeDmgInput = document.getElementById(
- "ele-charge-dmg",
- ) as HTMLInputElement;
- const eleChargeLevelInput = document.getElementById(
- "ele-charge-level",
- ) as HTMLInputElement;
- const caActiveInput = document.getElementById(
- "ca-active",
- ) as HTMLInputElement;
- const caDmgInput = document.getElementById("ca-dmg") as HTMLInputElement;
- const caLevelInput = document.getElementById(
- "ca-level",
- ) as HTMLInputElement;
- const caOrbsInput = document.getElementById("ca-orbs") as HTMLInputElement;
- const zerkActiveInput = document.getElementById(
- "zerk-active",
- ) as HTMLInputElement;
- const zerkDmgInput = document.getElementById(
- "zerk-dmg",
- ) as HTMLInputElement;
- const enemyWdefInput = document.getElementById(
- "enemy-wdef",
- ) as HTMLInputElement;
- const enemyMdefInput = document.getElementById(
- "enemy-mdef",
- ) as HTMLInputElement;
- const eleSusInput = document.getElementById(
- "ele-sus",
- ) as HTMLSelectElement;
- const enemyLevelInput = document.getElementById(
- "enemy-level",
- ) as HTMLInputElement;
- const enemyCountInput = document.getElementById(
- "enemy-count",
- ) as HTMLInputElement;
- const hitOrdInput = document.getElementById("hit-ord") as HTMLInputElement;
- const rangeOutput = document.getElementById("range") as HTMLSpanElement;
- const critRangeOutput = document.getElementById(
- "crit-range",
- ) as HTMLSpanElement;
- const expectedPerHitOutput = document.getElementById(
- "expected-per-hit",
- ) as HTMLSpanElement;
- const sdPerHitOutput = document.getElementById(
- "sd-per-hit",
- ) as HTMLSpanElement;
- const cvPerHitOutput = document.getElementById(
- "cv-per-hit",
- ) as HTMLSpanElement;
- const totalRangeOutput = document.getElementById(
- "total-range",
- ) as HTMLSpanElement;
- const expectedPerHitTotalOutput = document.getElementById(
- "expected-per-hit-total",
- ) as HTMLSpanElement;
- const sdPerHitTotalOutput = document.getElementById(
- "sd-per-hit-total",
- ) as HTMLSpanElement;
- const cvPerHitTotalOutput = document.getElementById(
- "cv-per-hit-total",
- ) as HTMLSpanElement;
- const expectedDpsOutput = document.getElementById(
- "expected-dps",
- ) as HTMLSpanElement;
- const sdDpsOutput = document.getElementById("sd-dps") as HTMLSpanElement;
- const cvDpsOutput = document.getElementById("cv-dps") as HTMLSpanElement;
- const rangeMagicOutput = document.getElementById(
- "range-magic",
- ) as HTMLSpanElement;
- const critRangeMagicOutput = document.getElementById(
- "crit-range-magic",
- ) as HTMLSpanElement;
- const expectedPerHitMagicOutput = document.getElementById(
- "expected-per-hit-magic",
- ) as HTMLSpanElement;
- const sdPerHitMagicOutput = document.getElementById(
- "sd-per-hit-magic",
- ) as HTMLSpanElement;
- const cvPerHitMagicOutput = document.getElementById(
- "cv-per-hit-magic",
- ) as HTMLSpanElement;
- const totalRangeMagicOutput = document.getElementById(
- "total-range-magic",
- ) as HTMLSpanElement;
- const expectedPerHitTotalMagicOutput = document.getElementById(
- "expected-per-hit-total-magic",
- ) as HTMLSpanElement;
- const sdPerHitTotalMagicOutput = document.getElementById(
- "sd-per-hit-total-magic",
- ) as HTMLSpanElement;
- const cvPerHitTotalMagicOutput = document.getElementById(
- "cv-per-hit-total-magic",
- ) as HTMLSpanElement;
- const expectedDpsMagicOutput = document.getElementById(
- "expected-dps-magic",
- ) as HTMLSpanElement;
- const sdDpsMagicOutput = document.getElementById(
- "sd-dps-magic",
- ) as HTMLSpanElement;
- const cvDpsMagicOutput = document.getElementById(
- "cv-dps-magic",
- ) as HTMLSpanElement;
- const warningsDiv = document.getElementById(
- "warnings-div",
- ) as HTMLDivElement;
- function readInputData(): InputData {
- let str = Math.max(parseInt(strInput.value, 10), 4);
- if (!Number.isFinite(str)) {
- str = 4;
- }
- strInput.value = "" + str;
- let dex = Math.max(parseInt(dexInput.value, 10), 4);
- if (!Number.isFinite(dex)) {
- dex = 4;
- }
- dexInput.value = "" + dex;
- let int = Math.max(parseInt(intInput.value, 10), 4);
- if (!Number.isFinite(int)) {
- int = 4;
- }
- intInput.value = "" + int;
- let luk = Math.max(parseInt(lukInput.value, 10), 4);
- if (!Number.isFinite(luk)) {
- luk = 4;
- }
- lukInput.value = "" + luk;
- let totalWatk = Math.max(parseInt(totalWatkInput.value, 10), 0);
- if (!Number.isFinite(totalWatk)) {
- totalWatk = 0;
- }
- totalWatkInput.value = "" + totalWatk;
- let totalMatk = Math.max(parseInt(totalMatkInput.value, 10), 0);
- if (!Number.isFinite(totalMatk)) {
- totalMatk = 0;
- }
- totalMatkInput.value = "" + totalMatk;
- let echo = Math.max(parseInt(echoInput.value, 10), 0);
- if (!Number.isFinite(echo)) {
- echo = 0;
- }
- echoInput.value = "" + echo;
- let mastery = Math.min(
- Math.max(parseInt(masteryInput.value, 10), 10),
- 90,
- );
- if (!Number.isFinite(mastery)) {
- mastery = 10;
- }
- mastery -= mastery % 5;
- masteryInput.value = "" + mastery;
- let skillDmgMulti = Math.max(
- parseInt(skillDmgMultiInput.value, 10),
- 0,
- );
- if (!Number.isFinite(skillDmgMulti)) {
- skillDmgMulti = 100;
- }
- skillDmgMultiInput.value = "" + skillDmgMulti;
- let skillBasicAtk = Math.max(
- parseInt(skillBasicAtkInput.value, 10),
- 0,
- );
- if (!Number.isFinite(skillBasicAtk)) {
- skillBasicAtk = 10;
- }
- skillBasicAtkInput.value = "" + skillBasicAtk;
- let skillLines = Math.max(parseInt(skillLinesInput.value, 10), 1);
- if (!Number.isFinite(skillLines)) {
- skillLines = 1;
- }
- skillLinesInput.value = "" + skillLines;
- let critProb = Math.min(
- Math.max(parseInt(critProbInput.value, 10), 0),
- 100,
- );
- if (!Number.isFinite(critProb)) {
- critProb = 0;
- }
- critProbInput.value = "" + critProb;
- let critDmg = Math.max(parseInt(critDmgInput.value, 10), 0);
- if (!Number.isFinite(critDmg)) {
- critDmg = 0;
- }
- critDmgInput.value = "" + critDmg;
- let clazz = parseInt(classInput.value, 10);
- if (!Number.isFinite(clazz) || !(clazz in Class)) {
- clazz = 0;
- }
- classInput.value = "" + clazz;
- let level = Math.min(Math.max(parseInt(levelInput.value, 10), 1), 200);
- if (!Number.isFinite(level)) {
- level = 30;
- }
- levelInput.value = "" + level;
- let wepType = parseInt(weaponTypeInput.value, 10);
- if (!Number.isFinite(wepType) || !(wepType in WeaponType)) {
- wepType = 30;
- }
- weaponTypeInput.value = "" + wepType;
- let goodAnimProb = Math.min(
- Math.max(parseFloat(goodAnimProbInput.value), 0),
- 100,
- );
- if (!Number.isFinite(goodAnimProb)) {
- goodAnimProb = 60;
- }
- goodAnimProbInput.value = "" + goodAnimProb;
- let attack = parseInt(attackInput.value, 10);
- if (!Number.isFinite(attack) || !(attack in Attack)) {
- attack = 0;
- }
- attackInput.value = "" + attack;
- let spell = parseInt(spellInput.value, 10);
- if (!Number.isFinite(spell) || !(spell in Spell)) {
- spell = 0;
- }
- spellInput.value = "" + spell;
- let speed = Math.min(Math.max(parseInt(speedInput.value, 10), 2), 9);
- if (!Number.isFinite(speed)) {
- speed = 6;
- }
- speedInput.value = "" + speed;
- let spellBooster = Math.min(
- Math.max(parseInt(spellBoosterInput.value, 10), -2),
- 0,
- );
- if (!Number.isFinite(spellBooster)) {
- spellBooster = 0;
- }
- spellBoosterInput.value = "" + spellBooster;
- let eleAmp = Math.max(parseInt(eleAmpInput.value, 10), 100);
- if (!Number.isFinite(eleAmp)) {
- eleAmp = 100;
- }
- eleAmpInput.value = "" + eleAmp;
- let eleBoost = Math.max(parseInt(eleBoostInput.value, 10), 0);
- if (!Number.isFinite(eleBoost)) {
- eleBoost = 0;
- }
- eleBoostInput.value = "" + eleBoost;
- let eleWep = Math.max(parseInt(eleWepInput.value, 10), 0);
- if (!Number.isFinite(eleWep)) {
- eleWep = 0;
- }
- eleWepInput.value = "" + eleWep;
- const eleChargeType: ChargeType = (() => {
- let eleChargeType: ChargeType | undefined = undefined;
- for (const eleChargeInput of eleChargeInputs) {
- if (eleChargeInput.checked) {
- eleChargeType = chargeTypeFromValue(eleChargeInput.value);
- break;
- }
- }
- if (eleChargeType === undefined) {
- eleChargeInputs.forEach(inp => (inp.checked = false));
- const noEleChargeInput = document.getElementById(
- "no-ele-charge",
- ) as HTMLInputElement;
- noEleChargeInput.checked = true;
- return ChargeType.None;
- }
- return eleChargeType;
- })();
- let eleChargeDmg = Math.max(
- parseInt(eleChargeDmgInput.value, 10),
- 100,
- );
- if (!Number.isFinite(eleChargeDmg)) {
- eleChargeDmg = 100;
- }
- eleChargeDmgInput.value = "" + eleChargeDmg;
- let eleChargeLevel = Math.min(
- Math.max(parseInt(eleChargeLevelInput.value, 10), 1),
- 30,
- );
- if (!Number.isFinite(eleChargeLevel)) {
- eleChargeLevel = 1;
- }
- eleChargeLevelInput.value = "" + eleChargeLevel;
- const caActive = caActiveInput.checked;
- let caDmg = Math.max(parseInt(caDmgInput.value, 10), 100);
- if (!Number.isFinite(caDmg)) {
- caDmg = 104;
- }
- caDmgInput.value = "" + caDmg;
- let caLevel = Math.min(
- Math.max(parseInt(caLevelInput.value, 10), 1),
- 30,
- );
- if (!Number.isFinite(caLevel)) {
- caLevel = 1;
- }
- caLevelInput.value = "" + caLevel;
- let caOrbs = Math.min(
- Math.max(parseInt(caOrbsInput.value, 10), 1),
- 10,
- );
- if (!Number.isFinite(caOrbs)) {
- caOrbs = 1;
- }
- caOrbsInput.value = "" + caOrbs;
- const zerkActive = zerkActiveInput.checked;
- let zerkDmg = Math.max(parseInt(zerkDmgInput.value, 10), 100);
- if (!Number.isFinite(zerkDmg)) {
- zerkDmg = 100;
- }
- zerkDmgInput.value = "" + zerkDmg;
- let enemyWdef = parseInt(enemyWdefInput.value, 10);
- if (!Number.isFinite(enemyWdef)) {
- enemyWdef = 0;
- }
- enemyWdefInput.value = "" + enemyWdef;
- let enemyMdef = parseInt(enemyMdefInput.value, 10);
- if (!Number.isFinite(enemyMdef)) {
- enemyMdef = 0;
- }
- enemyMdefInput.value = "" + enemyMdef;
- let eleSus = Math.min(Math.max(parseFloat(eleSusInput.value), 0), 1.5);
- if (
- !Number.isFinite(eleSus) ||
- !(eleSus === 0 || eleSus === 0.5 || eleSus === 1 || eleSus === 1.5)
- ) {
- eleSus = 1;
- }
- eleSusInput.value = "" + eleSus;
- let enemyLevel = Math.max(parseInt(enemyLevelInput.value, 10), 1);
- if (!Number.isFinite(enemyLevel)) {
- enemyLevel = 1;
- }
- enemyLevelInput.value = "" + enemyLevel;
- let enemyCount = Math.min(
- Math.max(parseInt(enemyCountInput.value, 10), 1),
- 15,
- );
- if (!Number.isFinite(enemyCount)) {
- enemyCount = 1;
- }
- enemyCountInput.value = "" + enemyCount;
- let hitOrd = Math.min(Math.max(parseInt(hitOrdInput.value, 10), 1), 6);
- if (!Number.isFinite(hitOrd)) {
- hitOrd = 1;
- }
- hitOrdInput.value = "" + hitOrd;
- return new InputData(
- new Stats(str, dex, int, luk),
- totalWatk,
- totalMatk,
- echo / 100,
- mastery / 100,
- skillDmgMulti / 100,
- skillBasicAtk,
- skillLines,
- critProb / 100,
- critDmg / 100,
- clazz,
- level,
- wepType,
- goodAnimProb / 100,
- attack,
- spell,
- speed,
- spellBooster,
- eleAmp / 100,
- eleBoost / 100,
- eleWep / 100,
- eleChargeType,
- eleChargeDmg / 100,
- eleChargeLevel,
- caActive,
- caDmg,
- caLevel,
- caOrbs,
- zerkActive,
- zerkDmg / 100,
- enemyWdef,
- enemyMdef,
- eleSus,
- enemyLevel,
- enemyCount,
- hitOrd,
- );
- }
- function writeInputData(inputData: InputData): void {
- strInput.value = "" + inputData.stats.str;
- dexInput.value = "" + inputData.stats.dex;
- intInput.value = "" + inputData.stats.int;
- lukInput.value = "" + inputData.stats.luk;
- totalWatkInput.value = "" + inputData.totalWatk;
- totalMatkInput.value = "" + inputData.totalMatk;
- echoInput.value = "" + inputData.echo * 100;
- masteryInput.value = "" + inputData.mastery * 100;
- skillDmgMultiInput.value = "" + inputData.skillDmgMulti * 100;
- skillBasicAtkInput.value = "" + inputData.skillBasicAtk;
- skillLinesInput.value = "" + inputData.skillLines;
- critProbInput.value = "" + inputData.critProb * 100;
- critDmgInput.value = "" + inputData.critDmg * 100;
- classInput.value = "" + inputData.clazz;
- levelInput.value = "" + inputData.level;
- weaponTypeInput.value = "" + inputData.wepType;
- goodAnimProbInput.value = "" + inputData.goodAnimProb * 100;
- attackInput.value = "" + inputData.attack;
- spellInput.value = "" + inputData.spell;
- speedInput.value = "" + inputData.speed;
- spellBoosterInput.value = "" + inputData.spellBooster;
- eleAmpInput.value = "" + inputData.eleAmp * 100;
- eleBoostInput.value = "" + inputData.eleBoost * 100;
- eleWepInput.value = "" + inputData.eleWep * 100;
- eleChargeInputs.forEach(inp => (inp.checked = false));
- const chargeInput = document.getElementById(
- chargeTypeToValue(inputData.eleChargeType),
- ) as HTMLInputElement;
- chargeInput.checked = true;
- eleChargeDmgInput.value = "" + inputData.eleChargeDmg * 100;
- eleChargeLevelInput.value = "" + inputData.eleChargeLevel;
- caActiveInput.checked = inputData.caActive;
- caDmgInput.value = "" + inputData.caDmg;
- caLevelInput.value = "" + inputData.caLevel;
- caOrbsInput.value = "" + inputData.caOrbs;
- zerkActiveInput.checked = inputData.zerkActive;
- zerkDmgInput.value = "" + inputData.zerkDmg * 100;
- enemyWdefInput.value = "" + inputData.enemyWdef;
- enemyMdefInput.value = "" + inputData.enemyMdef;
- eleSusInput.value = "" + inputData.eleSus;
- enemyLevelInput.value = "" + inputData.enemyLevel;
- enemyCountInput.value = "" + inputData.enemyCount;
- hitOrdInput.value = "" + inputData.hitOrd;
- }
- function recalculate(): void {
- const inputData = readInputData();
- const critQ = 1 - inputData.critProb;
- recalculatePhys(inputData, critQ);
- recalculateMagic(inputData, critQ);
- recalculateWarnings(inputData);
- }
- function recalculatePhys(inputData: InputData, critQ: number): void {
- const goodAnimProb = (() => {
- switch (inputData.attack) {
- case Attack.Rush:
- case Attack.CrusherLow:
- case Attack.CrusherHigh:
- case Attack.SomersaultKick:
- case Attack.AerialStrike:
- return swingProbToGoodAnimProb(inputData, 0);
- case Attack.Brandish:
- return 0.5;
- case Attack.Blast:
- return swingProbToGoodAnimProb(inputData, 0.6);
- case Attack.HeavensHammerXiuz:
- case Attack.HeavensHammerXiuzCorrected:
- return 1;
- case Attack.Fury:
- return swingProbToGoodAnimProb(inputData, 1);
- default:
- return inputData.goodAnimProb;
- }
- })();
- const badAnimProb = 1 - goodAnimProb;
- const [caMod, eleChargeMod] = isSummon(inputData.attack)
- ? [1, 1]
- : [caModifier(inputData), eleChargeModifier(inputData)];
- const eleSus = attackEffectiveEleSus(inputData);
- const [minDmgPhysBad, maxDmgPhysGood] = [
- (() => {
- const goodAnim = goodAnimProb >= 1;
- switch (inputData.attack) {
- case Attack.BowWhack:
- case Attack.PowerKnockBack:
- return minDmgBowWhack(inputData);
- case Attack.ClawPunch:
- return minDmgClawPunch(inputData);
- case Attack.Panic:
- case Attack.Coma:
- return minDmgCaFinisher(inputData, goodAnim);
- case Attack.HeavensHammerXiuz:
- return minDmgHhXiuz(inputData);
- case Attack.HeavensHammerXiuzCorrected:
- return minDmgPhys(inputData, true);
- case Attack.DragonRoar:
- return minDmgDragonRoar(inputData);
- // Massive hack to make Arrow Bomb easier to work with...
- case Attack.ArrowBombImpact:
- return minDmgArrowBombImpact(inputData, false);
- case Attack.ArrowBombSplash:
- return minDmgArrowBombSplash(inputData, false);
- case Attack.Phoenix:
- case Attack.Frostprey:
- case Attack.Octopus:
- case Attack.Gaviota:
- case Attack.WrathOfTheOctopi:
- return minDmgDexSummon(inputData);
- case Attack.LuckySeven:
- case Attack.TripleThrow:
- return minDmgLuckySeven(inputData);
- case Attack.NinjaAmbush:
- return dmgNinjaAmbush(inputData);
- case Attack.VenomousStar:
- case Attack.VenomousStab:
- return minDmgVenom(inputData);
- default:
- return minDmgPhys(inputData, goodAnim);
- }
- })(),
- (() => {
- const goodAnim = goodAnimProb > 0;
- switch (inputData.attack) {
- case Attack.BowWhack:
- case Attack.PowerKnockBack:
- return maxDmgBowWhack(inputData);
- case Attack.ClawPunch:
- return maxDmgClawPunch(inputData);
- case Attack.Panic:
- case Attack.Coma:
- return maxDmgCaFinisher(inputData, goodAnim);
- case Attack.HeavensHammerXiuz:
- case Attack.HeavensHammerXiuzCorrected:
- return maxDmgPhys(inputData, true);
- case Attack.DragonRoar:
- return maxDmgDragonRoar(inputData);
- // Massive hack to make Arrow Bomb easier to work with...
- case Attack.ArrowBombImpact:
- return maxDmgArrowBombImpact(inputData, true);
- case Attack.ArrowBombSplash:
- return maxDmgArrowBombSplash(inputData, true);
- case Attack.Phoenix:
- case Attack.Frostprey:
- case Attack.Octopus:
- case Attack.Gaviota:
- case Attack.WrathOfTheOctopi:
- return maxDmgDexSummon(inputData);
- case Attack.LuckySeven:
- case Attack.TripleThrow:
- return maxDmgLuckySeven(inputData);
- case Attack.NinjaAmbush:
- return dmgNinjaAmbush(inputData);
- case Attack.VenomousStar:
- case Attack.VenomousStab:
- return maxDmgVenom(inputData);
- default:
- return maxDmgPhys(inputData, goodAnim);
- }
- })(),
- ].map(dmg => dmg * eleSus * caMod * eleChargeMod);
- const [minDmgPhysGood, maxDmgPhysBad] = [
- (() => {
- if (goodAnimProb <= 0) {
- return minDmgPhysBad;
- }
- switch (inputData.attack) {
- case Attack.BowWhack:
- case Attack.PowerKnockBack:
- case Attack.ClawPunch:
- case Attack.Rush:
- case Attack.HeavensHammerXiuz:
- case Attack.HeavensHammerXiuzCorrected:
- case Attack.CrusherHigh:
- case Attack.CrusherLow:
- case Attack.Fury:
- case Attack.DragonRoar:
- case Attack.Phoenix:
- case Attack.Frostprey:
- case Attack.Octopus:
- case Attack.Gaviota:
- case Attack.WrathOfTheOctopi:
- case Attack.LuckySeven:
- case Attack.TripleThrow:
- case Attack.NinjaAmbush:
- case Attack.VenomousStar:
- case Attack.VenomousStab:
- case Attack.SomersaultKick:
- case Attack.AerialStrike:
- return minDmgPhysBad;
- case Attack.Panic:
- case Attack.Coma:
- return minDmgCaFinisher(inputData, true);
- // Massive hack to make Arrow Bomb easier to work with...
- case Attack.ArrowBombImpact:
- return minDmgArrowBombImpact(inputData, true);
- case Attack.ArrowBombSplash:
- return minDmgArrowBombSplash(inputData, true);
- default:
- return minDmgPhys(inputData, true);
- }
- })(),
- (() => {
- if (goodAnimProb >= 1) {
- return maxDmgPhysGood;
- }
- switch (inputData.attack) {
- case Attack.BowWhack:
- case Attack.PowerKnockBack:
- case Attack.ClawPunch:
- case Attack.Rush:
- case Attack.HeavensHammerXiuz:
- case Attack.HeavensHammerXiuzCorrected:
- case Attack.CrusherHigh:
- case Attack.CrusherLow:
- case Attack.Fury:
- case Attack.DragonRoar:
- case Attack.Phoenix:
- case Attack.Frostprey:
- case Attack.Octopus:
- case Attack.Gaviota:
- case Attack.WrathOfTheOctopi:
- case Attack.LuckySeven:
- case Attack.TripleThrow:
- case Attack.NinjaAmbush:
- case Attack.VenomousStar:
- case Attack.VenomousStab:
- case Attack.SomersaultKick:
- case Attack.AerialStrike:
- return maxDmgPhysGood;
- case Attack.Panic:
- case Attack.Coma:
- return maxDmgCaFinisher(inputData, false);
- // Massive hack to make Arrow Bomb easier to work with...
- case Attack.ArrowBombImpact:
- return maxDmgArrowBombImpact(inputData, false);
- case Attack.ArrowBombSplash:
- return maxDmgArrowBombSplash(inputData, false);
- default:
- return maxDmgPhys(inputData, false);
- }
- })(),
- ].map(dmg => dmg * eleSus * caMod * eleChargeMod);
- const [minDmgPhysBadAdjusted, maxDmgPhysGoodAdjusted] = (() => {
- switch (inputData.attack) {
- case Attack.HeavensHammerXiuz:
- case Attack.HeavensHammerXiuzCorrected:
- case Attack.Phoenix:
- case Attack.Frostprey:
- case Attack.Octopus:
- case Attack.Gaviota:
- case Attack.WrathOfTheOctopi:
- return [minDmgPhysBad, maxDmgPhysGood];
- case Attack.Assaulter:
- return inputData.level >= inputData.enemyLevel
- ? [minDmgPhysBad, maxDmgPhysGood]
- : adjustRangeForWdef(inputData, [
- minDmgPhysBad,
- maxDmgPhysGood,
- ]);
- default:
- return adjustRangeForWdef(inputData, [
- minDmgPhysBad,
- maxDmgPhysGood,
- ]);
- }
- })();
- const [minDmgPhysGoodAdjusted, maxDmgPhysBadAdjusted] = (() => {
- switch (inputData.attack) {
- case Attack.HeavensHammerXiuz:
- case Attack.HeavensHammerXiuzCorrected:
- case Attack.Phoenix:
- case Attack.Frostprey:
- case Attack.Octopus:
- case Attack.Gaviota:
- case Attack.WrathOfTheOctopi:
- return [minDmgPhysGood, maxDmgPhysBad];
- case Attack.Assaulter:
- return inputData.level >= inputData.enemyLevel
- ? [minDmgPhysGood, maxDmgPhysBad]
- : adjustRangeForWdef(inputData, [
- minDmgPhysGood,
- maxDmgPhysBad,
- ]);
- default:
- return adjustRangeForWdef(inputData, [
- minDmgPhysGood,
- maxDmgPhysBad,
- ]);
- }
- })();
- const [dmgMultiNoCrit, dmgMultiCrit] = isSummon(inputData.attack)
- ? [1, 1]
- : [
- dmgMulti(inputData, false),
- dmgMulti(
- inputData,
- inputData.attack !== Attack.HeavensHammerXiuz &&
- inputData.attack !==
- Attack.HeavensHammerXiuzCorrected &&
- inputData.attack !== Attack.VenomousStar &&
- inputData.attack !== Attack.VenomousStab,
- ),
- ];
- const afterModifier = isSummon(inputData.attack)
- ? 1
- : afterModPhys(inputData);
- const [
- minDmgPhysBadNoCrit,
- maxDmgPhysGoodNoCrit,
- minDmgPhysGoodNoCrit,
- maxDmgPhysBadNoCrit,
- ] =
- // Massive hack to make Arrow Bomb easier to work with...
- inputData.attack === Attack.ArrowBombImpact ||
- inputData.attack === Attack.ArrowBombSplash
- ? [
- minDmgPhysBadAdjusted,
- maxDmgPhysBadAdjusted,
- minDmgPhysBadAdjusted,
- maxDmgPhysBadAdjusted,
- ]
- : [
- minDmgPhysBadAdjusted,
- maxDmgPhysGoodAdjusted,
- minDmgPhysGoodAdjusted,
- maxDmgPhysBadAdjusted,
- ].map(x => Math.max(x * dmgMultiNoCrit, 1) * afterModifier);
- const [
- minDmgPhysBadCrit,
- maxDmgPhysGoodCrit,
- minDmgPhysGoodCrit,
- maxDmgPhysBadCrit,
- ] =
- // Massive hack to make Arrow Bomb easier to work with...
- inputData.attack === Attack.ArrowBombImpact ||
- inputData.attack === Attack.ArrowBombSplash
- ? [
- minDmgPhysGoodAdjusted,
- maxDmgPhysGoodAdjusted,
- minDmgPhysGoodAdjusted,
- maxDmgPhysGoodAdjusted,
- ]
- : [
- minDmgPhysBadAdjusted,
- maxDmgPhysGoodAdjusted,
- minDmgPhysGoodAdjusted,
- maxDmgPhysBadAdjusted,
- ].map(x => Math.max(x * dmgMultiCrit, 1) * afterModifier);
- // Lots of special-casing for Barrage, the only goddamn attack that
- // does this...
- const [maxDmgNoCritBarrage, maxDmgCritBarrage] = [
- maxDmgPhysGoodNoCrit,
- maxDmgPhysGoodCrit,
- ].map(
- x =>
- x *
- (inputData.attack === Attack.Barrage
- ? afterModBarrage(inputData.skillLines)
- : 0),
- );
- const range =
- inputData.attack === Attack.Barrage
- ? [
- Math.max(Math.trunc(minDmgPhysBadNoCrit), 1),
- Math.max(Math.trunc(maxDmgNoCritBarrage), 1),
- ]
- : [minDmgPhysBadNoCrit, maxDmgPhysGoodNoCrit].map(x =>
- Math.max(Math.trunc(x), 1),
- );
- const critRange =
- inputData.attack === Attack.Barrage
- ? [
- Math.max(Math.trunc(minDmgPhysBadCrit), 1),
- Math.max(Math.trunc(maxDmgCritBarrage), 1),
- ]
- : [minDmgPhysBadCrit, maxDmgPhysGoodCrit].map(x =>
- Math.max(Math.trunc(x), 1),
- );
- rangeOutput.textContent = `${range[0]} ~ ${range[1]}${
- range[1] ===
- (inputData.attack === Attack.Barrage
- ? maxDmgNoCritBarrage
- : maxDmgPhysGoodNoCrit) && range[1] !== 1
- ? "*"
- : ""
- }`;
- critRangeOutput.textContent = `${critRange[0]} ~ ${critRange[1]}${
- critRange[1] ===
- (inputData.attack === Attack.Barrage
- ? maxDmgCritBarrage
- : maxDmgPhysGoodCrit) && critRange[1] !== 1
- ? "*"
- : ""
- }`;
- const [combinedRangeTop, combinedRangeTopOneLine] =
- inputData.critProb > 0
- ? [critRange[1], maxDmgPhysGoodCrit]
- : [range[1], maxDmgPhysGoodNoCrit];
- if (inputData.attack === Attack.Barrage) {
- let totalRangeBottom =
- range[0] * Math.min(inputData.skillLines, 4);
- for (let i = 5; i <= inputData.skillLines; ++i) {
- totalRangeBottom += Math.max(
- Math.trunc(minDmgPhysBadNoCrit * afterModBarrage(i)),
- 1,
- );
- }
- let totalRangeTop =
- combinedRangeTop * Math.min(inputData.skillLines, 4);
- for (let i = 5; i <= inputData.skillLines; ++i) {
- totalRangeTop += Math.max(
- Math.trunc(combinedRangeTopOneLine * afterModBarrage(i)),
- 1,
- );
- }
- totalRangeOutput.textContent = `${totalRangeBottom} ~ \
- ${totalRangeTop}${
- combinedRangeTop === combinedRangeTopOneLine &&
- combinedRangeTop !== 1
- ? "*"
- : ""
- }`;
- } else {
- totalRangeOutput.textContent = `${
- range[0] * inputData.skillLines
- } ~ ${combinedRangeTop * inputData.skillLines}${
- combinedRangeTop === combinedRangeTopOneLine &&
- combinedRangeTop !== 1
- ? "*"
- : ""
- }`;
- }
- const [expectedPerHitBadNoCrit, expectedPerHitBadCrit] = [
- truncClampedExpectation(minDmgPhysBadNoCrit, maxDmgPhysBadNoCrit),
- truncClampedExpectation(minDmgPhysBadCrit, maxDmgPhysBadCrit),
- ];
- const [expectedPerHitGoodNoCrit, expectedPerHitGoodCrit] = [
- truncClampedExpectation(
- minDmgPhysGoodNoCrit,
- maxDmgPhysGoodNoCrit,
- ),
- truncClampedExpectation(minDmgPhysGoodCrit, maxDmgPhysGoodCrit),
- ];
- const expectedPerHitBad =
- critQ * expectedPerHitBadNoCrit +
- inputData.critProb * expectedPerHitBadCrit;
- const expectedPerHitGood =
- critQ * expectedPerHitGoodNoCrit +
- inputData.critProb * expectedPerHitGoodCrit;
- const expectedPerHit =
- expectedPerHitBad * badAnimProb +
- expectedPerHitGood * goodAnimProb;
- const expectedPerHitTotal =
- inputData.attack === Attack.Barrage
- ? (() => {
- let accum =
- expectedPerHit * Math.min(inputData.skillLines, 4);
- for (let i = 5; i <= inputData.skillLines; ++i) {
- accum += expectedPerHit * afterModBarrage(i);
- }
- return accum;
- })()
- : expectedPerHit * inputData.skillLines;
- expectedPerHitOutput.textContent = (
- expectedPerHit *
- (inputData.attack === Attack.Barrage
- ? barrageEffectiveMulti(inputData.skillLines) /
- inputData.skillLines
- : 1)
- ).toFixed(3);
- expectedPerHitTotalOutput.textContent = expectedPerHitTotal.toFixed(3);
- // The "mainVariance" in the following variable names is intended to
- // indicate that this is a variance against the expectation across
- // _all_ cases (`expectedPerHit`), not against the expected value of
- // the particular case in question.
- const mainVariancePerHitBadNoCrit = truncClampedVariance(
- minDmgPhysBadNoCrit,
- maxDmgPhysBadNoCrit,
- expectedPerHit,
- );
- const mainVariancePerHitGoodNoCrit = truncClampedVariance(
- minDmgPhysGoodNoCrit,
- maxDmgPhysGoodNoCrit,
- expectedPerHit,
- );
- const mainVariancePerHitBadCrit = truncClampedVariance(
- minDmgPhysBadCrit,
- maxDmgPhysBadCrit,
- expectedPerHit,
- );
- const mainVariancePerHitGoodCrit = truncClampedVariance(
- minDmgPhysGoodCrit,
- maxDmgPhysGoodCrit,
- expectedPerHit,
- );
- const variancePerHit = (() => {
- if (
- mainVariancePerHitBadNoCrit !== undefined &&
- mainVariancePerHitGoodNoCrit !== undefined &&
- mainVariancePerHitBadCrit !== undefined &&
- mainVariancePerHitGoodCrit !== undefined
- ) {
- const mainVariancePerHitBad =
- critQ * mainVariancePerHitBadNoCrit +
- inputData.critProb * mainVariancePerHitBadCrit;
- const mainVariancePerHitGood =
- critQ * mainVariancePerHitGoodNoCrit +
- inputData.critProb * mainVariancePerHitGoodCrit;
- return (
- mainVariancePerHitBad * badAnimProb +
- mainVariancePerHitGood * goodAnimProb
- );
- }
- return;
- })();
- let sdPerHitTotal: number | undefined = undefined;
- if (variancePerHit !== undefined) {
- sdPerHitOutput.classList.remove("error");
- cvPerHitOutput.classList.remove("error");
- sdPerHitTotalOutput.classList.remove("error");
- cvPerHitTotalOutput.classList.remove("error");
- const sdPerHit = Math.sqrt(
- inputData.attack === Attack.Barrage
- ? variancePerHit *
- (barrageEffectiveMulti(inputData.skillLines) /
- inputData.skillLines)
- : variancePerHit,
- );
- // This is mathematically valid because the damage/outcome of each
- // line is independent of the damage of any other line, thus
- // implying uncorrelatedness. Furthermore, this implies that the
- // variance of the sum of lines is the sum of the variance of said
- // lines (see the Bienaymé formula/identity).
- sdPerHitTotal = Math.sqrt(
- variancePerHit *
- (inputData.attack === Attack.Barrage
- ? barrageEffectiveMulti(inputData.skillLines)
- : inputData.skillLines),
- );
- sdPerHitOutput.textContent = sdPerHit.toFixed(3);
- cvPerHitOutput.textContent = (
- sdPerHit /
- (expectedPerHit *
- (inputData.attack === Attack.Barrage
- ? barrageEffectiveMulti(inputData.skillLines) /
- inputData.skillLines
- : 1))
- ).toFixed(5);
- sdPerHitTotalOutput.textContent = sdPerHitTotal.toFixed(3);
- cvPerHitTotalOutput.textContent = (
- sdPerHitTotal / expectedPerHitTotal
- ).toFixed(5);
- } else {
- sdPerHitOutput.classList.add("error");
- cvPerHitOutput.classList.add("error");
- sdPerHitTotalOutput.classList.add("error");
- cvPerHitTotalOutput.classList.add("error");
- sdPerHitOutput.textContent = "[undefined]";
- cvPerHitOutput.textContent = "[undefined]";
- sdPerHitTotalOutput.textContent = "[undefined]";
- cvPerHitTotalOutput.textContent = "[undefined]";
- }
- const period = attackPeriod(
- inputData.wepType,
- inputData.speed,
- inputData.attack,
- );
- if (period !== undefined) {
- expectedDpsOutput.classList.remove("error");
- const attackHz = 1000 / period;
- const expectedDps = attackHz * expectedPerHitTotal;
- expectedDpsOutput.textContent = expectedDps.toFixed(3);
- if (sdPerHitTotal !== undefined) {
- sdDpsOutput.classList.remove("error");
- cvDpsOutput.classList.remove("error");
- // This is mathematically valid because the damage/outcome of
- // each line is independent of the damage of any other line,
- // thus implying uncorrelatedness. Furthermore, this implies
- // that the variance of the sum of lines is the sum of the
- // variance of said lines (see the Bienaymé formula/identity).
- const sdDps = Math.sqrt(attackHz) * sdPerHitTotal; /*
- = sqrt(attackHz) * sqrt(variancePerHitTotal)
- = sqrt(attackHz * variancePerHitTotal)
- = sqrt(varianceDps). */
- sdDpsOutput.textContent = sdDps.toFixed(3);
- cvDpsOutput.textContent = (sdDps / expectedDps).toFixed(5);
- } else {
- sdDpsOutput.classList.add("error");
- cvDpsOutput.classList.add("error");
- sdDpsOutput.textContent = "[undefined]";
- cvDpsOutput.textContent = "[undefined]";
- }
- } else {
- expectedDpsOutput.classList.add("error");
- sdDpsOutput.classList.add("error");
- cvDpsOutput.classList.add("error");
- expectedDpsOutput.textContent = "[unknown attack speed value]";
- sdDpsOutput.textContent = "[undefined]";
- cvDpsOutput.textContent = "[undefined]";
- }
- }
- function recalculateMagic(inputData: InputData, critQ: number): void {
- const eleWepBonus =
- 1 + (isHolySpell(inputData.spell) ? 0 : inputData.eleWep);
- const [minDmg, maxDmg] = [
- (() => {
- switch (inputData.spell) {
- case Spell.Heal:
- return minDmgHeal(inputData);
- default:
- return minDmgMagic(inputData);
- }
- })() *
- inputData.eleAmp *
- eleWepBonus *
- inputData.eleSus,
- (() => {
- switch (inputData.spell) {
- case Spell.Heal:
- return maxDmgHeal(inputData);
- default:
- return maxDmgMagic(inputData);
- }
- })() *
- inputData.eleAmp *
- eleWepBonus *
- inputData.eleSus,
- ];
- const [minDmgNoCrit, maxDmgNoCrit] = adjustRangeForMdef(inputData, [
- minDmg,
- maxDmg,
- ]);
- const [minDmgCrit, maxDmgCrit] = [minDmgNoCrit, maxDmgNoCrit].map(
- x => x * inputData.critDmg,
- );
- const [
- minDmgNoCritAfter,
- maxDmgNoCritAfter,
- minDmgCritAfter,
- maxDmgCritAfter,
- ] = [minDmgNoCrit, maxDmgNoCrit, minDmgCrit, maxDmgCrit].map(
- x => Math.max(x, 1) * afterModMagic(inputData),
- );
- const range = [minDmgNoCritAfter, maxDmgNoCritAfter].map(x =>
- Math.max(Math.trunc(x), 1),
- );
- const critRange = [minDmgCritAfter, maxDmgCritAfter].map(x =>
- Math.max(Math.trunc(x), 1),
- );
- rangeMagicOutput.textContent = `${range[0]} ~ ${range[1]}${
- range[1] === maxDmgNoCritAfter && range[1] !== 1 ? "*" : ""
- }`;
- critRangeMagicOutput.textContent = `${critRange[0]} ~ ${critRange[1]}${
- critRange[1] === maxDmgCritAfter && critRange[1] !== 1 ? "*" : ""
- }`;
- const combinedRangeTop =
- inputData.critProb > 0 ? critRange[1] : range[1];
- totalRangeMagicOutput.textContent = `${
- range[0] * inputData.skillLines
- } ~ ${combinedRangeTop * inputData.skillLines}${
- combinedRangeTop ===
- (inputData.critProb > 0
- ? maxDmgCritAfter
- : maxDmgNoCritAfter) && combinedRangeTop !== 1
- ? "*"
- : ""
- }`;
- const [expectedPerHitNoCrit, expectedPerHitCrit] = [
- truncClampedExpectation(minDmgNoCritAfter, maxDmgNoCritAfter),
- truncClampedExpectation(minDmgCritAfter, maxDmgCritAfter),
- ];
- const expectedPerHit =
- critQ * expectedPerHitNoCrit +
- inputData.critProb * expectedPerHitCrit;
- const expectedPerHitTotal = expectedPerHit * inputData.skillLines;
- expectedPerHitMagicOutput.textContent = expectedPerHit.toFixed(3);
- expectedPerHitTotalMagicOutput.textContent =
- expectedPerHitTotal.toFixed(3);
- // The "mainVariance" in the following variable names is intended to
- // indicate that this is a variance against the expectation across
- // _all_ cases (`expectedPerHit`), not against the expected value of
- // the particular case in question.
- const mainVariancePerHitNoCrit = truncClampedVariance(
- minDmgNoCritAfter,
- maxDmgNoCritAfter,
- expectedPerHit,
- );
- const mainVariancePerHitCrit = truncClampedVariance(
- minDmgCritAfter,
- maxDmgCritAfter,
- expectedPerHit,
- );
- const variancePerHit =
- mainVariancePerHitNoCrit !== undefined &&
- mainVariancePerHitCrit !== undefined
- ? critQ * mainVariancePerHitNoCrit +
- inputData.critProb * mainVariancePerHitCrit
- : undefined;
- let sdPerHitTotal: number | undefined = undefined;
- if (variancePerHit !== undefined) {
- sdPerHitMagicOutput.classList.remove("error");
- cvPerHitMagicOutput.classList.remove("error");
- sdPerHitTotalMagicOutput.classList.remove("error");
- cvPerHitTotalMagicOutput.classList.remove("error");
- const sdPerHit = Math.sqrt(variancePerHit);
- // This is mathematically valid because the damage/outcome of each
- // hit is independent of the damage of any other hit, thus implying
- // uncorrelatedness. Furthermore, this implies that the variance
- // of the sum of hits is the sum of the variance of said hits (see
- // the Bienaymé formula/identity).
- sdPerHitTotal = Math.sqrt(variancePerHit * inputData.skillLines);
- sdPerHitMagicOutput.textContent = sdPerHit.toFixed(3);
- cvPerHitMagicOutput.textContent = (
- sdPerHit / expectedPerHit
- ).toFixed(5);
- sdPerHitTotalMagicOutput.textContent = sdPerHitTotal.toFixed(3);
- cvPerHitTotalMagicOutput.textContent = (
- sdPerHitTotal / expectedPerHitTotal
- ).toFixed(5);
- } else {
- sdPerHitMagicOutput.classList.add("error");
- cvPerHitMagicOutput.classList.add("error");
- sdPerHitTotalMagicOutput.classList.add("error");
- cvPerHitTotalMagicOutput.classList.add("error");
- sdPerHitMagicOutput.textContent = "[undefined]";
- cvPerHitMagicOutput.textContent = "[undefined]";
- sdPerHitTotalMagicOutput.textContent = "[undefined]";
- cvPerHitTotalMagicOutput.textContent = "[undefined]";
- }
- const period = magicAttackPeriod(
- inputData.spellBooster,
- inputData.spell,
- inputData.speed,
- );
- if (period !== undefined) {
- expectedDpsMagicOutput.classList.remove("error");
- const attackHz = 1000 / period;
- const expectedDps = attackHz * expectedPerHitTotal;
- expectedDpsMagicOutput.textContent = expectedDps.toFixed(3);
- if (sdPerHitTotal !== undefined) {
- sdDpsMagicOutput.classList.remove("error");
- cvDpsMagicOutput.classList.remove("error");
- // This is mathematically valid because the damage/outcome of
- // each hit is independent of the damage of any other hit, thus
- // implying uncorrelatedness. Furthermore, this implies that
- // the variance of the sum of hits is the sum of the variance
- // of said hits.
- const sdDps = Math.sqrt(attackHz) * sdPerHitTotal; /*
- = sqrt(attackHz) * sqrt(variancePerHitTotal)
- = sqrt(attackHz * variancePerHitTotal)
- = sqrt(varianceDps). */
- sdDpsMagicOutput.textContent = sdDps.toFixed(3);
- cvDpsMagicOutput.textContent = (sdDps / expectedDps).toFixed(
- 5,
- );
- } else {
- sdDpsMagicOutput.classList.add("error");
- cvDpsMagicOutput.classList.add("error");
- sdDpsMagicOutput.textContent = "[undefined]";
- cvDpsMagicOutput.textContent = "[undefined]";
- }
- } else {
- expectedDpsMagicOutput.classList.add("error");
- sdDpsMagicOutput.classList.add("error");
- cvDpsMagicOutput.classList.add("error");
- expectedDpsMagicOutput.textContent =
- "[unknown attack speed value]";
- sdDpsMagicOutput.textContent = "[undefined]";
- cvDpsMagicOutput.textContent = "[undefined]";
- }
- }
- function recalculateWarnings(inputData: InputData): void {
- const warnings: string[] = [];
- /*======== Accumulate warnings ========*/
- if (
- inputData.totalWatk === 0 &&
- inputData.wepType !== WeaponType.None
- ) {
- warnings.push(
- "Your total WATK is zero, but you have a weapon equipped.",
- );
- }
- if (inputData.totalMatk < inputData.stats.int) {
- warnings.push("Your total MATK is less than your total INT.");
- }
- switch (inputData.wepType) {
- case WeaponType.OneHandedSword:
- case WeaponType.OneHandedAxe:
- case WeaponType.OneHandedMace:
- case WeaponType.TwoHandedSword:
- case WeaponType.TwoHandedAxe:
- case WeaponType.TwoHandedMace:
- case WeaponType.Spear:
- case WeaponType.Polearm: {
- if (
- inputData.mastery > 0.1 &&
- inputData.clazz !== Class.Warrior
- ) {
- warnings.push(
- `You have >10% mastery with a ${weaponTypeName(
- inputData.wepType,
- )}, but you\u{2019}re not a warrior.`,
- );
- }
- break;
- }
- case WeaponType.Dagger:
- case WeaponType.Claw: {
- if (
- inputData.mastery > 0.1 &&
- inputData.clazz !== Class.Rogue
- ) {
- warnings.push(
- `You have >10% mastery with a ${weaponTypeName(
- inputData.wepType,
- )}, but you\u{2019}re not a rogue.`,
- );
- }
- break;
- }
- case WeaponType.Bow:
- case WeaponType.Crossbow: {
- if (
- inputData.mastery > 0.1 &&
- inputData.clazz !== Class.Archer
- ) {
- warnings.push(
- `You have >10% mastery with a ${weaponTypeName(
- inputData.wepType,
- )}, but you\u{2019}re not an archer.`,
- );
- }
- break;
- }
- case WeaponType.Knuckler:
- case WeaponType.Gun: {
- if (
- inputData.mastery > 0.1 &&
- inputData.clazz !== Class.Pirate2nd
- ) {
- warnings.push(
- `You have >10% mastery with a ${weaponTypeName(
- inputData.wepType,
- )}, but you\u{2019}re not a \u{2265}2\u{207f}\u{1d48} \
- job pirate.`,
- );
- }
- break;
- }
- default:
- break;
- }
- if (
- inputData.mastery > 0.6 &&
- inputData.wepType !== WeaponType.Bow &&
- inputData.wepType !== WeaponType.Crossbow &&
- inputData.wepType !== WeaponType.Polearm &&
- inputData.wepType !== WeaponType.Spear &&
- inputData.wepType !== WeaponType.None
- ) {
- warnings.push(
- `You have >60% mastery with a ${weaponTypeName(
- inputData.wepType,
- )}.`,
- );
- }
- if (
- !(
- inputData.clazz === Class.Rogue &&
- inputData.wepType === WeaponType.Claw
- ) &&
- !(
- inputData.clazz === Class.Archer &&
- (inputData.wepType === WeaponType.Bow ||
- inputData.wepType === WeaponType.Crossbow)
- ) &&
- !(
- inputData.clazz === Class.Pirate2nd &&
- (inputData.wepType === WeaponType.None ||
- inputData.wepType === WeaponType.Knuckler)
- )
- ) {
- if (inputData.critProb > 0.15) {
- warnings.push(
- "You have a >15% probability of critting, but you do not \
- have a class & weapon combo that has access to crits. You \
- can only crit due to Sharp Eyes, which normally grants a \
- 15% crit probably at best.",
- );
- }
- if (inputData.critDmg > 1.4) {
- warnings.push(
- "You have a >140% probability of critical multi, but you \
- do not have a class & weapon combo that has access to \
- crits. You can only crit due to Sharp Eyes, which \
- normally grants a 140% critical multi at best.",
- );
- }
- }
- const badWeps = BAD_WEPS.get(inputData.clazz);
- if (badWeps === undefined) {
- console.error(
- `Logic error: ${inputData.clazz} is not a key in BAD_WEPS`,
- );
- } else if (badWeps.has(inputData.wepType)) {
- switch (inputData.wepType) {
- case WeaponType.None: {
- warnings.push(
- `You\u{2019}re not wielding a weapon, but ${className(
- inputData.clazz,
- )}s normally cannot attack that way.`,
- );
- break;
- }
- case WeaponType.Staff: {
- warnings.push(
- `You\u{2019}re wielding a staff, but staves that are \
- equippable by ${className(
- inputData.clazz,
- )}s don\u{2019}t usually exist.`,
- );
- break;
- }
- case WeaponType.TwoHandedAxe:
- case WeaponType.Bow:
- case WeaponType.Crossbow:
- case WeaponType.Knuckler:
- case WeaponType.Gun: {
- const wepName = weaponTypeName(inputData.wepType);
- warnings.push(
- `You\u{2019}re wielding a ${wepName}, but \
- ${wepName}s that are equippable by ${className(
- inputData.clazz,
- )}s don\u{2019}t usually exist.`,
- );
- break;
- }
- default:
- break;
- }
- }
- switch (inputData.clazz) {
- case Class.Beginner: {
- if (inputData.skillDmgMulti !== 1) {
- warnings.push(
- "Your damage multi \u{2260}100%, but you\u{2019}re a \
- beginner.",
- );
- }
- if (inputData.skillLines !== 1) {
- warnings.push(
- "You\u{2019}re attacking with a number of lines \
- \u{2260}1, but you\u{2019}re a beginner.",
- );
- }
- break;
- }
- case Class.Warrior: {
- if (inputData.stats.str < 35) {
- warnings.push(
- "Your total STR <35, but you\u{2019}re a warrior.",
- );
- }
- if (inputData.skillDmgMulti !== 1) {
- if (inputData.wepType === WeaponType.Claw) {
- warnings.push(
- "Your damage multi \u{2260}100%, but \
- you\u{2019}re a warrior using a claw.",
- );
- }
- }
- break;
- }
- case Class.Magician: {
- if (inputData.stats.int < 20) {
- warnings.push(
- "Your total INT <20, but you\u{2019}re a magician.",
- );
- }
- if (
- inputData.skillDmgMulti !== 1 &&
- inputData.spell !== Spell.Heal
- ) {
- warnings.push(
- "Your damage multi \u{2260}100%, but you\u{2019}re a \
- magician who is not casting Heal.",
- );
- }
- break;
- }
- case Class.Archer: {
- if (inputData.stats.dex < 25) {
- warnings.push(
- "Your total DEX <25, but you\u{2019}re an archer.",
- );
- }
- if (inputData.skillDmgMulti !== 1) {
- if (
- inputData.wepType !== WeaponType.Bow &&
- inputData.wepType !== WeaponType.Crossbow
- ) {
- warnings.push(
- `Your damage multi \u{2260}100%, but \
- you\u{2019}re an archer using a ${weaponTypeName(
- inputData.wepType,
- )}.`,
- );
- }
- }
- break;
- }
- case Class.Rogue: {
- if (inputData.stats.dex < 25) {
- warnings.push(
- "Your total DEX <25, but you\u{2019}re a rogue.",
- );
- }
- break;
- }
- case Class.Pirate:
- case Class.Pirate2nd: {
- if (inputData.stats.dex < 20) {
- warnings.push(
- "Your total DEX <20, but you\u{2019}re a pirate.",
- );
- }
- if (inputData.skillDmgMulti !== 1) {
- if (inputData.wepType === WeaponType.Claw) {
- warnings.push(
- "Your damage multi \u{2260}100%, but \
- you\u{2019}re a pirate using a claw. \
- Claws\u{2019} interaction with Somersault \
- Kick/Aerial Strike is poorly understood.",
- );
- }
- }
- break;
- }
- }
- const attackReqs = ATTACK_REQS.get(inputData.attack);
- if (attackReqs === undefined) {
- console.error(
- `Logic error: ${inputData.attack} is not a key in ATTACK_REQS`,
- );
- } else {
- const [attackReqClasses, attackReqLvl, attackReqWepTypes] =
- attackReqs;
- if (!attackReqClasses.has(inputData.clazz)) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but you\u{2019}re not ${indefinite(
- Array.from(attackReqClasses).map(className).join("/"),
- )}.`,
- );
- }
- if (inputData.level < attackReqLvl) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but your level <${attackReqLvl}.`,
- );
- }
- if (!attackReqWepTypes.has(inputData.wepType)) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but you don\u{2019}t have ${indefinite(
- Array.from(attackReqWepTypes)
- .map(weaponTypeName)
- .join("/"),
- )} equipped.`,
- );
- }
- }
- if (
- inputData.spell !== Spell.Other &&
- inputData.clazz !== Class.Magician
- ) {
- warnings.push(
- "You have a specific spell selected, but you\u{2019}re not a \
- magician.",
- );
- }
- const spellLvlReq = SPELL_LVL_REQS.get(inputData.spell);
- if (spellLvlReq === undefined) {
- console.error(
- `Logic error: ${inputData.spell} is not a key in \
- SPELL_LVL_REQS`,
- );
- } else if (inputData.level < spellLvlReq) {
- warnings.push(
- `You\u{2019}re casting ${spellName(
- inputData.spell,
- )}, but your level <${spellLvlReq}.`,
- );
- }
- if (
- inputData.speed > Speed.Fast4 &&
- inputData.wepType === WeaponType.None
- ) {
- warnings.push(
- "You have no weapon equipped, but your speed >4. Bare fists \
- have speed 4 when unbuffed.",
- );
- }
- if (inputData.spellBooster !== 0) {
- if (isHolySpell(inputData.spell)) {
- warnings.push(
- "Your spell booster value is nonzero, but you\u{2019}re \
- casting a cleric/priest/bishop spell.",
- );
- }
- if (
- inputData.wepType !== WeaponType.Wand &&
- inputData.wepType !== WeaponType.Staff
- ) {
- warnings.push(
- "Your spell booster value is nonzero, but you \
- don\u{2019}t have a wand/staff equipped (Spell Booster \
- doesn\u{2019}t work with swords).",
- );
- }
- if (inputData.clazz !== Class.Magician) {
- warnings.push(
- "Your spell booster value is nonzero, but you\u{2019}re \
- not a magician.",
- );
- }
- if (inputData.spellBooster < -1 && inputData.level < 75) {
- warnings.push(
- "Your spell booster value <\u{2212}1, but your level <75.",
- );
- } else if (inputData.level < 71) {
- warnings.push(
- "Your spell booster value is nonzero, but your level <71.",
- );
- }
- }
- if (inputData.eleAmp !== 1) {
- if (inputData.clazz !== Class.Magician) {
- warnings.push(
- "Your element amplification >100%, but you\u{2019}re not \
- a magician.",
- );
- }
- if (inputData.eleAmp > 1.5) {
- warnings.push(
- "Your element amplification >150%, but Element \
- Amplification usually goes up to 150% at best.",
- );
- }
- if (isHolySpell(inputData.spell)) {
- warnings.push(
- "Your element amplification >100%, but you\u{2019}re \
- casting a cleric/priest/bishop spell.",
- );
- }
- if (inputData.level < 70) {
- warnings.push(
- "Your element amplification >100%, but your level <70.",
- );
- }
- }
- const jobLvlReq = JOB_LVL_REQS.get(inputData.clazz);
- if (jobLvlReq === undefined) {
- console.error(
- `Logic error: ${inputData.clazz} is not a key in JOB_LVL_REQS`,
- );
- } else if (inputData.level < jobLvlReq) {
- warnings.push(
- `You\u{2019}re ${indefinite(
- className(inputData.clazz),
- )}, but your level <${jobLvlReq}.`,
- );
- }
- const attackLines = ATTACK_LINES.get(inputData.attack);
- if (attackLines === undefined) {
- console.error(
- `Logic error: ${inputData.attack} is not a key in \
- ATTACK_LINES`,
- );
- } else {
- const [minLines, maxLines, maxTargets] = attackLines;
- if (inputData.skillLines < minLines) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but its number of lines <${minLines}.`,
- );
- }
- if (inputData.skillLines > maxLines) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but its number of lines >${maxLines}.`,
- );
- }
- if (inputData.enemyCount > maxTargets) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but your number of targets >${maxTargets}.`,
- );
- }
- }
- const spellLines = SPELL_LINES.get(inputData.spell);
- if (spellLines === undefined) {
- console.error(
- `Logic error: ${inputData.spell} is not a key in SPELL_LINES`,
- );
- } else {
- const [minLines, maxLines, maxTargets] = spellLines;
- if (inputData.skillLines < minLines) {
- warnings.push(
- `You\u{2019}re casting ${spellName(
- inputData.spell,
- )}, but its number of lines <${minLines}.`,
- );
- }
- if (inputData.skillLines > maxLines) {
- warnings.push(
- `You\u{2019}re casting ${spellName(
- inputData.spell,
- )}, but its number of lines >${maxLines}.`,
- );
- }
- if (inputData.enemyCount > maxTargets) {
- warnings.push(
- `You\u{2019}re casting ${spellName(
- inputData.spell,
- )}, but your number of targets >${maxTargets}.`,
- );
- }
- }
- if (
- inputData.attack === Attack.SomersaultKick ||
- inputData.attack === Attack.AerialStrike
- ) {
- if (
- inputData.attack === Attack.SomersaultKick &&
- inputData.wepType !== WeaponType.None &&
- inputData.wepType !== WeaponType.Knuckler
- ) {
- warnings.push(
- "You\u{2019}re attacking with Somersault Kick, and have a \
- weapon equipped that isn\u{2019}t a knuckler; the attack \
- period is given on a best-effort basis that may or may \
- not be accurate.",
- );
- }
- switch (inputData.wepType) {
- case WeaponType.Bow:
- case WeaponType.Crossbow:
- case WeaponType.Claw:
- case WeaponType.Gun:
- warnings.push(
- `You\u{2019}re using ${attackName(
- inputData.attack,
- )} with ${indefinite(
- weaponTypeName(inputData.wepType),
- )} equipped; the damage calculation is done on a \
- best-effort basis that may or may not be accurate.`,
- );
- break;
- default:
- break;
- }
- }
- if (inputData.attack === Attack.BowWhack) {
- warnings.push(
- "You\u{2019}re whacking with a (cross)bow; the damage \
- calculation is done on a best-effort basis that may or may \
- not be accurate.",
- );
- if (inputData.skillDmgMulti !== 1) {
- warnings.push(
- "You\u{2019}re whacking with a (cross)bow, but your \
- damage multi \u{2260}100%. Maybe you meant to use Power \
- Knock-Back?",
- );
- }
- }
- if (
- inputData.attack === Attack.ClawPunch &&
- inputData.skillDmgMulti !== 1
- ) {
- warnings.push(
- "You\u{2019}re punching with a claw, but your damage multi \
- \u{2260}100%.",
- );
- }
- if (inputData.attack === Attack.Gaviota) {
- warnings.push(
- "Gaviota\u{2019}s attack period is based on an idealization; \
- actual usage will almost certainly have a larger attack \
- period and thus lower DPS.",
- );
- }
- if (inputData.attack === Attack.WrathOfTheOctopi) {
- warnings.push(
- "The attack period for Wrath of the Octopi is unmeasured, so \
- the slower attack period of Octopus (the skill which it \
- upgrades) is used instead.",
- );
- }
- if (inputData.caActive) {
- if (inputData.clazz !== Class.Warrior) {
- warnings.push(
- "You have (Advanced) Combo Attack active, but \
- you\u{2019}re not a warrior.",
- );
- }
- if (inputData.level < 70) {
- warnings.push(
- "You have (Advanced) Combo Attack active, but your level \
- <70.",
- );
- }
- if (1 + (inputData.level - 70) * 3 < inputData.caLevel) {
- warnings.push(
- `You have ${inputData.caLevel} SP in the Combo Attack \
- skill, but you\u{2019}re not high enough level to have \
- that many third job SP.`,
- );
- }
- if (inputData.caOrbs > 5 && inputData.level < 120) {
- warnings.push(
- "You have >5 (Advanced) Combo Attack orbs, but your level \
- <120.",
- );
- }
- } else {
- if (
- inputData.attack === Attack.Panic ||
- inputData.attack === Attack.Coma
- ) {
- warnings.push(
- `You\u{2019}re attacking with ${attackName(
- inputData.attack,
- )}, but (Advanced) Combo Attack is inactive.`,
- );
- }
- }
- if (inputData.zerkActive) {
- if (inputData.clazz !== Class.Warrior) {
- warnings.push(
- "You have Berserk active, but you\u{2019}re not a \
- warrior.",
- );
- }
- if (inputData.level < 120) {
- warnings.push(
- "You have Berserk active, but you\u{2019}re not high \
- enough level to have access to that skill.",
- );
- }
- }
- if (inputData.hitOrd > inputData.enemyCount) {
- warnings.push(
- "The ordinal # of your hit is greater than the total number \
- of enemies being targeted.",
- );
- }
- function delayWarn(jobName: string) {
- const atkName = attackName(inputData.attack);
- warnings.push(
- `You\u{2019}re attacking with ${atkName}; the attack period \
- (and thus DPS value) is based on the spamming of solely \
- ${atkName}. The projected DPS will thus be less than that of \
- a hypothetical ${jobName}, who would be attacking in between \
- ${atkName}s.`,
- );
- }
- switch (inputData.attack) {
- case Attack.NinjaStorm:
- delayWarn("nightlord");
- break;
- case Attack.BoomerangStep:
- delayWarn("shadower");
- break;
- case Attack.BackspinBlow:
- case Attack.DoubleUppercut:
- delayWarn("brawler");
- break;
- case Attack.EnergyBlast:
- delayWarn("marauder");
- break;
- case Attack.DragonStrike:
- case Attack.Snatch:
- case Attack.Barrage:
- delayWarn("buccaneer");
- break;
- case Attack.RecoilShot:
- delayWarn("gunslinger");
- break;
- case Attack.Flamethrower:
- case Attack.IceSplitter:
- delayWarn("outlaw");
- break;
- case Attack.AerialStrike:
- delayWarn("corsair");
- break;
- }
- if (
- inputData.attack === Attack.Flamethrower ||
- inputData.attack === Attack.Inferno
- ) {
- warnings.push(
- `The damage calculation used here for ${inputData.attack} \
- does not take into account the flaming/burning damage over \
- time.`,
- );
- }
- if (inputData.echo !== 0 && inputData.echo !== 4 / 100) {
- warnings.push(
- "You have specified a nonzero value for Echo of Hero that is \
- not exactly 4%.",
- );
- }
- switch (inputData.eleChargeType) {
- case ChargeType.None: {
- if (inputData.eleChargeDmg !== 1) {
- warnings.push(
- "You have no elemental charge, but your elemental \
- charge damage \u{2260}100%.",
- );
- }
- break;
- }
- case ChargeType.Holy: {
- if (
- 1 + (inputData.level - 120) * 3 <
- inputData.eleChargeLevel
- ) {
- warnings.push(
- `You have level ${inputData.eleChargeLevel} \
- Holy/Divine Charge selected, but you aren\u{2019}t a \
- high enough level to have that much fourth job SP.`,
- );
- }
- if (inputData.eleChargeLevel > 20) {
- warnings.push("You have a Holy/Divine Charge level >20.");
- }
- break;
- }
- case ChargeType.Other: {
- if (
- 1 + (inputData.level - 70) * 3 <
- inputData.eleChargeLevel
- ) {
- warnings.push(
- `You have a level ${inputData.eleChargeLevel} \
- non-Holy/Divine Charge selected, but you \
- aren\u{2019}t a high enough level to have that much \
- third job SP.`,
- );
- }
- break;
- }
- }
- if (inputData.eleChargeType !== ChargeType.None) {
- if (inputData.clazz !== Class.Warrior) {
- warnings.push(
- "You have an elemental charge selected, but you\u{2019}re \
- not a warrior.",
- );
- }
- switch (inputData.wepType) {
- case WeaponType.OneHandedSword:
- case WeaponType.OneHandedMace:
- case WeaponType.TwoHandedSword:
- case WeaponType.TwoHandedMace:
- break;
- default:
- warnings.push(
- "You have an elemental charge selected, but \
- you\u{2019}re not using a sword nor a blunt weapon.",
- );
- break;
- }
- }
- if (inputData.eleBoost !== 0) {
- if (inputData.clazz !== Class.Pirate2nd) {
- warnings.push(
- "Your Elemental Boost \u{2260}0%, but you\u{2019}re not a \
- \u{2265}2\u{207f}\u{1d48} job pirate.",
- );
- }
- if (inputData.level < 120) {
- warnings.push(
- "Your Elemental Boost \u{2260}0%, but your level <120.",
- );
- }
- }
- if (inputData.eleWep !== 0) {
- if (
- inputData.wepType !== WeaponType.Wand &&
- inputData.wepType !== WeaponType.Staff
- ) {
- warnings.push(
- "You are getting a nonzero elemental bonus from your \
- weapon, but you aren\u{2019}t wielding a wand nor a \
- staff.",
- );
- } else if (inputData.clazz !== Class.Magician) {
- warnings.push(
- `You\u{2019}re using an Elemental ${
- inputData.wepType === WeaponType.Wand
- ? "Wand"
- : "Staff"
- }, but you\u{2019}re not a magician.`,
- );
- }
- if (inputData.eleWep !== 25 / 100) {
- warnings.push(
- "Your Elemental Wand/Staff is giving an elemental bonus \
- \u{2260}25%.",
- );
- }
- }
- switch (inputData.wepType) {
- case WeaponType.OneHandedAxe:
- case WeaponType.TwoHandedAxe:
- case WeaponType.OneHandedMace:
- case WeaponType.TwoHandedMace:
- case WeaponType.Polearm: {
- if (inputData.goodAnimProb !== 60 / 100) {
- warnings.push(
- `You\u{2019}re using ${indefinite(
- weaponTypeName(inputData.wepType),
- )}, but your good animation probability \u{2260}60%. \
- It is currently suspected that the actual probability \
- is 60% in pre-BB GMS.`,
- );
- }
- break;
- }
- case WeaponType.Spear: {
- if (inputData.goodAnimProb !== 40 / 100) {
- warnings.push(
- "You\u{2019}re using a spear, but your good \
- animation probability \u{2260}40%. It is currently \
- suspected that the actual probability is 40% in \
- pre-BB GMS.",
- );
- }
- break;
- }
- case WeaponType.Wand:
- case WeaponType.Staff: {
- if (inputData.goodAnimProb !== 1) {
- warnings.push(
- `You\u{2019}re using ${indefinite(
- weaponTypeName(inputData.wepType),
- )}, but your good animation probability \u{2260}100%.
- It is currently suspected that the actual probability
- is 100% in pre-BB GMS.`,
- );
- }
- break;
- }
- default:
- break;
- }
- /*======== Remove old warnings display ========*/
- {
- const warningsElem = document.getElementById("warnings");
- if (warningsElem) {
- warningsDiv.removeChild(warningsElem);
- }
- }
- /*======== Display warnings ========*/
- if (warnings.length === 0) {
- const warningsSpan = document.createElement("span");
- warningsSpan.id = "warnings";
- warningsSpan.classList.add("success");
- const warningsTextNode = document.createTextNode("No warnings.");
- warningsSpan.appendChild(warningsTextNode);
- warningsDiv.appendChild(warningsSpan);
- } else {
- const warningsUl = document.createElement("ul");
- warningsUl.id = "warnings";
- warningsUl.classList.add("warning");
- for (const warningText of warnings) {
- const warningLi = document.createElement("li");
- const warningTextNode = document.createTextNode(warningText);
- warningLi.appendChild(warningTextNode);
- warningsUl.appendChild(warningLi);
- }
- warningsDiv.appendChild(warningsUl);
- }
- }
- saveAsButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- const saveAsInputLabel = document.createElement("label");
- saveAsInputLabel.htmlFor = "save-as-input";
- saveAsInputLabel.append(document.createTextNode("New name: "));
- const saveAsInput = document.createElement("input");
- saveAsInput.id = "save-as-input";
- saveAsInput.name = "save-as-input";
- saveAsInput.type = "text";
- saveAsInputLabel.append(saveAsInput);
- saveLoadDialog.append(saveAsInputLabel);
- const saveAsSaveButton = document.createElement("button");
- saveAsSaveButton.type = "button";
- saveAsSaveButton.append(document.createTextNode("Save"));
- saveLoadDialog.append(saveAsSaveButton);
- saveLoadDialog.append(document.createTextNode(" "));
- const saveAsCancelButton = document.createElement("button");
- saveAsCancelButton.type = "button";
- saveAsCancelButton.append(document.createTextNode("Cancel"));
- saveLoadDialog.append(saveAsCancelButton);
- saveAsSaveButton.addEventListener("click", () => {
- if (!saveAsInput.value.trim()) {
- return;
- }
- const keyName = `dmg-calc.saved.${saveAsInput.value.trim()}`;
- if (
- window.localStorage.getItem(keyName) &&
- !window.confirm(
- "An entry is already saved with this name. Are you sure that you want to overwrite it?",
- )
- ) {
- return;
- }
- window.localStorage.setItem(
- keyName,
- JSON.stringify(readInputData()),
- );
- currentlyLoaded.replaceChildren();
- currentlyLoaded.append(
- document.createTextNode(saveAsInput.value.trim()),
- );
- saveLoadDialog.replaceChildren();
- });
- saveAsCancelButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- });
- saveAsInput.focus();
- });
- saveButton.addEventListener("click", () => {
- const cln = currentlyLoadedName();
- if (cln) {
- window.localStorage.setItem(
- `dmg-calc.saved.${cln}`,
- JSON.stringify(readInputData()),
- );
- } else {
- window.alert(
- "Nothing was saved. Try using \u{201c}Save as\u{201d}, instead.",
- );
- }
- });
- loadButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- const loadSelectLabel = document.createElement("label");
- loadSelectLabel.htmlFor = "load-select";
- loadSelectLabel.append(document.createTextNode("Name: "));
- const loadSelect = document.createElement("select");
- loadSelect.id = "load-select";
- loadSelect.name = "load-select";
- for (let i = 0; i < window.localStorage.length; ++i) {
- const key = window.localStorage.key(i);
- if (key === null || !key.startsWith("dmg-calc.saved.")) {
- continue;
- }
- const name = key.slice("dmg-calc.saved.".length);
- const opt = document.createElement("option");
- opt.value = key;
- opt.append(document.createTextNode(name));
- loadSelect.append(opt);
- }
- loadSelectLabel.append(loadSelect);
- saveLoadDialog.append(loadSelectLabel);
- const loadLoadButton = document.createElement("button");
- loadLoadButton.type = "button";
- loadLoadButton.append(document.createTextNode("Load"));
- saveLoadDialog.append(loadLoadButton);
- saveLoadDialog.append(document.createTextNode(" "));
- const loadCancelButton = document.createElement("button");
- loadCancelButton.type = "button";
- loadCancelButton.append(document.createTextNode("Cancel"));
- saveLoadDialog.append(loadCancelButton);
- loadLoadButton.addEventListener("click", () => {
- const val = window.localStorage.getItem(loadSelect.value);
- if (
- !loadSelect.value ||
- !val ||
- !window.confirm(
- "Loading will overwrite the current working state, causing you to lose any unsaved changes. Are you sure that you still want to load?",
- )
- ) {
- return;
- }
- let inputData: InputData = JSON.parse(val);
- writeInputData(inputData);
- currentlyLoaded.replaceChildren();
- currentlyLoaded.append(
- document.createTextNode(
- loadSelect.value.slice("dmg-calc.saved.".length),
- ),
- );
- saveLoadDialog.replaceChildren();
- recalculate();
- });
- loadCancelButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- });
- loadSelect.focus();
- });
- deleteButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- const deleteSelectLabel = document.createElement("label");
- deleteSelectLabel.htmlFor = "delete-select";
- deleteSelectLabel.append(document.createTextNode("Name: "));
- const deleteSelect = document.createElement("select");
- deleteSelect.id = "delete-select";
- deleteSelect.name = "delete-select";
- for (let i = 0; i < window.localStorage.length; ++i) {
- const key = window.localStorage.key(i);
- if (key === null || !key.startsWith("dmg-calc.saved.")) {
- continue;
- }
- const name = key.slice("dmg-calc.saved.".length);
- const opt = document.createElement("option");
- opt.value = key;
- opt.append(document.createTextNode(name));
- deleteSelect.append(opt);
- }
- deleteSelectLabel.append(deleteSelect);
- saveLoadDialog.append(deleteSelectLabel);
- const deleteDeleteButton = document.createElement("button");
- deleteDeleteButton.type = "button";
- deleteDeleteButton.append(document.createTextNode("Delete"));
- saveLoadDialog.append(deleteDeleteButton);
- saveLoadDialog.append(document.createTextNode(" "));
- const deleteCancelButton = document.createElement("button");
- deleteCancelButton.type = "button";
- deleteCancelButton.append(document.createTextNode("Cancel"));
- saveLoadDialog.append(deleteCancelButton);
- deleteDeleteButton.addEventListener("click", () => {
- const val = window.localStorage.getItem(deleteSelect.value);
- const name = deleteSelect.value.slice("dmg-calc.saved.".length);
- if (
- !deleteSelect.value ||
- !val ||
- !window.confirm(
- `Are you sure that you want to delete \u{201c}${name}\u{201d} permanently?`,
- )
- ) {
- return;
- }
- window.localStorage.removeItem(deleteSelect.value);
- const cln = currentlyLoadedName();
- if (cln === name) {
- currentlyLoaded.replaceChildren();
- }
- saveLoadDialog.replaceChildren();
- });
- deleteCancelButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- });
- deleteSelect.focus();
- });
- exportButton.addEventListener("click", () => {
- const anchor = document.createElement("a");
- anchor.href = URL.createObjectURL(
- new Blob([JSON.stringify(readInputData()) + "\n"], {
- type: "application/json",
- }),
- );
- const cln = currentlyLoadedName();
- anchor.download = cln
- ? cln.replaceAll(/\s/gi, "_").replaceAll(/[^a-z0-9_\.\-]/gi, "-") +
- ".json"
- : "dmg-calc-input.json";
- anchor.click();
- });
- importButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- const importInputLabel = document.createElement("label");
- importInputLabel.htmlFor = "import-input";
- importInputLabel.append(
- document.createTextNode("Choose a JSON file: "),
- );
- const importInput = document.createElement("input");
- importInput.type = "file";
- importInput.id = "import-input";
- importInput.name = "import-input";
- importInput.accept = ".json,application/json,text/plain,text/json";
- importInputLabel.append(importInput);
- saveLoadDialog.append(importInputLabel);
- const importImportButton = document.createElement("button");
- importImportButton.type = "button";
- importImportButton.append(document.createTextNode("Import"));
- saveLoadDialog.append(importImportButton);
- saveLoadDialog.append(document.createTextNode(" "));
- const importCancelButton = document.createElement("button");
- importCancelButton.type = "button";
- importCancelButton.append(document.createTextNode("Cancel"));
- saveLoadDialog.append(importCancelButton);
- importImportButton.addEventListener("click", () => {
- const files = importInput.files;
- if (files === null) {
- return;
- }
- const file = files[0];
- if (
- !file ||
- !window.confirm(
- "Importing will overwrite the current working state, causing you to lose any unsaved changes. Are you sure that you still want to import?",
- )
- ) {
- return;
- }
- file.text().then(s => {
- let inputData: InputData = JSON.parse(s);
- writeInputData(inputData);
- currentlyLoaded.replaceChildren();
- });
- saveLoadDialog.replaceChildren();
- recalculate();
- });
- importCancelButton.addEventListener("click", () => {
- saveLoadDialog.replaceChildren();
- });
- importInput.focus();
- });
- for (const input of [
- strInput,
- dexInput,
- intInput,
- lukInput,
- totalWatkInput,
- totalMatkInput,
- echoInput,
- masteryInput,
- skillDmgMultiInput,
- skillBasicAtkInput,
- skillLinesInput,
- critProbInput,
- critDmgInput,
- classInput,
- levelInput,
- weaponTypeInput,
- goodAnimProbInput,
- attackInput,
- spellInput,
- speedInput,
- spellBoosterInput,
- eleAmpInput,
- eleBoostInput,
- eleWepInput,
- eleChargeDmgInput,
- eleChargeLevelInput,
- caActiveInput,
- caDmgInput,
- caLevelInput,
- caOrbsInput,
- zerkActiveInput,
- zerkDmgInput,
- enemyWdefInput,
- enemyMdefInput,
- eleSusInput,
- enemyLevelInput,
- enemyCountInput,
- hitOrdInput,
- ]) {
- input.addEventListener("change", recalculate);
- }
- for (const eleChargeInput of eleChargeInputs) {
- eleChargeInput.addEventListener("change", () =>
- eleChargeInput.checked ? recalculate() : undefined,
- );
- }
- recalculate();
- }
- function dmgMulti(inputData: InputData, crit: boolean): number {
- return (
- inputData.skillDmgMulti +
- (crit ? inputData.critDmg : 0) +
- (inputData.attack === Attack.Flamethrower ||
- inputData.attack === Attack.IceSplitter
- ? inputData.eleBoost
- : 0)
- );
- }
- function attackEffectiveEleSus(inputData: InputData): number {
- if (!attackIsElemental(inputData.attack)) {
- return 1;
- }
- return inputData.eleSus;
- }
- function maxDmgPhys(inputData: InputData, goodAnim: boolean): number {
- return (
- ((primaryStat(
- inputData.stats,
- inputData.wepType,
- goodAnim,
- inputData.clazz,
- ) +
- secondaryStat(
- inputData.stats,
- inputData.wepType,
- inputData.clazz,
- )) *
- effectiveWatk(inputData)) /
- 100
- );
- }
- function minDmgPhys(inputData: InputData, goodAnim: boolean): number {
- return (
- ((primaryStat(
- inputData.stats,
- inputData.wepType,
- goodAnim,
- inputData.clazz,
- ) *
- 0.9 *
- effectiveMastery(inputData) +
- secondaryStat(
- inputData.stats,
- inputData.wepType,
- inputData.clazz,
- )) *
- effectiveWatk(inputData)) /
- 100
- );
- }
- function afterModPhys(inputData: InputData): number {
- const hitOrdAfterMod = (() => {
- switch (inputData.attack) {
- case Attack.IronArrow:
- return 0.9 ** (inputData.hitOrd - 1);
- case Attack.PiercingArrow:
- return 1.2 ** (inputData.hitOrd - 1);
- case Attack.EnergyOrb:
- return (2 / 3) ** (inputData.hitOrd - 1);
- default:
- return 1;
- }
- })();
- return inputData.zerkActive
- ? hitOrdAfterMod * inputData.zerkDmg
- : hitOrdAfterMod;
- }
- function afterModBarrage(ord: number): number {
- return 2 ** Math.max(ord - 4, 0);
- }
- function barrageEffectiveMulti(lines: number): number {
- return Math.min(lines + 1, 4) + 2 ** Math.max(lines - 3, 0) - 2;
- }
- function caModifier(inputData: InputData): number {
- if (!inputData.caActive) {
- return 1;
- }
- if (inputData.caOrbs < 6) {
- return (
- (inputData.caDmg +
- Math.floor((inputData.caOrbs - 1) * (inputData.caLevel / 6))) /
- 100
- );
- }
- return (inputData.caDmg + 20 + (inputData.caOrbs - 5) * 4) / 100;
- }
- function eleChargeModifier(inputData: InputData): number {
- switch (inputData.eleChargeType) {
- case ChargeType.None:
- return 1;
- case ChargeType.Holy: {
- if (inputData.eleSus === 1) {
- return inputData.eleChargeDmg;
- } else if (inputData.eleSus === 0) {
- return 0;
- } else if (inputData.eleSus < 1) {
- return (
- (inputData.eleChargeDmg *
- (80 - inputData.eleChargeLevel * 1.5)) /
- 100
- );
- }
- return (
- (inputData.eleChargeDmg *
- (120 + inputData.eleChargeLevel * 1.5)) /
- 100
- );
- }
- case ChargeType.Other: {
- if (inputData.eleSus === 1) {
- return inputData.eleChargeDmg;
- } else if (inputData.eleSus === 0) {
- return 0;
- } else if (inputData.eleSus < 1) {
- return (
- (inputData.eleChargeDmg *
- (95 - inputData.eleChargeLevel * 1.5)) /
- 100
- );
- }
- return (
- (inputData.eleChargeDmg *
- (105 + inputData.eleChargeLevel * 1.5)) /
- 100
- );
- }
- }
- }
- function maxDmgBowWhack(inputData: InputData): number {
- return (
- ((inputData.stats.dex * 3.4 + inputData.stats.str) *
- effectiveWatk(inputData)) /
- 150
- );
- }
- function minDmgBowWhack(inputData: InputData): number {
- return (
- ((inputData.stats.dex * 3.4 * 0.1 * 0.9 + inputData.stats.str) *
- effectiveWatk(inputData)) /
- 150
- );
- }
- function maxDmgClawPunch(inputData: InputData): number {
- return (
- ((inputData.stats.luk + inputData.stats.str + inputData.stats.dex) *
- effectiveWatk(inputData)) /
- 150
- );
- }
- function minDmgClawPunch(inputData: InputData): number {
- return (
- ((inputData.stats.luk * 0.1 +
- inputData.stats.str +
- inputData.stats.dex) *
- effectiveWatk(inputData)) /
- 150
- );
- }
- function orbMulti(inputData: InputData): number {
- switch (inputData.caOrbs) {
- case 1:
- return 1;
- case 2:
- return 1.2;
- case 3:
- return 1.54;
- case 4:
- return 2;
- default:
- return 2.5;
- }
- }
- function maxDmgCaFinisher(inputData: InputData, goodAnim: boolean): number {
- return maxDmgPhys(inputData, goodAnim) * orbMulti(inputData);
- }
- function minDmgCaFinisher(inputData: InputData, goodAnim: boolean): number {
- return minDmgPhys(inputData, goodAnim) * orbMulti(inputData);
- }
- function minDmgHhXiuz(inputData: InputData): number {
- return maxDmgPhys(inputData, true) * 0.8;
- }
- function maxDmgDragonRoar(inputData: InputData): number {
- return (
- ((inputData.stats.str * 4 + inputData.stats.dex) *
- effectiveWatk(inputData)) /
- 100
- );
- }
- function minDmgDragonRoar(inputData: InputData): number {
- return (
- ((inputData.stats.str * 4 * inputData.mastery * 0.9 +
- inputData.stats.dex) *
- effectiveWatk(inputData)) /
- 100
- );
- }
- function maxDmgArrowBombImpact(inputData: InputData, crit: boolean): number {
- return 0.5 * maxDmgPhys(inputData, true) * (crit ? inputData.critDmg : 1);
- }
- function minDmgArrowBombImpact(inputData: InputData, crit: boolean): number {
- return 0.5 * minDmgPhys(inputData, true) * (crit ? inputData.critDmg : 1);
- }
- function maxDmgArrowBombSplash(inputData: InputData, crit: boolean): number {
- return (
- inputData.skillDmgMulti *
- maxDmgPhys(inputData, true) *
- (crit ? inputData.critDmg : 1)
- );
- }
- function minDmgArrowBombSplash(inputData: InputData, crit: boolean): number {
- return (
- inputData.skillDmgMulti *
- minDmgPhys(inputData, true) *
- (crit ? inputData.critDmg : 1)
- );
- }
- function maxDmgDexSummon(inputData: InputData): number {
- return (
- ((inputData.stats.dex * 2.5 + inputData.stats.str) *
- inputData.skillBasicAtk) /
- 100
- );
- }
- function minDmgDexSummon(inputData: InputData): number {
- return (
- ((inputData.stats.dex * 2.5 * 0.7 + inputData.stats.str) *
- inputData.skillBasicAtk) /
- 100
- );
- }
- function maxDmgLuckySeven(inputData: InputData): number {
- return (inputData.stats.luk * 5 * effectiveWatk(inputData)) / 100;
- }
- function minDmgLuckySeven(inputData: InputData): number {
- return (inputData.stats.luk * 2.5 * effectiveWatk(inputData)) / 100;
- }
- function dmgNinjaAmbush(inputData: InputData): number {
- return (
- 2 *
- (inputData.stats.str + inputData.stats.luk) *
- inputData.skillDmgMulti
- );
- }
- function maxDmgVenom(inputData: InputData): number {
- return (
- ((18.5 * (inputData.stats.str + inputData.stats.luk) +
- inputData.stats.dex * 2) /
- 100) *
- inputData.skillBasicAtk
- );
- }
- function minDmgVenom(inputData: InputData): number {
- return (
- ((8 * (inputData.stats.str + inputData.stats.luk) +
- inputData.stats.dex * 2) /
- 100) *
- inputData.skillBasicAtk
- );
- }
- function swingProbToGoodAnimProb(
- inputData: InputData,
- swingProb: number,
- ): number {
- switch (inputData.wepType) {
- case WeaponType.OneHandedAxe:
- case WeaponType.OneHandedMace:
- case WeaponType.Wand:
- case WeaponType.Staff:
- case WeaponType.TwoHandedAxe:
- case WeaponType.TwoHandedMace:
- case WeaponType.Polearm:
- return swingProb;
- default:
- return 1 - swingProb;
- }
- }
- function maxDmgMagic(inputData: InputData): number {
- const matk = effectiveMatk(inputData);
- return (
- ((matk ** 2 / 1000 + matk) / 30 + inputData.stats.int / 200) *
- inputData.skillBasicAtk
- );
- }
- function minDmgMagic(inputData: InputData): number {
- const matk = effectiveMatk(inputData);
- return (
- ((matk ** 2 / 1000 + matk * inputData.mastery * 0.9) / 30 +
- inputData.stats.int / 200) *
- inputData.skillBasicAtk
- );
- }
- function afterModMagic(inputData: InputData): number {
- switch (inputData.spell) {
- case Spell.ChainLightning:
- return 0.7 ** (inputData.hitOrd - 1);
- default:
- return 1;
- }
- }
- function healTargetMulti(enemyCount: number): number {
- return 1.5 + 5 / (enemyCount + 1);
- }
- function maxDmgHeal(inputData: InputData): number {
- return (
- (((inputData.stats.int * 1.2 + inputData.stats.luk) *
- effectiveMatk(inputData)) /
- 1000) *
- healTargetMulti(inputData.enemyCount) *
- inputData.skillDmgMulti
- );
- }
- function minDmgHeal(inputData: InputData): number {
- return (
- (((inputData.stats.int * 0.3 + inputData.stats.luk) *
- effectiveMatk(inputData)) /
- 1000) *
- healTargetMulti(inputData.enemyCount) *
- inputData.skillDmgMulti
- );
- }
- function adjustRangeForWdef(
- inputData: InputData,
- range: [number, number],
- ): [number, number] {
- const [min, max] = range;
- const levelDelta = Math.max(inputData.enemyLevel - inputData.level, 0);
- const levelDeltaSlope = 1 - 0.01 * levelDelta;
- return [
- min * levelDeltaSlope - inputData.enemyWdef * 0.6,
- max * levelDeltaSlope - inputData.enemyWdef * 0.5,
- ];
- }
- function adjustRangeForMdef(
- inputData: InputData,
- range: [number, number],
- ): [number, number] {
- const [min, max] = range;
- const levelDelta = Math.max(inputData.enemyLevel - inputData.level, 0);
- const levelDeltaSlope = 1 + 0.01 * levelDelta;
- return [
- min - inputData.enemyMdef * 0.6 * levelDeltaSlope,
- max - inputData.enemyMdef * 0.5 * levelDeltaSlope,
- ];
- }
- function effectiveMastery(inputData: InputData): number {
- if (inputData.wepType === WeaponType.None) {
- return 0.1;
- }
- return inputData.mastery;
- }
- function effectiveWatk(inputData: InputData): number {
- if (inputData.wepType === WeaponType.None) {
- switch (inputData.clazz) {
- case Class.Pirate:
- case Class.Pirate2nd: {
- const totalWatk =
- inputData.totalWatk +
- Math.min(Math.trunc((2 * inputData.level + 31) / 3), 31);
- return totalWatk + totalWatk * inputData.echo;
- }
- default:
- return 0;
- }
- }
- return inputData.totalWatk + inputData.totalWatk * inputData.echo;
- }
- function effectiveMatk(inputData: InputData): number {
- return inputData.totalMatk + inputData.totalMatk * inputData.echo;
- }
|