import type { MainWeaponId } from "~/modules/in-game-lists"; import type { AbilityPoints, AnalyzedBuild, DamageType, InkConsumeType, MainWeaponParams, StatFunctionInput, SubWeaponParams, } from "./types"; import { DAMAGE_TYPE } from "./types"; import { INK_CONSUME_TYPES } from "./types"; import invariant from "tiny-invariant"; import { abilityPointsToEffects, apFromMap, weaponParams } from "./utils"; import { assertUnreachable } from "~/utils/types"; import { semiRandomId } from "~/utils/strings"; export function buildStats({ abilityPoints, weaponSplId, }: { abilityPoints: AbilityPoints; weaponSplId: MainWeaponId; }): 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 input: StatFunctionInput = { mainWeaponParams, subWeaponParams, abilityPoints, }; 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), swimSpeed: swimSpeed(input), runSpeedInEnemyInk: runSpeedInEnemyInk(input), damageTakenInEnemyInkPerSecond: damageTakenInEnemyInkPerSecond(input), enemyInkDamageLimit: enemyInkDamageLimit(input), framesBeforeTakingDamageInEnemyInk: framesBeforeTakingDamageInEnemyInk(input), quickRespawnTime: quickRespawnTime(input), superJumpTimeGroundFrames: superJumpTimeGroundFrames(input), superJumpTimeTotal: superJumpTimeTotal(input), }, }; } 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), }; } function specialSavedAfterDeath({ abilityPoints, mainWeaponParams, }: StatFunctionInput): AnalyzedBuild["stats"]["specialPoint"] { const SPECIAL_SAVED_AFTER_DEATH_ABILITY = "SS"; 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, }); return { baseValue: specialSavedAfterDeathForDisplay(baseEffect), value: specialSavedAfterDeathForDisplay(effect), modifiedBy: SPECIAL_SAVED_AFTER_DEATH_ABILITY, }; } 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( (1 - subWeaponInkConsume * subsFromFullInkTank) / mainWeaponInkConsume ), }); } } return result; } function effectToRounded(effect: number) { return Number(effect.toFixed(2)); } function subWeaponConsume({ mainWeaponParams, subWeaponParams, abilityPoints, }: StatFunctionInput) { const { effect } = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints, ability: "ISS", }), // xxx: placeholder fallback before prod key: `ConsumeRt_Sub_Lv${subWeaponParams.SubInkSaveLv ?? 0}`, weapon: mainWeaponParams, }); // xxx: placeholder fallback before prod const inkConsume = subWeaponParams.InkConsume ?? 0.6; const inkConsumeAfterISS = inkConsume * effect; return { inkConsume: inkConsumeAfterISS, maxSubsFromFullInkTank: Math.floor(1 / 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", }; 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) { 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), value: framesToSeconds(effect), 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 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, }); return { baseValue: effectToRounded(baseEffect * 10), value: effectToRounded(effect * 10), modifiedBy: SWIM_SPEED_ABILITY, }; } // xxx: take in account own use of RP and other use of RP const RESPAWN_CHASE_FRAME = 150; function quickRespawnTime( args: StatFunctionInput ): AnalyzedBuild["stats"]["quickRespawnTime"] { const QUICK_RESPAWN_TIME_ABILITY = "QR"; const chase = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: QUICK_RESPAWN_TIME_ABILITY, }), key: "Dying_ChaseFrm", weapon: args.mainWeaponParams, }); const around = abilityPointsToEffects({ abilityPoints: apFromMap({ abilityPoints: args.abilityPoints, ability: QUICK_RESPAWN_TIME_ABILITY, }), key: "Dying_AroundFrm", weapon: args.mainWeaponParams, }); return { baseValue: framesToSeconds( RESPAWN_CHASE_FRAME + chase.baseEffect + around.baseEffect ), value: framesToSeconds(RESPAWN_CHASE_FRAME + chase.effect + around.effect), modifiedBy: QUICK_RESPAWN_TIME_ABILITY, }; } 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 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, }; }