mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 15:17:43 -05:00
Build Analyzer visualizations (#1517)
* Initial * Pass mainWeaponId * Repeating args to "context" * Fix todos * Date header * Fix comparison showing for sub weapon def dmg even if no abilities selected * Sub damage table charts * Chart dot focus style * Color grid initial * Can change shots in the grid * Optimize perf * Style - cells * Grid hover effect * Mobile friendly
This commit is contained in:
@@ -8,6 +8,7 @@ import { useTranslation } from "~/hooks/useTranslation";
|
||||
const sizeMap = {
|
||||
MAIN: 42,
|
||||
SUB: 32,
|
||||
SUBTINY: 26,
|
||||
TINY: 22,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { type AxisOptions, Chart as ReactChart } from "react-charts";
|
||||
import type { TooltipRendererProps } from "react-charts/types/components/TooltipRenderer";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { Theme, useTheme } from "~/modules/theme";
|
||||
|
||||
export default function Chart({
|
||||
options,
|
||||
containerClassName,
|
||||
headerSuffix,
|
||||
valueSuffix,
|
||||
}: {
|
||||
options: [
|
||||
{ label: string; data: Array<{ primary: Date; secondary: number }> },
|
||||
];
|
||||
containerClassName?: string;
|
||||
headerSuffix?: string;
|
||||
valueSuffix?: string;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const isMounted = useIsMounted();
|
||||
@@ -33,19 +42,99 @@ export default function Chart({
|
||||
[],
|
||||
);
|
||||
|
||||
if (!isMounted) return <div className="chart__container" />;
|
||||
if (!isMounted) {
|
||||
return <div className={clsx("chart__container", containerClassName)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart__container">
|
||||
<div className={clsx("chart__container", containerClassName)}>
|
||||
<ReactChart
|
||||
options={{
|
||||
data: options,
|
||||
tooltip: {
|
||||
render: (props) => (
|
||||
<ChartTooltip
|
||||
{...props}
|
||||
headerSuffix={headerSuffix}
|
||||
valueSuffix={valueSuffix}
|
||||
/>
|
||||
),
|
||||
},
|
||||
primaryAxis,
|
||||
secondaryAxes,
|
||||
dark: theme.htmlThemeClass === Theme.DARK,
|
||||
defaultColors: ["var(--theme)"],
|
||||
defaultColors: [
|
||||
"var(--theme)",
|
||||
"var(--theme-secondary)",
|
||||
"var(--theme-info)",
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChartTooltipProps extends TooltipRendererProps<any> {
|
||||
headerSuffix?: string;
|
||||
valueSuffix?: string;
|
||||
}
|
||||
|
||||
function ChartTooltip({
|
||||
focusedDatum,
|
||||
headerSuffix = "",
|
||||
valueSuffix = "",
|
||||
}: ChartTooltipProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const dataPoints = focusedDatum?.interactiveGroup ?? [];
|
||||
|
||||
const header = () => {
|
||||
const primaryValue = dataPoints[0]?.primaryValue;
|
||||
if (!primaryValue) return null;
|
||||
|
||||
if (primaryValue instanceof Date) {
|
||||
return primaryValue.toLocaleDateString(i18n.language, {
|
||||
weekday: "short",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
});
|
||||
}
|
||||
|
||||
return primaryValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chart__tooltip">
|
||||
<h3 className="text-center text-md">
|
||||
{header()}
|
||||
{headerSuffix}
|
||||
</h3>
|
||||
{dataPoints.map((dataPoint, index) => {
|
||||
const color = dataPoint.style?.fill ?? "var(--theme)";
|
||||
|
||||
return (
|
||||
<div key={index} className="stack horizontal items-center sm">
|
||||
<div
|
||||
className={clsx("chart__dot", {
|
||||
chart__dot__focused:
|
||||
focusedDatum?.seriesId === dataPoint.seriesId,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
"--dot-color": color,
|
||||
"--dot-color-outline": color.replace(")", "-transparent)"),
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
<div className="chart__tooltip__label">
|
||||
{dataPoint.originalSeries.label}
|
||||
</div>
|
||||
<div className="chart__tooltip__value">
|
||||
{dataPoint.secondaryValue}
|
||||
{valueSuffix}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
3
app/components/Table.tsx
Normal file
3
app/components/Table.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function Table({ children }: { children: React.ReactNode }) {
|
||||
return <table className="my-table">{children}</table>;
|
||||
}
|
||||
18
app/components/icons/Beaker.tsx
Normal file
18
app/components/icons/Beaker.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function BeakerIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -73,6 +73,16 @@
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__stat-popover-trigger {
|
||||
border-radius: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.analyzer__stat-popover-trigger__icon {
|
||||
width: 16px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.analyzer__ap-text {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
@@ -161,6 +171,9 @@
|
||||
|
||||
.analyzer__stat-card__ability-container {
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__popover {
|
||||
@@ -256,3 +269,65 @@
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.analyzer__stat-popover {
|
||||
min-width: 360px;
|
||||
|
||||
--chart-bg: transparent;
|
||||
--chart-height: 250px;
|
||||
--chart-width: 340px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 700px) {
|
||||
.analyzer__stat-popover {
|
||||
min-width: calc(360px * 1.75);
|
||||
--chart-height: calc(250px * 1.75);
|
||||
--chart-width: calc(340px * 1.75);
|
||||
}
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__container {
|
||||
overflow: auto;
|
||||
min-width: min(100vw, 1100px);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(41, 1fr);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__vertical-ability {
|
||||
grid-row: span 41;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding-inline-end: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__horizontal-ability {
|
||||
grid-column: span 41;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding-block-end: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__ap {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: var(--semi-bold);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__ap__focused {
|
||||
color: var(--theme-secondary);
|
||||
font-weight: var(--bold);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.analyzer__ink-grid__cell {
|
||||
width: 25px;
|
||||
font-size: var(--fonts-xs);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: var(--cell-color);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
328
app/features/build-analyzer/components/PerInkTankGrid.tsx
Normal file
328
app/features/build-analyzer/components/PerInkTankGrid.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { Popover } from "~/components/Popover";
|
||||
import { MAX_AP } from "~/constants";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { fullInkTankOptions } from "../core/stats";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { weaponParams } from "../core/utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface PerInkTankGridProps {
|
||||
weaponSplId: MainWeaponId;
|
||||
}
|
||||
|
||||
export function PerInkTankGrid(props: PerInkTankGridProps) {
|
||||
return (
|
||||
<Popover
|
||||
buttonChildren={<>Show consumption grid</>}
|
||||
contentClassName="analyzer__ink-grid__container"
|
||||
>
|
||||
<Grid {...props} />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
// if adding to this, also update analyzer.css
|
||||
const AP_VALUES_TO_SHOW = [
|
||||
0, 3, 6, 9, 10, 12, 13, 15, 16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
|
||||
29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 44, 45, 47, 48, 51, 54,
|
||||
57,
|
||||
];
|
||||
|
||||
function Grid({ weaponSplId }: PerInkTankGridProps) {
|
||||
const { t } = useTranslation(["weapons", "analyzer"]);
|
||||
const [subsUsed, setSubsUsed] = React.useState(1);
|
||||
const [ismHovered, setIsmHovered] = React.useState<number>();
|
||||
const [issHovered, setIssHovered] = React.useState<number>();
|
||||
|
||||
const values = React.useMemo(
|
||||
() => calculateGrid({ weaponSplId, subsUsed }),
|
||||
[weaponSplId, subsUsed],
|
||||
);
|
||||
|
||||
const handleHover = ({ ismAP, issAP }: { ismAP: number; issAP: number }) => {
|
||||
setIsmHovered(ismAP);
|
||||
setIssHovered(issAP);
|
||||
};
|
||||
|
||||
const handleMouseLeaveGrid = () => {
|
||||
setIsmHovered(undefined);
|
||||
setIssHovered(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg text-center">
|
||||
{t("analyzer:perInkTankGrid.header", {
|
||||
weapon: t(`weapons:MAIN_${weaponSplId}`),
|
||||
count: subsUsed,
|
||||
})}
|
||||
</h2>
|
||||
<div className="stack horizontal md justify-center my-2">
|
||||
{[0, 1, 2].map((subsUsedOption) => {
|
||||
const id = String(subsUsedOption);
|
||||
return (
|
||||
<div
|
||||
key={subsUsedOption}
|
||||
className="stack horizontal xs items-center"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={id}
|
||||
name="subsUsed"
|
||||
value={subsUsedOption}
|
||||
checked={subsUsedOption === subsUsed}
|
||||
onChange={(e) => setSubsUsed(Number(e.target.value))}
|
||||
/>
|
||||
<label htmlFor={id} className="mb-0">
|
||||
{subsUsedOption}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="stack horizontal sm" onMouseLeave={handleMouseLeaveGrid}>
|
||||
<div className="analyzer__ink-grid__horizontal-ability">
|
||||
<Ability ability="ISS" size="SUBTINY" />
|
||||
</div>
|
||||
<div className="analyzer__ink-grid">
|
||||
<div className="analyzer__ink-grid__horizontal-ability">
|
||||
<Ability ability="ISM" size="SUBTINY" />
|
||||
</div>
|
||||
<div />
|
||||
{AP_VALUES_TO_SHOW.map((ap) => (
|
||||
<div
|
||||
className={clsx("analyzer__ink-grid__ap", {
|
||||
"analyzer__ink-grid__ap__focused": ismHovered === ap,
|
||||
})}
|
||||
key={ap}
|
||||
>
|
||||
{ap}
|
||||
</div>
|
||||
))}
|
||||
{values.map((row, i) =>
|
||||
[
|
||||
<div
|
||||
className={clsx("analyzer__ink-grid__ap", {
|
||||
"analyzer__ink-grid__ap__focused":
|
||||
issHovered === AP_VALUES_TO_SHOW[i],
|
||||
})}
|
||||
key={i}
|
||||
>
|
||||
{AP_VALUES_TO_SHOW[i]}
|
||||
</div>,
|
||||
].concat(
|
||||
row.map((cell, j) => {
|
||||
const key = `${i}-${j}`;
|
||||
|
||||
if (cell === "N/A") {
|
||||
return <div className="analyzer__ink-grid__cell" key={key} />;
|
||||
}
|
||||
|
||||
const title = `${cell.shots ?? "-"} (ISM: ${cell.ismAP}, ISS: ${
|
||||
cell.issAP
|
||||
})`;
|
||||
|
||||
if (typeof cell.shots !== "number") {
|
||||
return (
|
||||
<div
|
||||
className="analyzer__ink-grid__cell"
|
||||
key={key}
|
||||
style={{ "--cell-color": "var(--bg-lighter)" } as any}
|
||||
title={title}
|
||||
onMouseEnter={() =>
|
||||
handleHover({ ismAP: cell.ismAP, issAP: cell.issAP })
|
||||
}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="analyzer__ink-grid__cell"
|
||||
style={{ "--cell-color": cell.hex } as any}
|
||||
title={title}
|
||||
onMouseEnter={() =>
|
||||
handleHover({ ismAP: cell.ismAP, issAP: cell.issAP })
|
||||
}
|
||||
>
|
||||
{cell.shots}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ShotCellData = { shots: number | null; ismAP: number; issAP: number };
|
||||
|
||||
// LDE boosts both ISM and ISS by max 18 AP each, but you need 10 AP to wear it.
|
||||
const MAX_LDE_AP = 18 * 2;
|
||||
const AP_NEEDED_TO_WEAR_LDE = 10;
|
||||
const apsArePossible = (issAP: number, ismAP: number) =>
|
||||
issAP + ismAP - MAX_LDE_AP + AP_NEEDED_TO_WEAR_LDE <= MAX_AP;
|
||||
function calculateGrid({
|
||||
weaponSplId,
|
||||
subsUsed,
|
||||
}: {
|
||||
weaponSplId: MainWeaponId;
|
||||
subsUsed: number;
|
||||
}) {
|
||||
const result: ("N/A" | ShotCellData)[][] = [];
|
||||
for (
|
||||
let issAPIndex = 0;
|
||||
issAPIndex < AP_VALUES_TO_SHOW.length;
|
||||
issAPIndex++
|
||||
) {
|
||||
const issAP = AP_VALUES_TO_SHOW[issAPIndex];
|
||||
|
||||
const row: ("N/A" | ShotCellData)[] = [];
|
||||
for (
|
||||
let ismAPIndex = 0;
|
||||
ismAPIndex < AP_VALUES_TO_SHOW.length;
|
||||
ismAPIndex++
|
||||
) {
|
||||
const ismAP = AP_VALUES_TO_SHOW[ismAPIndex];
|
||||
|
||||
if (!apsArePossible(issAP, ismAP)) {
|
||||
row.push("N/A" as const);
|
||||
continue;
|
||||
}
|
||||
|
||||
const option = inkTankOptionsWhenNSubsUsed({
|
||||
ismAP,
|
||||
issAP,
|
||||
subsUsed,
|
||||
weaponSplId,
|
||||
});
|
||||
|
||||
row.push({
|
||||
shots: option?.value ? Math.floor(option.value) : null,
|
||||
issAP,
|
||||
ismAP,
|
||||
});
|
||||
}
|
||||
|
||||
result.push(row);
|
||||
}
|
||||
|
||||
const withColors = addGridColors(result);
|
||||
|
||||
return withColors;
|
||||
}
|
||||
|
||||
// this is a performance optimization over simply calling "buildStats"
|
||||
// as it would be doing a lot of unnecessary work
|
||||
function inkTankOptionsWhenNSubsUsed({
|
||||
issAP,
|
||||
ismAP,
|
||||
subsUsed,
|
||||
weaponSplId,
|
||||
}: {
|
||||
issAP: number;
|
||||
ismAP: number;
|
||||
subsUsed: number;
|
||||
weaponSplId: MainWeaponId;
|
||||
}) {
|
||||
const mainWeaponParams = weaponParams().mainWeapons[weaponSplId];
|
||||
|
||||
const subWeaponParams =
|
||||
weaponParams().subWeapons[mainWeaponParams.subWeaponId];
|
||||
|
||||
const specialWeaponParams =
|
||||
weaponParams().specialWeapons[mainWeaponParams.specialWeaponId];
|
||||
|
||||
const options = fullInkTankOptions({
|
||||
abilityPoints: new Map([
|
||||
["ISS", issAP],
|
||||
["ISM", ismAP],
|
||||
]),
|
||||
weaponSplId,
|
||||
hasTacticooler: false,
|
||||
mainOnlyAbilities: [],
|
||||
mainWeaponParams,
|
||||
specialWeaponParams,
|
||||
subWeaponParams,
|
||||
});
|
||||
|
||||
return options.find((o) => o.subsUsed === subsUsed);
|
||||
}
|
||||
|
||||
function addGridColors(grid: ("N/A" | ShotCellData)[][]) {
|
||||
const maxValue = grid
|
||||
.flat()
|
||||
.filter(
|
||||
(v): v is { shots: number; ismAP: number; issAP: number } =>
|
||||
v !== "N/A" && v.shots !== null,
|
||||
)
|
||||
.reduce((max, v) => Math.max(max, v.shots), 0);
|
||||
|
||||
const minValue = grid
|
||||
.flat()
|
||||
.filter(
|
||||
(v): v is { shots: number; ismAP: number; issAP: number } =>
|
||||
v !== "N/A" && v.shots !== null,
|
||||
)
|
||||
.reduce((min, v) => Math.min(min, v.shots), Infinity);
|
||||
|
||||
const result = grid.map((row) =>
|
||||
row.map((cell) => {
|
||||
if (cell === "N/A") return cell;
|
||||
if (cell.shots === null) return { ...cell, hex: undefined };
|
||||
|
||||
const { shots } = cell;
|
||||
const hex = generateHexCode({
|
||||
minValue,
|
||||
maxValue,
|
||||
value: shots,
|
||||
});
|
||||
|
||||
return {
|
||||
...cell,
|
||||
hex,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateHexCode({
|
||||
minValue,
|
||||
maxValue,
|
||||
value,
|
||||
}: {
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
value: number;
|
||||
}) {
|
||||
const clampedValue = Math.max(minValue, Math.min(value, maxValue));
|
||||
const normalizedPosition = (clampedValue - minValue) / (maxValue - minValue);
|
||||
|
||||
// Adjust saturation and brightness to lighten the colors
|
||||
const saturationFactor = 0.5; // Adjust this to control the saturation
|
||||
const brightnessFactor = 0.8; // Adjust this to control the brightness
|
||||
|
||||
const red =
|
||||
Math.floor(255 * (1 - normalizedPosition) * saturationFactor) +
|
||||
Math.floor(255 * (1 - brightnessFactor));
|
||||
const green =
|
||||
Math.floor(255 * normalizedPosition * saturationFactor) +
|
||||
Math.floor(255 * (1 - brightnessFactor));
|
||||
const blue = Math.floor(255 * (1 - brightnessFactor));
|
||||
|
||||
const hex = ((1 << 24) | (red << 16) | (green << 8) | blue)
|
||||
.toString(16)
|
||||
.slice(1)
|
||||
.toUpperCase();
|
||||
|
||||
return `#${hex}`;
|
||||
}
|
||||
@@ -265,7 +265,7 @@ function subWeaponInkConsumptionPercentage(args: StatFunctionInput) {
|
||||
};
|
||||
}
|
||||
|
||||
function fullInkTankOptions(
|
||||
export function fullInkTankOptions(
|
||||
args: StatFunctionInput,
|
||||
): AnalyzedBuild["stats"]["fullInkTankOptions"] {
|
||||
const result: AnalyzedBuild["stats"]["fullInkTankOptions"] = [];
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import invariant from "tiny-invariant";
|
||||
import { EMPTY_BUILD } from "~/constants";
|
||||
import type {
|
||||
Ability,
|
||||
AbilityType,
|
||||
AbilityWithUnknown,
|
||||
BuildAbilitiesTupleWithUnknown,
|
||||
MainWeaponId,
|
||||
SpecialWeaponId,
|
||||
SubWeaponId,
|
||||
Ability,
|
||||
BuildAbilitiesTupleWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
MainWeaponId,
|
||||
} from "~/modules/in-game-lists";
|
||||
import {
|
||||
subWeaponIds,
|
||||
abilities,
|
||||
mainWeaponIds,
|
||||
nonBombSubWeaponIds,
|
||||
nonDamagingSpecialWeaponIds,
|
||||
specialWeaponIds,
|
||||
abilities,
|
||||
mainWeaponIds,
|
||||
subWeaponIds,
|
||||
weaponCategories,
|
||||
} from "~/modules/in-game-lists";
|
||||
import weaponParamsJson from "./weapon-params.json";
|
||||
import abilityValuesJson from "./ability-values.json";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import { UNKNOWN_SHORT } from "../analyzer-constants";
|
||||
import type {
|
||||
AbilityPoints,
|
||||
AnalyzedBuild,
|
||||
@@ -28,10 +30,8 @@ import type {
|
||||
SubWeaponDamage,
|
||||
SubWeaponParams,
|
||||
} from "../analyzer-types";
|
||||
import invariant from "tiny-invariant";
|
||||
import { EMPTY_BUILD } from "~/constants";
|
||||
import { UNKNOWN_SHORT } from "../analyzer-constants";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import abilityValuesJson from "./ability-values.json";
|
||||
import weaponParamsJson from "./weapon-params.json";
|
||||
|
||||
export function weaponParams(): ParamsJson {
|
||||
return weaponParamsJson as ParamsJson;
|
||||
@@ -71,6 +71,14 @@ export function isStackableAbility(
|
||||
return abilityObj.type === "STACKABLE";
|
||||
}
|
||||
|
||||
export function isMainOnlyAbility(
|
||||
ability: AbilityWithUnknown,
|
||||
): ability is Ability {
|
||||
if (ability === "UNKNOWN") return false;
|
||||
|
||||
return !isStackableAbility(ability);
|
||||
}
|
||||
|
||||
export function apFromMap({
|
||||
abilityPoints,
|
||||
ability,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,6 @@ export class DiscordStrategy extends OAuth2Strategy<
|
||||
}
|
||||
|
||||
private async fetchProfileViaDiscordApi(token: string) {
|
||||
console.log("authenticating via discord...");
|
||||
const authHeader: [string, string] = ["Authorization", `Bearer ${token}`];
|
||||
|
||||
return Promise.all([
|
||||
@@ -102,7 +101,6 @@ export class DiscordStrategy extends OAuth2Strategy<
|
||||
}
|
||||
|
||||
private async fetchProfileViaGateway(token: string) {
|
||||
console.log("authenticating via gateway...");
|
||||
const url = `${process.env["AUTH_GATEWAY_PROFILE_URL"]}?token=${token}`;
|
||||
|
||||
return fetch(url).then(this.jsonIfOk);
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
} from "~/utils/urls";
|
||||
import { actualNumber, id } from "~/utils/zod";
|
||||
import { Tags } from "../components/Tags";
|
||||
import { Table } from "~/components/Table";
|
||||
|
||||
export const action: ActionFunction = async ({ params, request }) => {
|
||||
const user = await requireUserId(request);
|
||||
@@ -270,7 +271,7 @@ function Results() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<table>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("calendar:forms.team.placing")}</th>
|
||||
@@ -310,7 +311,7 @@ function Results() {
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
tournamentTeamPage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { Table } from "~/components/Table";
|
||||
|
||||
export type UserResultsTableProps = {
|
||||
results: UserPageLoaderData["results"];
|
||||
@@ -31,7 +32,7 @@ export function UserResultsTable({
|
||||
const placementHeaderId = `${id}-th-placement`;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
{hasHighlightCheckboxes && <th />}
|
||||
@@ -136,6 +137,6 @@ export function UserResultsTable({
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ function PowerChart() {
|
||||
const chartOptions = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: "Power",
|
||||
label: "SP",
|
||||
data: data.skills.map((s) => {
|
||||
// hack to force shorter bottom axis text
|
||||
const date = new Date(s.date);
|
||||
|
||||
@@ -293,7 +293,7 @@ select:focus {
|
||||
outline: 2px solid var(--theme);
|
||||
}
|
||||
|
||||
table {
|
||||
.my-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0 var(--s-1-5);
|
||||
@@ -301,19 +301,19 @@ table {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table > thead {
|
||||
.my-table > thead {
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
table > tbody > tr:nth-child(2n) {
|
||||
.my-table > tbody > tr:nth-child(2n) {
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
table > thead > tr > th {
|
||||
.my-table > thead > tr > th {
|
||||
padding-inline: var(--s-1);
|
||||
}
|
||||
|
||||
table > tbody > tr > td {
|
||||
.my-table > tbody > tr > td {
|
||||
padding-inline: var(--s-1);
|
||||
}
|
||||
|
||||
@@ -1310,11 +1310,40 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
.chart__container {
|
||||
height: 175px;
|
||||
background-color: var(--bg-lighter);
|
||||
height: var(--chart-height, 175px);
|
||||
width: var(--chart-width);
|
||||
background-color: var(--chart-bg, var(--bg-lighter));
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.chart__tooltip {
|
||||
border: 1.75px solid var(--border);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
padding: var(--s-1) var(--s-2);
|
||||
font-weight: var(--semi-bold);
|
||||
font-size: var(--fonts-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.chart__tooltip__value {
|
||||
margin-inline-start: auto;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.chart__dot {
|
||||
background-color: var(--dot-color);
|
||||
border-radius: 100%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.chart__dot__focused {
|
||||
outline: 3px solid var(--dot-color-outline);
|
||||
}
|
||||
|
||||
.chat__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.text-md {
|
||||
font-size: var(--fonts-md);
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ html {
|
||||
--theme-vibrant: #f73e8b;
|
||||
--theme-semi-transparent: #ff99c477;
|
||||
--theme-secondary: rgb(63 58 255);
|
||||
--theme-secondary-transparent: rgb(63 58 255 / 55%);
|
||||
--backdrop-filter: blur(10px) brightness(95%);
|
||||
--rounded: 16px;
|
||||
--rounded-full: 200px;
|
||||
@@ -130,6 +131,7 @@ html.dark {
|
||||
--theme-semi-transparent: #ff99c477;
|
||||
--theme-transparent: #ffc6de52;
|
||||
--theme-secondary: rgb(239 229 83);
|
||||
--theme-secondary-transparent: rgb(239 229 83 / 55%);
|
||||
--backdrop-filter: blur(10px) brightness(75%);
|
||||
--inactive-image-filter: grayscale(100%) brightness(130%);
|
||||
}
|
||||
|
||||
@@ -153,5 +153,7 @@
|
||||
"labels.damageType": "Damage type",
|
||||
"labels.weapon": "Weapon",
|
||||
"dmgHtdExplanation": "DMG = Damage | HTD = Hits to destroy",
|
||||
"noDmgData": "No damage data yet for this weapon. Check back later!"
|
||||
"noDmgData": "No damage data yet for this weapon. Check back later!",
|
||||
"perInkTankGrid.header_one": "{{weapon}} shots after ×{{count}} sub weapon used",
|
||||
"perInkTankGrid.header_other": "{{weapon}} shots after ×{{count}} sub weapons used"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user