mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-20 10:04:57 -05:00
Map planner special weapon ranges
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<SpecialWeaponWithRange["rangeType"], string> = {
|
||||
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 (
|
||||
<div className={styles.container} data-testid="special-range-visualization">
|
||||
<div className={styles.legend}>
|
||||
<div className={styles.legendItem}>
|
||||
<span
|
||||
className={styles.legendSwatch}
|
||||
style={{ backgroundColor: RANGE_TYPE_COLOR.projectile }}
|
||||
/>
|
||||
projectile
|
||||
</div>
|
||||
<div className={styles.legendItem}>
|
||||
<span
|
||||
className={styles.legendSwatch}
|
||||
style={{ backgroundColor: RANGE_TYPE_COLOR.radius }}
|
||||
/>
|
||||
radius
|
||||
</div>
|
||||
</div>
|
||||
{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 (
|
||||
<div key={special.specialWeaponId} className={styles.row}>
|
||||
<Image
|
||||
path={specialWeaponImageUrl(special.specialWeaponId)}
|
||||
width={28}
|
||||
height={28}
|
||||
alt={t(`weapons:SPECIAL_${special.specialWeaponId}`)}
|
||||
title={t(`weapons:SPECIAL_${special.specialWeaponId}`)}
|
||||
/>
|
||||
<div className={styles.track}>
|
||||
{blastWidth > 0 ? (
|
||||
<span
|
||||
className={styles.blast}
|
||||
style={{
|
||||
insetInlineStart: `${rangeWidth}%`,
|
||||
width: `${blastWidth}%`,
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<span
|
||||
className={styles.bar}
|
||||
style={{ width: `${rangeWidth}%`, backgroundColor: color }}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.range}>{special.range.toFixed(1)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
app/features/comp-analyzer/core/special-weapon-range.test.ts
Normal file
57
app/features/comp-analyzer/core/special-weapon-range.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
88
app/features/comp-analyzer/core/special-weapon-range.ts
Normal file
88
app/features/comp-analyzer/core/special-weapon-range.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
<section>
|
||||
<h2 style={{ textTransform: "capitalize" }}>specials</h2>
|
||||
<SpecialRangeVisualization />
|
||||
</section>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Editor["getCurrentPageShapes"]>[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;
|
||||
|
||||
@@ -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<string, number | undefined> {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user