import type { Ability, MainWeaponId } from "~/modules/in-game-lists"; import { ANGLE_SHOOTER_ID } from "~/modules/in-game-lists"; import { INK_MINE_ID, POINT_SENSOR_ID } from "~/modules/in-game-lists"; import type { AbilityPoints, AnalyzedBuild, DamageType, InkConsumeType, MainWeaponParams, StatFunctionInput, SubWeaponParams, } from "./types"; import { DAMAGE_TYPE } from "./constants"; import { INK_CONSUME_TYPES } from "./types"; import invariant from "tiny-invariant"; import { abilityPointsToEffects, apFromMap, hasEffect, weaponParams, } from "./utils"; import { assertUnreachable } from "~/utils/types"; import { semiRandomId } from "~/utils/strings"; import { roundToTwoDecimalPlaces } from "~/utils/number"; export function buildStats({ weaponSplId, abilityPoints = new Map(), mainOnlyAbilities = [], }: { weaponSplId: MainWeaponId; abilityPoints?: AbilityPoints; mainOnlyAbilities?: Array; }): AnalyzedBuild { const mainWeaponParams = weaponParams().mainWeapons[weaponSplId]; invariant(mainWeaponParams, `Weapon with splId ${weaponSplId} not found`); const subWeaponParams = weaponParams().subWeapons[mainWeaponParams.subWeaponId]; invariant( subWeaponParams, `Sub weapon with splId ${mainWeaponParams.subWeaponId} not found` ); const specialWeaponParams = weaponParams().specialWeapons[mainWeaponParams.specialWeaponId]; invariant( specialWeaponParams, `Special weapon with splId ${mainWeaponParams.specialWeaponId} not found` ); const input: StatFunctionInput = { weaponSplId, mainWeaponParams, subWeaponParams, specialWeaponParams, abilityPoints, mainOnlyAbilities, }; return { weapon: { subWeaponSplId: mainWeaponParams.subWeaponId, specialWeaponSplId: mainWeaponParams.specialWeaponId, brellaCanopyHp: mainWeaponParams.CanopyHP, fullChargeSeconds: mainWeaponParams.ChargeFrameFullCharge ? framesToSeconds(mainWeaponParams.ChargeFrameFullCharge) : undefined, maxChargeHoldSeconds: mainWeaponParams.KeepChargeFullFrame ? framesToSeconds(mainWeaponParams.KeepChargeFullFrame) : undefined, speedType: mainWeaponParams.WeaponSpeedType ?? "Normal", isTripleShooter: Boolean(mainWeaponParams.TripleShotSpanFrame), }, stats: { specialPoint: specialPoint(input), specialSavedAfterDeath: specialSavedAfterDeath(input), fullInkTankOptions: fullInkTankOptions(input), damages: damages(input), mainWeaponWhiteInkSeconds: typeof mainWeaponParams.InkRecoverStop === "number" ? framesToSeconds(mainWeaponParams.InkRecoverStop) : undefined, subWeaponWhiteInkSeconds: framesToSeconds(subWeaponParams.InkRecoverStop), squidFormInkRecoverySeconds: squidFormInkRecoverySeconds(input), runSpeed: runSpeed(input), shootingRunSpeed: shootingRunSpeed(input, "MoveSpeed"), shootingRunSpeedCharging: shootingRunSpeed(input, "MoveSpeed_Charge"), shootingRunSpeedFullCharge: shootingRunSpeed( input, "MoveSpeedFullCharge" ), swimSpeed: swimSpeed(input), runSpeedInEnemyInk: runSpeedInEnemyInk(input), damageTakenInEnemyInkPerSecond: damageTakenInEnemyInkPerSecond(input), enemyInkDamageLimit: enemyInkDamageLimit(input), framesBeforeTakingDamageInEnemyInk: framesBeforeTakingDamageInEnemyInk(input), quickRespawnTime: quickRespawnTime(input), superJumpTimeGroundFrames: superJumpTimeGroundFrames(input), superJumpTimeTotal: superJumpTimeTotal(input), shotSpreadAir: shotSpreadAir(input), shotSpreadGround: mainWeaponParams.Stand_DegSwerve, squidSurgeChargeFrames: squidSurgeChargeFrames(input), subDefPointSensorMarkedTimeInSeconds: subDefPointSensorMarkedTimeInSeconds(input), subDefInkMineMarkedTimeInSeconds: subDefInkMineMarkedTimeInSeconds(input), subDefAngleShooterMarkedTimeInSeconds: subDefAngleShooterMarkedTimeInSeconds(input), subDefToxicMistMovementReduction: subDefToxicMistMovementReduction(input), subDefAngleShooterDamage: subDefAngleShooterDamage(input), subDefSplashWallDamagePercentage: subDefSplashWallDamagePercentage(input), subDefSprinklerDamagePercentage: subDefSprinklerDamagePercentage(input), subDefBombDamageLightPercentage: subDefBombDamageLightPercentage(input), subDefBombDamageHeavyPercentage: subDefBombDamageHeavyPercentage(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), specialAutoChargeRate: specialAutoChargeRate(input), specialMaxRadius: specialMaxRadius(input), specialRadiusRange: specialRadiusRange(input), specialPowerUpDuration: specialPowerUpDuration(input), }, }; } const SPLATTERSHOT_JR_ID = 10; function inkTankSize(weaponSplId: StatFunctionInput["weaponSplId"]) { if (weaponSplId === SPLATTERSHOT_JR_ID) 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; function specialSavedAfterDeath({ abilityPoints, mainWeaponParams, mainOnlyAbilities, }: StatFunctionInput): 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: hasRespawnPunisher ? "apBeforeTacticooler" : "ap", }), key: "SpecialGaugeRt_Restart", weapon: mainWeaponParams, }); return { baseValue: specialSavedAfterDeathForDisplay(baseEffect), value: specialSavedAfterDeathForDisplay(effect - extraSpecialLost), modifiedBy: [SPECIAL_SAVED_AFTER_DEATH_ABILITY, "RP"], }; } 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: semiRandomId(), subsUsed: subsFromFullInkTank, type, value: effectToRounded( (inkTankSize(args.weaponSplId) - subWeaponInkConsume * subsFromFullInkTank) / mainWeaponInkConsume ), }); } } return result; } function effectToRounded(effect: number) { return Number(effect.toFixed(2)); } 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 { 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 > = { NORMAL_MIN: "DamageParam_ValueMin", NORMAL_MAX: "DamageParam_ValueMax", DIRECT: "DamageParam_ValueDirect", DISTANCE: "BlastParam_DistanceDamage", FULL_CHARGE: "DamageParam_ValueFullCharge", MAX_CHARGE: "DamageParam_ValueMaxCharge", TAP_SHOT: "DamageParam_ValueMinCharge", BOMB_NORMAL: "DistanceDamage", BOMB_DIRECT: "DirectDamage", }; function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] { const result: AnalyzedBuild["stats"]["damages"] = []; for (const type of DAMAGE_TYPE) { const key = damageTypeToParamsKey[type]; const value = args.mainWeaponParams[key as keyof MainWeaponParams] ?? args.subWeaponParams[key as keyof SubWeaponParams]; if (Array.isArray(value)) { for (const subValue of value.flat()) { result.push({ type, value: subValue.Damage / 10, distance: subValue.Distance, id: semiRandomId(), }); } continue; } if (typeof value !== "number") continue; result.push({ id: semiRandomId(), type, value: value / 10, shotsToSplat: shotsToSplat({ value, type, isTripleShooter: Boolean(args.mainWeaponParams.TripleShotSpanFrame), }), }); } return result; } function shotsToSplat({ value, type, isTripleShooter, }: { value: number; type: DamageType; isTripleShooter: boolean; }) { if (type !== "NORMAL_MAX") return; const multiplier = isTripleShooter ? 3 : 1; return Math.ceil(1000 / (value * multiplier)); } 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 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" ): 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"], }; } const RESPAWN_CHASE_FRAME = 150; const OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 68; const SPLATOON_3_FASTER_RESPAWN = 60; function quickRespawnTime( args: StatFunctionInput ): AnalyzedBuild["stats"]["quickRespawnTime"] { const QUICK_RESPAWN_TIME_ABILITY = "QR"; const hasRespawnPunisher = args.mainOnlyAbilities.includes("RP"); const chase = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: QUICK_RESPAWN_TIME_ABILITY, key: hasRespawnPunisher ? "apBeforeTacticooler" : "ap", }), key: "Dying_ChaseFrm", weapon: args.mainWeaponParams, }); const around = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: QUICK_RESPAWN_TIME_ABILITY, key: hasRespawnPunisher ? "apBeforeTacticooler" : "ap", }), key: "Dying_AroundFrm", weapon: args.mainWeaponParams, }); const extraFrames = hasRespawnPunisher ? OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES : 0; return { baseValue: framesToSeconds( RESPAWN_CHASE_FRAME + chase.baseEffect + around.baseEffect - SPLATOON_3_FASTER_RESPAWN ), value: framesToSeconds( RESPAWN_CHASE_FRAME + chase.effect + around.effect + extraFrames - 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: roundToTwoDecimalPlaces(jumpSpread), value: roundToTwoDecimalPlaces(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: "NO_CHANGE", }, { 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; function subStats(args: StatFunctionInput) { const result: Partial = {}; 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 roundToTwoDecimalPlaces(effect); case "HP": return roundToTwoDecimalPlaces(effect / 10); 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: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), modifiedBy: SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY, }; } function subDefAngleShooterDamage( args: StatFunctionInput ): AnalyzedBuild["stats"]["subDefAngleShooterDamage"] { const SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY = "SRU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY, }), key: "DamageRt_LineMarker", weapon: args.mainWeaponParams, }); const angleShooterDirectDamage = weaponParams().subWeapons[ANGLE_SHOOTER_ID].DirectDamage; invariant(angleShooterDirectDamage); return { baseValue: roundToTwoDecimalPlaces( (angleShooterDirectDamage * baseEffect) / 10 ), value: roundToTwoDecimalPlaces((angleShooterDirectDamage * effect) / 10), modifiedBy: SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY, }; } function subDefSplashWallDamagePercentage( args: StatFunctionInput ): AnalyzedBuild["stats"]["subDefSplashWallDamagePercentage"] { const SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY = "SRU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY, }), key: "DamageRt_Shield", weapon: args.mainWeaponParams, }); return { baseValue: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), modifiedBy: SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY, }; } function subDefSprinklerDamagePercentage( args: StatFunctionInput ): AnalyzedBuild["stats"]["subDefSprinklerDamagePercentage"] { const SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY = "SRU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY, }), key: "DamageRt_Sprinkler", weapon: args.mainWeaponParams, }); return { baseValue: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), modifiedBy: SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY, }; } function subDefBombDamageLightPercentage( args: StatFunctionInput ): AnalyzedBuild["stats"]["subDefBombDamageLightPercentage"] { const SUB_DEF_BOMB_DAMAGE_LIGHT_PERCENTAGE_KEY = "SRU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SUB_DEF_BOMB_DAMAGE_LIGHT_PERCENTAGE_KEY, }), key: "DamageRt_BombL", weapon: args.mainWeaponParams, }); return { baseValue: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), modifiedBy: SUB_DEF_BOMB_DAMAGE_LIGHT_PERCENTAGE_KEY, }; } function subDefBombDamageHeavyPercentage( args: StatFunctionInput ): AnalyzedBuild["stats"]["subDefBombDamageHeavyPercentage"] { const SUB_DEF_BOMB_DAMAGE_HEAVY_PERCENTAGE_KEY = "SRU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SUB_DEF_BOMB_DAMAGE_HEAVY_PERCENTAGE_KEY, }), key: "DamageRt_BombH", weapon: args.mainWeaponParams, }); return { baseValue: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), modifiedBy: SUB_DEF_BOMB_DAMAGE_HEAVY_PERCENTAGE_KEY, }; } function subQsjBoost( args: StatFunctionInput ): AnalyzedBuild["stats"]["subQsjBoost"] { if ( !hasEffect({ key: "SubSpecUpParam", weapon: args.subWeaponParams, }) ) { return; } const SUB_QSJ_BOOST_KEY = "BRU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SUB_QSJ_BOOST_KEY, }), key: "SubSpecUpParam", weapon: args.subWeaponParams, }); return { baseValue: Math.floor(baseEffect), value: Math.floor(effect), 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: roundToTwoDecimalPlaces(baseEffect), value: roundToTwoDecimalPlaces(effect), modifiedBy: SPECIAL_DAMAGE_DISTANCE_KEY, }; } function specialPaintRadius( args: StatFunctionInput ): AnalyzedBuild["stats"]["specialPaintRadius"] { if (!hasEffect({ key: "PaintRadius", weapon: args.specialWeaponParams })) { return; } const SPECIAL_PAINT_RADIUS_KEY = "SPU"; const { baseEffect, effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SPECIAL_PAINT_RADIUS_KEY, }), key: "PaintRadius", weapon: args.specialWeaponParams, }); return { baseValue: roundToTwoDecimalPlaces(baseEffect), value: roundToTwoDecimalPlaces(effect), modifiedBy: SPECIAL_PAINT_RADIUS_KEY, }; } function specialFieldHp( args: StatFunctionInput ): 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, }; } function specialDeviceHp( args: StatFunctionInput ): 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(baseEffect / 10), value: Math.round(effect / 10), modifiedBy: SPECIAL_DEVICE_HP_KEY, }; } 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: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), 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: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(effect * 100), 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: roundToTwoDecimalPlaces(baseEffect), value: roundToTwoDecimalPlaces(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: roundToTwoDecimalPlaces(baseEffect), value: roundToTwoDecimalPlaces(effect), modifiedBy: SPECIAL_THROW_DISTANCE_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: roundToTwoDecimalPlaces(baseEffect * 100), value: roundToTwoDecimalPlaces(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: roundToTwoDecimalPlaces(baseEffect), value: roundToTwoDecimalPlaces(effect), modifiedBy: SPECIAL_MAX_RADIUS_KEY, }; } function specialRadiusRange( args: StatFunctionInput ): AnalyzedBuild["stats"]["specialRadiusRange"] { if ( !hasEffect({ key: "RadiusMax", weapon: args.specialWeaponParams, }) || !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, }); const radiusMax = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: SPECIAL_RADIUS_RANGE_KEY, }), key: "RadiusMax", weapon: args.specialWeaponParams, }); return { baseValue: `${roundToTwoDecimalPlaces( radiusMin.baseEffect )}-${roundToTwoDecimalPlaces(radiusMax.baseEffect)}`, value: `${roundToTwoDecimalPlaces( radiusMin.effect )}-${roundToTwoDecimalPlaces(radiusMax.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, }; }