mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-19 05:30:44 -05:00
1831 lines
48 KiB
TypeScript
1831 lines
48 KiB
TypeScript
import { nanoid } from "nanoid";
|
|
import * as R from "remeda";
|
|
import type {
|
|
Ability,
|
|
MainWeaponId,
|
|
SubWeaponId,
|
|
} from "~/modules/in-game-lists/types";
|
|
import {
|
|
ANGLE_SHOOTER_ID,
|
|
AUTO_BOMB_ID,
|
|
BURST_BOMB_ID,
|
|
CRAB_TANK_ID,
|
|
CURLING_BOMB_ID,
|
|
FIZZY_BOMB_ID,
|
|
INK_MINE_ID,
|
|
POINT_SENSOR_ID,
|
|
SPLASH_WALL_ID,
|
|
SPLAT_BOMB_ID,
|
|
SPRINKLER_ID,
|
|
SUCTION_BOMB_ID,
|
|
subWeaponIds,
|
|
TORPEDO_ID,
|
|
ZIPCASTER_ID,
|
|
} from "~/modules/in-game-lists/weapon-ids";
|
|
import invariant from "~/utils/invariant";
|
|
import { cutToNDecimalPlaces, roundToNDecimalPlaces } from "~/utils/number";
|
|
import { assertUnreachable } from "~/utils/types";
|
|
import {
|
|
DAMAGE_TYPE,
|
|
RAINMAKER_SPEED_PENALTY_MODIFIER,
|
|
} from "../analyzer-constants";
|
|
import type {
|
|
AbilityPoints,
|
|
AnalyzedBuild,
|
|
DamageType,
|
|
InkConsumeType,
|
|
MainWeaponParams,
|
|
SpecialWeaponParams,
|
|
StatFunctionInput,
|
|
SubWeaponParams,
|
|
} from "../analyzer-types";
|
|
import { INK_CONSUME_TYPES } from "../analyzer-types";
|
|
import type { abilityValues as abilityValuesJson } from "./ability-values";
|
|
import {
|
|
abilityPointsToEffects,
|
|
abilityValues,
|
|
apFromMap,
|
|
mainWeaponParams as getMainWeaponParams,
|
|
hasEffect,
|
|
hpDivided,
|
|
weaponIdToMultiShotCount,
|
|
weaponParams,
|
|
} from "./utils";
|
|
|
|
export function buildStats({
|
|
weaponSplId,
|
|
abilityPoints = new Map(),
|
|
mainOnlyAbilities = [],
|
|
hasTacticooler,
|
|
}: {
|
|
weaponSplId: MainWeaponId;
|
|
abilityPoints?: AbilityPoints;
|
|
mainOnlyAbilities?: Array<Ability>;
|
|
hasTacticooler: boolean;
|
|
}): AnalyzedBuild {
|
|
const mainWeaponParams = getMainWeaponParams(weaponSplId);
|
|
|
|
const subWeaponParams =
|
|
weaponParams().subWeapons[mainWeaponParams.subWeaponId];
|
|
invariant(
|
|
subWeaponParams,
|
|
`Sub weapon with splId ${mainWeaponParams.subWeaponId} not found`,
|
|
);
|
|
|
|
const specialWeaponParams = weaponParams().specialWeapons[
|
|
mainWeaponParams.specialWeaponId
|
|
] as SpecialWeaponParams;
|
|
invariant(
|
|
specialWeaponParams,
|
|
`Special weapon with splId ${mainWeaponParams.specialWeaponId} not found`,
|
|
);
|
|
|
|
const input: StatFunctionInput = {
|
|
weaponSplId,
|
|
mainWeaponParams,
|
|
subWeaponParams,
|
|
specialWeaponParams,
|
|
abilityPoints,
|
|
mainOnlyAbilities,
|
|
hasTacticooler,
|
|
};
|
|
|
|
return {
|
|
weapon: {
|
|
subWeaponSplId: mainWeaponParams.subWeaponId,
|
|
specialWeaponSplId: mainWeaponParams.specialWeaponId,
|
|
brellaCanopyHp:
|
|
mainWeaponParams.CanopyHP && mainWeaponParams.CanopyHP / 10,
|
|
fullChargeSeconds: mainWeaponParams.ChargeFrameFullCharge
|
|
? framesToSeconds(mainWeaponParams.ChargeFrameFullCharge)
|
|
: undefined,
|
|
maxChargeHoldSeconds: mainWeaponParams.KeepChargeFullFrame
|
|
? framesToSeconds(mainWeaponParams.KeepChargeFullFrame)
|
|
: undefined,
|
|
speedType: mainWeaponParams.WeaponSpeedType ?? "Normal",
|
|
multiShots: weaponIdToMultiShotCount(weaponSplId),
|
|
},
|
|
stats: {
|
|
specialPoint: specialPoint(input),
|
|
specialLost: specialLost(input),
|
|
specialLostSplattedByRP: specialLost(input, true),
|
|
fullInkTankOptions: fullInkTankOptions(input),
|
|
damages: damages(input),
|
|
specialWeaponDamages: specialWeaponDamages(input),
|
|
subWeaponDefenseDamages: subWeaponDefenseDamages(input),
|
|
mainWeaponWhiteInkSeconds:
|
|
typeof mainWeaponParams.InkRecoverStop === "number"
|
|
? framesToSeconds(mainWeaponParams.InkRecoverStop)
|
|
: undefined,
|
|
subWeaponWhiteInkSeconds: framesToSeconds(subWeaponParams.InkRecoverStop),
|
|
subWeaponInkConsumptionPercentage:
|
|
subWeaponInkConsumptionPercentage(input),
|
|
squidFormInkRecoverySeconds: squidFormInkRecoverySeconds(input),
|
|
humanoidFormInkRecoverySeconds: humanoidFormInkRecoverySeconds(input),
|
|
runSpeed: runSpeed(input),
|
|
shootingRunSpeed: shootingRunSpeed(input, "MoveSpeed"),
|
|
shootingRunSpeedCharging: shootingRunSpeed(input, "MoveSpeed_Charge"),
|
|
shootingRunSpeedFullCharge: shootingRunSpeed(
|
|
input,
|
|
"MoveSpeedFullCharge",
|
|
),
|
|
shootingRunSpeedSecondaryMode: shootingRunSpeed(
|
|
input,
|
|
"MoveSpeedVariable",
|
|
),
|
|
swimSpeed: swimSpeed(input),
|
|
swimSpeedHoldingRainmaker: swimSpeedHoldingRainmaker(input),
|
|
runSpeedInEnemyInk: runSpeedInEnemyInk(input),
|
|
damageTakenInEnemyInkPerSecond: damageTakenInEnemyInkPerSecond(input),
|
|
enemyInkDamageLimit: enemyInkDamageLimit(input),
|
|
framesBeforeTakingDamageInEnemyInk:
|
|
framesBeforeTakingDamageInEnemyInk(input),
|
|
quickRespawnTime: respawnTime(input),
|
|
quickRespawnTimeSplattedByRP: respawnTime(input, true),
|
|
superJumpTimeGroundFrames: superJumpTimeGroundFrames(input),
|
|
superJumpTimeTotal: superJumpTimeTotal(input),
|
|
shotSpreadAir: shotSpreadAir(input),
|
|
shotSpreadGround: mainWeaponParams.Stand_DegSwerve,
|
|
shotAutofireSpreadAir: shotAutofireSpreadAir(input),
|
|
shotAutofireSpreadGround: mainWeaponParams.Variable_Stand_DegSwerve,
|
|
squidSurgeChargeFrames: squidSurgeChargeFrames(input),
|
|
subDefPointSensorMarkedTimeInSeconds:
|
|
subDefPointSensorMarkedTimeInSeconds(input),
|
|
subDefInkMineMarkedTimeInSeconds: subDefInkMineMarkedTimeInSeconds(input),
|
|
subDefAngleShooterMarkedTimeInSeconds:
|
|
subDefAngleShooterMarkedTimeInSeconds(input),
|
|
subDefToxicMistMovementReduction: subDefToxicMistMovementReduction(input),
|
|
subQsjBoost: subQsjBoost(input),
|
|
...subStats(input),
|
|
specialDurationInSeconds: specialDurationInSeconds(input),
|
|
specialDamageDistance: specialDamageDistance(input),
|
|
specialPaintRadius: specialPaintRadius(input),
|
|
specialFieldHp: specialFieldHp(input),
|
|
specialDeviceHp: specialDeviceHp(input),
|
|
specialHookInkConsumptionPercentage:
|
|
specialHookInkConsumptionPercentage(input),
|
|
specialInkConsumptionPerSecondPercentage:
|
|
specialInkConsumptionPerSecondPercentage(input),
|
|
specialReticleRadius: specialReticleRadius(input),
|
|
specialThrowDistance: specialThrowDistance(input),
|
|
specialMoveSpeed: specialMoveSpeed(input),
|
|
specialAutoChargeRate: specialAutoChargeRate(input),
|
|
specialMaxRadius: specialMaxRadius(input),
|
|
specialRadiusRangeMin: specialRadiusRangeMin(input),
|
|
specialRadiusRangeMax: specialRadiusRangeMax(input),
|
|
specialPowerUpDuration: specialPowerUpDuration(input),
|
|
},
|
|
};
|
|
}
|
|
|
|
const SPLATTERSHOT_JR_ID = 10;
|
|
const CUSTOM_SPLATTERSHOT_JR_ID = 11;
|
|
function inkTankSize(weaponSplId: StatFunctionInput["weaponSplId"]) {
|
|
if ([SPLATTERSHOT_JR_ID, CUSTOM_SPLATTERSHOT_JR_ID].includes(weaponSplId)) {
|
|
return 1.1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
function specialPoint({
|
|
abilityPoints,
|
|
mainWeaponParams,
|
|
}: StatFunctionInput): AnalyzedBuild["stats"]["specialPoint"] {
|
|
const SPECIAL_POINT_ABILITY = "SCU";
|
|
|
|
const { effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: abilityPoints,
|
|
ability: SPECIAL_POINT_ABILITY,
|
|
}),
|
|
key: "IncreaseRt_Special",
|
|
weapon: mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: mainWeaponParams.SpecialPoint,
|
|
modifiedBy: SPECIAL_POINT_ABILITY,
|
|
value: Math.ceil(mainWeaponParams.SpecialPoint / effect),
|
|
};
|
|
}
|
|
|
|
const OWN_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST = 0.225;
|
|
const ENEMY_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST = 0.15;
|
|
function specialLost(
|
|
{ abilityPoints, mainWeaponParams, mainOnlyAbilities }: StatFunctionInput,
|
|
splattedByRP = false,
|
|
): AnalyzedBuild["stats"]["specialPoint"] {
|
|
const SPECIAL_SAVED_AFTER_DEATH_ABILITY = "SS";
|
|
const hasRespawnPunisher = mainOnlyAbilities.includes("RP");
|
|
const extraSpecialLost = hasRespawnPunisher
|
|
? OWN_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST
|
|
: 0;
|
|
|
|
const specialSavedAfterDeathForDisplay = (effect: number) =>
|
|
Number(((1.0 - effect) * 100).toFixed(2));
|
|
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: abilityPoints,
|
|
ability: SPECIAL_SAVED_AFTER_DEATH_ABILITY,
|
|
}),
|
|
key: "SpecialGaugeRt_Restart",
|
|
weapon: mainWeaponParams,
|
|
});
|
|
|
|
const splattedByExtraPenalty = splattedByRP
|
|
? ENEMY_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST
|
|
: 0;
|
|
|
|
return {
|
|
baseValue: specialSavedAfterDeathForDisplay(
|
|
baseEffect - splattedByExtraPenalty,
|
|
),
|
|
value: specialSavedAfterDeathForDisplay(
|
|
effect - splattedByExtraPenalty - extraSpecialLost,
|
|
),
|
|
modifiedBy: [SPECIAL_SAVED_AFTER_DEATH_ABILITY, "RP"],
|
|
};
|
|
}
|
|
|
|
function subWeaponInkConsumptionPercentage(args: StatFunctionInput) {
|
|
return {
|
|
modifiedBy: "ISS" as const,
|
|
baseValue: roundToNDecimalPlaces(
|
|
(args.subWeaponParams.InkConsume * 100) / inkTankSize(args.weaponSplId),
|
|
),
|
|
value: roundToNDecimalPlaces(
|
|
// + 0.004 is a hack to avoid situation where the value is e.g. 50.0005
|
|
// -> rounds to 50% so it appears you can throw two subs
|
|
// which is not correct so we force the round upwards
|
|
(subWeaponConsume(args).inkConsume * 100 + 0.0045) /
|
|
inkTankSize(args.weaponSplId),
|
|
),
|
|
};
|
|
}
|
|
|
|
export function fullInkTankOptions(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["fullInkTankOptions"] {
|
|
const result: AnalyzedBuild["stats"]["fullInkTankOptions"] = [];
|
|
|
|
const { inkConsume: subWeaponInkConsume, maxSubsFromFullInkTank } =
|
|
subWeaponConsume(args);
|
|
|
|
for (
|
|
let subsFromFullInkTank = 0;
|
|
subsFromFullInkTank <= maxSubsFromFullInkTank;
|
|
subsFromFullInkTank++
|
|
) {
|
|
for (const type of INK_CONSUME_TYPES) {
|
|
const mainWeaponInkConsume = mainWeaponInkConsumeByType({
|
|
type,
|
|
...args,
|
|
});
|
|
|
|
if (typeof mainWeaponInkConsume !== "number") continue;
|
|
|
|
result.push({
|
|
id: nanoid(),
|
|
subsUsed: subsFromFullInkTank,
|
|
type,
|
|
value: effectToRounded(
|
|
(inkTankSize(args.weaponSplId) -
|
|
subWeaponInkConsume * subsFromFullInkTank) /
|
|
mainWeaponInkConsume,
|
|
2,
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function effectToRounded(effect: number, decimals = 3) {
|
|
return Number(effect.toFixed(decimals));
|
|
}
|
|
|
|
function subWeaponConsume({
|
|
mainWeaponParams,
|
|
subWeaponParams,
|
|
abilityPoints,
|
|
weaponSplId,
|
|
}: StatFunctionInput) {
|
|
const { effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints,
|
|
ability: "ISS",
|
|
}),
|
|
key: `ConsumeRt_Sub_Lv${subWeaponParams.SubInkSaveLv}`,
|
|
weapon: mainWeaponParams,
|
|
});
|
|
|
|
const inkConsume = subWeaponParams.InkConsume;
|
|
|
|
const inkConsumeAfterISS = inkConsume * effect;
|
|
|
|
return {
|
|
inkConsume: inkConsumeAfterISS,
|
|
maxSubsFromFullInkTank: Math.floor(
|
|
inkTankSize(weaponSplId) / inkConsumeAfterISS,
|
|
),
|
|
};
|
|
}
|
|
|
|
function mainWeaponInkConsumeByType({
|
|
mainWeaponParams,
|
|
abilityPoints,
|
|
type,
|
|
}: {
|
|
type: InkConsumeType;
|
|
} & StatFunctionInput) {
|
|
const { effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints,
|
|
ability: "ISM",
|
|
}),
|
|
key: "ConsumeRt_Main",
|
|
weapon: mainWeaponParams,
|
|
});
|
|
|
|
// these keys are always mutually exclusive i.e. even if inkConsumeTypeToParamsKeys() returns many keys
|
|
// then weapon params of this weapon should only have one defined
|
|
for (const key of inkConsumeTypeToParamsKeys(type)) {
|
|
const value = mainWeaponParams[key];
|
|
|
|
if (typeof value === "number") {
|
|
return value * effect;
|
|
}
|
|
}
|
|
|
|
// not all weapons have all ink consume types
|
|
// i.e. blaster does not (hopefully) perform dualie dodge rolls
|
|
return;
|
|
}
|
|
|
|
function inkConsumeTypeToParamsKeys(
|
|
type: InkConsumeType,
|
|
): Array<keyof MainWeaponParams> {
|
|
switch (type) {
|
|
case "NORMAL":
|
|
return ["InkConsume", "InkConsume_WeaponShelterShotgunParam"];
|
|
case "SWING":
|
|
return ["InkConsume_SwingParam", "InkConsume_WeaponSwingParam"];
|
|
case "SLOSH":
|
|
return ["InkConsumeSlosher"];
|
|
case "TAP_SHOT":
|
|
return ["InkConsumeMinCharge"];
|
|
case "FULL_CHARGE":
|
|
return ["InkConsumeFullCharge", "InkConsumeFullCharge_ChargeParam"];
|
|
case "SPLATLING_CHARGE":
|
|
return ["InkConsumeFullChargeSplatling"];
|
|
case "HORIZONTAL_SWING":
|
|
return ["InkConsume_WeaponWideSwingParam"];
|
|
case "VERTICAL_SWING":
|
|
return ["InkConsume_WeaponVerticalSwingParam"];
|
|
case "DUALIE_ROLL":
|
|
return ["InkConsume_SideStepParam"];
|
|
case "SHIELD_LAUNCH":
|
|
return ["InkConsumeUmbrella_WeaponShelterCanopyParam"];
|
|
default: {
|
|
assertUnreachable(type);
|
|
}
|
|
}
|
|
}
|
|
|
|
const damageTypeToParamsKey: Record<
|
|
DamageType,
|
|
| keyof MainWeaponParams
|
|
| keyof SubWeaponParams
|
|
| keyof SpecialWeaponParams
|
|
| Array<
|
|
keyof MainWeaponParams | keyof SubWeaponParams | keyof SpecialWeaponParams
|
|
>
|
|
> = {
|
|
NORMAL_MIN: "DamageParam_ValueMin",
|
|
NORMAL_MAX: "DamageParam_ValueMax",
|
|
NORMAL_MAX_FULL_CHARGE: "DamageParam_ValueFullChargeMax",
|
|
TURRET_MAX: "DamageLapOverParam_ValueMax",
|
|
TURRET_MIN: "DamageLapOverParam_ValueMin",
|
|
SECONDARY_MODE_MAX: "Variable_Damage_ValueMax",
|
|
SECONDARY_MODE_MIN: "Variable_Damage_ValueMin",
|
|
DIRECT: "DamageParam_ValueDirect",
|
|
DIRECT_MIN: "DamageParam_ValueDirectMin",
|
|
DIRECT_MAX: "DamageParam_ValueDirectMax",
|
|
DIRECT_SECONDARY_MIN: "DamageParam_Secondary_ValueDirectMin",
|
|
DIRECT_SECONDARY_MAX: "DamageParam_Secondary_ValueDirectMax",
|
|
DISTANCE: ["BlastParam_DistanceDamage", "DistanceDamage_BlastParamArray"],
|
|
SPLASH: ["BlastParam_SplashDamage", "DistanceDamage_SplashBlastParam"],
|
|
SPLASH_MIN: "SwingUnitGroupParam_DamageParam_DamageMinValue",
|
|
SPLASH_MAX: "SwingUnitGroupParam_DamageParam_DamageMaxValue",
|
|
SPLASH_HORIZONTAL_MIN: "WideSwingUnitGroupParam_DamageParam_DamageMinValue",
|
|
SPLASH_HORIZONTAL_MAX: "WideSwingUnitGroupParam_DamageParam_DamageMaxValue",
|
|
SPLASH_VERTICAL_MIN: "VerticalSwingUnitGroupParam_DamageParam_DamageMinValue",
|
|
SPLASH_VERTICAL_MAX: "VerticalSwingUnitGroupParam_DamageParam_DamageMaxValue",
|
|
ROLL_OVER: "BodyParam_Damage",
|
|
FULL_CHARGE: "DamageParam_ValueFullCharge",
|
|
MAX_CHARGE: "DamageParam_ValueMaxCharge",
|
|
TAP_SHOT: "DamageParam_ValueMinCharge",
|
|
SPLATANA_VERTICAL: "DamageParam_SplatanaVertical",
|
|
SPLATANA_VERTICAL_DIRECT: "DamageParam_SplatanaVerticalDirect",
|
|
SPLATANA_HORIZONTAL: "DamageParam_SplatanaHorizontal",
|
|
SPLATANA_HORIZONTAL_DIRECT: "DamageParam_SplatanaHorizontalDirect",
|
|
BOMB_NORMAL: "DistanceDamage",
|
|
BOMB_DIRECT: ["DirectDamage", "DistanceDamage_BlastParamChase"],
|
|
WAVE: "WaveDamage",
|
|
SPECIAL_MAX_CHARGE: "ExhaleBlastParamMaxChargeDistanceDamage",
|
|
SPECIAL_MIN_CHARGE: "ExhaleBlastParamMinChargeDistanceDamage",
|
|
SPECIAL_SWING: "SwingDamage",
|
|
SPECIAL_THROW: "ThrowDamage",
|
|
SPECIAL_THROW_DIRECT: "ThrowDirectDamage",
|
|
SPECIAL_BULLET_MAX: "BulletDamageMax",
|
|
SPECIAL_BULLET_MIN: "BulletDamageMin",
|
|
SPECIAL_CANNON: "CannonDamage",
|
|
SPECIAL_BUMP: "BumpDamage",
|
|
SPECIAL_JUMP: "JumpDamage",
|
|
SPECIAL_TICK: "TickDamage",
|
|
};
|
|
|
|
function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] {
|
|
const result: AnalyzedBuild["stats"]["damages"] = [];
|
|
|
|
for (const type of DAMAGE_TYPE) {
|
|
for (const key of [damageTypeToParamsKey[type]].flat()) {
|
|
const value = args.mainWeaponParams[key as keyof MainWeaponParams];
|
|
|
|
if (Array.isArray(value)) {
|
|
for (const subValue of value.flat()) {
|
|
result.push({
|
|
type,
|
|
value: subValue.Damage / 10,
|
|
distance: subValue.Distance,
|
|
id: nanoid(),
|
|
multiShots: weaponIdToMultiShotCount(args.weaponSplId),
|
|
});
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (typeof value !== "number") continue;
|
|
|
|
result.push({
|
|
id: nanoid(),
|
|
type,
|
|
value: value / 10,
|
|
shotsToSplat: shotsToSplat({
|
|
value,
|
|
type,
|
|
multiShots: weaponIdToMultiShotCount(args.weaponSplId),
|
|
}),
|
|
multiShots: weaponIdToMultiShotCount(args.weaponSplId),
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function specialWeaponDamages(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialWeaponDamages"] {
|
|
const result: AnalyzedBuild["stats"]["specialWeaponDamages"] = [];
|
|
|
|
for (const type of DAMAGE_TYPE) {
|
|
for (const key of [damageTypeToParamsKey[type]].flat()) {
|
|
const value = args.specialWeaponParams[key as keyof SpecialWeaponParams];
|
|
|
|
if (Array.isArray(value)) {
|
|
for (const subValue of value.flat()) {
|
|
result.push({
|
|
type,
|
|
value: subValue.Damage / 10,
|
|
distance: subValue.Distance,
|
|
id: nanoid(),
|
|
multiShots: weaponIdToMultiShotCount(args.weaponSplId),
|
|
});
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (typeof value !== "number") continue;
|
|
|
|
result.push({
|
|
id: nanoid(),
|
|
type,
|
|
value: value / 10,
|
|
shotsToSplat: shotsToSplat({
|
|
value,
|
|
type,
|
|
multiShots: weaponIdToMultiShotCount(args.weaponSplId),
|
|
}),
|
|
multiShots: weaponIdToMultiShotCount(args.weaponSplId),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Artifically combined damages
|
|
if (args.mainWeaponParams.specialWeaponId === ZIPCASTER_ID) {
|
|
result.unshift({
|
|
id: nanoid(),
|
|
distance: 0,
|
|
value: R.sum(result.map((v) => v.value)),
|
|
type: result[0].type,
|
|
});
|
|
}
|
|
if (args.mainWeaponParams.specialWeaponId === CRAB_TANK_ID) {
|
|
const cannonDamages = result.filter((d) => d.type === "SPECIAL_CANNON");
|
|
const firstCannonDamageIdx = result.findIndex(
|
|
(d) => d.type === "SPECIAL_CANNON",
|
|
);
|
|
|
|
result.splice(firstCannonDamageIdx, 0, {
|
|
id: nanoid(),
|
|
distance: 0,
|
|
value: R.sum(cannonDamages.map((v) => v.value)),
|
|
type: "SPECIAL_CANNON",
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function shotsToSplat({
|
|
value,
|
|
type,
|
|
multiShots,
|
|
}: {
|
|
value: number;
|
|
type: DamageType;
|
|
multiShots?: number;
|
|
}) {
|
|
if (!["NORMAL_MAX", "NORMAL_MAX_FULL_CHARGE", "DIRECT"].includes(type)) {
|
|
return;
|
|
}
|
|
|
|
const multiplier = multiShots ? multiShots : 1;
|
|
|
|
return Math.ceil(1000 / (value * multiplier));
|
|
}
|
|
|
|
function subWeaponDefenseDamages(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["subWeaponDefenseDamages"] {
|
|
const result: AnalyzedBuild["stats"]["subWeaponDefenseDamages"] = [];
|
|
|
|
const abilityPoints = apFromMap({
|
|
ability: "SRU",
|
|
abilityPoints: args.abilityPoints,
|
|
});
|
|
|
|
for (const id of subWeaponIds) {
|
|
const params = weaponParams().subWeapons[id];
|
|
|
|
for (const type of DAMAGE_TYPE) {
|
|
for (const key of [damageTypeToParamsKey[type]].flat()) {
|
|
const value = params[key as keyof SubWeaponParams];
|
|
|
|
if (Array.isArray(value)) {
|
|
let arrayValues: AnalyzedBuild["stats"]["subWeaponDefenseDamages"] =
|
|
[];
|
|
for (const subValue of value.flat()) {
|
|
arrayValues.push({
|
|
type,
|
|
baseValue: subValue.Damage / 10,
|
|
value: subWeaponDamageValue({
|
|
baseValue: subValue.Damage / 10,
|
|
subWeaponId: id,
|
|
abilityPoints,
|
|
params: args.subWeaponParams,
|
|
}),
|
|
distance: subValue.Distance,
|
|
id: nanoid(),
|
|
subWeaponId: id,
|
|
});
|
|
}
|
|
|
|
// Burst Bomb direct damage
|
|
if (id === BURST_BOMB_ID) {
|
|
arrayValues.unshift({
|
|
id: nanoid(),
|
|
subWeaponId: id,
|
|
distance: 0,
|
|
baseValue: R.sum(arrayValues.map((v) => v.baseValue)),
|
|
value: cutToNDecimalPlaces(
|
|
R.sum(arrayValues.map((v) => v.value)),
|
|
1,
|
|
),
|
|
type,
|
|
});
|
|
}
|
|
|
|
// Flatten many values into one
|
|
if (id === FIZZY_BOMB_ID || id === CURLING_BOMB_ID) {
|
|
const allArrayValues = arrayValues.sort(
|
|
(a, b) => a.baseValue - b.baseValue,
|
|
);
|
|
const firstHalfValues = allArrayValues.slice(
|
|
0,
|
|
allArrayValues.length / 2,
|
|
);
|
|
const secondHalfValues = allArrayValues.slice(
|
|
allArrayValues.length / 2,
|
|
);
|
|
|
|
arrayValues = [
|
|
{
|
|
id: nanoid(),
|
|
subWeaponId: id,
|
|
distance: [
|
|
Math.min(
|
|
...secondHalfValues.map(
|
|
(value) => value.distance as number,
|
|
),
|
|
),
|
|
Math.max(
|
|
...secondHalfValues.map(
|
|
(value) => value.distance as number,
|
|
),
|
|
),
|
|
],
|
|
baseValue: secondHalfValues[0].baseValue,
|
|
value: secondHalfValues[0].value,
|
|
type,
|
|
},
|
|
{
|
|
id: nanoid(),
|
|
subWeaponId: id,
|
|
distance: [
|
|
Math.min(
|
|
...firstHalfValues.map((value) => value.distance as number),
|
|
),
|
|
Math.max(
|
|
...firstHalfValues.map((value) => value.distance as number),
|
|
),
|
|
],
|
|
baseValue: firstHalfValues[0].baseValue,
|
|
value: firstHalfValues[0].value,
|
|
type,
|
|
},
|
|
];
|
|
}
|
|
|
|
result.push(...arrayValues);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (typeof value !== "number") continue;
|
|
|
|
result.push({
|
|
id: nanoid(),
|
|
type,
|
|
baseValue: value / 10,
|
|
value: subWeaponDamageValue({
|
|
baseValue: value / 10,
|
|
subWeaponId: id,
|
|
abilityPoints,
|
|
params: args.subWeaponParams,
|
|
}),
|
|
subWeaponId: id,
|
|
distance: 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function subWeaponIdToEffectKey(
|
|
subWeaponId: SubWeaponId,
|
|
): keyof typeof abilityValuesJson {
|
|
switch (subWeaponId) {
|
|
case SPLAT_BOMB_ID:
|
|
case SUCTION_BOMB_ID:
|
|
case CURLING_BOMB_ID:
|
|
case AUTO_BOMB_ID:
|
|
case INK_MINE_ID:
|
|
case TORPEDO_ID:
|
|
return "DamageRt_BombH";
|
|
case BURST_BOMB_ID:
|
|
case FIZZY_BOMB_ID:
|
|
return "DamageRt_BombL";
|
|
case ANGLE_SHOOTER_ID:
|
|
return "DamageRt_LineMarker";
|
|
case SPRINKLER_ID:
|
|
return "DamageRt_Sprinkler";
|
|
case SPLASH_WALL_ID:
|
|
return "DamageRt_Shield";
|
|
default:
|
|
throw new Error(
|
|
`No damage rate for the sub weapon with id: ${subWeaponId}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function subWeaponDamageValue({
|
|
baseValue,
|
|
subWeaponId,
|
|
abilityPoints,
|
|
params,
|
|
}: {
|
|
baseValue: number;
|
|
subWeaponId: SubWeaponId;
|
|
params: SubWeaponParams;
|
|
abilityPoints: number;
|
|
}): number {
|
|
// lethal damage cannot be lowered
|
|
if (baseValue > 100) return baseValue;
|
|
|
|
const { effect } = abilityPointsToEffects({
|
|
abilityPoints,
|
|
key: subWeaponIdToEffectKey(subWeaponId),
|
|
weapon: params,
|
|
});
|
|
|
|
// Lean: The HP are ints between 0 and 1000 consistently
|
|
return cutToNDecimalPlaces(baseValue * effect, 1);
|
|
}
|
|
|
|
const framesToSeconds = (frames: number) =>
|
|
effectToRounded(Math.ceil(frames) / 60);
|
|
function squidFormInkRecoverySeconds(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["squidFormInkRecoverySeconds"] {
|
|
const SQUID_FORM_INK_RECOVERY_SECONDS_ABILITY = "IRU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SQUID_FORM_INK_RECOVERY_SECONDS_ABILITY,
|
|
}),
|
|
key: "InkRecoverFrm_Stealth",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(baseEffect * inkTankSize(args.weaponSplId)),
|
|
value: framesToSeconds(effect * inkTankSize(args.weaponSplId)),
|
|
modifiedBy: SQUID_FORM_INK_RECOVERY_SECONDS_ABILITY,
|
|
};
|
|
}
|
|
|
|
function humanoidFormInkRecoverySeconds(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["humanoidFormInkRecoverySeconds"] {
|
|
const HUMANOID_FORM_INK_RECOVERY_SECONDS_ABILITY = "IRU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: HUMANOID_FORM_INK_RECOVERY_SECONDS_ABILITY,
|
|
}),
|
|
key: "InkRecoverFrm_Std",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(baseEffect * inkTankSize(args.weaponSplId)),
|
|
value: framesToSeconds(effect * inkTankSize(args.weaponSplId)),
|
|
modifiedBy: HUMANOID_FORM_INK_RECOVERY_SECONDS_ABILITY,
|
|
};
|
|
}
|
|
|
|
function runSpeed(args: StatFunctionInput): AnalyzedBuild["stats"]["runSpeed"] {
|
|
const key =
|
|
args.mainWeaponParams.WeaponSpeedType === "Fast"
|
|
? "_Fast"
|
|
: args.mainWeaponParams.WeaponSpeedType === "Slow"
|
|
? "_Slow"
|
|
: "";
|
|
const RUN_SPEED_ABILITY = "RSU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: RUN_SPEED_ABILITY,
|
|
}),
|
|
key: `MoveVel_Human${key}`,
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: effectToRounded(baseEffect * 10),
|
|
value: effectToRounded(effect * 10),
|
|
modifiedBy: RUN_SPEED_ABILITY,
|
|
};
|
|
}
|
|
|
|
function runSpeedInEnemyInk(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["runSpeedInEnemyInk"] {
|
|
const RUN_SPEED_IN_ENEMY_INK_ABILITY = "RES";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: RUN_SPEED_IN_ENEMY_INK_ABILITY,
|
|
}),
|
|
key: "OpInk_MoveVel",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: effectToRounded(baseEffect * 10),
|
|
value: effectToRounded(effect * 10),
|
|
modifiedBy: RUN_SPEED_IN_ENEMY_INK_ABILITY,
|
|
};
|
|
}
|
|
|
|
function shootingRunSpeed(
|
|
args: StatFunctionInput,
|
|
keyName:
|
|
| "MoveSpeed"
|
|
| "MoveSpeed_Charge"
|
|
| "MoveSpeedFullCharge"
|
|
| "MoveSpeedVariable",
|
|
): AnalyzedBuild["stats"]["shootingRunSpeed"] {
|
|
const SHOOTING_RUN_SPEED_ABILITY = "RSU";
|
|
const moveSpeed = args.mainWeaponParams[keyName];
|
|
|
|
if (!moveSpeed) return;
|
|
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SHOOTING_RUN_SPEED_ABILITY,
|
|
}),
|
|
key: "MoveVelRt_Shot",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: effectToRounded(moveSpeed * baseEffect * 10),
|
|
value: effectToRounded(moveSpeed * effect * 10),
|
|
modifiedBy: SHOOTING_RUN_SPEED_ABILITY,
|
|
};
|
|
}
|
|
|
|
function swimSpeed(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["swimSpeed"] {
|
|
const key =
|
|
args.mainWeaponParams.WeaponSpeedType === "Fast"
|
|
? "_Fast"
|
|
: args.mainWeaponParams.WeaponSpeedType === "Slow"
|
|
? "_Slow"
|
|
: "";
|
|
const SWIM_SPEED_ABILITY = "SSU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SWIM_SPEED_ABILITY,
|
|
}),
|
|
key: `MoveVel_Stealth${key}`,
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const ninjaSquidMultiplier = args.mainOnlyAbilities.includes("NS") ? 0.9 : 1;
|
|
|
|
return {
|
|
baseValue: effectToRounded(baseEffect * 10),
|
|
value: effectToRounded(effect * 10 * ninjaSquidMultiplier),
|
|
modifiedBy: [SWIM_SPEED_ABILITY, "NS"],
|
|
};
|
|
}
|
|
|
|
function swimSpeedHoldingRainmaker(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["swimSpeedHoldingRainmaker"] {
|
|
const withoutRM = swimSpeed(args);
|
|
|
|
return {
|
|
...withoutRM,
|
|
baseValue: effectToRounded(
|
|
withoutRM.baseValue * RAINMAKER_SPEED_PENALTY_MODIFIER,
|
|
),
|
|
value: effectToRounded(withoutRM.value * RAINMAKER_SPEED_PENALTY_MODIFIER),
|
|
};
|
|
}
|
|
|
|
const qrApAfterRespawnPunish = ({
|
|
ap,
|
|
hasTacticooler,
|
|
}: {
|
|
ap: number;
|
|
hasTacticooler: boolean;
|
|
}) => (hasTacticooler ? ap : Math.ceil(ap * 0.15));
|
|
|
|
const RESPAWN_CHASE_FRAME = 150;
|
|
const OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 68;
|
|
const ENEMY_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 45;
|
|
const SPLATOON_3_FASTER_RESPAWN = 60;
|
|
function respawnTime(
|
|
args: StatFunctionInput,
|
|
splattedByRP = false,
|
|
): AnalyzedBuild["stats"]["quickRespawnTime"] {
|
|
const QUICK_RESPAWN_TIME_ABILITY = "QR";
|
|
const hasRespawnPunisher = args.mainOnlyAbilities.includes("RP");
|
|
|
|
const ap = apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: QUICK_RESPAWN_TIME_ABILITY,
|
|
});
|
|
const abilityPoints = splattedByRP
|
|
? qrApAfterRespawnPunish({
|
|
ap,
|
|
hasTacticooler: args.hasTacticooler,
|
|
})
|
|
: ap;
|
|
|
|
const chase = abilityPointsToEffects({
|
|
abilityPoints,
|
|
key: "Dying_ChaseFrm",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
const around = abilityPointsToEffects({
|
|
abilityPoints,
|
|
key: "Dying_AroundFrm",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const ownRPExtraFrames = hasRespawnPunisher
|
|
? OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES
|
|
: 0;
|
|
|
|
const splattedByExtraFrames = splattedByRP
|
|
? ENEMY_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES
|
|
: 0;
|
|
|
|
return {
|
|
baseValue: framesToSeconds(
|
|
RESPAWN_CHASE_FRAME +
|
|
chase.baseEffect +
|
|
splattedByExtraFrames +
|
|
around.baseEffect -
|
|
SPLATOON_3_FASTER_RESPAWN,
|
|
),
|
|
value: framesToSeconds(
|
|
RESPAWN_CHASE_FRAME +
|
|
chase.effect +
|
|
around.effect +
|
|
splattedByExtraFrames +
|
|
ownRPExtraFrames -
|
|
SPLATOON_3_FASTER_RESPAWN,
|
|
),
|
|
modifiedBy: [QUICK_RESPAWN_TIME_ABILITY, "RP"],
|
|
};
|
|
}
|
|
|
|
function superJumpTimeGroundFrames(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["superJumpTimeGroundFrames"] {
|
|
const SUPER_JUMP_TIME_GROUND_ABILITY = "QSJ";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUPER_JUMP_TIME_GROUND_ABILITY,
|
|
}),
|
|
key: "SuperJump_ChargeFrm",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: Math.ceil(baseEffect),
|
|
value: Math.ceil(effect),
|
|
modifiedBy: SUPER_JUMP_TIME_GROUND_ABILITY,
|
|
};
|
|
}
|
|
|
|
function superJumpTimeTotal(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["superJumpTimeTotal"] {
|
|
const SUPER_JUMP_TIME_TOTAL_ABILITY = "QSJ";
|
|
|
|
const charge = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUPER_JUMP_TIME_TOTAL_ABILITY,
|
|
}),
|
|
key: "SuperJump_ChargeFrm",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
const move = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUPER_JUMP_TIME_TOTAL_ABILITY,
|
|
}),
|
|
key: "SuperJump_MoveFrm",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(
|
|
Math.ceil(charge.baseEffect) + Math.ceil(move.baseEffect),
|
|
),
|
|
value: framesToSeconds(Math.ceil(charge.effect) + Math.ceil(move.effect)),
|
|
modifiedBy: SUPER_JUMP_TIME_TOTAL_ABILITY,
|
|
};
|
|
}
|
|
|
|
function shotSpreadAir(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["shotSpreadAir"] {
|
|
const SHOT_SPREAD_AIR_ABILITY = "IA";
|
|
const groundSpread = args.mainWeaponParams.Stand_DegSwerve;
|
|
const jumpSpread = args.mainWeaponParams.Jump_DegSwerve;
|
|
|
|
if (
|
|
typeof jumpSpread !== "number" ||
|
|
typeof groundSpread !== "number" ||
|
|
jumpSpread === groundSpread
|
|
)
|
|
return;
|
|
|
|
const { effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SHOT_SPREAD_AIR_ABILITY,
|
|
}),
|
|
key: "ReduceJumpSwerveRate",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const extraSpread = jumpSpread - groundSpread;
|
|
const reducedExtraSpread = extraSpread * (1 - effect);
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(jumpSpread),
|
|
value: roundToNDecimalPlaces(reducedExtraSpread + groundSpread),
|
|
modifiedBy: SHOT_SPREAD_AIR_ABILITY,
|
|
};
|
|
}
|
|
|
|
function shotAutofireSpreadAir(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["shotAutofireSpreadAir"] {
|
|
const SHOT_SPREAD_AIR_ABILITY = "IA";
|
|
const groundSpread = args.mainWeaponParams.Variable_Stand_DegSwerve;
|
|
const jumpSpread = args.mainWeaponParams.Variable_Jump_DegSwerve;
|
|
|
|
if (
|
|
typeof jumpSpread !== "number" ||
|
|
typeof groundSpread !== "number" ||
|
|
jumpSpread === groundSpread
|
|
)
|
|
return;
|
|
|
|
const { effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SHOT_SPREAD_AIR_ABILITY,
|
|
}),
|
|
key: "ReduceJumpSwerveRate",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const extraSpread = jumpSpread - groundSpread;
|
|
const reducedExtraSpread = extraSpread * (1 - effect);
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(jumpSpread),
|
|
value: roundToNDecimalPlaces(reducedExtraSpread + groundSpread),
|
|
modifiedBy: SHOT_SPREAD_AIR_ABILITY,
|
|
};
|
|
}
|
|
|
|
function squidSurgeChargeFrames(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["squidSurgeChargeFrames"] {
|
|
const SQUID_SURGE_CHARGE_FRAMES_ABILITY = "IA";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SQUID_SURGE_CHARGE_FRAMES_ABILITY,
|
|
}),
|
|
key: "WallJumpChargeFrm",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: Math.ceil(baseEffect),
|
|
value: Math.ceil(effect),
|
|
modifiedBy: SQUID_SURGE_CHARGE_FRAMES_ABILITY,
|
|
};
|
|
}
|
|
|
|
function damageTakenInEnemyInkPerSecond(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["damageTakenInEnemyInkPerSecond"] {
|
|
const DAMAGE_TAKEN_IN_ENEMY_INK_ABILITY = "RES";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: DAMAGE_TAKEN_IN_ENEMY_INK_ABILITY,
|
|
}),
|
|
key: "OpInk_DamagePerFrame",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: effectToDamage(baseEffect) * 60,
|
|
value: effectToDamage(effect) * 60,
|
|
modifiedBy: DAMAGE_TAKEN_IN_ENEMY_INK_ABILITY,
|
|
};
|
|
}
|
|
|
|
function enemyInkDamageLimit(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["enemyInkDamageLimit"] {
|
|
const ENEMY_INK_DAMAGE_LIMIT_ABILITY = "RES";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: ENEMY_INK_DAMAGE_LIMIT_ABILITY,
|
|
}),
|
|
key: "OpInk_DamageLmt",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: effectToDamage(baseEffect),
|
|
value: effectToDamage(effect),
|
|
modifiedBy: ENEMY_INK_DAMAGE_LIMIT_ABILITY,
|
|
};
|
|
}
|
|
|
|
function effectToDamage(effect: number) {
|
|
// not sure where the 0.05 is coming from. Old analyzer had it as well so assuming it's correct.
|
|
return Number((effect * 100 - 0.05).toFixed(1));
|
|
}
|
|
|
|
function framesBeforeTakingDamageInEnemyInk(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["framesBeforeTakingDamageInEnemyInk"] {
|
|
const FRAMES_BEFORE_TAKING_DAMAGE_IN_ENEMY_INK_ABILITY = "RES";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: FRAMES_BEFORE_TAKING_DAMAGE_IN_ENEMY_INK_ABILITY,
|
|
}),
|
|
key: "OpInk_ArmorHP",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: Math.ceil(baseEffect),
|
|
value: Math.ceil(effect),
|
|
modifiedBy: FRAMES_BEFORE_TAKING_DAMAGE_IN_ENEMY_INK_ABILITY,
|
|
};
|
|
}
|
|
|
|
const SUB_WEAPON_STATS = [
|
|
{
|
|
analyzedBuildKey: "subVelocity",
|
|
abilityValuesKey: "SpawnSpeedZSpecUp",
|
|
type: "SUB_VELOCITY",
|
|
},
|
|
{
|
|
analyzedBuildKey: "subFirstPhaseDuration",
|
|
abilityValuesKey: "PeriodFirst",
|
|
type: "TIME",
|
|
},
|
|
{
|
|
analyzedBuildKey: "subSecondPhaseDuration",
|
|
abilityValuesKey: "PeriodSecond",
|
|
type: "TIME",
|
|
},
|
|
{
|
|
analyzedBuildKey: "subMarkingTimeInSeconds",
|
|
abilityValuesKey: "MarkingFrameSubSpec",
|
|
type: "TIME",
|
|
},
|
|
{
|
|
analyzedBuildKey: "subMarkingRadius",
|
|
abilityValuesKey: "SensorRadius",
|
|
type: "NO_CHANGE",
|
|
},
|
|
{
|
|
analyzedBuildKey: "subExplosionRadius",
|
|
abilityValuesKey: "ExplosionRadius",
|
|
type: "NO_CHANGE",
|
|
},
|
|
{ analyzedBuildKey: "subHp", abilityValuesKey: "MaxHP", type: "HP" },
|
|
] as const;
|
|
export function subStats(
|
|
args: Pick<StatFunctionInput, "subWeaponParams" | "abilityPoints">,
|
|
) {
|
|
const result: Partial<AnalyzedBuild["stats"]> = {};
|
|
const SUB_STATS_KEY = "BRU";
|
|
|
|
for (const { analyzedBuildKey, abilityValuesKey, type } of SUB_WEAPON_STATS) {
|
|
if (!hasEffect({ key: abilityValuesKey, weapon: args.subWeaponParams })) {
|
|
continue;
|
|
}
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUB_STATS_KEY,
|
|
}),
|
|
key: abilityValuesKey,
|
|
weapon: args.subWeaponParams,
|
|
});
|
|
|
|
const toValue = (effect: number) => {
|
|
switch (type) {
|
|
case "NO_CHANGE":
|
|
return roundToNDecimalPlaces(effect);
|
|
case "SUB_VELOCITY":
|
|
return roundToNDecimalPlaces(effect, 3);
|
|
case "HP":
|
|
return roundToNDecimalPlaces(hpDivided(effect), 1);
|
|
case "TIME":
|
|
return framesToSeconds(effect);
|
|
default:
|
|
assertUnreachable(type);
|
|
}
|
|
};
|
|
|
|
result[analyzedBuildKey] = {
|
|
baseValue: toValue(baseEffect),
|
|
modifiedBy: SUB_STATS_KEY,
|
|
value: toValue(effect),
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function subDefPointSensorMarkedTimeInSeconds(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["subDefPointSensorMarkedTimeInSeconds"] {
|
|
const SUB_DEF_POINT_SENSOR_MARKED_TIME_IN_SECONDS_KEY = "SRU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUB_DEF_POINT_SENSOR_MARKED_TIME_IN_SECONDS_KEY,
|
|
}),
|
|
key: "MarkingTimeRt",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const pointSensorParams = weaponParams().subWeapons[POINT_SENSOR_ID];
|
|
|
|
const { baseEffect: markingTimeEffect } = abilityPointsToEffects({
|
|
abilityPoints: 0,
|
|
key: "MarkingFrameSubSpec",
|
|
weapon: pointSensorParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(markingTimeEffect * baseEffect),
|
|
modifiedBy: SUB_DEF_POINT_SENSOR_MARKED_TIME_IN_SECONDS_KEY,
|
|
value: framesToSeconds(markingTimeEffect * effect),
|
|
};
|
|
}
|
|
|
|
function subDefInkMineMarkedTimeInSeconds(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["subDefInkMineMarkedTimeInSeconds"] {
|
|
const SUB_DEF_INK_MINE_MARKED_TIME_IN_SECONDS_KEY = "SRU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUB_DEF_INK_MINE_MARKED_TIME_IN_SECONDS_KEY,
|
|
}),
|
|
key: "MarkingTimeRt_Trap",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const inkMineParams = weaponParams().subWeapons[INK_MINE_ID];
|
|
|
|
const { baseEffect: markingTimeEffect } = abilityPointsToEffects({
|
|
abilityPoints: 0,
|
|
key: "MarkingFrameSubSpec",
|
|
weapon: inkMineParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(markingTimeEffect * baseEffect),
|
|
modifiedBy: SUB_DEF_INK_MINE_MARKED_TIME_IN_SECONDS_KEY,
|
|
value: framesToSeconds(markingTimeEffect * effect),
|
|
};
|
|
}
|
|
|
|
function subDefAngleShooterMarkedTimeInSeconds(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["subDefAngleShooterMarkedTimeInSeconds"] {
|
|
const SUB_DEF_ANGLE_SHOOTER_MARKED_TIME_IN_SECONDS_KEY = "SRU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUB_DEF_ANGLE_SHOOTER_MARKED_TIME_IN_SECONDS_KEY,
|
|
}),
|
|
key: "MarkingTimeRt",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
const angleShooterParams = weaponParams().subWeapons[ANGLE_SHOOTER_ID];
|
|
|
|
const { baseEffect: markingTimeEffect } = abilityPointsToEffects({
|
|
abilityPoints: 0,
|
|
key: "MarkingFrameSubSpec",
|
|
weapon: angleShooterParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(markingTimeEffect * baseEffect),
|
|
modifiedBy: SUB_DEF_ANGLE_SHOOTER_MARKED_TIME_IN_SECONDS_KEY,
|
|
value: framesToSeconds(markingTimeEffect * effect),
|
|
};
|
|
}
|
|
|
|
function subDefToxicMistMovementReduction(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["subDefToxicMistMovementReduction"] {
|
|
const SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY = "SRU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY,
|
|
}),
|
|
key: "MoveDownRt_PoisonMist",
|
|
weapon: args.mainWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
|
value: roundToNDecimalPlaces(effect * 100),
|
|
modifiedBy: SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY,
|
|
};
|
|
}
|
|
|
|
function subQsjBoost(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["subQsjBoost"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "SubSpecUpParam",
|
|
weapon: args.subWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SUB_QSJ_BOOST_KEY = "BRU";
|
|
|
|
// Lean: This is the base that is used with their weird formula (I didn't even bother renaming the vars and just used what my disassembler gave me)
|
|
const calculate = (ap: number) => {
|
|
const multiplier = abilityValues({
|
|
key: "SubSpecUpParam",
|
|
weapon: args.subWeaponParams,
|
|
});
|
|
|
|
const v7 =
|
|
((multiplier[1] - multiplier[2]) / multiplier[0] - 17.8 / multiplier[0]) /
|
|
((17.8 / multiplier[0]) * (17.8 / multiplier[0] + -1.0));
|
|
|
|
const v8 = (ap / multiplier[0]) * ((ap / multiplier[0]) * v7 + (1.0 - v7));
|
|
|
|
return Math.floor(multiplier[2] + (multiplier[0] - multiplier[2]) * v8);
|
|
};
|
|
|
|
return {
|
|
baseValue: calculate(0),
|
|
value: calculate(args.abilityPoints.get(SUB_QSJ_BOOST_KEY) ?? 0),
|
|
modifiedBy: SUB_QSJ_BOOST_KEY,
|
|
};
|
|
}
|
|
|
|
function specialDurationInSeconds(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialDurationInSeconds"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "SpecialDurationFrame",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_DURATION_IN_SECONDS_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_DURATION_IN_SECONDS_KEY,
|
|
}),
|
|
key: "SpecialDurationFrame",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(baseEffect),
|
|
value: framesToSeconds(effect),
|
|
modifiedBy: SPECIAL_DURATION_IN_SECONDS_KEY,
|
|
};
|
|
}
|
|
|
|
function specialDamageDistance(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialDamageDistance"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "DistanceDamageDistanceRate",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_DAMAGE_DISTANCE_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_DAMAGE_DISTANCE_KEY,
|
|
}),
|
|
key: "DistanceDamageDistanceRate",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect, 4),
|
|
value: roundToNDecimalPlaces(effect, 4),
|
|
modifiedBy: SPECIAL_DAMAGE_DISTANCE_KEY,
|
|
};
|
|
}
|
|
|
|
function specialPaintRadius(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialPaintRadius"] {
|
|
for (const key of ["PaintRadius", "CrossPaintRadius"] as const) {
|
|
if (!hasEffect({ key, weapon: args.specialWeaponParams })) {
|
|
continue;
|
|
}
|
|
|
|
const SPECIAL_PAINT_RADIUS_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_PAINT_RADIUS_KEY,
|
|
}),
|
|
key,
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect, 4),
|
|
value: roundToNDecimalPlaces(effect, 4),
|
|
modifiedBy: SPECIAL_PAINT_RADIUS_KEY,
|
|
};
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
export function specialFieldHp(
|
|
args: Pick<StatFunctionInput, "specialWeaponParams" | "abilityPoints">,
|
|
): AnalyzedBuild["stats"]["specialFieldHp"] {
|
|
if (!hasEffect({ key: "MaxFieldHP", weapon: args.specialWeaponParams })) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_FIELD_HP_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_FIELD_HP_KEY,
|
|
}),
|
|
key: "MaxFieldHP",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: Math.round(baseEffect / 10),
|
|
value: Math.round(effect / 10),
|
|
modifiedBy: SPECIAL_FIELD_HP_KEY,
|
|
};
|
|
}
|
|
|
|
export function specialDeviceHp(
|
|
args: Pick<StatFunctionInput, "specialWeaponParams" | "abilityPoints">,
|
|
): AnalyzedBuild["stats"]["specialDeviceHp"] {
|
|
if (!hasEffect({ key: "MaxHP", weapon: args.specialWeaponParams })) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_DEVICE_HP_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_DEVICE_HP_KEY,
|
|
}),
|
|
key: "MaxHP",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: Math.round(hpDivided(baseEffect)),
|
|
value: Math.round(hpDivided(effect)),
|
|
modifiedBy: SPECIAL_DEVICE_HP_KEY,
|
|
};
|
|
}
|
|
|
|
// GameParameters -> WeaponParam -> InkCapacityRt
|
|
const ZIPCASTER_INKTANK_SIZE = 1.5;
|
|
|
|
function specialHookInkConsumptionPercentage(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialHookInkConsumptionPercentage"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "InkConsume_Hook",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_HOOK_INK_CONSUMPTION_PERCENTAGE_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_HOOK_INK_CONSUMPTION_PERCENTAGE_KEY,
|
|
}),
|
|
key: "InkConsume_Hook",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(
|
|
(baseEffect * 100) / ZIPCASTER_INKTANK_SIZE,
|
|
),
|
|
value: roundToNDecimalPlaces((effect * 100) / ZIPCASTER_INKTANK_SIZE),
|
|
modifiedBy: SPECIAL_HOOK_INK_CONSUMPTION_PERCENTAGE_KEY,
|
|
};
|
|
}
|
|
|
|
function specialInkConsumptionPerSecondPercentage(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialInkConsumptionPerSecondPercentage"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "InkConsume_PerSec",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_INK_CONSUMPTION_PER_SECOND_PERCENTAGE_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_INK_CONSUMPTION_PER_SECOND_PERCENTAGE_KEY,
|
|
}),
|
|
key: "InkConsume_PerSec",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(
|
|
(baseEffect * 100) / ZIPCASTER_INKTANK_SIZE,
|
|
),
|
|
value: roundToNDecimalPlaces((effect * 100) / ZIPCASTER_INKTANK_SIZE),
|
|
modifiedBy: SPECIAL_INK_CONSUMPTION_PER_SECOND_PERCENTAGE_KEY,
|
|
};
|
|
}
|
|
|
|
function specialReticleRadius(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialReticleRadius"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "TargetInCircleRadius",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_RETICLE_RADIUS_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_RETICLE_RADIUS_KEY,
|
|
}),
|
|
key: "TargetInCircleRadius",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect),
|
|
value: roundToNDecimalPlaces(effect),
|
|
modifiedBy: SPECIAL_RETICLE_RADIUS_KEY,
|
|
};
|
|
}
|
|
|
|
function specialThrowDistance(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialThrowDistance"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "SpawnSpeedZSpecUp",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_THROW_DISTANCE_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_THROW_DISTANCE_KEY,
|
|
}),
|
|
key: "SpawnSpeedZSpecUp",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect),
|
|
value: roundToNDecimalPlaces(effect),
|
|
modifiedBy: SPECIAL_THROW_DISTANCE_KEY,
|
|
};
|
|
}
|
|
|
|
function specialMoveSpeed(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialMoveSpeed"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "MoveSpeed",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_MOVE_SPEED_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_MOVE_SPEED_KEY,
|
|
}),
|
|
key: "MoveSpeed",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect, 4),
|
|
value: roundToNDecimalPlaces(effect, 4),
|
|
modifiedBy: SPECIAL_MOVE_SPEED_KEY,
|
|
};
|
|
}
|
|
|
|
function specialAutoChargeRate(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialAutoChargeRate"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "ChargeRateAutoPerFrame",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_AUTO_CHARGE_RATE_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_AUTO_CHARGE_RATE_KEY,
|
|
}),
|
|
key: "ChargeRateAutoPerFrame",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
|
value: roundToNDecimalPlaces(effect * 100),
|
|
modifiedBy: SPECIAL_AUTO_CHARGE_RATE_KEY,
|
|
};
|
|
}
|
|
|
|
function specialMaxRadius(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialMaxRadius"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "MaxRadius",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_MAX_RADIUS_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_MAX_RADIUS_KEY,
|
|
}),
|
|
key: "MaxRadius",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(baseEffect),
|
|
value: roundToNDecimalPlaces(effect),
|
|
modifiedBy: SPECIAL_MAX_RADIUS_KEY,
|
|
};
|
|
}
|
|
|
|
function specialRadiusRangeMax(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialRadiusRangeMax"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "RadiusMax",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_RADIUS_RANGE_KEY = "SPU";
|
|
|
|
const radiusMax = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_RADIUS_RANGE_KEY,
|
|
}),
|
|
key: "RadiusMax",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(radiusMax.baseEffect),
|
|
value: roundToNDecimalPlaces(radiusMax.effect),
|
|
modifiedBy: SPECIAL_RADIUS_RANGE_KEY,
|
|
};
|
|
}
|
|
function specialRadiusRangeMin(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialRadiusRangeMin"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "RadiusMin",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_RADIUS_RANGE_KEY = "SPU";
|
|
|
|
const radiusMin = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_RADIUS_RANGE_KEY,
|
|
}),
|
|
key: "RadiusMin",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: roundToNDecimalPlaces(radiusMin.baseEffect),
|
|
value: roundToNDecimalPlaces(radiusMin.effect),
|
|
modifiedBy: SPECIAL_RADIUS_RANGE_KEY,
|
|
};
|
|
}
|
|
|
|
function specialPowerUpDuration(
|
|
args: StatFunctionInput,
|
|
): AnalyzedBuild["stats"]["specialPowerUpDuration"] {
|
|
if (
|
|
!hasEffect({
|
|
key: "PowerUpFrame",
|
|
weapon: args.specialWeaponParams,
|
|
})
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const SPECIAL_POWER_UP_DURATION_KEY = "SPU";
|
|
const { baseEffect, effect } = abilityPointsToEffects({
|
|
abilityPoints: apFromMap({
|
|
abilityPoints: args.abilityPoints,
|
|
ability: SPECIAL_POWER_UP_DURATION_KEY,
|
|
}),
|
|
key: "PowerUpFrame",
|
|
weapon: args.specialWeaponParams,
|
|
});
|
|
|
|
return {
|
|
baseValue: framesToSeconds(baseEffect),
|
|
value: framesToSeconds(effect),
|
|
modifiedBy: SPECIAL_POWER_UP_DURATION_KEY,
|
|
};
|
|
}
|