mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
TankopediaCharacteristics
This commit is contained in:
1 parent
5cae5d1583
commit
74fbd15c5f
9 files changed
+521
-68
No files matched your search
@@ -1,16 +1,45 @@
|
|||||||
export function literals(
|
export function literals(
|
||||||
string: string,
|
string: string,
|
||||||
literals?: Record<string, string | number | undefined>
|
literals?: Record<string, string | number | undefined>,
|
||||||
) {
|
) {
|
||||||
for (const key in literals) {
|
for (const key in literals) {
|
||||||
const mutated = string.replaceAll(`{${key}}`, `${literals[key]}`);
|
string = string.replaceAll(`{${key}}`, `${literals[key]}`);
|
||||||
|
|
||||||
if (import.meta.env.DEV && mutated === string) {
|
|
||||||
console.warn(`Could not find literal {${key}} in string "${string}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
string = mutated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function literalsArray<Type, Literal = Type | string>(
|
||||||
|
string: string,
|
||||||
|
literals?: Record<string, Literal>,
|
||||||
|
) {
|
||||||
|
if (!literals) return [string];
|
||||||
|
|
||||||
|
let array: (string | Literal)[] = [string];
|
||||||
|
|
||||||
|
for (const key in literals) {
|
||||||
|
const value = literals[key];
|
||||||
|
const next: (string | Literal)[] = [];
|
||||||
|
|
||||||
|
for (const chunk of array) {
|
||||||
|
if (typeof chunk !== "string") {
|
||||||
|
next.push(chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = chunk.split(`{${key}}`);
|
||||||
|
|
||||||
|
for (let index = 0; index < parts.length; index++) {
|
||||||
|
if (parts[index]) next.push(parts[index]);
|
||||||
|
|
||||||
|
if (index < parts.length - 1) {
|
||||||
|
next.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
array = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"units": {
|
||||||
|
"m": "{value}m",
|
||||||
|
"mm": "{value}mm",
|
||||||
|
"deg": "{value}°",
|
||||||
|
"hp": "{value}hp",
|
||||||
|
"x": "x{value}",
|
||||||
|
"percentage": "{value}%",
|
||||||
|
"deg_s": "{value}°/s",
|
||||||
|
"plus": "+{value}"
|
||||||
|
},
|
||||||
|
|
||||||
"tanks": {
|
"tanks": {
|
||||||
"characteristics": {
|
"characteristics": {
|
||||||
"titles": {
|
"titles": {
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
.characteristics-wrapper {
|
||||||
|
margin: var(--space-4);
|
||||||
|
|
||||||
|
--base-cell-width: 20rem;
|
||||||
|
--group-span: 7;
|
||||||
|
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
|
.characteristics {
|
||||||
|
--N: round(down, 100cqw / var(--base-cell-width));
|
||||||
|
--cell-width: calc(100cqw / var(--N));
|
||||||
|
--cell-height: calc(var(--cell-width) / var(--group-span));
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(
|
||||||
|
auto-fill,
|
||||||
|
minmax(var(--base-cell-width), 1fr)
|
||||||
|
);
|
||||||
|
grid-auto-rows: var(--cell-height);
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.group {
|
||||||
|
--gradient-size: min(100%, var(--space-8));
|
||||||
|
|
||||||
|
grid-column: span 1;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: inherit;
|
||||||
|
grid-auto-rows: inherit;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
grid-row: span 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 calc(var(--space-4));
|
||||||
|
|
||||||
|
.center {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-3);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
.align {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 0 var(--space-4);
|
||||||
|
|
||||||
|
gap: inherit;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: inherit;
|
||||||
|
grid-auto-rows: inherit;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 0 var(--space-3);
|
||||||
|
|
||||||
|
grid-row: span 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.units {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
.bar-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
height: var(--space-1);
|
||||||
|
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toy {
|
||||||
|
grid-row: span var(--group-span);
|
||||||
|
padding: var(--space-4) 0;
|
||||||
|
|
||||||
|
.sandbox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background-color: var(--gray-2);
|
||||||
|
outline: var(--gray-5) solid var(--space-1);
|
||||||
|
border-radius: var(--space-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dead {
|
||||||
|
grid-row: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dead {
|
||||||
|
grid-row: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { characteristicsGroups } from "../../config/characteristicsGroups";
|
||||||
|
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
|
||||||
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
|
import type {
|
||||||
|
CharacteristicRenderConfig,
|
||||||
|
CharacteristicReturnType,
|
||||||
|
CharacteristicsGroup,
|
||||||
|
ComputedCharacteristics,
|
||||||
|
} from "../../types/characteristics";
|
||||||
|
import { Heading } from "../Heading";
|
||||||
|
import { Text } from "../Text";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
interface TankopediaCharacteristicsProps {
|
||||||
|
characteristics: ComputedCharacteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TankopediaCharacteristics({
|
||||||
|
characteristics,
|
||||||
|
}: TankopediaCharacteristicsProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles["characteristics-wrapper"]}>
|
||||||
|
<div className={styles.characteristics}>
|
||||||
|
{characteristicsGroups.map((group) => {
|
||||||
|
const items = group.order.filter(
|
||||||
|
(item) => "name" in item && characteristics[item.name] !== null,
|
||||||
|
).length;
|
||||||
|
const shouldRender = items > 0;
|
||||||
|
|
||||||
|
if (!shouldRender) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
key={group.name}
|
||||||
|
group={group}
|
||||||
|
characteristics={characteristics}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupProps {
|
||||||
|
group: CharacteristicsGroup;
|
||||||
|
characteristics: ComputedCharacteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Group({ group, characteristics }: GroupProps) {
|
||||||
|
const strings = useStrings();
|
||||||
|
const toys = group.order.filter((item) => "toy" in item).length;
|
||||||
|
const items = group.order.filter(
|
||||||
|
(item) => "name" in item && characteristics[item.name] !== null,
|
||||||
|
).length;
|
||||||
|
const groupSpan = `calc(${toys} * var(--group-span) + ${items} + 2)`;
|
||||||
|
const contentSpan = `calc(${toys} * var(--group-span) + ${items})`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.group}
|
||||||
|
style={{
|
||||||
|
gridRow: `span ${groupSpan}`,
|
||||||
|
gridTemplateRows: `repeat(${groupSpan}, 1fr)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Heading size="3" className={styles.text}>
|
||||||
|
<div className={styles.align}>
|
||||||
|
{
|
||||||
|
(
|
||||||
|
strings.tanks.characteristics.titles as Record<string, string>
|
||||||
|
)[group.name]
|
||||||
|
}
|
||||||
|
<img src={`/media/groups/${group.name}.png`} />
|
||||||
|
</div>
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.content}
|
||||||
|
style={{
|
||||||
|
gridRow: `span ${contentSpan}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{group.order.map((item) => (
|
||||||
|
<>
|
||||||
|
{"toy" in item && <Toy {...item} />}
|
||||||
|
{"name" in item && characteristics[item.name] !== null && (
|
||||||
|
<Item config={item} value={characteristics[item.name]!} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={styles.dead} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemProps {
|
||||||
|
config: CharacteristicRenderConfig;
|
||||||
|
value: CharacteristicReturnType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({ config, value }: ItemProps) {
|
||||||
|
const strings = useStrings();
|
||||||
|
const renderCharacteristic = useCharacteristicRenderer();
|
||||||
|
const hasUnits = config.units !== undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.item}>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<Text lowContrast>
|
||||||
|
{
|
||||||
|
(strings.tanks.characteristics.items as Record<string, string>)[
|
||||||
|
config.name
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text lowContrast={hasUnits} size={hasUnits ? "minor" : undefined}>
|
||||||
|
<div className={styles.units}>
|
||||||
|
{renderCharacteristic(value, config)}
|
||||||
|
</div>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.ranking}>
|
||||||
|
<Bar value={0.5} />
|
||||||
|
|
||||||
|
<Text lowContrast size="minor" className={styles.number}>
|
||||||
|
1 / 132
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarProps {
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const barColors = ["red", "orange", "amber", "jade"];
|
||||||
|
|
||||||
|
function Bar({ value }: BarProps) {
|
||||||
|
const index = (barColors.length - 1) * value;
|
||||||
|
const iA = Math.floor(index);
|
||||||
|
const iB = Math.ceil(index);
|
||||||
|
const A = barColors[iA];
|
||||||
|
const B = barColors[iB];
|
||||||
|
const x = index % 1;
|
||||||
|
const p = (1 - x) * 100;
|
||||||
|
const v = 7;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["bar-wrapper"]}>
|
||||||
|
<div
|
||||||
|
className={styles.bar}
|
||||||
|
style={{
|
||||||
|
backgroundColor: `color-mix(in srgb, var(--${A}-${v}) ${p}%, var(--${B}-${v}))`,
|
||||||
|
width: `${value * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToyProps {
|
||||||
|
toy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toy({ toy }: ToyProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.toy}>
|
||||||
|
<div className={styles.sandbox}>
|
||||||
|
<Text lowContrast>{toy} toy</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { CharacteristicsGroup } from "../types/characteristics";
|
||||||
|
|
||||||
|
export const characteristicsGroups: CharacteristicsGroup[] = [
|
||||||
|
{
|
||||||
|
name: "firepower",
|
||||||
|
order: [
|
||||||
|
{
|
||||||
|
name: "gun_type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "damage",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clip_size",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -613,56 +613,3 @@ export function tankCharacteristics(
|
|||||||
burstInterShell,
|
burstInterShell,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TankCharacteristicsCondition {
|
|
||||||
Regular,
|
|
||||||
AutoLoader,
|
|
||||||
AutoReloader,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const characteristicsOrder: {
|
|
||||||
name: string;
|
|
||||||
items: TankCharacteristicsKey[];
|
|
||||||
}[] = [
|
|
||||||
{
|
|
||||||
name: "Firepower",
|
|
||||||
items: [
|
|
||||||
"gunType",
|
|
||||||
"dpm",
|
|
||||||
"shells",
|
|
||||||
"shellReload",
|
|
||||||
"shellReloads",
|
|
||||||
"intraClip",
|
|
||||||
"penetration",
|
|
||||||
"penetrationAt250m",
|
|
||||||
"damage",
|
|
||||||
"clipDamage",
|
|
||||||
"moduleDamage",
|
|
||||||
"caliber",
|
|
||||||
"shellNormalization",
|
|
||||||
"shellRicochet",
|
|
||||||
"shellVelocity",
|
|
||||||
"shellRange",
|
|
||||||
"shellCapacity",
|
|
||||||
"aimTime",
|
|
||||||
"dispersion",
|
|
||||||
"dispersionMoving",
|
|
||||||
"dispersionHullTraversing",
|
|
||||||
"dispersionTurretTraversing",
|
|
||||||
"dispersionShooting",
|
|
||||||
"dispersionGunDamaged",
|
|
||||||
"gunDepression",
|
|
||||||
"gunElevation",
|
|
||||||
"gunFrontalDepression",
|
|
||||||
"gunFrontalElevation",
|
|
||||||
"gunRearDepression",
|
|
||||||
"gunRearElevation",
|
|
||||||
"azimuthLeft",
|
|
||||||
"azimuthRight",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Crew",
|
|
||||||
items: ["crewCount"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { literalsArray } from "@blitzkit/i18n/src/literals";
|
||||||
|
import { useMemo, type ReactNode } from "react";
|
||||||
|
import { Text } from "../components/Text";
|
||||||
|
import { characteristicsGroups } from "../config/characteristicsGroups";
|
||||||
|
import type {
|
||||||
|
CharacteristicRenderConfig,
|
||||||
|
CharacteristicReturnType,
|
||||||
|
} from "../types/characteristics";
|
||||||
|
import { useLocale } from "./useLocale";
|
||||||
|
import { useStrings } from "./useStrings";
|
||||||
|
|
||||||
|
export function useCharacteristicRenderer() {
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
const groups: string[] = [];
|
||||||
|
|
||||||
|
for (const group of characteristicsGroups) {
|
||||||
|
for (const item of group.order) {
|
||||||
|
if ("strings" in item && item.strings) {
|
||||||
|
groups.push(item.strings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const locale = useLocale();
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
function renderCharacteristic(
|
||||||
|
characteristic: CharacteristicReturnType,
|
||||||
|
config: CharacteristicRenderConfig,
|
||||||
|
): ReactNode {
|
||||||
|
if (characteristic === null) return null;
|
||||||
|
|
||||||
|
if (config.render)
|
||||||
|
return config.render({ output: characteristic, strings });
|
||||||
|
|
||||||
|
if (typeof characteristic === "number") {
|
||||||
|
if (Number.isFinite(characteristic)) {
|
||||||
|
let value: ReactNode = characteristic;
|
||||||
|
|
||||||
|
if (config.decimals !== undefined) {
|
||||||
|
value *= 10 ** config.decimals;
|
||||||
|
value = Math.round(value);
|
||||||
|
value /= 10 ** config.decimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.localize) {
|
||||||
|
value = value.toLocaleString(locale);
|
||||||
|
} else {
|
||||||
|
value = value.toFixed(config.decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.units !== undefined) {
|
||||||
|
value = literalsArray(strings.units[config.units], {
|
||||||
|
value: <Text>{value}</Text>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${characteristic < 0 ? "-" : ""}∞`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof characteristic === "string") {
|
||||||
|
return characteristic;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unknown characteristic type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderCharacteristic;
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { api } from "../../../../../api/dynamic";
|
import { api } from "../../../../../api/dynamic";
|
||||||
|
import type { ThicknessRange } from "../../../../../components/StaticArmor";
|
||||||
|
import { TankopediaCharacteristics } from "../../../../../components/TankopediaCharacteristics";
|
||||||
import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout";
|
import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout";
|
||||||
import { TankopediaSandbox } from "../../../../../components/TankopediaSandbox";
|
import { TankopediaSandbox } from "../../../../../components/TankopediaSandbox";
|
||||||
|
import { defaultEqualizer } from "../../../../../config/equalizer";
|
||||||
import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper";
|
import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper";
|
||||||
import { withLocale } from "../../../../../hocs/withLocale";
|
import { withLocale } from "../../../../../hocs/withLocale";
|
||||||
import { useAwait } from "../../../../../hooks/useAwait";
|
import { useAwait } from "../../../../../hooks/useAwait";
|
||||||
@@ -13,6 +16,8 @@ interface PageProps {
|
|||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tanks = await api.tanks();
|
||||||
|
|
||||||
export const Page = withErrorWrapper(
|
export const Page = withErrorWrapper(
|
||||||
withLocale<PageProps>(({ id }) => {
|
withLocale<PageProps>(({ id }) => {
|
||||||
const protagonistTank = useAwait(() => api.tank(id), `tank-${id}`);
|
const protagonistTank = useAwait(() => api.tank(id), `tank-${id}`);
|
||||||
@@ -27,6 +32,27 @@ export const Page = withErrorWrapper(
|
|||||||
[protagonist],
|
[protagonist],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const equalize = Tankopedia.use((state) => state.equalize);
|
||||||
|
const thicknessRange = useMemo(() => {
|
||||||
|
const entries = Object.values(tanks.tanks);
|
||||||
|
const filtered = entries.filter(
|
||||||
|
(thisTank) => thisTank.tier === protagonistTank.tier,
|
||||||
|
);
|
||||||
|
const value =
|
||||||
|
(filtered.reduce((accumulator, thisTank) => {
|
||||||
|
return (
|
||||||
|
accumulator +
|
||||||
|
thisTank.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration!.near *
|
||||||
|
((equalize ? thisTank.equalizer : undefined) ?? defaultEqualizer)
|
||||||
|
.penetration
|
||||||
|
);
|
||||||
|
}, 0) /
|
||||||
|
filtered.length) *
|
||||||
|
(3 / 4);
|
||||||
|
|
||||||
|
return { value } satisfies ThicknessRange;
|
||||||
|
}, [protagonist, equalize]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
<div className={styles.loadout}>
|
<div className={styles.loadout}>
|
||||||
@@ -34,8 +60,10 @@ export const Page = withErrorWrapper(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.sandbox}>
|
<div className={styles.sandbox}>
|
||||||
<TankopediaSandbox parameters={parameters} />
|
<TankopediaSandbox thicknessRange={thicknessRange} />
|
||||||
<TankopediaCharacteristics characteristics={characteristics} />
|
<TankopediaCharacteristics
|
||||||
|
computedCharacteristics={characteristics}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import type { Strings } from "@blitzkit/i18n";
|
||||||
import type {
|
import type {
|
||||||
Equalizer,
|
Equalizer,
|
||||||
GunDefinition,
|
GunDefinition,
|
||||||
ShellDefinition,
|
ShellDefinition,
|
||||||
} from "@blitzkit/protos";
|
} from "@blitzkit/protos";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { characteristics } from "../config/characteristics";
|
import { characteristics } from "../config/characteristics";
|
||||||
|
|
||||||
export type Characteristic = {
|
export type Characteristic = {
|
||||||
@@ -36,10 +38,32 @@ export enum CharacteristicType {
|
|||||||
Number,
|
Number,
|
||||||
}
|
}
|
||||||
|
|
||||||
type CharacteristicName = keyof typeof characteristics;
|
export type CharacteristicName = keyof typeof characteristics;
|
||||||
type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
|
export type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
|
||||||
|
|
||||||
export type ComputedCharacteristics = Record<
|
export type ComputedCharacteristics = Partial<
|
||||||
CharacteristicName,
|
Record<CharacteristicName, CharacteristicReturnType>
|
||||||
CharacteristicReturnType
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export interface CharacteristicRenderConfig {
|
||||||
|
name: CharacteristicName;
|
||||||
|
|
||||||
|
decimals?: number;
|
||||||
|
units?: keyof Strings["units"];
|
||||||
|
localize?: boolean;
|
||||||
|
strings?: string;
|
||||||
|
|
||||||
|
render?: (data: {
|
||||||
|
output: CharacteristicReturnType;
|
||||||
|
strings: Strings;
|
||||||
|
}) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToyRenderConfig {
|
||||||
|
toy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CharacteristicsGroup = {
|
||||||
|
name: string;
|
||||||
|
order: (CharacteristicRenderConfig | ToyRenderConfig)[];
|
||||||
|
};
|
||||||
Reference in new issue
Block a user