mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-07 19:55:46 -05:00
Build Analyzer: AP comparing
This commit is contained in:
@@ -47,7 +47,7 @@ export function useAnalyzeBuild() {
|
||||
newBuild2?: BuildAbilitiesTupleWithUnknown;
|
||||
newLdeIntensity?: number;
|
||||
newEffects?: Array<SpecialEffectType>;
|
||||
newFocused?: 1 | 2;
|
||||
newFocused?: 1 | 2 | 3;
|
||||
}) => {
|
||||
setSearchParams(
|
||||
{
|
||||
@@ -93,13 +93,15 @@ export function useAnalyzeBuild() {
|
||||
return {
|
||||
build,
|
||||
build2,
|
||||
focusedBuild: focused === 1 ? build : focused === 2 ? build2 : null,
|
||||
focused,
|
||||
mainWeaponId,
|
||||
handleChange,
|
||||
analyzed,
|
||||
analyzed2,
|
||||
abilityPoints,
|
||||
effects,
|
||||
abilityPoints2,
|
||||
allEffects: Array.from(new Set([...effects, ...effects2])),
|
||||
ldeIntensity,
|
||||
};
|
||||
}
|
||||
@@ -229,6 +231,7 @@ function validatedFocusedFromSearchParams({
|
||||
const focused = searchParams.get("focused");
|
||||
|
||||
if (focused === "2") return 2;
|
||||
if (focused === "3") return 3;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export interface Stat<T = number> {
|
||||
}
|
||||
|
||||
export type AbilityPoints = Map<
|
||||
Ability,
|
||||
AbilityWithUnknown,
|
||||
{ ap: number; apBeforeTacticooler: number }
|
||||
>;
|
||||
|
||||
|
||||
@@ -14,6 +14,23 @@
|
||||
gap: var(--s-8);
|
||||
}
|
||||
|
||||
.analyzer__ap-compare {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content max-content max-content 1fr;
|
||||
gap: var(--s-2);
|
||||
font-size: var(--fonts-xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.analyzer__ap-compare__bar {
|
||||
height: 100%;
|
||||
background-color: var(--theme);
|
||||
}
|
||||
|
||||
.analyzer__ap-compare__bar.analyzer__better {
|
||||
background-color: var(--theme-success);
|
||||
}
|
||||
|
||||
.analyzer__effects-selector {
|
||||
display: grid;
|
||||
gap: var(--s-2);
|
||||
|
||||
@@ -12,12 +12,6 @@ const EMPTY_ROW: [
|
||||
AbilityWithUnknown
|
||||
] = ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"];
|
||||
|
||||
BuildToAbilityPoints("Empty build leads to empty AP map", () => {
|
||||
const aps = buildToAbilityPoints([EMPTY_ROW, EMPTY_ROW, EMPTY_ROW]);
|
||||
|
||||
assert.equal(aps.size, 0);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Calculates ability points", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["SS", "SS", "RSU", "RSU"],
|
||||
@@ -27,6 +21,7 @@ BuildToAbilityPoints("Calculates ability points", () => {
|
||||
|
||||
assert.equal(aps.get("SS")?.ap, 13);
|
||||
assert.equal(aps.get("RSU")?.ap, 6);
|
||||
assert.equal(aps.get("UNKNOWN")?.ap, 38);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Handles ability doubler", () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ export function buildToAbilityPoints(build: BuildAbilitiesTupleWithUnknown) {
|
||||
if (ability === "AD") {
|
||||
abilityDoublerActive = true;
|
||||
}
|
||||
if (!isStackableAbility(ability)) {
|
||||
if (!isStackableAbility(ability) && ability !== "UNKNOWN") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Popover } from "~/components/Popover";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import {
|
||||
abilities,
|
||||
ANGLE_SHOOTER_ID,
|
||||
INK_MINE_ID,
|
||||
INK_STORM_ID,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
type BuildAbilitiesTupleWithUnknown,
|
||||
type MainWeaponId,
|
||||
type SubWeaponId,
|
||||
abilitiesShort,
|
||||
} from "~/modules/in-game-lists";
|
||||
import styles from "../analyzer.css";
|
||||
import { damageTypeTranslationString } from "~/utils/i18next";
|
||||
@@ -84,21 +84,22 @@ export const handle: SendouRouteHandle = {
|
||||
// Resolves this Github issue: https://github.com/Sendouc/sendou.ink/issues/1053
|
||||
export const unstable_shouldReload: ShouldReloadFunction = () => false;
|
||||
|
||||
// xxx: for comparison AP bar charts show green if the better value (except for unknown), green can be on both sides too
|
||||
export default function BuildAnalyzerPage() {
|
||||
const { t } = useTranslation(["analyzer", "common", "weapons"]);
|
||||
useSetTitle(t("common:pages.analyzer"));
|
||||
const {
|
||||
build,
|
||||
build2,
|
||||
focusedBuild,
|
||||
mainWeaponId,
|
||||
handleChange,
|
||||
analyzed,
|
||||
analyzed2,
|
||||
focused,
|
||||
abilityPoints,
|
||||
abilityPoints2,
|
||||
ldeIntensity,
|
||||
effects,
|
||||
allEffects,
|
||||
} = useAnalyzeBuild();
|
||||
|
||||
const statKeyToTuple = (key: keyof AnalyzedBuild["stats"]) => {
|
||||
@@ -201,8 +202,8 @@ export default function BuildAnalyzerPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stack md items-center">
|
||||
<div>
|
||||
<div className="stack md items-center w-full">
|
||||
<div className="w-full">
|
||||
<Tabs className="analyzer__sub-nav">
|
||||
<Tab
|
||||
active={focused === 1}
|
||||
@@ -216,15 +217,28 @@ export default function BuildAnalyzerPage() {
|
||||
>
|
||||
{t("analyzer:build2")}
|
||||
</Tab>
|
||||
<Tab
|
||||
active={focused === 3}
|
||||
onClick={() => handleChange({ newFocused: 3 })}
|
||||
>
|
||||
{t("analyzer:compare")}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<AbilitiesSelector
|
||||
selectedAbilities={focused === 1 ? build : build2}
|
||||
onChange={(newBuild) => {
|
||||
handleChange({
|
||||
[focused === 1 ? "newBuild" : "newBuild2"]: newBuild,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{focusedBuild ? (
|
||||
<AbilitiesSelector
|
||||
selectedAbilities={focusedBuild}
|
||||
onChange={(newBuild) => {
|
||||
handleChange({
|
||||
[focused === 1 ? "newBuild" : "newBuild2"]: newBuild,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<APCompare
|
||||
abilityPoints={abilityPoints}
|
||||
abilityPoints2={abilityPoints2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<EffectsSelector
|
||||
build={build}
|
||||
@@ -234,19 +248,15 @@ export default function BuildAnalyzerPage() {
|
||||
handleChange({ newLdeIntensity })
|
||||
}
|
||||
handleAddEffect={(newEffect) =>
|
||||
handleChange({ newEffects: [...effects, newEffect] })
|
||||
handleChange({ newEffects: [...allEffects, newEffect] })
|
||||
}
|
||||
handleRemoveEffect={(effectToRemove) =>
|
||||
handleChange({
|
||||
newEffects: effects.filter((e) => e !== effectToRemove),
|
||||
newEffects: allEffects.filter((e) => e !== effectToRemove),
|
||||
})
|
||||
}
|
||||
effects={effects}
|
||||
effects={allEffects}
|
||||
/>
|
||||
{/* xxx: rework these - maybe 3rd tab "Comparison" */}
|
||||
{abilityPoints.size > 0 && (
|
||||
<AbilityPointsDetails abilityPoints={abilityPoints} />
|
||||
)}
|
||||
{showAbilityChunksRequired && (
|
||||
<AbilityChunksRequired build={build} />
|
||||
)}
|
||||
@@ -845,6 +855,56 @@ export default function BuildAnalyzerPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function APCompare({
|
||||
abilityPoints,
|
||||
abilityPoints2,
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
abilityPoints2: AbilityPoints;
|
||||
}) {
|
||||
const { t } = useTranslation(["analyzer"]);
|
||||
|
||||
return (
|
||||
<div className="analyzer__ap-compare">
|
||||
{([...abilitiesShort, "UNKNOWN"] as const).map((ability) => {
|
||||
const ap = abilityPoints.get(ability)?.ap ?? 0;
|
||||
const ap2 = abilityPoints2.get(ability)?.ap ?? 0;
|
||||
|
||||
if (!ap && !ap2) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={clsx("justify-self-end", {
|
||||
invisible: !ap,
|
||||
})}
|
||||
>
|
||||
{ap}AP
|
||||
</div>
|
||||
<div
|
||||
className={clsx("analyzer__ap-compare__bar", "justify-self-end", {
|
||||
analyzer__better: ap >= ap2,
|
||||
})}
|
||||
style={{ width: `${ap}px` }}
|
||||
/>
|
||||
<Ability ability={ability} size="TINY" />
|
||||
<div
|
||||
className={clsx("analyzer__ap-compare__bar", {
|
||||
analyzer__better: ap <= ap2,
|
||||
})}
|
||||
style={{ width: `${ap2}px` }}
|
||||
/>
|
||||
<div className={clsx({ invisible: !ap2 })}>
|
||||
{ap2}
|
||||
{t("analyzer:abilityPoints.short")}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EffectsSelector({
|
||||
build,
|
||||
build2,
|
||||
@@ -929,40 +989,6 @@ function EffectsSelector({
|
||||
);
|
||||
}
|
||||
|
||||
function AbilityPointsDetails({
|
||||
abilityPoints,
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
}) {
|
||||
const { t } = useTranslation("analyzer");
|
||||
|
||||
return (
|
||||
<details className="w-full">
|
||||
<summary className="analyzer__ap-summary">{t("abilityPoints")}</summary>
|
||||
<div className="stack sm horizontal flex-wrap mt-4">
|
||||
{abilities
|
||||
.filter((a) => (abilityPoints.get(a.name)?.ap ?? 0) > 0)
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
abilityPoints.get(b.name)!.ap - abilityPoints.get(a.name)!.ap
|
||||
);
|
||||
})
|
||||
.map((a) => (
|
||||
<div
|
||||
key={`abilityPointsDetails_${a.name}`}
|
||||
className="stack items-center"
|
||||
>
|
||||
<Ability ability={a.name} size="TINY" />
|
||||
<div className="analyzer__ap-text">
|
||||
{abilityPoints.get(a.name)?.ap}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function AbilityChunksRequired({
|
||||
build,
|
||||
}: {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Outlet,
|
||||
Scripts,
|
||||
useLoaderData,
|
||||
useLocation,
|
||||
type ShouldReloadFunction,
|
||||
} from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -194,6 +194,10 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-self-end {
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
.self-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
"value": "Værdi",
|
||||
"build": "Udrustning",
|
||||
"patch": "Patch:",
|
||||
"abilityPoints": "Egenskabspoint (AP)",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Pris i Egenskabs-fragmenter",
|
||||
"consumptionExplanation": "Dette diagram viser, hvor mange handlinger man kan udføre med det primære våben efter brug af det sekundære våben 0-{{maxSubsToUse}} gange. Du kan bruge {{maxSubsToUse}} sekundære våben per fuld blæktank.",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"value": "Wert",
|
||||
"build": "Ausrüstung",
|
||||
"patch": "Patch:",
|
||||
"abilityPoints": "Effekt-Punkte",
|
||||
"abilityPoints.short": "EP",
|
||||
"abilityChunks": "Effekt-Fragmente erforderlich",
|
||||
"consumptionExplanation": "Dieses Diagramm zeigt die Anzahl der Aktionen, die mit der Hauptwaffe noch durchgeführt werden können, nachdem die Sekundärwaffe 0-{{maxSubsToUse}} mal benutzt wurde. Die maximale Anzahl an aufeinanderfolgenden Sekundärwaffen, die bei vollem Tintentank verwendet werden können, ist {{maxSubsToUse}}.",
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
"build": "Build",
|
||||
"build1": "Build 1",
|
||||
"build2": "Build 2",
|
||||
"compare": "Compare",
|
||||
"patch": "Patch:",
|
||||
"abilityPoints": "Ability points",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Ability Chunks Required",
|
||||
"consumptionExplanation": "This chart shows the amount of actions left to perform with main weapon after using sub 0-{{maxSubsToUse}} times. Max amount of consecutive subs to use with full ink tank is {{maxSubsToUse}}.",
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"value": "Valor",
|
||||
"build": "Build",
|
||||
"patch": "Parche:",
|
||||
"abilityPoints": "Puntos de habilidad (AP)",
|
||||
"abilityPoints.short": "AP",
|
||||
"consumptionExplanation": "Este gráfico muestra la cantidad de acciones restantes con el arma principal después de usar un arma secundaria 0-{{maxSubsToUse}} veces. La cantidad máxima de armas secundarias consecutivas que se pueden usar con el tanque lleno es {{maxSubsToUse}}.",
|
||||
"trackingSubDefExplanation": "Tiempos de rastreo de Rastreador, Mina de Tinta y Dardo Resaltador son calculados contra un oponente con 0AP de Resistencia Secundaria."
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"value": "Valor",
|
||||
"build": "Build",
|
||||
"patch": "Parche:",
|
||||
"abilityPoints": "Puntos de habilidad (AP)",
|
||||
"abilityPoints.short": "AP",
|
||||
"consumptionExplanation": "Este gráfico muestra la cantidad de acciones restantes con el arma principal después de usar un arma secundaria 0-{{maxSubsToUse}} veces. La cantidad máxima de armas secundarias consecutivas que se pueden usar con el tanque lleno es {{maxSubsToUse}}.",
|
||||
"trackingSubDefExplanation": "Tiempos de rastreo de Rastreador, Mina de Tinta y Dardo Resaltador son calculados contra un oponente con 0AP de Resistencia Secundaria."
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
"value": "Valeur",
|
||||
"build": "Equipement",
|
||||
"patch": "Version:",
|
||||
"abilityPoints": "Points de compétence",
|
||||
"abilityPoints.short": " PC",
|
||||
"abilityChunks": "Fragments de bonus requis",
|
||||
"consumptionExplanation": "Ce tableau montre le nombre d'actions restantes à effectuer avec l'arme principale après avoir utilisé l'arme secondaire 0-{{maxSubsToUse}} fois. Le nombre max d'utilisations d'armes secondaires consécutives à utiliser avec un réservoir d'encre plein est de {{maxSubsToUse}}.",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"value": "Valore",
|
||||
"build": "Kit",
|
||||
"patch": "Versione gioco:",
|
||||
"abilityPoints": "Punti abilità",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Frammenti abilità necessari",
|
||||
"consumptionExplanation": "Questa tabella mostra la quantità di azioni rimaste da eseguire con l'arma principale dopo aver usato l'arma secondaria da 0 a {{maxSubsToUse}} volte. Il numero massimo di armi secondarie da utilizzare consecutivamente con un serbatoio pieno è {{maxSubsToUse}}.",
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
"value": "値",
|
||||
"build": "ギアパワー/ドリンクあり",
|
||||
"patch": "Patch:",
|
||||
"abilityPoints": "ギアパワー",
|
||||
"abilityPoints.short": "GP",
|
||||
"abilityChunks": "ギアパワーのかけらが必要です",
|
||||
"consumptionExplanation": "このチャートはサブを 0-{{maxSubsToUse}} 回使用したあとに何回メインを使用できるかを表しています。インクタンクが満タンの状態でサブが連続使用できる回数は {{maxSubsToUse}} です。",
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"value": "Waarde",
|
||||
"build": "Uitrusting",
|
||||
"patch": "Softwareversie:",
|
||||
"abilityPoints": "Effect punten",
|
||||
"abilityPoints.short": "EP",
|
||||
"consumptionExplanation": "Deze tabel laat zien hoeveel keer je nog een bepaalde actie kunt doen met het hoofdwapen na het 0-{{maxSubsToUse}} keer gebruiken van een subwapen. Het maximale aantal dat je een subwapen achter elkaar kan gebruiken is {{maxSubsToUse}} keer.",
|
||||
"trackingSubDefExplanation": "Detector, Inktmijn en Ketskogel markeringstijden worden berekend voor een tegenstander met 0 Subtopper EP.",
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
"value": "Значение",
|
||||
"build": "Сборка",
|
||||
"patch": "Патч:",
|
||||
"abilityPoints": "Свойства (AP)",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Требуется чанков свойства",
|
||||
"consumptionExplanation": "Данная таблица показывает, сколько действий можно совершить после 0-{{maxSubsToUse}}кратного использования запасного оружия. С полным баллоном запасное оружие можно использовать {{maxSubsToUse}} раз(а) подряд.",
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
"value": "值",
|
||||
"build": "配装",
|
||||
"patch": "游戏版本:",
|
||||
"abilityPoints": "能力点数",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "所需碎片",
|
||||
"consumptionExplanation": "表中的数据表示在使用了副武器 0-{{maxSubsToUse}} 次后主武器的可攻击次数。在满墨下的最大副武器连续使用次数为 {{maxSubsToUse}}。",
|
||||
|
||||
Reference in New Issue
Block a user