diff --git a/app/modules/analyzer/damageMultipliers.ts b/app/modules/analyzer/damageMultipliers.ts index 21ca5a706..f1ab1b6c3 100644 --- a/app/modules/analyzer/damageMultipliers.ts +++ b/app/modules/analyzer/damageMultipliers.ts @@ -1,4 +1,4 @@ -import type { DamageType } from "./types"; +import type { DamageReceiver, DamageType } from "./types"; import objectDamages from "./object-dmg.json"; import type { MainWeaponId, @@ -78,11 +78,13 @@ export function damageTypeToMultipliers({ return null; } -export function fallbackRates( +export function multipliersToRecordWithFallbacks( multipliers: ReturnType ) { - return DAMAGE_RECEIVERS.map((receiver) => ({ - target: receiver, - rate: multipliers?.find((m) => m.target === receiver)?.rate ?? 1, - })); + return Object.fromEntries( + DAMAGE_RECEIVERS.map((receiver) => [ + receiver, + multipliers?.find((m) => m.target === receiver)?.rate ?? 1, + ]) + ) as Record; } diff --git a/app/modules/analyzer/useObjectDamage.ts b/app/modules/analyzer/useObjectDamage.ts index c49fa468f..2207d89ee 100644 --- a/app/modules/analyzer/useObjectDamage.ts +++ b/app/modules/analyzer/useObjectDamage.ts @@ -1,7 +1,10 @@ import { useSearchParams } from "@remix-run/react"; import { type MainWeaponId } from "../in-game-lists"; import { damageTypeToWeaponType } from "./constants"; -import { damageTypeToMultipliers, fallbackRates } from "./damageMultipliers"; +import { + damageTypeToMultipliers, + multipliersToRecordWithFallbacks, +} from "./damageMultipliers"; import { objectHitPoints } from "./objectHitPoints"; import { buildStats } from "./stats"; import { validatedWeaponIdFromSearchParams } from "./utils"; @@ -40,7 +43,7 @@ export function useObjectDamage() { return [ damage.type, - fallbackRates( + multipliersToRecordWithFallbacks( damageTypeToMultipliers({ type: damage.type, weapon: { type: weaponType, id: weaponId }, @@ -52,6 +55,7 @@ export function useObjectDamage() { return { mainWeaponId, + subWeaponId: analyzed.weapon.subWeaponSplId, handleChange, multipliers, damages: analyzed.stats.damages, diff --git a/app/routes/object-damage.tsx b/app/routes/object-damage.tsx index 90d89e3e9..46a3342fc 100644 --- a/app/routes/object-damage.tsx +++ b/app/routes/object-damage.tsx @@ -1,11 +1,7 @@ import { WeaponCombobox } from "~/components/Combobox"; import { Image } from "~/components/Image"; import { Main } from "~/components/Main"; -import { - DAMAGE_RECEIVERS, - useObjectDamage, - type HitPoints, -} from "~/modules/analyzer"; +import { DAMAGE_RECEIVERS, useObjectDamage } from "~/modules/analyzer"; import { type MainWeaponId, BIG_BUBBLER_ID, @@ -27,26 +23,32 @@ import styles from "~/styles/object-damage.css"; import type { LinksFunction } from "@remix-run/node"; import type { SendouRouteHandle } from "~/utils/remix"; import type { DamageReceiver } from "~/modules/analyzer/types"; +import React from "react"; +import { roundToTwoDecimalPlaces } from "~/utils/number"; +import { useTranslation } from "react-i18next"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; export const handle: SendouRouteHandle = { - i18n: ["weapons"], + i18n: ["weapons", "analyzer"], }; export default function ObjectDamagePage() { - const { mainWeaponId, handleChange, multipliers, damages, hitPoints } = - useObjectDamage(); + const { + mainWeaponId, + subWeaponId, + handleChange, + multipliers, + damages, + hitPoints, + } = useObjectDamage(); if (process.env.NODE_ENV !== "development") { return
WIP :)
; } - console.log(JSON.stringify(multipliers, null, 2)); - console.log(JSON.stringify(damages, null, 2)); - return (
-
- -
+
); } @@ -88,21 +90,75 @@ const damageReceiverImages: Record = { BulletUmbrellaCanopyWide: mainWeaponImageUrl(6010), BulletUmbrellaCanopyCompact: mainWeaponImageUrl(6020), }; -function DamageReceiversHeader({ hitPoints }: { hitPoints: HitPoints }) { + +function DamageReceiversGrid({ + subWeaponId, + hitPoints, + damages, + multipliers, +}: Pick< + ReturnType, + "hitPoints" | "damages" | "multipliers" | "subWeaponId" +>) { + const { t } = useTranslation(["weapons", "analyzer"]); + return ( - <> - {DAMAGE_RECEIVERS.map((receiver, i) => ( - <> - -
{hitPoints[receiver]}
- +
+
+
+ {damages.map((damage) => ( +
+ {damage.type.startsWith("BOMB_") + ? t(`weapons:SUB_${subWeaponId}`) + : t(`analyzer:damage.${damage.type as "NORMAL_MIN"}`)} +
))} - + {DAMAGE_RECEIVERS.map((receiver, i) => { + const damageReceiverHp = hitPoints[receiver]; + + return ( + + +
{damageReceiverHp}hp
+ {damages.map((damage) => { + const multiplier = multipliers[damage.type]![receiver]; + const damagePerHit = roundToTwoDecimalPlaces( + damage.value * multiplier + ); + + const hitsToDestroy = Math.ceil(damageReceiverHp / damagePerHit); + + return ( +
+
+ + DMG + +
{damagePerHit}
+ + HTD + +
{hitsToDestroy}
+
+
×{multiplier}
+
+ ); + })} +
+ ); + })} +
); } diff --git a/app/styles/object-damage.css b/app/styles/object-damage.css index fc072f924..3aa602cd1 100644 --- a/app/styles/object-damage.css +++ b/app/styles/object-damage.css @@ -1,6 +1,59 @@ .object-damage__grid { display: grid; width: 100%; - gap: var(--s-3); - grid-template-columns: repeat(var(--columns-count), 1fr); + column-gap: var(--s-6); + + /* xxx: make dynamic */ + grid-template-columns: max-content max-content 1fr 1fr 1fr 1fr 1fr; + place-items: center; + row-gap: var(--s-3); +} + +.object-damage__hp { + font-size: var(--fonts-xs); + font-weight: var(--semi-bold); +} + +.object-damage__table-header { + position: sticky; + top: 0; + width: 100%; + border-radius: var(--rounded); + background-color: var(--bg-darker); + font-size: var(--fonts-xs); + font-weight: var(--semi-bold); + padding-block: var(--s-2); + text-align: center; +} + +.object-damage__table-card { + display: grid; + width: 100%; + height: 100%; + padding: var(--s-2); + border-radius: var(--rounded); + background-color: var(--bg-lighter); + font-size: var(--fonts-xs); + font-weight: var(--semi-bold); + place-items: center; +} + +.object-damage__table-card__results { + display: grid; + column-gap: var(--s-2); + grid-template-columns: 1fr 1fr; +} + +.object-damage__abbr { + color: var(--text-lighter); + font-weight: var(--bold); + text-decoration: none; +} + +.object-damage__multiplier { + color: var(--theme); + font-size: var(--fonts-xxs); + font-weight: var(--bold); + letter-spacing: 0.5px; + margin-block-start: var(--s-2); }