diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx index 49f944f2f..b68697f09 100644 --- a/app/components/Combobox.tsx +++ b/app/components/Combobox.tsx @@ -14,12 +14,17 @@ import { subWeaponIds, weaponCategories, } from "~/modules/in-game-lists"; -import { nonBombSubWeaponIds } from "~/modules/in-game-lists/weapon-ids"; +import { + nonBombSubWeaponIds, + nonDamagingSpecialWeaponIds, + specialWeaponIds, +} from "~/modules/in-game-lists/weapon-ids"; import { type SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; import type { Unpacked } from "~/utils/types"; import { gearImageUrl, mainWeaponImageUrl, + specialWeaponImageUrl, subWeaponImageUrl, } from "~/utils/urls"; import { Image } from "./Image"; @@ -307,6 +312,16 @@ export function AllWeaponCombobox({ }); } + for (const specialWeaponId of specialWeaponIds) { + if (nonDamagingSpecialWeaponIds.includes(specialWeaponId)) continue; + + result.push({ + value: `SPECIAL_${specialWeaponId}`, + label: t(`SPECIAL_${specialWeaponId}`), + imgPath: specialWeaponImageUrl(specialWeaponId), + }); + } + return result; }; diff --git a/app/features/build-analyzer/analyzer-constants.ts b/app/features/build-analyzer/analyzer-constants.ts index b8fc6069c..b6b4b302c 100644 --- a/app/features/build-analyzer/analyzer-constants.ts +++ b/app/features/build-analyzer/analyzer-constants.ts @@ -30,6 +30,8 @@ export const DAMAGE_TYPE = [ "ROLL_OVER", ] as const; +// a bit weird there is no SPECIAL here listed but it is used +// only to determine if the damage type is for main weapon or not export const damageTypeToWeaponType: Record< DamageType, "MAIN" | "SUB" | "SPECIAL" diff --git a/app/features/build-analyzer/analyzer-types.ts b/app/features/build-analyzer/analyzer-types.ts index bc9a0ac22..798724883 100644 --- a/app/features/build-analyzer/analyzer-types.ts +++ b/app/features/build-analyzer/analyzer-types.ts @@ -206,6 +206,7 @@ export interface AnalyzedBuild { subWeaponInkConsumptionPercentage: Stat; fullInkTankOptions: Array; damages: Array; + specialWeaponDamages: Array; subWeaponDefenseDamages: Array; squidFormInkRecoverySeconds: Stat; humanoidFormInkRecoverySeconds: Stat; @@ -263,4 +264,5 @@ export type AbilityValuesKeys = keyof typeof abilityValues; export type AnyWeapon = | { type: "MAIN"; id: MainWeaponId } - | { type: "SUB"; id: SubWeaponId }; + | { type: "SUB"; id: SubWeaponId } + | { type: "SPECIAL"; id: SpecialWeaponId }; diff --git a/app/features/build-analyzer/core/stats.ts b/app/features/build-analyzer/core/stats.ts index e1f5284e4..7678f592f 100644 --- a/app/features/build-analyzer/core/stats.ts +++ b/app/features/build-analyzer/core/stats.ts @@ -21,6 +21,7 @@ import type { DamageType, InkConsumeType, MainWeaponParams, + SpecialWeaponParams, StatFunctionInput, SubWeaponParams, } from "../analyzer-types"; @@ -107,6 +108,7 @@ export function buildStats({ specialLostSplattedByRP: specialLost(input, true), fullInkTankOptions: fullInkTankOptions(input), damages: damages(input), + specialWeaponDamages: specialWeaponDamages(input), subWeaponDefenseDamages: subWeaponDefenseDamages(input), mainWeaponWhiteInkSeconds: typeof mainWeaponParams.InkRecoverStop === "number" @@ -386,7 +388,9 @@ const damageTypeToParamsKey: Record< DamageType, | keyof MainWeaponParams | keyof SubWeaponParams - | Array + | Array< + keyof MainWeaponParams | keyof SubWeaponParams | keyof SpecialWeaponParams + > > = { NORMAL_MIN: "DamageParam_ValueMin", NORMAL_MAX: "DamageParam_ValueMax", @@ -458,6 +462,49 @@ function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] { return result; } +// xxx: TODO: handle damage distance increasing e.g. inkzooka, small special power up icon next to the distance +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: semiRandomId(), + multiShots: multiShot[args.weaponSplId], + }); + } + + continue; + } + + if (typeof value !== "number") continue; + + result.push({ + id: semiRandomId(), + type, + value: value / 10, + shotsToSplat: shotsToSplat({ + value, + type, + multiShots: multiShot[args.weaponSplId], + }), + multiShots: multiShot[args.weaponSplId], + }); + } + } + + return result; +} + function shotsToSplat({ value, type, diff --git a/app/features/build-analyzer/core/utils.ts b/app/features/build-analyzer/core/utils.ts index 2a1c2cfe8..c79ab3d3d 100644 --- a/app/features/build-analyzer/core/utils.ts +++ b/app/features/build-analyzer/core/utils.ts @@ -1,5 +1,14 @@ -import type { AbilityType, SubWeaponId } from "~/modules/in-game-lists"; -import { subWeaponIds } from "~/modules/in-game-lists"; +import type { + AbilityType, + SpecialWeaponId, + SubWeaponId, +} from "~/modules/in-game-lists"; +import { + subWeaponIds, + nonBombSubWeaponIds, + nonDamagingSpecialWeaponIds, + specialWeaponIds, +} from "~/modules/in-game-lists"; import { abilities, mainWeaponIds, @@ -25,7 +34,6 @@ import invariant from "tiny-invariant"; import { EMPTY_BUILD } from "~/constants"; import { UNKNOWN_SHORT } from "../analyzer-constants"; import type { Unpacked } from "~/utils/types"; -import { nonBombSubWeaponIds } from "~/modules/in-game-lists/weapon-ids"; export function weaponParams(): ParamsJson { return weaponParamsJson as ParamsJson; @@ -190,6 +198,20 @@ export function validatedAnyWeaponFromSearchParams( return { type: "SUB", id: id as SubWeaponId }; } + if (rawWeapon?.startsWith("SPECIAL_")) { + const id = Number(rawWeapon.replace("SPECIAL_", "")); + + if ( + !specialWeaponIds + .filter((id) => !nonDamagingSpecialWeaponIds.includes(id)) + .includes(id as any) + ) { + return DEFAULT_ANY_WEAPON; + } + + return { type: "SPECIAL", id: id as SpecialWeaponId }; + } + if (rawWeapon?.startsWith("MAIN_")) { const id = Number(rawWeapon.replace("MAIN_", "")); diff --git a/app/features/build-analyzer/core/weapon-params.json b/app/features/build-analyzer/core/weapon-params.json index 400f4f47d..3de756f14 100644 --- a/app/features/build-analyzer/core/weapon-params.json +++ b/app/features/build-analyzer/core/weapon-params.json @@ -1688,7 +1688,14 @@ "Low": 360, "Mid": 420 } - } + }, + "DistanceDamage": [ + { + "Damage": 400, + "Distance": 4 + } + ], + "DirectDamage": 2200 }, "2": { "overwrites": { @@ -1814,7 +1821,17 @@ "Low": 0.63, "Mid": 0.665 } - } + }, + "DistanceDamage": [ + { + "Damage": 500, + "Distance": 3 + }, + { + "Damage": 300, + "Distance": 5 + } + ] }, "11": { "overwrites": { diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx index 624788789..e7f6c2bb2 100644 --- a/app/features/build-analyzer/routes/analyzer.tsx +++ b/app/features/build-analyzer/routes/analyzer.tsx @@ -703,6 +703,19 @@ export default function BuildAnalyzerPage() { )} + {analyzed.stats.specialWeaponDamages.length > 0 && ( + + + + )} + {analyzed.stats.fullInkTankOptions.length > 0 && ( d.type === damageType); + const found = damages.find((d) => d.type === damageType); if (found) return found.type; const fallbackFound = damageTypePriorityList.find((type) => - analyzed.stats.damages.some((d) => d.type === type) + damages.some((d) => d.type === type) ); return fallbackFound; diff --git a/app/features/object-damage-calculator/core/objectDamage.ts b/app/features/object-damage-calculator/core/objectDamage.ts index 85e76af11..b45aef4e6 100644 --- a/app/features/object-damage-calculator/core/objectDamage.ts +++ b/app/features/object-damage-calculator/core/objectDamage.ts @@ -114,6 +114,8 @@ export function resolveAllUniqueDamageTypes({ ? analyzed.stats.subWeaponDefenseDamages .filter((damage) => damage.subWeaponId === anyWeapon.id) .map((d) => d.type) + : anyWeapon.type === "SPECIAL" + ? analyzed.stats.specialWeaponDamages.map((d) => d.type) : analyzed.stats.damages.map((d) => d.type); return removeDuplicates(damageTypes); @@ -138,7 +140,12 @@ function resolveFilteredDamages({ ); } - return analyzed.stats.damages + const damages = + anyWeapon.type === "SPECIAL" + ? analyzed.stats.specialWeaponDamages + : analyzed.stats.damages; + + return damages .filter((d) => d.type === damageType || toCombine?.combineWith === d.type) .map((damage) => { if (!isMultiShot || !damage.multiShots) return damage; diff --git a/app/features/object-damage-calculator/routes/object-damage-calculator.tsx b/app/features/object-damage-calculator/routes/object-damage-calculator.tsx index eb9f30aa2..dd1391d96 100644 --- a/app/features/object-damage-calculator/routes/object-damage-calculator.tsx +++ b/app/features/object-damage-calculator/routes/object-damage-calculator.tsx @@ -284,7 +284,7 @@ function DamageReceiversGrid({ variant="build" className="object-damage__weapon-image" /> - ) : ( + ) : weapon.type === "SUB" ? ( + ) : ( + )} {t(`weapons:${weapon.type}_${weapon.id}` as any)} diff --git a/app/modules/in-game-lists/index.ts b/app/modules/in-game-lists/index.ts index 26681fa5c..b7d0efcc4 100644 --- a/app/modules/in-game-lists/index.ts +++ b/app/modules/in-game-lists/index.ts @@ -6,6 +6,9 @@ export { weaponIdIsNotAlt, subWeaponIds, specialWeaponIds, + exampleMainWeaponIdWithSpecialWeaponId, + nonBombSubWeaponIds, + nonDamagingSpecialWeaponIds, SPLAT_BOMB_ID, SUCTION_BOMB_ID, BURST_BOMB_ID, diff --git a/app/modules/in-game-lists/weapon-ids.ts b/app/modules/in-game-lists/weapon-ids.ts index a25bd5d51..125e66db8 100644 --- a/app/modules/in-game-lists/weapon-ids.ts +++ b/app/modules/in-game-lists/weapon-ids.ts @@ -1,4 +1,5 @@ -import type { MainWeaponId, SubWeaponId } from "./types"; +import { assertUnreachable } from "~/utils/types"; +import type { MainWeaponId, SpecialWeaponId, SubWeaponId } from "./types"; export const weaponCategories = [ { @@ -137,3 +138,49 @@ export const specialWeaponIds = [ SUPER_CHUMP_ID, KRAKEN_ROYALE_ID, ] as const; + +export const nonDamagingSpecialWeaponIds = [BIG_BUBBLER_ID, TACTICOOLER_ID]; + +export const exampleMainWeaponIdWithSpecialWeaponId = ( + specialWeaponId: SpecialWeaponId +): MainWeaponId => { + switch (specialWeaponId) { + case TRIZOOKA_ID: + return 40; + case BIG_BUBBLER_ID: + return 10; + case ZIPCASTER_ID: + return 8000; + case TENTA_MISSILES_ID: + return 1030; + case INK_STORM_ID: + return 3040; + case BOOYAH_BOMB_ID: + return 3020; + case WAVE_BREAKER_ID: + return 220; + case INK_VAC_ID: + return 2010; + case KILLER_WAIL_ID: + return 50; + case INKJET_ID: + return 3010; + case ULTRA_STAMP_ID: + return 4000; + case CRAB_TANK_ID: + return 20; + case REEF_SLIDER_ID: + return 5040; + case TRIPLE_INKSTRIKE_ID: + return 41; + case TACTICOOLER_ID: + return 60; + case SUPER_CHUMP_ID: + return 61; + case KRAKEN_ROYALE_ID: + return 4011; + default: { + assertUnreachable(specialWeaponId); + } + } +}; diff --git a/public/locales/en/analyzer.json b/public/locales/en/analyzer.json index 81741f550..a407b97e8 100644 --- a/public/locales/en/analyzer.json +++ b/public/locales/en/analyzer.json @@ -11,6 +11,7 @@ "stat.category.subDef": "Sub weapon effect defense", "stat.category.actionsPerInkTank": "Actions per ink tank", "stat.category.damage": "Main weapon damage", + "stat.category.special.damage": "{{specialWeapon}} damage", "stat.category.subWeaponDefenseDamages": "Sub weapon damage defense", "stat.category.movement": "Movement", "stat.category.misc": "Miscellaneous", diff --git a/scripts/create-analyzer-json.ts b/scripts/create-analyzer-json.ts index 988111825..9d2e6051f 100644 --- a/scripts/create-analyzer-json.ts +++ b/scripts/create-analyzer-json.ts @@ -461,6 +461,8 @@ function parametersToSpecialWeaponResult(params: any) { return { ArmorHP: params["WeaponSpChariotParam"]?.["ArmorHP"], overwrites: resultUnwrapped, + DistanceDamage: params["BlastParam"]?.["DistanceDamage"], + DirectDamage: params["DamageParam"]?.["DirectHitDamage"], }; }