Object Damage Calc: Separate subs in the combobox Closes #1220

Also due to a bug the subs were not accessible at all before.
This was a bug introduced in c014ba5e18
This commit is contained in:
Kalle
2023-04-15 12:06:07 +03:00
parent 31484e6b26
commit cfbd18b4fe
15 changed files with 272 additions and 94 deletions

View File

@@ -1,8 +1,7 @@
import { Combobox as HeadlessCombobox } from "@headlessui/react";
import * as React from "react";
import Fuse from "fuse.js";
import clsx from "clsx";
import type { Unpacked } from "~/utils/types";
import Fuse from "fuse.js";
import * as React from "react";
import type { GearType, UserWithPlusTier } from "~/db/types";
import { useAllEventsWithMapPools, useUsers } from "~/hooks/swr";
import { useTranslation } from "~/hooks/useTranslation";
@@ -10,13 +9,20 @@ import type { MainWeaponId } from "~/modules/in-game-lists";
import {
clothesGearIds,
headGearIds,
shoesGearIds,
mainWeaponIds,
shoesGearIds,
subWeaponIds,
weaponCategories,
} from "~/modules/in-game-lists";
import { gearImageUrl, mainWeaponImageUrl } from "~/utils/urls";
import { Image } from "./Image";
import { nonBombSubWeaponIds } 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,
subWeaponImageUrl,
} from "~/utils/urls";
import { Image } from "./Image";
const MAX_RESULTS_SHOWN = 6;
@@ -266,6 +272,56 @@ export function WeaponCombobox({
);
}
export function AllWeaponCombobox({
id,
inputName,
onChange,
fullWidth,
}: Pick<
ComboboxProps<ComboboxBaseOption>,
"inputName" | "onChange" | "id" | "fullWidth"
>) {
const { t } = useTranslation("weapons");
const options = () => {
const result: ComboboxProps<
Record<string, string | null | number>
>["options"] = [];
for (const mainWeaponId of mainWeaponIds) {
result.push({
value: `MAIN_${mainWeaponId}`,
label: t(`MAIN_${mainWeaponId}`),
imgPath: mainWeaponImageUrl(mainWeaponId),
});
}
for (const subWeaponId of subWeaponIds) {
if (nonBombSubWeaponIds.includes(subWeaponId)) continue;
result.push({
value: `SUB_${subWeaponId}`,
label: t(`SUB_${subWeaponId}`),
imgPath: subWeaponImageUrl(subWeaponId),
});
}
return result;
};
return (
<Combobox
inputName={inputName}
options={options()}
initialValue={null}
placeholder={t(`MAIN_${weaponCategories[0].weaponIds[0]}`)}
onChange={onChange}
id={id}
fullWidth={fullWidth}
/>
);
}
export function GearCombobox({
id,
required,

View File

@@ -253,3 +253,7 @@ export interface AnalyzedBuild {
export type SpecialEffectType = (typeof SPECIAL_EFFECTS)[number]["type"];
export type AbilityValuesKeys = keyof typeof abilityValues;
export type AnyWeapon =
| { type: "MAIN"; id: MainWeaponId }
| { type: "SUB"; id: SubWeaponId };

View File

@@ -1,4 +1,5 @@
import type { AbilityType } from "~/modules/in-game-lists";
import type { AbilityType, SubWeaponId } from "~/modules/in-game-lists";
import { subWeaponIds } from "~/modules/in-game-lists";
import {
abilities,
mainWeaponIds,
@@ -13,6 +14,7 @@ import abilityValuesJson from "./ability-values.json";
import type {
AbilityPoints,
AnalyzedBuild,
AnyWeapon,
MainWeaponParams,
ParamsJson,
SpecialWeaponParams,
@@ -23,6 +25,7 @@ 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;
@@ -163,6 +166,43 @@ export function hasEffect({
return high !== mid || mid !== low;
}
const DEFAULT_ANY_WEAPON = {
type: "MAIN",
id: weaponCategories[0].weaponIds[0],
} as const;
export function validatedAnyWeaponFromSearchParams(
searchParams: URLSearchParams
): AnyWeapon {
const rawWeapon = searchParams.get("weapon");
if (!rawWeapon) return DEFAULT_ANY_WEAPON;
if (rawWeapon?.startsWith("SUB_")) {
const id = Number(rawWeapon.replace("SUB_", ""));
if (
!subWeaponIds
.filter((id) => !nonBombSubWeaponIds.includes(id))
.includes(id as any)
) {
return DEFAULT_ANY_WEAPON;
}
return { type: "SUB", id: id as SubWeaponId };
}
if (rawWeapon?.startsWith("MAIN_")) {
const id = Number(rawWeapon.replace("MAIN_", ""));
if (!mainWeaponIds.includes(id as any)) {
return DEFAULT_ANY_WEAPON;
}
return { type: "MAIN", id: id as MainWeaponId };
}
return { type: "MAIN", id: validatedWeaponIdFromSearchParams(searchParams) };
}
export function validatedWeaponIdFromSearchParams(
searchParams: URLSearchParams
): MainWeaponId {

View File

@@ -4,6 +4,7 @@ export {
validatedBuildFromSearchParams,
serializeBuild,
hpDivided,
validatedAnyWeaponFromSearchParams,
} from "./core/utils";
export type {
DamageType,
@@ -11,6 +12,7 @@ export type {
AnalyzedBuild,
SpecialWeaponParams,
SubWeaponParams,
AnyWeapon,
} from "./analyzer-types";
export {
buildStats,

View File

@@ -1281,7 +1281,7 @@ function DamageTable({
: val.value;
const typeRowName = damageIsSubWeaponDamage(val)
? t(`weapons:SUB_${val.subWeaponId}`)
? (`weapons:SUB_${val.subWeaponId}` as const)
: damageTypeTranslationString({
damageType: val.type,
});

View File

@@ -2,6 +2,7 @@ import { type MainWeaponId, mainWeaponIds } from "~/modules/in-game-lists";
import type { DamageType } from "~/features/build-analyzer";
import type objectDamages from "./core/object-dmg.json";
import invariant from "tiny-invariant";
import type { CombineWith } from "./calculator-types";
export const DAMAGE_RECEIVERS = [
"Chariot", // Crab Tank
@@ -136,15 +137,7 @@ export const objectDamageJsonKeyPriority: Record<
};
export const damageTypesToCombine: Partial<
Record<
MainWeaponId,
Array<{
when: DamageType;
combineWith: DamageType;
/** for this weapon "when" damage already includes "combineWith" damage, so calculating multiplier only */
multiplierOnly?: boolean;
}>
>
Record<MainWeaponId, Array<CombineWith>>
> = {
// Explosher
3040: [{ when: "DIRECT", combineWith: "DISTANCE" }],

View File

@@ -1,15 +1,18 @@
import { useSearchParams } from "@remix-run/react";
import { assertType } from "~/utils/types";
import { type MainWeaponId } from "~/modules/in-game-lists";
import {
type DAMAGE_TYPE,
buildStats,
possibleApValues,
validatedWeaponIdFromSearchParams,
validatedAnyWeaponFromSearchParams,
type AnalyzedBuild,
type DamageType,
} from "~/features/build-analyzer";
import { calculateDamage } from "./core/objectDamage";
import {
calculateDamage,
resolveAllUniqueDamageTypes,
} from "./core/objectDamage";
import type { AnyWeapon } from "../build-analyzer/analyzer-types";
const ABILITY_POINTS_SP_KEY = "ap";
const DAMAGE_TYPE_SP_KEY = "dmg";
@@ -18,11 +21,11 @@ const MULTI_SHOT_SP_KEY = "multi";
export function useObjectDamage() {
const [searchParams, setSearchParams] = useSearchParams();
const mainWeaponId = validatedWeaponIdFromSearchParams(searchParams);
const anyWeapon = validatedAnyWeaponFromSearchParams(searchParams);
const abilityPoints = validatedAbilityPointsFromSearchParams(searchParams);
const isMultiShot = validatedMultiShotFromSearchParams(searchParams);
const analyzed = buildStats({
weaponSplId: mainWeaponId,
weaponSplId: anyWeapon.type === "MAIN" ? anyWeapon.id : 0,
hasTacticooler: false,
});
@@ -32,19 +35,19 @@ export function useObjectDamage() {
});
const handleChange = ({
newMainWeaponId = mainWeaponId,
newAnyWeapon = anyWeapon,
newAbilityPoints = abilityPoints,
newDamageType = damageType,
newIsMultiShot = isMultiShot,
}: {
newMainWeaponId?: MainWeaponId;
newAnyWeapon?: AnyWeapon;
newAbilityPoints?: number;
newDamageType?: DamageType;
newIsMultiShot?: boolean;
}) => {
setSearchParams(
{
weapon: String(newMainWeaponId),
weapon: `${newAnyWeapon.type}_${newAnyWeapon.id}`,
[ABILITY_POINTS_SP_KEY]: String(newAbilityPoints),
[DAMAGE_TYPE_SP_KEY]: newDamageType ?? "",
[MULTI_SHOT_SP_KEY]: String(newIsMultiShot),
@@ -54,10 +57,7 @@ export function useObjectDamage() {
};
return {
weapon: {
type: "MAIN" as const,
id: mainWeaponId,
},
weapon: anyWeapon,
isMultiShot,
multiShotCount: analyzed.stats.damages.find((d) => d.type === damageType)
?.multiShots,
@@ -69,16 +69,14 @@ export function useObjectDamage() {
["SPU", abilityPoints],
]),
analyzed,
mainWeaponId,
anyWeapon,
damageType,
isMultiShot,
})
: null,
abilityPoints: String(abilityPoints),
damageType,
allDamageTypes: Array.from(
new Set(analyzed.stats.damages.map((d) => d.type))
),
allDamageTypes: resolveAllUniqueDamageTypes({ analyzed, anyWeapon }),
};
}

View File

@@ -1,5 +1,13 @@
import type { DamageType } from "../build-analyzer";
import type { DAMAGE_RECEIVERS } from "./calculator-constants";
export type DamageReceiver = (typeof DAMAGE_RECEIVERS)[number];
export type HitPoints = Record<DamageReceiver, number>;
export interface CombineWith {
when: DamageType;
combineWith: DamageType;
/** for this weapon "when" damage already includes "combineWith" damage, so calculating multiplier only */
multiplierOnly?: boolean;
}

View File

@@ -47,9 +47,12 @@
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
padding-block: var(--s-2);
padding-inline: var(--s-2);
padding-inline: var(--s-4);
text-align: center;
white-space: nowrap;
display: flex;
flex-direction: column;
gap: var(--s-1-5);
}
.object-damage__weapon-image {

View File

@@ -25,7 +25,7 @@ function calculate({
return calculateDamage({
abilityPoints,
analyzed,
mainWeaponId,
anyWeapon: { type: "MAIN", id: mainWeaponId },
damageType,
isMultiShot: true,
});

View File

@@ -3,7 +3,6 @@ import type {
AnalyzedBuild,
DamageType,
} from "~/features/build-analyzer";
import { damageTypeToWeaponType } from "~/features/build-analyzer";
import objectDamages from "./object-dmg.json";
import type {
MainWeaponId,
@@ -18,7 +17,9 @@ import {
DAMAGE_RECEIVERS,
objectDamageJsonKeyPriority,
} from "../calculator-constants";
import type { DamageReceiver } from "../calculator-types";
import type { CombineWith, DamageReceiver } from "../calculator-types";
import type { AnyWeapon } from "~/features/build-analyzer";
import { removeDuplicates } from "~/utils/arrays";
export function damageTypeToMultipliers({
type,
@@ -101,25 +102,43 @@ export function multipliersToRecordWithFallbacks(
) as Record<DamageReceiver, number>;
}
const objectShredderMultipliers = objectDamages.ObjectEffect_Up.rates;
export function calculateDamage({
export function resolveAllUniqueDamageTypes({
analyzed,
mainWeaponId,
abilityPoints,
damageType,
isMultiShot,
anyWeapon,
}: {
analyzed: AnalyzedBuild;
anyWeapon: AnyWeapon;
}) {
const damageTypes =
anyWeapon.type === "SUB"
? analyzed.stats.subWeaponDefenseDamages
.filter((damage) => damage.subWeaponId === anyWeapon.id)
.map((d) => d.type)
: analyzed.stats.damages.map((d) => d.type);
return removeDuplicates(damageTypes);
}
function resolveFilteredDamages({
analyzed,
damageType,
isMultiShot,
toCombine,
anyWeapon,
}: {
analyzed: AnalyzedBuild;
mainWeaponId: MainWeaponId;
abilityPoints: AbilityPoints;
damageType: DamageType;
isMultiShot: boolean;
toCombine?: CombineWith;
anyWeapon: AnyWeapon;
}) {
const toCombine = (damageTypesToCombine[mainWeaponId] ?? []).find(
(c) => c.when === damageType
);
if (anyWeapon.type === "SUB") {
return analyzed.stats.subWeaponDefenseDamages.filter(
(damage) => damage.subWeaponId === anyWeapon.id
);
}
const filteredDamages = analyzed.stats.damages
return analyzed.stats.damages
.filter((d) => d.type === damageType || toCombine?.combineWith === d.type)
.map((damage) => {
if (!isMultiShot || !damage.multiShots) return damage;
@@ -129,24 +148,46 @@ export function calculateDamage({
value: damage.value * damage.multiShots,
};
});
}
const objectShredderMultipliers = objectDamages.ObjectEffect_Up.rates;
export function calculateDamage({
analyzed,
anyWeapon,
abilityPoints,
damageType,
isMultiShot,
}: {
analyzed: AnalyzedBuild;
anyWeapon: AnyWeapon;
abilityPoints: AbilityPoints;
damageType: DamageType;
isMultiShot: boolean;
}) {
const toCombine =
anyWeapon.type == "MAIN"
? (damageTypesToCombine[anyWeapon.id] ?? []).find(
(c) => c.when === damageType
)
: undefined;
const filteredDamages = resolveFilteredDamages({
analyzed,
damageType,
isMultiShot,
toCombine,
anyWeapon,
});
const hitPoints = objectHitPoints(abilityPoints);
const multipliers = Object.fromEntries(
filteredDamages.map((damage) => {
const weaponType = damageTypeToWeaponType[damage.type];
const weaponId: any =
weaponType === "MAIN"
? mainWeaponId
: weaponType === "SUB"
? analyzed.weapon.subWeaponSplId
: analyzed.weapon.specialWeaponSplId;
return [
damage.type,
multipliersToRecordWithFallbacks(
damageTypeToMultipliers({
type: damage.type,
weapon: { type: weaponType, id: weaponId },
weapon: anyWeapon,
})
),
];

View File

@@ -1,40 +1,40 @@
import { WeaponCombobox } from "~/components/Combobox";
import type { LinksFunction } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
import clsx from "clsx";
import React from "react";
import { Ability } from "~/components/Ability";
import { AllWeaponCombobox } from "~/components/Combobox";
import { Image, WeaponImage } from "~/components/Image";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { Toggle } from "~/components/Toggle";
import type { AnyWeapon } from "~/features/build-analyzer";
import { possibleApValues, type DamageType } from "~/features/build-analyzer";
import { useSetTitle } from "~/hooks/useSetTitle";
import { useTranslation } from "~/hooks/useTranslation";
import {
type MainWeaponId,
BIG_BUBBLER_ID,
BOOYAH_BOMB_ID,
CRAB_TANK_ID,
SPLASH_WALL_ID,
SPRINKLER_ID,
SQUID_BEAKON_ID,
TORPEDO_ID,
WAVE_BREAKER_ID,
SPRINKLER_ID,
} from "~/modules/in-game-lists";
import { damageTypeTranslationString } from "~/utils/i18next";
import type { SendouRouteHandle } from "~/utils/remix";
import {
OBJECT_DAMAGE_CALCULATOR_URL,
mainWeaponImageUrl,
modeImageUrl,
navIconUrl,
OBJECT_DAMAGE_CALCULATOR_URL,
specialWeaponImageUrl,
subWeaponImageUrl,
} from "~/utils/urls";
import styles from "../calculator.css";
import type { LinksFunction } from "@remix-run/node";
import type { SendouRouteHandle } from "~/utils/remix";
import React from "react";
import { useTranslation } from "~/hooks/useTranslation";
import clsx from "clsx";
import { Label } from "~/components/Label";
import { Ability } from "~/components/Ability";
import { damageTypeTranslationString } from "~/utils/i18next";
import { useSetTitle } from "~/hooks/useSetTitle";
import type { ShouldRevalidateFunction } from "@remix-run/react";
import { Toggle } from "~/components/Toggle";
import { useObjectDamage } from "../calculator-hooks";
import { type DamageType, possibleApValues } from "~/features/build-analyzer";
import type { DamageReceiver } from "../calculator-types";
import styles from "../calculator.css";
export const CURRENT_PATCH = "3.1";
@@ -72,19 +72,29 @@ export default function ObjectDamagePage() {
<div className="object-damage__selects">
<div className="object-damage__selects__weapon">
<Label htmlFor="weapon">{t("analyzer:labels.weapon")}</Label>
<WeaponCombobox
<AllWeaponCombobox
id="weapon"
inputName="weapon"
onChange={(opt) =>
opt &&
onChange={(opt) => {
if (!opt) return;
const [type, id] = opt.value.split("_");
handleChange({
newMainWeaponId: Number(opt.value) as MainWeaponId,
})
}
newAnyWeapon: {
id: Number(id),
type,
} as AnyWeapon,
});
}}
fullWidth
/>
</div>
<div className={clsx({ invisible: !damagesToReceivers })}>
<div
className={clsx({
invisible: !damagesToReceivers || allDamageTypes.length === 1,
})}
>
<Label htmlFor="damage">{t("analyzer:labels.damageType")}</Label>
<DamageTypesSelect
handleChange={handleChange}
@@ -259,17 +269,31 @@ function DamageReceiversGrid({
invisible: !damage.distance,
})}
>
{t("analyzer:distanceInline", { value: damage.distance })}
{t("analyzer:distanceInline", {
value: Array.isArray(damage.distance)
? damage.distance.join("-")
: damage.distance,
})}
</div>
<div className="text-lighter stack horizontal sm justify-center items-center">
<WeaponImage
weaponSplId={weapon.id}
width={24}
height={24}
variant="build"
className="object-damage__weapon-image"
/>
{t(`weapons:MAIN_${weapon.id}`)}
{weapon.type === "MAIN" ? (
<WeaponImage
weaponSplId={weapon.id}
width={24}
height={24}
variant="build"
className="object-damage__weapon-image"
/>
) : (
<Image
alt=""
path={subWeaponImageUrl(weapon.id)}
width={24}
height={24}
className="object-damage__weapon-image"
/>
)}
{t(`weapons:${weapon.type}_${weapon.id}` as any)}
</div>
</div>
))}

View File

@@ -1,4 +1,4 @@
import type { MainWeaponId } from "./types";
import type { MainWeaponId, SubWeaponId } from "./types";
export const weaponCategories = [
{
@@ -92,6 +92,14 @@ export const subWeaponIds = [
TORPEDO_ID,
] as const;
export const nonBombSubWeaponIds = [
SPRINKLER_ID,
SPLASH_WALL_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
TOXIC_MIST_ID,
] as SubWeaponId[];
export const TRIZOOKA_ID = 1;
export const BIG_BUBBLER_ID = 2;
export const ZIPCASTER_ID = 3;

View File

@@ -1,8 +1,7 @@
import type { DamageType } from "~/features/build-analyzer";
// TODO: type this correctly
export const damageTypeTranslationString = ({
damageType,
}: {
damageType: DamageType;
}): any => `analyzer:damage.${damageType as "NORMAL_MIN"}`;
}) => `analyzer:damage.${damageType}` as const;

View File

@@ -100,6 +100,8 @@
"damage.SPLATANA_VERTICAL": "Vertical",
"damage.SPLATANA_HORIZONTAL_DIRECT": "Horizontal direct",
"damage.SPLATANA_HORIZONTAL": "Horizontal",
"damage.BOMB_DIRECT": "Direct",
"damage.BOMB_NORMAL": "Splash",
"suffix.seconds": "s",
"suffix.hp": "hp",
"suffix.specialPointsShort": "p",