sendou.ink/app/routes/object-damage-calculator.tsx
Kalle 34ca290bdd
Redesign (#1179)
* Remove light mode

* Trim header

* New front page initial

* Get rid of build layout

* Breadcrumbs

* Desktop side nav

* Overhaul colors

* Add breadcrumbs

* New sub nav style

* Front page action buttons

* Add back add new build button

* Add articles page with icon

* Minor Object damage page layout tweaks

* Remove one unnecessary render from object damage

* Fix wrong link in article page

* Profile -> My Page in header

* Log in/out buttons in front

* Add drawings to front page

* Remove unnecessary comment
2022-12-05 16:05:51 +02:00

313 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { WeaponCombobox } from "~/components/Combobox";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { possibleApValues, useObjectDamage } from "~/modules/analyzer";
import {
type MainWeaponId,
BIG_BUBBLER_ID,
BOOYAH_BOMB_ID,
CRAB_TANK_ID,
SPLASH_WALL_ID,
SQUID_BEAKON_ID,
TORPEDO_ID,
WAVE_BREAKER_ID,
SPRINKLER_ID,
} from "~/modules/in-game-lists";
import {
mainWeaponImageUrl,
modeImageUrl,
navIconUrl,
OBJECT_DAMAGE_CALCULATOR_URL,
specialWeaponImageUrl,
subWeaponImageUrl,
} from "~/utils/urls";
import styles from "~/styles/object-damage.css";
import type { LinksFunction } from "@remix-run/node";
import type { SendouRouteHandle } from "~/utils/remix";
import type { DamageReceiver, DamageType } from "~/modules/analyzer";
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 { ShouldReloadFunction } from "@remix-run/react";
export const CURRENT_PATCH = "2.0";
export const unstable_shouldReload: ShouldReloadFunction = () => false;
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
};
export const handle: SendouRouteHandle = {
i18n: ["weapons", "analyzer"],
breadcrumb: () => ({
imgPath: navIconUrl("object-damage-calculator"),
href: OBJECT_DAMAGE_CALCULATOR_URL,
type: "IMAGE",
}),
};
export default function ObjectDamagePage() {
const { t } = useTranslation(["analyzer"]);
const {
mainWeaponId,
subWeaponId,
handleChange,
damagesToReceivers,
abilityPoints,
damageType,
allDamageTypes,
} = useObjectDamage();
return (
<Main className="stack lg">
<div className="object-damage__controls">
<div>
<Label htmlFor="weapon">{t("analyzer:labels.weapon")}</Label>
<WeaponCombobox
id="weapon"
inputName="weapon"
initialWeaponId={mainWeaponId}
onChange={(opt) =>
opt &&
handleChange({
newMainWeaponId: Number(opt.value) as MainWeaponId,
})
}
className="w-full-important"
clearsInputOnFocus
/>
</div>
<div className={clsx({ invisible: !damagesToReceivers })}>
<Label htmlFor="damage">{t("analyzer:labels.damageType")}</Label>
<DamageTypesSelect
handleChange={handleChange}
subWeaponId={subWeaponId}
damageType={damageType}
allDamageTypes={allDamageTypes}
/>
</div>
</div>
{damagesToReceivers ? (
<DamageReceiversGrid
subWeaponId={subWeaponId}
damagesToReceivers={damagesToReceivers}
abilityPoints={abilityPoints}
>
<div>
<select
className="object-damage__select"
id="ap"
value={abilityPoints}
onChange={(e) =>
handleChange({ newAbilityPoints: Number(e.target.value) })
}
>
{possibleApValues().map((ap) => (
<option key={ap} value={ap}>
{ap}
{t("analyzer:abilityPoints.short")}
</option>
))}
</select>
</div>
</DamageReceiversGrid>
) : (
<div>{t("analyzer:noDmgData")}</div>
)}
<div className="object-damage__bottom-container">
<div className="text-lighter text-xs">
{t("analyzer:dmgHtdExplanation")}
</div>
<div className="object-damage__patch">
{t("analyzer:patch")} {CURRENT_PATCH}
</div>
</div>
</Main>
);
}
function DamageTypesSelect({
allDamageTypes,
handleChange,
subWeaponId,
damageType,
}: Pick<
ReturnType<typeof useObjectDamage>,
"handleChange" | "subWeaponId" | "damageType" | "allDamageTypes"
>) {
const { t } = useTranslation(["analyzer"]);
return (
<select
className="object-damage__select"
id="damage"
value={damageType}
onChange={(e) =>
handleChange({ newDamageType: e.target.value as DamageType })
}
>
{allDamageTypes.map((damageType) => {
return (
<option key={damageType} value={damageType}>
{t(
damageTypeTranslationString({
damageType,
subWeaponId,
})
)}
</option>
);
})}
</select>
);
}
const damageReceiverImages: Record<DamageReceiver, string> = {
Bomb_TorpedoBullet: subWeaponImageUrl(TORPEDO_ID),
Chariot: specialWeaponImageUrl(CRAB_TANK_ID),
Gachihoko_Barrier: modeImageUrl("RM"),
GreatBarrier_Barrier: specialWeaponImageUrl(BIG_BUBBLER_ID),
GreatBarrier_WeakPoint: specialWeaponImageUrl(BIG_BUBBLER_ID),
NiceBall_Armor: specialWeaponImageUrl(BOOYAH_BOMB_ID),
ShockSonar: specialWeaponImageUrl(WAVE_BREAKER_ID),
Wsb_Flag: subWeaponImageUrl(SQUID_BEAKON_ID),
Wsb_Shield: subWeaponImageUrl(SPLASH_WALL_ID),
Wsb_Sprinkler: subWeaponImageUrl(SPRINKLER_ID),
BulletUmbrellaCanopyNormal: mainWeaponImageUrl(6000),
BulletUmbrellaCanopyWide: mainWeaponImageUrl(6010),
BulletUmbrellaCanopyCompact: mainWeaponImageUrl(6020),
};
const damageReceiverAp: Partial<Record<DamageReceiver, JSX.Element>> = {
GreatBarrier_Barrier: (
<Ability ability="SPU" size="TINY" className="object-damage__ability" />
),
GreatBarrier_WeakPoint: (
<Ability ability="SPU" size="TINY" className="object-damage__ability" />
),
Wsb_Shield: (
<Ability ability="BRU" size="TINY" className="object-damage__ability" />
),
};
function DamageReceiversGrid({
subWeaponId,
damagesToReceivers,
children,
abilityPoints,
}: {
subWeaponId: ReturnType<typeof useObjectDamage>["subWeaponId"];
damagesToReceivers: NonNullable<
ReturnType<typeof useObjectDamage>["damagesToReceivers"]
>;
children: React.ReactNode;
abilityPoints: string;
}): JSX.Element {
const { t } = useTranslation(["weapons", "analyzer", "common"]);
useSetTitle(t("common:pages.object-damage-calculator"));
return (
<div
className="object-damage__grid"
style={{
gridTemplateColumns: gridTemplateColumnsValue(
damagesToReceivers[0]?.damages.length ?? 0
),
}}
>
<div>
<Label htmlFor="ap">
{t("analyzer:labels.amountOf")}
<div className="object-damage__ap-label">
<Ability ability="BRU" size="TINY" />
<Ability ability="SPU" size="TINY" />
</div>
</Label>
</div>
<div>{children}</div>
{damagesToReceivers[0]?.damages.map((damage) => (
<div key={damage.id} className="object-damage__table-header">
<div className="stack horizontal sm justify-center items-center">
{t(
damageTypeTranslationString({
damageType: damage.type,
subWeaponId: subWeaponId,
})
)}
{damage.objectShredder && <Ability ability="OS" size="TINY" />}
</div>
<div
className={clsx("object-damage__distance", {
invisible: !damage.distance,
})}
>
{t("analyzer:distanceInline", { value: damage.distance })}
</div>
</div>
))}
{damagesToReceivers.map((damageToReceiver, i) => {
return (
<React.Fragment key={damageToReceiver.receiver}>
<div>
<Label htmlFor="ap">
<div className="object-damage__ap-label">
{abilityPoints !== "0" &&
damageReceiverAp[damageToReceiver.receiver]}
</div>
</Label>
<Image
className="object-damage__receiver-image"
key={i}
alt=""
path={damageReceiverImages[damageToReceiver.receiver]}
width={40}
height={40}
/>
</div>
<div className="object-damage__hp">
{damageToReceiver.hitPoints}
{t("analyzer:suffix.hp")}
</div>
{damageToReceiver.damages.map((damage) => {
return (
<div key={damage.id} className="object-damage__table-card">
<div className="object-damage__table-card__results">
<abbr
className="object-damage__abbr"
title={t("analyzer:stat.category.damage")}
>
{t("analyzer:damageShort")}
</abbr>
<div>{damage.value}</div>
<abbr
className="object-damage__abbr"
title={t("analyzer:hitsToDestroyLong")}
>
{t("analyzer:hitsToDestroyShort")}
</abbr>
<div>{damage.hitsToDestroy}</div>
</div>
<div className="object-damage__multiplier">
×{damage.multiplier}
</div>
</div>
);
})}
</React.Fragment>
);
})}
</div>
);
}
function gridTemplateColumnsValue(dataColumnsCount: number) {
return `max-content max-content ${new Array(dataColumnsCount)
.fill(null)
.map(() => `1fr`)
.join(" ")}`;
}