Build Analyzer: AP comparing

This commit is contained in:
Kalle
2023-01-21 12:10:40 +02:00
parent 8ba5c715fe
commit f48c49aaa5
19 changed files with 110 additions and 76 deletions

View File

@@ -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;
}

View File

@@ -135,7 +135,7 @@ export interface Stat<T = number> {
}
export type AbilityPoints = Map<
Ability,
AbilityWithUnknown,
{ ap: number; apBeforeTacticooler: number }
>;

View File

@@ -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);

View File

@@ -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", () => {

View File

@@ -31,7 +31,7 @@ export function buildToAbilityPoints(build: BuildAbilitiesTupleWithUnknown) {
if (ability === "AD") {
abilityDoublerActive = true;
}
if (!isStackableAbility(ability)) {
if (!isStackableAbility(ability) && ability !== "UNKNOWN") {
continue;
}

View File

@@ -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,
}: {

View File

@@ -12,7 +12,6 @@ import {
Outlet,
Scripts,
useLoaderData,
useLocation,
type ShouldReloadFunction,
} from "@remix-run/react";
import * as React from "react";

View File

@@ -194,6 +194,10 @@
justify-content: flex-end;
}
.justify-self-end {
justify-self: flex-end;
}
.self-center {
align-self: center;
}

View File

@@ -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.",

View File

@@ -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}}.",

View File

@@ -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}}.",

View File

@@ -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."

View File

@@ -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."

View File

@@ -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}}.",

View File

@@ -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}}.",

View File

@@ -97,7 +97,6 @@
"value": "値",
"build": "ギアパワー/ドリンクあり",
"patch": "Patch:",
"abilityPoints": "ギアパワー",
"abilityPoints.short": "GP",
"abilityChunks": "ギアパワーのかけらが必要です",
"consumptionExplanation": "このチャートはサブを 0-{{maxSubsToUse}} 回使用したあとに何回メインを使用できるかを表しています。インクタンクが満タンの状態でサブが連続使用できる回数は {{maxSubsToUse}} です。",

View File

@@ -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.",

View File

@@ -102,7 +102,6 @@
"value": "Значение",
"build": "Сборка",
"patch": "Патч:",
"abilityPoints": "Свойства (AP)",
"abilityPoints.short": "AP",
"abilityChunks": "Требуется чанков свойства",
"consumptionExplanation": "Данная таблица показывает, сколько действий можно совершить после 0-{{maxSubsToUse}}кратного использования запасного оружия. С полным баллоном запасное оружие можно использовать {{maxSubsToUse}} раз(а) подряд.",

View File

@@ -96,7 +96,6 @@
"value": "值",
"build": "配装",
"patch": "游戏版本:",
"abilityPoints": "能力点数",
"abilityPoints.short": "AP",
"abilityChunks": "所需碎片",
"consumptionExplanation": "表中的数据表示在使用了副武器 0-{{maxSubsToUse}} 次后主武器的可攻击次数。在满墨下的最大副武器连续使用次数为 {{maxSubsToUse}}。",