From 0c5a55fcdf527fe561c0179b17930106e91ab2ca Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:48:41 +0300 Subject: [PATCH] Map planner special weapon ranges --- app/features/build-analyzer/analyzer-types.ts | 17 ++++ app/features/build-analyzer/core/utils.ts | 8 ++ .../build-analyzer/data/weapon-params.ts | 28 ++++++ .../SpecialRangeVisualization.module.css | 60 ++++++++++++ .../components/SpecialRangeVisualization.tsx | 85 +++++++++++++++++ .../core/special-weapon-range.test.ts | 57 ++++++++++++ .../core/special-weapon-range.ts | 88 ++++++++++++++++++ .../comp-analyzer/core/weapon-range.ts | 8 +- .../routes/comp-analyzer.all-ranges.tsx | 5 + .../map-planner/components/Planner.tsx | 40 +++++++- scripts/create-analyzer-json.ts | 92 +++++++++++++++++++ 11 files changed, 480 insertions(+), 8 deletions(-) create mode 100644 app/features/comp-analyzer/components/SpecialRangeVisualization.module.css create mode 100644 app/features/comp-analyzer/components/SpecialRangeVisualization.tsx create mode 100644 app/features/comp-analyzer/core/special-weapon-range.test.ts create mode 100644 app/features/comp-analyzer/core/special-weapon-range.ts diff --git a/app/features/build-analyzer/analyzer-types.ts b/app/features/build-analyzer/analyzer-types.ts index c99c2d960..4ca6dc007 100644 --- a/app/features/build-analyzer/analyzer-types.ts +++ b/app/features/build-analyzer/analyzer-types.ts @@ -188,6 +188,23 @@ export type SpecialWeaponParams = SpecialWeaponParamsObject[SpecialWeaponId] & { BumpDamage?: number; JumpDamage?: number; TickDamage?: number; + + // Map planner range circle params (populated by scripts/create-analyzer-json.ts). + /** Effect radius for area specials (Big Bubbler, Ink Storm, ...) */ + Range_Radius?: number; + /** Straight-flight range for projectile specials with a fixed distance (Inkjet) */ + Range_Distance?: number; + /** Outer blast radius drawn around a projectile special's impact */ + Range_BlastRadius?: number; + /** Projectile trajectory params (Trizooka, Crab Tank); see comp-analyzer weapon-range */ + Range_SpawnSpeed?: number; + Range_GoStraightStateEndMaxSpeed?: number; + Range_GoStraightToBrakeStateFrame?: number; + Range_FreeGravity?: number; + Range_FreeAirResist?: number; + Range_BrakeAirResist?: number; + Range_BrakeGravity?: number; + Range_BrakeToFreeStateFrame?: number; }; export type ParamsJson = { diff --git a/app/features/build-analyzer/core/utils.ts b/app/features/build-analyzer/core/utils.ts index 175fca829..18312a3f0 100644 --- a/app/features/build-analyzer/core/utils.ts +++ b/app/features/build-analyzer/core/utils.ts @@ -51,6 +51,14 @@ export function mainWeaponParams(weaponId: MainWeaponId): MainWeaponParams { return { ...baseStats, ...kit } as MainWeaponParams; } +export function specialWeaponParams( + specialWeaponId: SpecialWeaponId, +): SpecialWeaponParams { + const params = rawWeaponParams as unknown as ParamsJson; + + return params.specialWeapons[specialWeaponId] as SpecialWeaponParams; +} + export function buildToAbilityPoints(build: BuildAbilitiesTupleWithUnknown) { const result: AbilityPoints = new Map(); diff --git a/app/features/build-analyzer/data/weapon-params.ts b/app/features/build-analyzer/data/weapon-params.ts index b52015df1..75f64bfd9 100644 --- a/app/features/build-analyzer/data/weapon-params.ts +++ b/app/features/build-analyzer/data/weapon-params.ts @@ -2326,6 +2326,15 @@ export const weaponParams = { }, ], DirectDamage: 2200, + Range_SpawnSpeed: 1, + Range_GoStraightStateEndMaxSpeed: 1, + Range_GoStraightToBrakeStateFrame: 18, + Range_FreeGravity: 0.0190565, + Range_FreeAirResist: 0.01985, + Range_BrakeAirResist: 0.09, + Range_BrakeGravity: 0.09, + Range_BrakeToFreeStateFrame: 10, + Range_BlastRadius: 4, }, "2": { overwrites: { @@ -2364,6 +2373,7 @@ export const weaponParams = { Distance: 6, }, ], + Range_Radius: 35, }, "4": { overwrites: { @@ -2412,6 +2422,8 @@ export const weaponParams = { }, }, TickDamage: 33, + Range_Distance: 27, + Range_BlastRadius: 12.6, }, "7": { overwrites: { @@ -2428,6 +2440,7 @@ export const weaponParams = { }, DirectDamage: 300, WaveDamage: 450, + Range_Radius: 20, }, "8": { overwrites: { @@ -2504,6 +2517,8 @@ export const weaponParams = { }, ], DirectDamage: 1200, + Range_Distance: 30, + Range_BlastRadius: 5, }, "11": { overwrites: { @@ -2534,6 +2549,8 @@ export const weaponParams = { }, ], ThrowDirectDamage: 2200, + Range_Distance: 24, + Range_BlastRadius: 8, }, "12": { ArmorHP: 5000, @@ -2557,6 +2574,11 @@ export const weaponParams = { }, ], BumpDamage: 400, + Range_SpawnSpeed: 3.36, + Range_GoStraightStateEndMaxSpeed: 2.232, + Range_GoStraightToBrakeStateFrame: 7, + Range_FreeGravity: 0.016, + Range_BlastRadius: 4.8, }, "13": { overwrites: { @@ -2591,6 +2613,7 @@ export const weaponParams = { Distance: 14.9, }, ], + Range_Radius: 9, }, "14": { overwrites: { @@ -2601,6 +2624,8 @@ export const weaponParams = { }, }, TickDamage: 75, + Range_Distance: 28, + Range_BlastRadius: 7.7, }, "15": { overwrites: { @@ -2641,6 +2666,8 @@ export const weaponParams = { Distance: 6, }, ], + Range_Distance: 30, + Range_BlastRadius: 6, }, "17": { overwrites: { @@ -2676,6 +2703,7 @@ export const weaponParams = { Distance: 10.5, }, ], + Range_Radius: 7, }, "19": { overwrites: { diff --git a/app/features/comp-analyzer/components/SpecialRangeVisualization.module.css b/app/features/comp-analyzer/components/SpecialRangeVisualization.module.css new file mode 100644 index 000000000..2361934de --- /dev/null +++ b/app/features/comp-analyzer/components/SpecialRangeVisualization.module.css @@ -0,0 +1,60 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--s-2); +} + +.legend { + display: flex; + gap: var(--s-4); + font-size: var(--font-xs); + color: var(--color-text-high); +} + +.legendItem { + display: flex; + align-items: center; + gap: var(--s-1-5); +} + +.legendSwatch { + width: 12px; + height: 12px; + border-radius: 2px; +} + +.row { + display: grid; + grid-template-columns: 32px 1fr 3rem; + align-items: center; + gap: var(--s-2); +} + +.track { + position: relative; + height: 14px; + border-radius: var(--radius-field); + background: var(--color-bg-high); + overflow: hidden; +} + +.bar { + position: absolute; + inset-block: 0; + inset-inline-start: 0; + border-radius: var(--radius-field); +} + +.blast { + position: absolute; + inset-block: 0; + opacity: 0.4; +} + +.range { + font-size: var(--font-xs); + font-weight: var(--weight-semi); + color: var(--color-text-high); + text-align: end; + font-variant-numeric: tabular-nums; +} diff --git a/app/features/comp-analyzer/components/SpecialRangeVisualization.tsx b/app/features/comp-analyzer/components/SpecialRangeVisualization.tsx new file mode 100644 index 000000000..6ca7f606d --- /dev/null +++ b/app/features/comp-analyzer/components/SpecialRangeVisualization.tsx @@ -0,0 +1,85 @@ +// note: dev only component, not used in production code + +import { useTranslation } from "react-i18next"; +import { Image } from "~/components/Image"; +import { specialWeaponImageUrl } from "~/utils/urls"; +import { + getSpecialsWithRange, + type SpecialWeaponWithRange, +} from "../core/special-weapon-range"; +import styles from "./SpecialRangeVisualization.module.css"; + +const RANGE_TYPE_COLOR: Record = { + projectile: "#8cd4f5", + radius: "#f5b8d0", +}; + +export function SpecialRangeVisualization() { + const { t } = useTranslation(["weapons"]); + + const specials = getSpecialsWithRange(); + if (specials.length === 0) { + return null; + } + + const maxRange = Math.max( + ...specials.map((special) => special.range + (special.blastRadius ?? 0)), + ); + + return ( +
+
+
+ + projectile +
+
+ + radius +
+
+ {specials.map((special) => { + const color = RANGE_TYPE_COLOR[special.rangeType]; + const rangeWidth = (special.range / maxRange) * 100; + const blastWidth = special.blastRadius + ? (special.blastRadius / maxRange) * 100 + : 0; + + return ( +
+ {t(`weapons:SPECIAL_${special.specialWeaponId}`)} +
+ {blastWidth > 0 ? ( + + ) : null} + +
+ {special.range.toFixed(1)} +
+ ); + })} +
+ ); +} diff --git a/app/features/comp-analyzer/core/special-weapon-range.test.ts b/app/features/comp-analyzer/core/special-weapon-range.test.ts new file mode 100644 index 000000000..3acbba68c --- /dev/null +++ b/app/features/comp-analyzer/core/special-weapon-range.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "vitest"; +import { + BIG_BUBBLER_ID, + BOOYAH_BOMB_ID, + CRAB_TANK_ID, + SPLATTERCOLOR_SCREEN_ID, + specialWeaponIds, + TRIZOOKA_ID, + WAVE_BREAKER_ID, +} from "~/modules/in-game-lists/weapon-ids"; +import { getSpecialWeaponRange } from "./special-weapon-range"; + +describe("special weapon range", () => { + test("computes a projectile range for Trizooka", () => { + const result = getSpecialWeaponRange(TRIZOOKA_ID); + + expect(result?.rangeType).toBe("projectile"); + expect(result?.range).toBeGreaterThan(0); + }); + + test("Crab Tank reaches further than Trizooka", () => { + const crab = getSpecialWeaponRange(CRAB_TANK_ID); + const trizooka = getSpecialWeaponRange(TRIZOOKA_ID); + + expect(crab!.range).toBeGreaterThan(trizooka!.range); + }); + + test("throws-and-bursts specials expose a throw range plus explosion blast", () => { + const booyah = getSpecialWeaponRange(BOOYAH_BOMB_ID); + + expect(booyah?.rangeType).toBe("projectile"); + expect(booyah?.range).toBeGreaterThan(0); + expect(booyah?.blastRadius).toBeGreaterThan(0); + }); + + test("returns the effect radius for area specials", () => { + const waveBreaker = getSpecialWeaponRange(WAVE_BREAKER_ID); + + expect(waveBreaker?.rangeType).toBe("radius"); + expect(waveBreaker?.range).toBeGreaterThan(0); + }); + + test("returns null for global / utility specials without a meaningful circle", () => { + expect(getSpecialWeaponRange(BIG_BUBBLER_ID)).toBeNull(); + expect(getSpecialWeaponRange(SPLATTERCOLOR_SCREEN_ID)).toBeNull(); + }); + + test("every special is either unsupported or has a positive finite range", () => { + for (const id of specialWeaponIds) { + const result = getSpecialWeaponRange(id); + if (result === null) continue; + + expect(Number.isFinite(result.range)).toBe(true); + expect(result.range).toBeGreaterThan(0); + } + }); +}); diff --git a/app/features/comp-analyzer/core/special-weapon-range.ts b/app/features/comp-analyzer/core/special-weapon-range.ts new file mode 100644 index 000000000..4c22cb0b5 --- /dev/null +++ b/app/features/comp-analyzer/core/special-weapon-range.ts @@ -0,0 +1,88 @@ +import { specialWeaponParams } from "~/features/build-analyzer/core/utils"; +import type { SpecialWeaponId } from "~/modules/in-game-lists/types"; +import { specialWeaponIds } from "~/modules/in-game-lists/weapon-ids"; +import { + calculateGroundRange, + simulateTrajectoryPoints, + type TrajectoryParams, +} from "./weapon-range"; + +const DEFAULT_BRAKE_AIR_RESIST = 0.36; +const DEFAULT_BRAKE_GRAVITY = 0.07; +const DEFAULT_BRAKE_TO_FREE_FRAME = 4; +const DEFAULT_FREE_GRAVITY = 0.016; +const DEFAULT_FREE_AIR_RESIST = 0; + +export interface SpecialWeaponRangeResult { + /** Radius of the range circle, in game distance units. */ + range: number; + /** Extra outer radius covered by the projectile's blast, in game distance units. */ + blastRadius?: number; + rangeType: "projectile" | "radius"; +} + +/** + * Range circle definition for a special weapon, or `null` when the special has no meaningful + * range to draw. The underlying values come from `weapon-params.ts` (populated by + * `scripts/create-analyzer-json.ts`): projectile specials reuse the main weapon trajectory + * model, the rest carry a single effect radius. + */ +export function getSpecialWeaponRange( + specialWeaponId: SpecialWeaponId, +): SpecialWeaponRangeResult | null { + const params = specialWeaponParams(specialWeaponId); + + if (params.Range_Radius !== undefined) { + return { range: params.Range_Radius, rangeType: "radius" }; + } + + if (params.Range_Distance !== undefined) { + return { + range: params.Range_Distance, + blastRadius: params.Range_BlastRadius, + rangeType: "projectile", + }; + } + + if (params.Range_SpawnSpeed === undefined) return null; + + const trajectoryParams: TrajectoryParams = { + spawnSpeed: params.Range_SpawnSpeed, + goStraightStateEndMaxSpeed: + params.Range_GoStraightStateEndMaxSpeed ?? params.Range_SpawnSpeed, + goStraightToBrakeStateFrame: params.Range_GoStraightToBrakeStateFrame ?? 4, + freeGravity: params.Range_FreeGravity ?? DEFAULT_FREE_GRAVITY, + freeAirResist: params.Range_FreeAirResist ?? DEFAULT_FREE_AIR_RESIST, + brakeAirResist: params.Range_BrakeAirResist ?? DEFAULT_BRAKE_AIR_RESIST, + brakeGravity: params.Range_BrakeGravity ?? DEFAULT_BRAKE_GRAVITY, + brakeToFreeFrame: + params.Range_BrakeToFreeStateFrame ?? DEFAULT_BRAKE_TO_FREE_FRAME, + }; + + const range = calculateGroundRange( + simulateTrajectoryPoints(trajectoryParams), + ); + + return { + range, + blastRadius: params.Range_BlastRadius, + rangeType: "projectile", + }; +} + +export interface SpecialWeaponWithRange extends SpecialWeaponRangeResult { + specialWeaponId: SpecialWeaponId; +} + +/** + * Every special weapon that has a range to draw, widest first. Specials without a meaningful + * range circle are omitted. + */ +export function getSpecialsWithRange(): SpecialWeaponWithRange[] { + return specialWeaponIds + .flatMap((specialWeaponId): SpecialWeaponWithRange[] => { + const result = getSpecialWeaponRange(specialWeaponId); + return result ? [{ specialWeaponId, ...result }] : []; + }) + .sort((a, b) => b.range - a.range); +} diff --git a/app/features/comp-analyzer/core/weapon-range.ts b/app/features/comp-analyzer/core/weapon-range.ts index 073addb13..8e3827b99 100644 --- a/app/features/comp-analyzer/core/weapon-range.ts +++ b/app/features/comp-analyzer/core/weapon-range.ts @@ -2,7 +2,7 @@ import { mainWeaponParams } from "~/features/build-analyzer/core/utils"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { weaponCategories } from "~/modules/in-game-lists/weapon-ids"; -interface TrajectoryParams { +export interface TrajectoryParams { spawnSpeed: number; goStraightStateEndMaxSpeed: number; goStraightToBrakeStateFrame: number; @@ -37,7 +37,7 @@ function getWeaponCategoryName(weaponId: MainWeaponId): string | undefined { const PLAYER_HEIGHT = 1.0; -function calculateGroundRange(trajectory: TrajectoryPoint[]): number { +export function calculateGroundRange(trajectory: TrajectoryPoint[]): number { for (let i = 1; i < trajectory.length; i++) { const point = trajectory[i]; const prevPoint = trajectory[i - 1]; @@ -55,7 +55,9 @@ function calculateBouncingRange(trajectory: TrajectoryPoint[]): number { return lastPoint?.z ?? 0; } -function simulateTrajectoryPoints(params: TrajectoryParams): TrajectoryPoint[] { +export function simulateTrajectoryPoints( + params: TrajectoryParams, +): TrajectoryPoint[] { const { spawnSpeed, goStraightStateEndMaxSpeed, diff --git a/app/features/comp-analyzer/routes/comp-analyzer.all-ranges.tsx b/app/features/comp-analyzer/routes/comp-analyzer.all-ranges.tsx index ffcfd06fc..b586d3f09 100644 --- a/app/features/comp-analyzer/routes/comp-analyzer.all-ranges.tsx +++ b/app/features/comp-analyzer/routes/comp-analyzer.all-ranges.tsx @@ -5,6 +5,7 @@ import { weaponIdToType, } from "~/modules/in-game-lists/weapon-ids"; import { RangeVisualization } from "../components/RangeVisualization"; +import { SpecialRangeVisualization } from "../components/SpecialRangeVisualization"; export default function AllRangesPage() { return ( @@ -31,6 +32,10 @@ export default function AllRangesPage() { ); })} +
+

specials

+ +
); } diff --git a/app/features/map-planner/components/Planner.tsx b/app/features/map-planner/components/Planner.tsx index 3964d1430..7cb097651 100644 --- a/app/features/map-planner/components/Planner.tsx +++ b/app/features/map-planner/components/Planner.tsx @@ -33,6 +33,7 @@ import { } from "lucide-react"; import * as React from "react"; import { useTranslation } from "react-i18next"; +import { getSpecialWeaponRange } from "~/features/comp-analyzer/core/special-weapon-range"; import { getWeaponRange } from "~/features/comp-analyzer/core/weapon-range"; import { useTheme } from "~/features/theme/core/provider"; import type { LanguageCode } from "~/modules/i18n/config"; @@ -41,6 +42,7 @@ import { stageIds } from "~/modules/in-game-lists/stage-ids"; import type { MainWeaponId, ModeShort, + SpecialWeaponId, StageId, } from "~/modules/in-game-lists/types"; import { @@ -71,6 +73,7 @@ const GAME_UNITS_TO_PX: Record<"MINI" | "OVER", number> = { OVER: 8.4, }; const MAIN_WEAPON_URL_PATTERN = /main-weapons-outlined\/(\d+)/; +const SPECIAL_WEAPON_URL_PATTERN = /special-weapons\/(\d+)/; export default function Planner() { const { t, i18n } = useTranslation(["common"]); @@ -783,6 +786,36 @@ function extractMainWeaponIdFromSrc(src: string): MainWeaponId | null { return id as MainWeaponId; } +function extractSpecialWeaponIdFromSrc(src: string): SpecialWeaponId | null { + const match = src.match(SPECIAL_WEAPON_URL_PATTERN); + if (!match) return null; + + const id = Number(match[1]); + if (!specialWeaponIds.includes(id as SpecialWeaponId)) return null; + + return id as SpecialWeaponId; +} + +function rangeForSrc( + src: string, +): { range: number; blastRadius?: number } | null { + const mainWeaponId = extractMainWeaponIdFromSrc(src); + if (mainWeaponId !== null) { + const result = getWeaponRange(mainWeaponId); + if (result.rangeType === "unsupported" || result.range <= 0) return null; + return { range: result.range, blastRadius: result.blastRadius }; + } + + const specialWeaponId = extractSpecialWeaponIdFromSrc(src); + if (specialWeaponId !== null) { + const result = getSpecialWeaponRange(specialWeaponId); + if (!result || result.range <= 0) return null; + return { range: result.range, blastRadius: result.blastRadius }; + } + + return null; +} + function createRangeCircleForShape( editor: Editor, shape: ReturnType[number], @@ -796,11 +829,8 @@ function createRangeCircleForShape( const asset = editor.getAsset(assetId as TLAssetId); if (asset?.type !== "image" || !asset.props.src) return; - const weaponId = extractMainWeaponIdFromSrc(asset.props.src); - if (!weaponId) return; - - const rangeResult = getWeaponRange(weaponId); - if (rangeResult.rangeType === "unsupported" || rangeResult.range <= 0) return; + const rangeResult = rangeForSrc(asset.props.src); + if (!rangeResult) return; const centerX = shape.x + (shape.props as { w: number }).w / 2; const centerY = shape.y + (shape.props as { h: number }).h / 2; diff --git a/scripts/create-analyzer-json.ts b/scripts/create-analyzer-json.ts index f8ea1ba3a..a9dfcec1e 100644 --- a/scripts/create-analyzer-json.ts +++ b/scripts/create-analyzer-json.ts @@ -129,6 +129,8 @@ async function main() { params.DistanceDamage = undefined; } + Object.assign(params, specialWeaponRangeParams(specialWeapon, rawParams)); + if (hasLangDicts) { translationsToArray({ arr: translations, @@ -693,6 +695,96 @@ function parametersToSubWeaponResult( }; } +// Thrown targeting specials (Booyah Bomb, Super Chump, Triple Inkstrike, Ultra Stamp) are +// player-aimed and have no throw-distance param in the game data, so their throw (fly) range is +// approximated with a constant. Kept as named per-special constants so each can be tuned later; +// for now they all reuse Booyah Bomb's value. +const BOOYAH_BOMB_FLY_DISTANCE = 27; +const SUPER_CHUMP_FLY_DISTANCE = 30; +const TRIPLE_INKSTRIKE_FLY_DISTANCE = 28; +const ULTRA_STAMP_FLY_DISTANCE = 24; + +// Range circle data for the map planner. Projectile / thrown specials store the distance the shot +// travels plus its blast; a few store a single effect radius. Specials whose reach is global +// (Tenta Missiles, Killer Wail), placed or utility (Big Bubbler, Tacticooler, Ink Storm, Ink Vac), +// or otherwise has no meaningful circle (Kraken Royale, Splattercolor Screen) are left out. +// See app/features/comp-analyzer/core/special-weapon-range.ts. +function specialWeaponRangeParams( + specialWeapon: SpecialWeapon, + rawParams: any, +): Record { + const outerBlastDistance = (blastParam: any): number | undefined => + blastParam?.DistanceDamage?.at(-1)?.Distance; + + const trajectoryFromMoveParam = (moveParam: any) => ({ + Range_SpawnSpeed: moveParam?.SpawnSpeed, + Range_GoStraightStateEndMaxSpeed: moveParam?.GoStraightStateEndMaxSpeed, + Range_GoStraightToBrakeStateFrame: moveParam?.GoStraightToBrakeStateFrame, + Range_FreeGravity: moveParam?.FreeGravity, + Range_FreeAirResist: moveParam?.FreeAirResist, + Range_BrakeAirResist: moveParam?.BrakeAirResist, + Range_BrakeGravity: moveParam?.BrakeGravity, + Range_BrakeToFreeStateFrame: moveParam?.BrakeToFreeStateFrame, + }); + + switch (specialWeapon.__RowId) { + case "SpUltraShot": // Trizooka + return { + ...trajectoryFromMoveParam(rawParams.MoveParam), + Range_BlastRadius: outerBlastDistance(rawParams.BlastParam), + }; + case "SpJetpack": // Inkjet (shot flies straight for a fixed distance) + return { + Range_Distance: rawParams.MoveParam?.Distance, + Range_BlastRadius: outerBlastDistance(rawParams.BlastParam), + }; + case "SpChariot": // Crab Tank (rapid gun reach) + return { + ...trajectoryFromMoveParam(rawParams.ShooterMoveParam), + Range_BlastRadius: outerBlastDistance( + rawParams.CannonParam?.BlastParam, + ), + }; + case "SpNiceBall": // Booyah Bomb (thrown, then bursts on impact) + return { + Range_Distance: BOOYAH_BOMB_FLY_DISTANCE, + Range_BlastRadius: rawParams.BlastParam?.DamageRadiusEnd, + }; + case "SpFirework": // Super Chump (thrown, chumps burst on impact) + return { + Range_Distance: SUPER_CHUMP_FLY_DISTANCE, + Range_BlastRadius: outerBlastDistance(rawParams.IceParam?.BlastParam), + }; + case "SpTripleTornado": // Triple Inkstrike (thrown beacons, strikes burst) + return { + Range_Distance: TRIPLE_INKSTRIKE_FLY_DISTANCE, + Range_BlastRadius: rawParams.BlastParam?.DamageRadiusEnd, + }; + case "SpUltraStamp": // Ultra Stamp (thrown stamp bursts on impact) + return { + Range_Distance: ULTRA_STAMP_FLY_DISTANCE, + Range_BlastRadius: outerBlastDistance(rawParams.ThrowBlastParam), + }; + case "SpSuperHook": // Zipcaster (grapple reach) + return { Range_Radius: rawParams.WeaponParam?.MaxLengthHook }; + case "SpShockSonar": // Wave Breaker (tracking wave max radius, base Special Power Up) + return { + Range_Radius: + rawParams.spl__BulletSpShockSonarParam?.WaveParam?.MaxRadius?.Low, + }; + case "SpSkewer": // Reefslider (lethal blast) + return { + Range_Radius: rawParams.BulletBlastParam?.DistanceDamage?.[0]?.Distance, + }; + case "SpPogo": // Triple Splashdown (lethal blast, per splashdown) + return { + Range_Radius: rawParams.BlastParamNormal?.DistanceDamage?.[0]?.Distance, + }; + default: + return {}; + } +} + // some specials lack damage values in the params // so they are instead hardcoded here as a workaround function parametersToSpecialWeaponResult(params: any) {