402 lines
12 KiB
TypeScript
402 lines
12 KiB
TypeScript
/*
|
|
* @licstart The following is the entire license notice for the JavaScript
|
|
* code in this page.
|
|
*
|
|
* This file is part of gish-ap-calc.
|
|
*
|
|
* gish-ap-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.
|
|
*
|
|
* gish-ap-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 gish-ap-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 { allocateAp } from "./lib.js";
|
|
import {
|
|
mDps,
|
|
meleePeriod,
|
|
mHitRate,
|
|
psm,
|
|
spellPeriod,
|
|
wacc,
|
|
wDps,
|
|
wHitRate,
|
|
} from "./mechanics.js";
|
|
import {
|
|
Monster,
|
|
Speed,
|
|
Spell,
|
|
SpellType,
|
|
Stats,
|
|
Weapon,
|
|
WeaponType,
|
|
} from "./types.js";
|
|
|
|
document.addEventListener("readystatechange", () => {
|
|
if (document.readyState === "complete") {
|
|
main();
|
|
}
|
|
});
|
|
|
|
function main(): void {
|
|
/**************** INPUTS ****************/
|
|
|
|
// Base stats
|
|
const strBaseInput = document.getElementById(
|
|
"str-base",
|
|
) as HTMLInputElement;
|
|
const dexBaseInput = document.getElementById(
|
|
"dex-base",
|
|
) as HTMLInputElement;
|
|
const intBaseInput = document.getElementById(
|
|
"int-base",
|
|
) as HTMLInputElement;
|
|
const lukBaseInput = document.getElementById(
|
|
"luk-base",
|
|
) as HTMLInputElement;
|
|
|
|
// Additional stats
|
|
const strAdditionalInput = document.getElementById(
|
|
"str-additional",
|
|
) as HTMLInputElement;
|
|
const dexAdditionalInput = document.getElementById(
|
|
"dex-additional",
|
|
) as HTMLInputElement;
|
|
const intAdditionalInput = document.getElementById(
|
|
"int-additional",
|
|
) as HTMLInputElement;
|
|
const lukAdditionalInput = document.getElementById(
|
|
"luk-additional",
|
|
) as HTMLInputElement;
|
|
const waccAdditionalInput = document.getElementById(
|
|
"wacc-additional",
|
|
) as HTMLInputElement;
|
|
const matkAdditionalInput = document.getElementById(
|
|
"matk-additional",
|
|
) as HTMLInputElement;
|
|
|
|
// Total WATK
|
|
const totalWatkInput = document.getElementById(
|
|
"total-watk",
|
|
) as HTMLInputElement;
|
|
|
|
// AP available
|
|
const apAvailableInput = document.getElementById(
|
|
"ap-available",
|
|
) as HTMLInputElement;
|
|
|
|
// Character
|
|
const levelInput = document.getElementById("level") as HTMLInputElement;
|
|
|
|
// Weapon/spell
|
|
const weaponTypeInput = document.getElementById(
|
|
"weapon-type",
|
|
) as HTMLSelectElement;
|
|
const speedInput = document.getElementById("speed") as HTMLSelectElement;
|
|
const spellInput = document.getElementById("spell") as HTMLSelectElement;
|
|
const spellBasicAtkInput = document.getElementById(
|
|
"spell-basic-atk",
|
|
) as HTMLInputElement;
|
|
const spellLinesInput = document.getElementById(
|
|
"spell-lines",
|
|
) as HTMLSelectElement;
|
|
const masteryInput = document.getElementById(
|
|
"mastery",
|
|
) as HTMLInputElement;
|
|
const spellBoosterInput = document.getElementById(
|
|
"spell-booster",
|
|
) as HTMLInputElement;
|
|
|
|
// Elemental stuff
|
|
const eleAmpInput = document.getElementById("ele-amp") as HTMLInputElement;
|
|
const eleWepInput = document.getElementById("ele-wep") as HTMLInputElement;
|
|
|
|
// Enemy
|
|
const wdefInput = document.getElementById(
|
|
"enemy-wdef",
|
|
) as HTMLInputElement;
|
|
const mdefInput = document.getElementById(
|
|
"enemy-mdef",
|
|
) as HTMLInputElement;
|
|
const avoidInput = document.getElementById(
|
|
"enemy-avoid",
|
|
) 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;
|
|
|
|
// wDominanceFactor
|
|
const wDominanceFactorInput = document.getElementById(
|
|
"w-dominance-factor",
|
|
) as HTMLInputElement;
|
|
|
|
/**************** OUTPUTS ****************/
|
|
|
|
// Stats
|
|
const strOutput = document.getElementById("str-output") as HTMLSpanElement;
|
|
const strBaseOutput = document.getElementById(
|
|
"str-base-output",
|
|
) as HTMLSpanElement;
|
|
const strAdditionalOutput = document.getElementById(
|
|
"str-additional-output",
|
|
) as HTMLSpanElement;
|
|
const dexOutput = document.getElementById("dex-output") as HTMLSpanElement;
|
|
const dexBaseOutput = document.getElementById(
|
|
"dex-base-output",
|
|
) as HTMLSpanElement;
|
|
const dexAdditionalOutput = document.getElementById(
|
|
"dex-additional-output",
|
|
) as HTMLSpanElement;
|
|
const intOutput = document.getElementById("int-output") as HTMLSpanElement;
|
|
const intBaseOutput = document.getElementById(
|
|
"int-base-output",
|
|
) as HTMLSpanElement;
|
|
const intAdditionalOutput = document.getElementById(
|
|
"int-additional-output",
|
|
) as HTMLSpanElement;
|
|
const lukOutput = document.getElementById("luk-output") as HTMLSpanElement;
|
|
const lukBaseOutput = document.getElementById(
|
|
"luk-base-output",
|
|
) as HTMLSpanElement;
|
|
const lukAdditionalOutput = document.getElementById(
|
|
"luk-additional-output",
|
|
) as HTMLSpanElement;
|
|
|
|
// Combat
|
|
const meleeDpsOutput = document.getElementById(
|
|
"melee-dps",
|
|
) as HTMLSpanElement;
|
|
const spellDpsOutput = document.getElementById(
|
|
"spell-dps",
|
|
) as HTMLSpanElement;
|
|
const meleeHitRateOutput = document.getElementById(
|
|
"melee-hit-rate",
|
|
) as HTMLSpanElement;
|
|
const spellHitRateOutput = document.getElementById(
|
|
"spell-hit-rate",
|
|
) as HTMLSpanElement;
|
|
|
|
function recalculate(): void {
|
|
const initialBaseStats = new Stats(
|
|
handleIntInput(strBaseInput, 4, 4),
|
|
handleIntInput(dexBaseInput, 4, 4),
|
|
handleIntInput(intBaseInput, 4, 4),
|
|
handleIntInput(lukBaseInput, 4, 4),
|
|
);
|
|
|
|
const level = handleIntInput(levelInput, 8, 30, 200);
|
|
|
|
const wepTypeInt = handleIntInput(weaponTypeInput, 30, 30, 44);
|
|
const wepType = (() => {
|
|
if (!(wepTypeInt in WeaponType)) {
|
|
weaponTypeInput.value = "30";
|
|
|
|
return WeaponType.OneHandedSword;
|
|
}
|
|
|
|
return wepTypeInt as WeaponType;
|
|
})();
|
|
const wepSpeed = handleIntInput(speedInput, 6, 2, 9) as Speed;
|
|
const weapon = new Weapon(
|
|
psm(wepType),
|
|
meleePeriod(wepType, wepSpeed),
|
|
);
|
|
|
|
const spellTypeInt = handleIntInput(spellInput, 0, 0, 2321008);
|
|
const spellType = (() => {
|
|
if (!(spellTypeInt in SpellType)) {
|
|
spellInput.value = "0";
|
|
|
|
return SpellType.Other;
|
|
}
|
|
|
|
return spellTypeInt as SpellType;
|
|
})();
|
|
const spellT = (() => {
|
|
let t = spellPeriod(
|
|
handleIntInput(spellBoosterInput, 0, -2, 0),
|
|
spellType,
|
|
wepSpeed,
|
|
);
|
|
if (t === undefined) {
|
|
console.error(
|
|
`spellPeriod(${handleIntInput(
|
|
spellBoosterInput,
|
|
0,
|
|
-2,
|
|
0,
|
|
)}, ${spellType}, ${wepSpeed}) is undefined`,
|
|
);
|
|
|
|
t = 0.81;
|
|
}
|
|
|
|
return t;
|
|
})();
|
|
const spell = new Spell(
|
|
handleIntInput(spellBasicAtkInput, 10, 1),
|
|
handleIntInput(masteryInput, 15, 10, 90) / 100,
|
|
spellT,
|
|
handleIntInput(spellLinesInput, 1, 1),
|
|
);
|
|
|
|
const totalWatk = handleIntInput(totalWatkInput, 1, 0);
|
|
const rawMatk = handleIntInput(matkAdditionalInput, 0, 0);
|
|
const rawWacc = handleIntInput(waccAdditionalInput, 0, 0);
|
|
|
|
const monster = new Monster(
|
|
handleIntInput(enemyLevelInput, 1, 1),
|
|
handleIntInput(avoidInput, 1, 1),
|
|
handleIntInput(wdefInput, 0),
|
|
handleIntInput(mdefInput, 0),
|
|
);
|
|
|
|
const [baseStats, totalStats] = allocateAp(
|
|
handleIntInput(apAvailableInput, 1, 1),
|
|
initialBaseStats,
|
|
new Stats(
|
|
handleIntInput(strAdditionalInput, 0, 0),
|
|
handleIntInput(dexAdditionalInput, 0, 0),
|
|
handleIntInput(intAdditionalInput, 0, 0),
|
|
handleIntInput(lukAdditionalInput, 0, 0),
|
|
).add(initialBaseStats),
|
|
level,
|
|
weapon,
|
|
spell,
|
|
totalWatk,
|
|
rawMatk,
|
|
rawWacc,
|
|
monster,
|
|
handleFloatInput(wDominanceFactorInput, 2, 1),
|
|
);
|
|
const additionalStats = totalStats.clone().sub(baseStats);
|
|
|
|
strOutput.textContent = "" + totalStats.str;
|
|
dexOutput.textContent = "" + totalStats.dex;
|
|
intOutput.textContent = "" + totalStats.int;
|
|
lukOutput.textContent = "" + totalStats.luk;
|
|
|
|
strBaseOutput.textContent = "" + baseStats.str;
|
|
dexBaseOutput.textContent = "" + baseStats.dex;
|
|
intBaseOutput.textContent = "" + baseStats.int;
|
|
lukBaseOutput.textContent = "" + baseStats.luk;
|
|
|
|
strAdditionalOutput.textContent = "" + additionalStats.str;
|
|
dexAdditionalOutput.textContent = "" + additionalStats.dex;
|
|
intAdditionalOutput.textContent = "" + additionalStats.int;
|
|
lukAdditionalOutput.textContent = "" + additionalStats.luk;
|
|
|
|
const d = Math.max(monster.level - level, 0);
|
|
|
|
meleeDpsOutput.textContent = wDps(
|
|
totalStats,
|
|
rawWacc,
|
|
totalWatk,
|
|
weapon,
|
|
monster,
|
|
d,
|
|
).toFixed(3);
|
|
spellDpsOutput.textContent = mDps(
|
|
totalStats,
|
|
rawMatk,
|
|
spell,
|
|
monster,
|
|
d,
|
|
).toFixed(3);
|
|
|
|
meleeHitRateOutput.textContent = (
|
|
100 *
|
|
wHitRate(
|
|
wacc(totalStats.dex, totalStats.luk, rawWacc),
|
|
monster.avoid,
|
|
d,
|
|
)
|
|
).toFixed(2);
|
|
spellHitRateOutput.textContent = (
|
|
100 * mHitRate(totalStats.int, totalStats.luk, monster.avoid, d)
|
|
).toFixed(2);
|
|
}
|
|
|
|
for (const input of [
|
|
strBaseInput,
|
|
dexBaseInput,
|
|
intBaseInput,
|
|
lukBaseInput,
|
|
strAdditionalInput,
|
|
dexAdditionalInput,
|
|
intAdditionalInput,
|
|
lukAdditionalInput,
|
|
waccAdditionalInput,
|
|
matkAdditionalInput,
|
|
totalWatkInput,
|
|
apAvailableInput,
|
|
levelInput,
|
|
weaponTypeInput,
|
|
speedInput,
|
|
spellInput,
|
|
spellBasicAtkInput,
|
|
spellLinesInput,
|
|
masteryInput,
|
|
spellBoosterInput,
|
|
eleAmpInput,
|
|
eleWepInput,
|
|
wdefInput,
|
|
mdefInput,
|
|
avoidInput,
|
|
eleSusInput,
|
|
enemyLevelInput,
|
|
enemyCountInput,
|
|
wDominanceFactorInput,
|
|
]) {
|
|
input.addEventListener("change", recalculate);
|
|
}
|
|
|
|
recalculate();
|
|
}
|
|
|
|
function handleIntInput(
|
|
input: HTMLInputElement | HTMLSelectElement,
|
|
def: number,
|
|
min: number = Number.NEGATIVE_INFINITY,
|
|
max: number = Number.POSITIVE_INFINITY,
|
|
): number {
|
|
let x = Math.min(Math.max(parseInt(input.value, 10), min), max);
|
|
if (!Number.isFinite(x)) {
|
|
x = def;
|
|
}
|
|
input.value = "" + x;
|
|
|
|
return x;
|
|
}
|
|
|
|
function handleFloatInput(
|
|
input: HTMLInputElement | HTMLSelectElement,
|
|
def: number,
|
|
min: number = Number.NEGATIVE_INFINITY,
|
|
max: number = Number.POSITIVE_INFINITY,
|
|
): number {
|
|
let x = Math.min(Math.max(parseFloat(input.value), min), max);
|
|
if (!Number.isFinite(x)) {
|
|
x = def;
|
|
}
|
|
input.value = "" + x;
|
|
|
|
return x;
|
|
}
|