mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Compare commits
6
Commits
eb0c35d6c6
...
de80519f06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de80519f06 | ||
|
|
f935adb70e | ||
|
|
3902c63a65 | ||
|
|
e7b81f39b8 | ||
|
|
9692110733 | ||
|
|
ae2839cb83 |
No files matched your search
@@ -15,11 +15,29 @@
|
|||||||
"bhp_t": "{value}bhp/t",
|
"bhp_t": "{value}bhp/t",
|
||||||
"kN": "{value}kN",
|
"kN": "{value}kN",
|
||||||
"hp_s": "{value}hp/s",
|
"hp_s": "{value}hp/s",
|
||||||
"plus": "+{value}"
|
"plus": "+{value}",
|
||||||
|
"fraction": "{numerator} / {denominator}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"tanks": {
|
"tanks": {
|
||||||
"characteristics": {
|
"characteristics": {
|
||||||
|
"compare": {
|
||||||
|
"label": "Compare against",
|
||||||
|
"class": "Tier {tier} {class}",
|
||||||
|
"tier": "All tier {tier}",
|
||||||
|
"all": "All tanks"
|
||||||
|
},
|
||||||
|
|
||||||
|
"toys": {
|
||||||
|
"ricochet": {
|
||||||
|
"angle": "Angle",
|
||||||
|
"normalization": "Normalization",
|
||||||
|
"effective": "Effective",
|
||||||
|
"armor": "Armor",
|
||||||
|
"no_ricochet": "Explosive shells don't ricochet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"titles": {
|
"titles": {
|
||||||
"gun": "Gun",
|
"gun": "Gun",
|
||||||
"clip": "Clip",
|
"clip": "Clip",
|
||||||
@@ -114,6 +132,7 @@
|
|||||||
"provisions": "Provisions",
|
"provisions": "Provisions",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"equipment": "Equipment",
|
"equipment": "Equipment",
|
||||||
|
"shells": "Shells",
|
||||||
"alternative": "Alt"
|
"alternative": "Alt"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
@@ -0,0 +1,54 @@
|
|||||||
|
.options {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--accent-3);
|
||||||
|
outline: var(--outline-focus);
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
|
||||||
|
transition: var(--duration-brief);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
|
||||||
|
color: var(--gray-11);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:checked) {
|
||||||
|
color: var(--gray-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:focus-visible) {
|
||||||
|
outline: 2px solid var(--accent-8);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--radius-max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { type ReactNode } from "react";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
export interface SegmentedControlOption<Value extends string | number> {
|
||||||
|
value: Value;
|
||||||
|
label: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SegmentedControlProps<Value extends string | number> {
|
||||||
|
options: readonly SegmentedControlOption<Value>[];
|
||||||
|
value: Value;
|
||||||
|
onValueChange(value: Value): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SegmentedControl<Value extends string | number>({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
}: SegmentedControlProps<Value>) {
|
||||||
|
const selectedIndex = options.findIndex((option) => option.value === value);
|
||||||
|
const segmentWidth = 100 / options.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.options}
|
||||||
|
role="radiogroup"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.indicator}
|
||||||
|
style={{
|
||||||
|
left: `${Math.max(selectedIndex, 0) * segmentWidth}%`,
|
||||||
|
width: `${segmentWidth}%`,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{options.map((option) => (
|
||||||
|
<label className={styles.option} key={option.value}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={option.value}
|
||||||
|
checked={option.value === value}
|
||||||
|
onChange={() => onValueChange(option.value)}
|
||||||
|
/>
|
||||||
|
<span className={styles.label}>{option.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
|
||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
|
||||||
import { Flex, SegmentedControl, Text, type FlexProps } from "@radix-ui/themes";
|
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
|
||||||
import {
|
|
||||||
Tankopedia,
|
|
||||||
TankopediaRelativeAgainst,
|
|
||||||
} from "../../../../../../stores/tankopedia";
|
|
||||||
|
|
||||||
export function CompareOptions(props: FlexProps) {
|
|
||||||
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
gap="2"
|
|
||||||
justify="center"
|
|
||||||
direction={{ initial: "column", sm: "row" }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Text color="gray">
|
|
||||||
{strings.website.tools.tankopedia.characteristics.compare.label}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<SegmentedControl.Root
|
|
||||||
variant="classic"
|
|
||||||
size="1"
|
|
||||||
value={`${relativeAgainst}`}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Class}`}>
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.characteristics.compare.class,
|
|
||||||
{
|
|
||||||
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
|
||||||
class:
|
|
||||||
strings.common.tank_class_short_plural_lowercase[tank.class],
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.characteristics.compare.tier,
|
|
||||||
{ tier: TIER_ROMAN_NUMERALS[tank.tier] }
|
|
||||||
)}
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
|
||||||
{strings.website.tools.tankopedia.characteristics.compare.all}
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
</SegmentedControl.Root>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+1
-1
@@ -296,7 +296,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
opacity: 2 ** -3,
|
||||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
backgroundImage: `url(/media/random/armor-hash.png)`,
|
||||||
boxShadow: "inset 0 0 4rem 1rem black",
|
boxShadow: "inset 0 0 4rem 1rem black",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
-185
@@ -1,185 +0,0 @@
|
|||||||
import { createDefaultSkills } from "@blitzkit/core";
|
|
||||||
import { Flex, Progress, Text } from "@radix-ui/themes";
|
|
||||||
import { clamp } from "lodash-es";
|
|
||||||
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
|
||||||
import { api } from "../../../../../../api/dynamic";
|
|
||||||
import {
|
|
||||||
tankCharacteristics,
|
|
||||||
type TankCharacteristics,
|
|
||||||
} from "../../../../../../core/blitzkit/tankCharacteristics";
|
|
||||||
import { tankToDuelMember } from "../../../../../../core/blitzkit/tankToDuelMember";
|
|
||||||
import { useDelta } from "../../../../../../hooks/useDelta";
|
|
||||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../../../stores/duel";
|
|
||||||
import {
|
|
||||||
Tankopedia,
|
|
||||||
TankopediaRelativeAgainst,
|
|
||||||
} from "../../../../../../stores/tankopedia";
|
|
||||||
import { Info, type InfoProps } from "./Info";
|
|
||||||
|
|
||||||
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
|
||||||
stats: TankCharacteristics;
|
|
||||||
noRanking?: boolean;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
value: keyof TankCharacteristics;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
value: (tank: TankCharacteristics) => number | undefined;
|
|
||||||
name: ReactNode;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [
|
|
||||||
tankDefinitions,
|
|
||||||
provisionDefinitions,
|
|
||||||
equipmentDefinitions,
|
|
||||||
modelDefinitions,
|
|
||||||
skillDefinitions,
|
|
||||||
] = await Promise.all([
|
|
||||||
api.tanks(),
|
|
||||||
api.provisions(),
|
|
||||||
api.equipment(),
|
|
||||||
api.models(),
|
|
||||||
api.skills(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|
||||||
({ stats, indent, noRanking, deltaType, ...props }) => {
|
|
||||||
const { strings } = useLocale();
|
|
||||||
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
|
||||||
const uhWhatDoICallThisVariable =
|
|
||||||
typeof props.value === "function"
|
|
||||||
? props.value(stats)!
|
|
||||||
: (stats[props.value] as number);
|
|
||||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const shellIndex = Duel.use((state) =>
|
|
||||||
state.protagonist.gun.shells.indexOf(state.protagonist.shell),
|
|
||||||
);
|
|
||||||
const equipmentMatrix = Duel.use(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
const equalize = Duel.use((state) => state.equalize);
|
|
||||||
const others = useMemo(() => {
|
|
||||||
const defaultSkills = createDefaultSkills(skillDefinitions);
|
|
||||||
|
|
||||||
return Object.values(tankDefinitions.tanks)
|
|
||||||
.filter(
|
|
||||||
(tank) =>
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Class &&
|
|
||||||
tank.tier === protagonistTank.tier &&
|
|
||||||
tank.class === protagonistTank.class) ||
|
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
|
||||||
tank.tier === protagonistTank.tier) ||
|
|
||||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
|
||||||
)
|
|
||||||
.map((tank) => {
|
|
||||||
const member = tankToDuelMember(tank, provisionDefinitions);
|
|
||||||
|
|
||||||
if (member.gun.shells[shellIndex] === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tankCharacteristics(
|
|
||||||
{
|
|
||||||
applyDynamicArmor: false,
|
|
||||||
applyReactiveArmor: false,
|
|
||||||
applySpallLiner: false,
|
|
||||||
camouflage: member.camouflage,
|
|
||||||
consumables: member.consumables,
|
|
||||||
crewSkills: defaultSkills,
|
|
||||||
provisions: member.provisions,
|
|
||||||
engine: member.engine,
|
|
||||||
gun: member.gun,
|
|
||||||
equipmentMatrix: equipmentMatrix,
|
|
||||||
shell: member.gun.shells[shellIndex],
|
|
||||||
stockEngine: tank.engines[0],
|
|
||||||
stockGun: tank.turrets[0].guns[0],
|
|
||||||
stockTrack: tank.tracks[0],
|
|
||||||
stockTurret: tank.turrets[0],
|
|
||||||
tank,
|
|
||||||
track: member.track,
|
|
||||||
turret: member.turret,
|
|
||||||
assaultDistance: member.assaultDistance,
|
|
||||||
equalize,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
equipmentDefinitions,
|
|
||||||
provisionDefinitions,
|
|
||||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter((tank) => {
|
|
||||||
if (tank === undefined) return false;
|
|
||||||
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === "function"
|
|
||||||
? props.value(tank)!
|
|
||||||
: (tank[props.value] as number);
|
|
||||||
|
|
||||||
return othersValue !== undefined;
|
|
||||||
}) as TankCharacteristics[];
|
|
||||||
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
|
|
||||||
const betterTanks = others.filter((tank) => {
|
|
||||||
const othersValue =
|
|
||||||
typeof props.value === "function"
|
|
||||||
? props.value(tank)!
|
|
||||||
: (tank[props.value] as number);
|
|
||||||
|
|
||||||
if (othersValue === undefined) return false;
|
|
||||||
return deltaType === "lowerIsBetter"
|
|
||||||
? othersValue < uhWhatDoICallThisVariable
|
|
||||||
: othersValue > uhWhatDoICallThisVariable;
|
|
||||||
});
|
|
||||||
const goodness = (others.length - betterTanks.length) / others.length;
|
|
||||||
let color: ComponentProps<typeof Progress>["color"];
|
|
||||||
|
|
||||||
if (goodness <= 0.25) color = "red";
|
|
||||||
else if (goodness <= 0.5) color = "orange";
|
|
||||||
else if (goodness <= 0.75) color = "yellow";
|
|
||||||
else color = "green";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column">
|
|
||||||
<Info
|
|
||||||
deltaType={deltaType}
|
|
||||||
indent={indent}
|
|
||||||
name={
|
|
||||||
"name" in props
|
|
||||||
? props.name
|
|
||||||
: strings.website.tools.tankopedia.characteristics.values[
|
|
||||||
props.value
|
|
||||||
]
|
|
||||||
}
|
|
||||||
delta={delta}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{uhWhatDoICallThisVariable}
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
{!noRanking && (
|
|
||||||
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
|
||||||
<Progress
|
|
||||||
variant="soft"
|
|
||||||
size="1"
|
|
||||||
value={goodness * 100}
|
|
||||||
color={color}
|
|
||||||
style={{ height: "0.125rem", opacity: 0.5 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
|
||||||
{others.length}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
(a, b) =>
|
|
||||||
(typeof a.value === "function" ? a.value(a.stats) : a.stats[a.value]) ===
|
|
||||||
(typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value]),
|
|
||||||
);
|
|
||||||
+1
-1
@@ -60,7 +60,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
opacity: 2 ** -3,
|
||||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
backgroundImage: `url(/media/random/armor-hash.png)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
.characteristics-wrapper {
|
.characteristics-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: var(--space-4);
|
||||||
margin: var(--space-4);
|
margin: var(--space-4);
|
||||||
|
|
||||||
--base-cell-width: 20rem;
|
--base-cell-width: 20rem;
|
||||||
--group-span: 7;
|
--group-span: 7;
|
||||||
|
|
||||||
container-type: inline-size;
|
.compare-options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.characteristics {
|
.characteristics {
|
||||||
--N: round(down, 100cqw / var(--base-cell-width));
|
--N: round(down, 100cqw / var(--base-cell-width));
|
||||||
--cell-width: calc(100cqw / var(--N));
|
--cell-width: calc(100cqw / var(--N));
|
||||||
--cell-height: calc(var(--cell-width) / var(--group-span));
|
--cell-height: calc(var(--cell-width) / var(--group-span));
|
||||||
|
|
||||||
|
margin-bottom: calc(-1 * var(--cell-height));
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
auto-fill,
|
auto-fill,
|
||||||
@@ -124,10 +135,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
background-color: var(--gray-2);
|
background-color: var(--gray-2);
|
||||||
outline: var(--gray-5) solid var(--space-1);
|
outline: var(--gray-5) solid var(--space-1);
|
||||||
border-radius: var(--space-3);
|
border-radius: var(--space-3);
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
|
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
|
import { useRef } from "react";
|
||||||
import { characteristicsGroups } from "../../config/characteristicsGroups";
|
import { characteristicsGroups } from "../../config/characteristicsGroups";
|
||||||
|
import { toys } from "../../config/toys";
|
||||||
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
|
import { useCharacteristicRenderer } from "../../hooks/useCharacteristicRenderer";
|
||||||
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
||||||
|
import { useRankings } from "../../hooks/useRankings";
|
||||||
import { useStrings } from "../../hooks/useStrings";
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
|
import { Tankopedia, TankopediaRelativeAgainst } from "../../stores/tankopedia";
|
||||||
|
import type { CharacteristicRank } from "../../tankopedia/calculateRankings";
|
||||||
import type {
|
import type {
|
||||||
|
CharacteristicName,
|
||||||
CharacteristicRenderConfig,
|
CharacteristicRenderConfig,
|
||||||
CharacteristicReturnType,
|
CharacteristicReturnType,
|
||||||
CharacteristicsGroup,
|
CharacteristicsGroup,
|
||||||
ComputedCharacteristics,
|
ComputedCharacteristics,
|
||||||
|
ToyName,
|
||||||
} from "../../types/characteristics";
|
} from "../../types/characteristics";
|
||||||
import { Heading } from "../Heading";
|
import { Heading } from "../Heading";
|
||||||
|
import { SegmentedControl } from "../SegmentedControl";
|
||||||
import { Text } from "../Text";
|
import { Text } from "../Text";
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
@@ -18,8 +29,12 @@ interface TankopediaCharacteristicsProps {
|
|||||||
export function TankopediaCharacteristics({
|
export function TankopediaCharacteristics({
|
||||||
characteristics,
|
characteristics,
|
||||||
}: TankopediaCharacteristicsProps) {
|
}: TankopediaCharacteristicsProps) {
|
||||||
|
const rankings = useRankings(characteristics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["characteristics-wrapper"]}>
|
<div className={styles["characteristics-wrapper"]}>
|
||||||
|
<CompareOptions />
|
||||||
|
|
||||||
<div className={styles.characteristics}>
|
<div className={styles.characteristics}>
|
||||||
{characteristicsGroups.map((group) => {
|
{characteristicsGroups.map((group) => {
|
||||||
const items = group.order.filter(
|
const items = group.order.filter(
|
||||||
@@ -34,6 +49,7 @@ export function TankopediaCharacteristics({
|
|||||||
key={group.name}
|
key={group.name}
|
||||||
group={group}
|
group={group}
|
||||||
characteristics={characteristics}
|
characteristics={characteristics}
|
||||||
|
rankings={rankings}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -45,9 +61,10 @@ export function TankopediaCharacteristics({
|
|||||||
interface GroupProps {
|
interface GroupProps {
|
||||||
group: CharacteristicsGroup;
|
group: CharacteristicsGroup;
|
||||||
characteristics: ComputedCharacteristics;
|
characteristics: ComputedCharacteristics;
|
||||||
|
rankings: Map<CharacteristicName, CharacteristicRank>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Group({ group, characteristics }: GroupProps) {
|
function Group({ group, characteristics, rankings }: GroupProps) {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const toys = group.order.filter((item) => "toy" in item).length;
|
const toys = group.order.filter((item) => "toy" in item).length;
|
||||||
const characteristicsCount = group.order.filter(
|
const characteristicsCount = group.order.filter(
|
||||||
@@ -92,10 +109,16 @@ function Group({ group, characteristics }: GroupProps) {
|
|||||||
>
|
>
|
||||||
{group.order.map((item) => (
|
{group.order.map((item) => (
|
||||||
<>
|
<>
|
||||||
{"toy" in item && <Toy {...item} />}
|
{"toy" in item && (
|
||||||
|
<ToyAssigner characteristics={characteristics} {...item} />
|
||||||
|
)}
|
||||||
{"label" in item && <Label {...item} />}
|
{"label" in item && <Label {...item} />}
|
||||||
{"name" in item && characteristics.has(item.name) && (
|
{"name" in item && characteristics.has(item.name) && (
|
||||||
<Item config={item} value={characteristics.get(item.name)!} />
|
<Item
|
||||||
|
config={item}
|
||||||
|
value={characteristics.get(item.name)!}
|
||||||
|
ranking={rankings.get(item.name)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
@@ -125,9 +148,10 @@ function Label({ label }: LabelProps) {
|
|||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
config: CharacteristicRenderConfig;
|
config: CharacteristicRenderConfig;
|
||||||
value: CharacteristicReturnType;
|
value: CharacteristicReturnType;
|
||||||
|
ranking?: CharacteristicRank;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item({ config, value }: ItemProps) {
|
function Item({ config, value, ranking }: ItemProps) {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const renderCharacteristic = useCharacteristicRenderer();
|
const renderCharacteristic = useCharacteristicRenderer();
|
||||||
const hasUnits = config.units !== undefined;
|
const hasUnits = config.units !== undefined;
|
||||||
@@ -150,17 +174,64 @@ function Item({ config, value }: ItemProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{ranking && (
|
||||||
<div className={styles.ranking}>
|
<div className={styles.ranking}>
|
||||||
<Bar value={Math.random()} />
|
<Bar value={ranking.score} />
|
||||||
|
|
||||||
<Text lowContrast size="minor" className={styles.number}>
|
<Text lowContrast size="minor" className={styles.number}>
|
||||||
1 / 132
|
{literals(strings.units.fraction, {
|
||||||
|
numerator: ranking.rank,
|
||||||
|
denominator: ranking.total,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CompareOptions() {
|
||||||
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
|
const relativeAgainst = Tankopedia.use((state) => state.relative_against);
|
||||||
|
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
const options = useRef([
|
||||||
|
{
|
||||||
|
value: TankopediaRelativeAgainst.ClassAndTier,
|
||||||
|
label: literals(strings.tanks.characteristics.compare.class, {
|
||||||
|
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
||||||
|
class: strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TankopediaRelativeAgainst.Tier,
|
||||||
|
label: literals(strings.tanks.characteristics.compare.tier, {
|
||||||
|
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: TankopediaRelativeAgainst.All,
|
||||||
|
label: strings.tanks.characteristics.compare.all,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["compare-options"]}>
|
||||||
|
<Text lowContrast>{strings.tanks.characteristics.compare.label}</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
options={options.current}
|
||||||
|
value={relativeAgainst}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.relative_against = value;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface BarProps {
|
interface BarProps {
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
@@ -190,15 +261,22 @@ function Bar({ value }: BarProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ToyProps {
|
interface ToyAssignerProps {
|
||||||
toy: string;
|
toy: ToyName;
|
||||||
|
characteristics: ComputedCharacteristics;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Toy({ toy }: ToyProps) {
|
function ToyAssigner({ toy, characteristics }: ToyAssignerProps) {
|
||||||
|
const Toy = toys[toy];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.toy}>
|
<div className={styles.toy}>
|
||||||
<div className={styles.sandbox}>
|
<div className={styles.sandbox}>
|
||||||
<Text lowContrast>{toy} toy</Text>
|
{Toy ? (
|
||||||
|
<Toy characteristics={characteristics} />
|
||||||
|
) : (
|
||||||
|
<Text lowContrast>{toy}</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
File renamed without changes.
+1
-1
@@ -15,7 +15,7 @@ const [gameDefinitions, consumableDefinitions, provisionDefinitions, tanks] =
|
|||||||
api.tanks(),
|
api.tanks(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function GameModeSection() {
|
export function TankopediaGameModeAbilities() {
|
||||||
const unwrap = useUnwrapper();
|
const unwrap = useUnwrapper();
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const tank = useDuel("protagonist", DuelModule.Tank);
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shells {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useStrings } from "../../hooks/useStrings";
|
|||||||
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
|
||||||
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
import { useUnwrapper } from "../../hooks/useUnwrapper";
|
||||||
import { Tankopedia } from "../../stores/tankopedia";
|
import { Tankopedia } from "../../stores/tankopedia";
|
||||||
import { hasUpgrades } from "../../tankopedia/hasUpgrades";
|
|
||||||
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
import { vehicleStatusKeys } from "../../tankopedia/tankState";
|
||||||
import type { ComputedCharacteristics } from "../../types/characteristics";
|
import type { ComputedCharacteristics } from "../../types/characteristics";
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
@@ -31,11 +30,12 @@ interface TankopediaLoadoutProps {
|
|||||||
|
|
||||||
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
|
||||||
const tank = useDuel("protagonist", DuelModule.Tank);
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
const showModules = hasUpgrades(tank);
|
// const showModules = hasUpgrades(tank);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* {showModules && <Modules />} */}
|
{/* {showModules && <Modules />} */}
|
||||||
|
<Shells />
|
||||||
<Equipment />
|
<Equipment />
|
||||||
<Provisions />
|
<Provisions />
|
||||||
<Consumables characteristics={characteristics} />
|
<Consumables characteristics={characteristics} />
|
||||||
@@ -105,7 +105,6 @@ function Status() {
|
|||||||
key={key}
|
key={key}
|
||||||
radius="1"
|
radius="1"
|
||||||
className={styles.state}
|
className={styles.state}
|
||||||
data-selected={status[key]}
|
|
||||||
color={status[key] ? "amber" : "gray"}
|
color={status[key] ? "amber" : "gray"}
|
||||||
variant={status[key] ? "surface" : "soft"}
|
variant={status[key] ? "surface" : "soft"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -125,6 +124,52 @@ function Status() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Shells() {
|
||||||
|
const strings = useStrings();
|
||||||
|
const unwrap = useUnwrapper();
|
||||||
|
|
||||||
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
||||||
|
const shell = useDuel("protagonist", DuelModule.Shell);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Heading size="4">{strings.tanks.loadout.shells}</Heading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.shells}>
|
||||||
|
{gun.shells.map((candidate) => {
|
||||||
|
const isSelected = candidate.id === shell.id;
|
||||||
|
const name = unwrap(candidate.name!);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={candidate.id} tooltip={name}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={name}
|
||||||
|
size="major"
|
||||||
|
radius="1"
|
||||||
|
color={isSelected ? undefined : "gray"}
|
||||||
|
variant={isSelected ? "surface" : "soft"}
|
||||||
|
parentListChild
|
||||||
|
onClick={() => {
|
||||||
|
Tankopedia.mutate((draft) => {
|
||||||
|
draft.protagonist.shell = candidate.id;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={name}
|
||||||
|
src={alias("api", `/icons/shells/${candidate.icon}.webp`)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Provisions() {
|
function Provisions() {
|
||||||
const strings = useStrings();
|
const strings = useStrings();
|
||||||
const tank = useDuel("protagonist", DuelModule.Tank);
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
@@ -447,7 +492,6 @@ function EquipmentOption({
|
|||||||
size="major"
|
size="major"
|
||||||
color={isSelected ? undefined : "gray"}
|
color={isSelected ? undefined : "gray"}
|
||||||
variant={isSelected ? "surface" : "soft"}
|
variant={isSelected ? "surface" : "soft"}
|
||||||
data-selected={isSelected}
|
|
||||||
radius="1"
|
radius="1"
|
||||||
parentListChild
|
parentListChild
|
||||||
className={styles.slot}
|
className={styles.slot}
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
.visualizer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: none;
|
||||||
|
border-radius: inherit;
|
||||||
|
background-color: var(--gray-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.armor {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 33%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to right, transparent, var(--gray-a4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.armor-texture {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.125;
|
||||||
|
background-image: url("/media/random/armor-hash.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.zones {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 33%;
|
||||||
|
width: 67%;
|
||||||
|
height: 100%;
|
||||||
|
background: conic-gradient(
|
||||||
|
at 0 50%,
|
||||||
|
var(--ruby-a2),
|
||||||
|
var(--ruby-a1) calc(90deg - var(--ricochet-angle)),
|
||||||
|
var(--jade-a2) calc(90deg - var(--ricochet-angle)),
|
||||||
|
var(--jade-a4) calc(90deg - var(--normalization-angle)),
|
||||||
|
var(--cyan-a5) calc(90deg - var(--normalization-angle)),
|
||||||
|
var(--cyan-a3) calc(90deg + var(--normalization-angle)),
|
||||||
|
var(--jade-a2) calc(90deg + var(--normalization-angle)),
|
||||||
|
var(--jade-a4) calc(90deg + var(--ricochet-angle)),
|
||||||
|
var(--ruby-a1) calc(90deg + var(--ricochet-angle)),
|
||||||
|
var(--ruby-a2) 180deg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-path,
|
||||||
|
.effective-path {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 33%;
|
||||||
|
transform: translateX(-50%) rotate(var(--rotation-angle));
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-path {
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-path-extension {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 100%;
|
||||||
|
width: var(--space-0);
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--gray-a8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-path-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
width: var(--space-2);
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(var(--gray-a11), var(--gray-a4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
position: absolute;
|
||||||
|
top: 6rem;
|
||||||
|
left: 0;
|
||||||
|
width: var(--space-5);
|
||||||
|
height: var(--space-5);
|
||||||
|
object-fit: contain;
|
||||||
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
|
filter: drop-shadow(0 0 var(--space-4) black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effective-path {
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effective-path-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: -100%;
|
||||||
|
width: var(--space-2);
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(var(--gray-a4), var(--gray-a11));
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-stat {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--gray-11);
|
||||||
|
font-size: var(--font-size-minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-stat-value {
|
||||||
|
color: var(--gray-12);
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-stat[data-position="top-left"] {
|
||||||
|
top: var(--space-3);
|
||||||
|
left: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-stat[data-position="top-right"] {
|
||||||
|
top: var(--space-3);
|
||||||
|
right: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-stat[data-position="bottom-left"] {
|
||||||
|
bottom: var(--space-3);
|
||||||
|
left: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-stat[data-position="bottom-right"] {
|
||||||
|
right: var(--space-3);
|
||||||
|
bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explosive-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-3);
|
||||||
|
color: var(--gray-11);
|
||||||
|
text-align: center;
|
||||||
|
background: radial-gradient(var(--black-a7), var(--black-a11));
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { alias, isExplosive } from "@blitzkit/core";
|
||||||
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import {
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type CSSProperties,
|
||||||
|
type PointerEvent,
|
||||||
|
} from "react";
|
||||||
|
import { clamp, degToRad, radToDeg } from "three/src/math/MathUtils.js";
|
||||||
|
import type { ToyProps } from "../../config/toys";
|
||||||
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
||||||
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
|
import { Code } from "../Code";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
export function RicochetToy({ characteristics }: ToyProps) {
|
||||||
|
const strings = useStrings();
|
||||||
|
const shell = useDuel("protagonist", DuelModule.Shell);
|
||||||
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
const normalizationValue = characteristics.get("normalization");
|
||||||
|
const ricochetValue = characteristics.get("ricochet");
|
||||||
|
const normalization = degToRad(
|
||||||
|
typeof normalizationValue === "number" ? normalizationValue : 0,
|
||||||
|
);
|
||||||
|
const ricochet = degToRad(
|
||||||
|
typeof ricochetValue === "number" ? ricochetValue : 90,
|
||||||
|
);
|
||||||
|
const explodes = isExplosive(shell.type);
|
||||||
|
const [rawAngle, setAngle] = useState(degToRad(-25));
|
||||||
|
const angle = explodes ? degToRad(-70) : rawAngle;
|
||||||
|
const doesRicochet = Math.abs(angle) >= ricochet;
|
||||||
|
const effectiveAngle = doesRicochet
|
||||||
|
? -angle + Math.PI
|
||||||
|
: Math.sign(angle) * Math.max(0, Math.abs(angle) - normalization);
|
||||||
|
const armorPercentage = Math.min(999, 100 / Math.cos(effectiveAngle));
|
||||||
|
|
||||||
|
function updateAngle(event: PointerEvent<HTMLDivElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!container.current || explodes) return;
|
||||||
|
|
||||||
|
const rect = container.current.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left - rect.width / 3;
|
||||||
|
const y = event.clientY - rect.top - rect.height / 2;
|
||||||
|
|
||||||
|
setAngle(clamp(Math.atan2(-y, x), -Math.PI / 2, Math.PI / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={container}
|
||||||
|
className={styles.visualizer}
|
||||||
|
onPointerMove={updateAngle}
|
||||||
|
>
|
||||||
|
<div className={styles.armor}>
|
||||||
|
<div className={styles["armor-texture"]} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.zones}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normalization-angle": `${normalization}rad`,
|
||||||
|
"--ricochet-angle": `${ricochet}rad`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["shell-path"]}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--rotation-angle": `${-Math.PI / 2 - angle}rad`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles["shell-path-extension"]} />
|
||||||
|
<div className={styles["shell-path-line"]} />
|
||||||
|
<img
|
||||||
|
className={styles.shell}
|
||||||
|
alt=""
|
||||||
|
src={alias("api", `/icons/shells/${shell.icon}.webp`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["effective-path"]}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--rotation-angle": `${-Math.PI / 2 - effectiveAngle}rad`,
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles["effective-path-line"]} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CornerStat
|
||||||
|
label={strings.tanks.characteristics.toys.ricochet.angle}
|
||||||
|
value={literals(strings.common.units.deg, {
|
||||||
|
value: radToDeg(Math.abs(angle)).toFixed(0),
|
||||||
|
})}
|
||||||
|
position={CornerStatPosition.TopLeft}
|
||||||
|
/>
|
||||||
|
<CornerStat
|
||||||
|
label={strings.tanks.characteristics.toys.ricochet.normalization}
|
||||||
|
value={
|
||||||
|
doesRicochet
|
||||||
|
? "-"
|
||||||
|
: literals(strings.common.units.deg, {
|
||||||
|
value: radToDeg(
|
||||||
|
Math.min(normalization, Math.abs(angle)),
|
||||||
|
).toFixed(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
position={CornerStatPosition.TopRight}
|
||||||
|
/>
|
||||||
|
<CornerStat
|
||||||
|
label={strings.tanks.characteristics.toys.ricochet.effective}
|
||||||
|
value={
|
||||||
|
doesRicochet
|
||||||
|
? "-"
|
||||||
|
: literals(strings.common.units.deg, {
|
||||||
|
value: radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
position={CornerStatPosition.BottomLeft}
|
||||||
|
/>
|
||||||
|
<CornerStat
|
||||||
|
label={strings.tanks.characteristics.toys.ricochet.armor}
|
||||||
|
value={
|
||||||
|
doesRicochet
|
||||||
|
? "-"
|
||||||
|
: literals(strings.common.units.percentage, {
|
||||||
|
value: armorPercentage.toFixed(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
position={CornerStatPosition.BottomRight}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{explodes && (
|
||||||
|
<div className={styles["explosive-overlay"]}>
|
||||||
|
{strings.tanks.characteristics.toys.ricochet.no_ricochet}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CornerStatPosition {
|
||||||
|
TopLeft = "top-left",
|
||||||
|
TopRight = "top-right",
|
||||||
|
BottomLeft = "bottom-left",
|
||||||
|
BottomRight = "bottom-right",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CornerStatProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
position: CornerStatPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CornerStat({ label, value, position }: CornerStatProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles["corner-stat"]} data-position={position}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<Code className={styles["corner-stat-value"]} size="minor">
|
||||||
|
{value}
|
||||||
|
</Code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -661,13 +661,6 @@ export const characteristics = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
hull_traverse_speed: {
|
|
||||||
type: CharacteristicType.Number,
|
|
||||||
compute({ track, progressive }) {
|
|
||||||
return track.traverse_speed * progressive("driver");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
hull_traverse_speed_hard: {
|
hull_traverse_speed_hard: {
|
||||||
type: CharacteristicType.Number,
|
type: CharacteristicType.Number,
|
||||||
compute(context): number {
|
compute(context): number {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CaseType } from "@blitzkit/core";
|
import type { CaseType } from "@blitzkit/core";
|
||||||
import type { GunDefinition } from "@blitzkit/protos";
|
import type { GunDefinition } from "@blitzkit/protos";
|
||||||
|
import { CharacteristicRanking } from "../types/characteristics";
|
||||||
import type { CharacteristicsGroup } from "../types/characteristics";
|
import type { CharacteristicsGroup } from "../types/characteristics";
|
||||||
|
|
||||||
export const characteristicsGroups: CharacteristicsGroup[] = [
|
export const characteristicsGroups: CharacteristicsGroup[] = [
|
||||||
@@ -80,7 +81,12 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
{ name: "damage_range", decimals: 0, units: "hp", localize: true },
|
{ name: "damage_range", decimals: 0, units: "hp", localize: true },
|
||||||
{ name: "module_damage", decimals: 0, units: "hp", localize: true },
|
{ name: "module_damage", decimals: 0, units: "hp", localize: true },
|
||||||
{ name: "module_damage_range", decimals: 0, units: "hp", localize: true },
|
{ name: "module_damage_range", decimals: 0, units: "hp", localize: true },
|
||||||
{ name: "reload", decimals: 1, units: "s" },
|
{
|
||||||
|
name: "reload",
|
||||||
|
decimals: 1,
|
||||||
|
units: "s",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
{ name: "reloads", decimals: 1, units: "s" },
|
{ name: "reloads", decimals: 1, units: "s" },
|
||||||
|
|
||||||
{ toy: "reload" },
|
{ toy: "reload" },
|
||||||
@@ -94,7 +100,12 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
{ name: "clipping_potential", localize: true, units: "hp" },
|
{ name: "clipping_potential", localize: true, units: "hp" },
|
||||||
{ name: "clip_size" },
|
{ name: "clip_size" },
|
||||||
{ name: "optimal_shell" },
|
{ name: "optimal_shell" },
|
||||||
{ name: "intra_clip", decimals: 2, units: "s" },
|
{
|
||||||
|
name: "intra_clip",
|
||||||
|
decimals: 2,
|
||||||
|
units: "s",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
{ name: "burst_size" },
|
{ name: "burst_size" },
|
||||||
{ name: "intra_burst", decimals: 2, units: "s" },
|
{ name: "intra_burst", decimals: 2, units: "s" },
|
||||||
|
|
||||||
@@ -114,18 +125,28 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
name: "penetration_loss_by_distance",
|
name: "penetration_loss_by_distance",
|
||||||
units: "percentage",
|
units: "percentage",
|
||||||
indent: true,
|
indent: true,
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "penetration_loss_after_ricochet",
|
name: "penetration_loss_after_ricochet",
|
||||||
units: "percentage",
|
units: "percentage",
|
||||||
indent: true,
|
indent: true,
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
},
|
},
|
||||||
{ name: "shell_velocity", units: "m_s" },
|
{ name: "shell_velocity", units: "m_s" },
|
||||||
{ name: "shell_range", units: "m" },
|
{ name: "shell_range", units: "m" },
|
||||||
{ name: "shell_capacity" },
|
{ name: "shell_capacity" },
|
||||||
{ name: "caliber", units: "mm" },
|
{ name: "caliber", units: "mm" },
|
||||||
{ name: "ricochet", units: "deg" },
|
{
|
||||||
{ name: "normalization", units: "deg" },
|
name: "ricochet",
|
||||||
|
units: "deg",
|
||||||
|
ranking: CharacteristicRanking.None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normalization",
|
||||||
|
units: "deg",
|
||||||
|
ranking: CharacteristicRanking.None,
|
||||||
|
},
|
||||||
|
|
||||||
{ toy: "ricochet" },
|
{ toy: "ricochet" },
|
||||||
],
|
],
|
||||||
@@ -135,8 +156,18 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
name: "accuracy",
|
name: "accuracy",
|
||||||
// render: CharacteristicsGroupRender.Statistical,
|
// render: CharacteristicsGroupRender.Statistical,
|
||||||
order: [
|
order: [
|
||||||
{ name: "aim_time", decimals: 2, units: "s" },
|
{
|
||||||
{ name: "dispersion", decimals: 3, units: "m" },
|
name: "aim_time",
|
||||||
|
decimals: 2,
|
||||||
|
units: "s",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dispersion",
|
||||||
|
decimals: 3,
|
||||||
|
units: "m",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
{ name: "dispersion_angle", decimals: 3, units: "deg" },
|
{ name: "dispersion_angle", decimals: 3, units: "deg" },
|
||||||
{ toy: "aim_time" },
|
{ toy: "aim_time" },
|
||||||
],
|
],
|
||||||
@@ -167,7 +198,12 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
{ name: "speed_forward", units: "km_h", indent: true },
|
{ name: "speed_forward", units: "km_h", indent: true },
|
||||||
{ name: "speed_backward", units: "km_h", indent: true },
|
{ name: "speed_backward", units: "km_h", indent: true },
|
||||||
|
|
||||||
{ name: "weight", decimals: 1, units: "t" },
|
{
|
||||||
|
name: "weight",
|
||||||
|
decimals: 1,
|
||||||
|
units: "t",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
{ name: "engine_power", units: "bhp" },
|
{ name: "engine_power", units: "bhp" },
|
||||||
{ name: "power_to_weight", decimals: 1, units: "bhp_t" },
|
{ name: "power_to_weight", decimals: 1, units: "bhp_t" },
|
||||||
// { name: "brake_force", decimals: 1, units: "kN" },
|
// { name: "brake_force", decimals: 1, units: "kN" },
|
||||||
@@ -185,9 +221,24 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
{ name: "soft_terrain_coefficient", decimals: 0, indent: true },
|
{ name: "soft_terrain_coefficient", decimals: 0, indent: true },
|
||||||
|
|
||||||
{ label: "raw_terrain_coefficient" },
|
{ label: "raw_terrain_coefficient" },
|
||||||
{ name: "hard_terrain_coefficient_raw", decimals: 2, indent: true },
|
{
|
||||||
{ name: "medium_terrain_coefficient_raw", decimals: 2, indent: true },
|
name: "hard_terrain_coefficient_raw",
|
||||||
{ name: "soft_terrain_coefficient_raw", decimals: 2, indent: true },
|
decimals: 2,
|
||||||
|
indent: true,
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "medium_terrain_coefficient_raw",
|
||||||
|
decimals: 2,
|
||||||
|
indent: true,
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "soft_terrain_coefficient_raw",
|
||||||
|
decimals: 2,
|
||||||
|
indent: true,
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
|
||||||
{ toy: "traverse" },
|
{ toy: "traverse" },
|
||||||
|
|
||||||
@@ -202,7 +253,11 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
// render: CharacteristicsGroupRender.Statistical,
|
// render: CharacteristicsGroupRender.Statistical,
|
||||||
order: [
|
order: [
|
||||||
{ name: "health", units: "hp" },
|
{ name: "health", units: "hp" },
|
||||||
{ name: "fire_chance", units: "percentage" },
|
{
|
||||||
|
name: "fire_chance",
|
||||||
|
units: "percentage",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
// { name: "fire_rate", units: "hp_s" },
|
// { name: "fire_rate", units: "hp_s" },
|
||||||
// { name: "ramming_resistance", units: "percentage" },
|
// { name: "ramming_resistance", units: "percentage" },
|
||||||
|
|
||||||
@@ -221,9 +276,21 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
|
|
||||||
{ toy: "view_range" },
|
{ toy: "view_range" },
|
||||||
|
|
||||||
{ name: "width", units: "m" },
|
{
|
||||||
{ name: "height", units: "m" },
|
name: "width",
|
||||||
{ name: "length", units: "m" },
|
units: "m",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "height",
|
||||||
|
units: "m",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "length",
|
||||||
|
units: "m",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { RicochetToy } from "../components/TankopediaToys";
|
||||||
|
import type { ComputedCharacteristics } from "../types/characteristics";
|
||||||
|
|
||||||
|
export interface ToyProps {
|
||||||
|
characteristics: ComputedCharacteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toys: Record<string, (props: ToyProps) => ReactNode> = {
|
||||||
|
ricochet: RicochetToy,
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { api } from "../api/dynamic";
|
||||||
|
import { Tankopedia, TankopediaRelativeAgainst } from "../stores/tankopedia";
|
||||||
|
import { createTankState } from "../tankopedia/createTankState";
|
||||||
|
import { computeCharacteristics } from "./useCharacteristics";
|
||||||
|
import { DuelModule, useDuel } from "./useDuel";
|
||||||
|
|
||||||
|
const tankDefinitions = await api.tanks();
|
||||||
|
|
||||||
|
const candidateStates = new Map(
|
||||||
|
Object.values(tankDefinitions.tanks).map((candidate) => {
|
||||||
|
const state = createTankState(candidate);
|
||||||
|
const turret = candidate.turrets.find((t) => t.id === state.turret)!;
|
||||||
|
const gun = turret.guns.find((g) => g.id === state.gun)!;
|
||||||
|
|
||||||
|
return [
|
||||||
|
candidate.id,
|
||||||
|
{
|
||||||
|
state,
|
||||||
|
shells: gun.shells.map((shell) => shell.id),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useComparisonCharacteristics() {
|
||||||
|
const environment = Tankopedia.use((state) => state.environment);
|
||||||
|
|
||||||
|
const tank = useDuel("protagonist", DuelModule.Tank);
|
||||||
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
||||||
|
const shell = useDuel("protagonist", DuelModule.Shell);
|
||||||
|
const protagonistShellIndex = gun.shells.findIndex((s) => s.id === shell.id);
|
||||||
|
|
||||||
|
const relativeAgainst = Tankopedia.use((state) => state.relative_against);
|
||||||
|
|
||||||
|
const candidates = useMemo(() => {
|
||||||
|
return Object.values(tankDefinitions.tanks).filter((candidate) => {
|
||||||
|
switch (relativeAgainst) {
|
||||||
|
case TankopediaRelativeAgainst.ClassAndTier:
|
||||||
|
return candidate.tier === tank.tier && candidate.class === tank.class;
|
||||||
|
|
||||||
|
case TankopediaRelativeAgainst.Tier:
|
||||||
|
return candidate.tier === tank.tier;
|
||||||
|
|
||||||
|
case TankopediaRelativeAgainst.All:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [tank, relativeAgainst]);
|
||||||
|
|
||||||
|
const characteristics = useMemo(() => {
|
||||||
|
return candidates.flatMap((candidate) => {
|
||||||
|
const { state: candidateState, shells } = candidateStates.get(
|
||||||
|
candidate.id,
|
||||||
|
)!;
|
||||||
|
const maxStateShellIndex = shells.length - 1;
|
||||||
|
const stateShellIndex = Math.min(
|
||||||
|
maxStateShellIndex,
|
||||||
|
protagonistShellIndex,
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = { ...candidateState, shell: shells[stateShellIndex] };
|
||||||
|
|
||||||
|
return [
|
||||||
|
computeCharacteristics(
|
||||||
|
{ protagonist: state, antagonist: state },
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}, [candidates, environment, protagonistShellIndex]);
|
||||||
|
|
||||||
|
return characteristics;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
calculateRankings,
|
||||||
|
type CharacteristicRank,
|
||||||
|
} from "../tankopedia/calculateRankings";
|
||||||
|
import type {
|
||||||
|
CharacteristicName,
|
||||||
|
ComputedCharacteristics,
|
||||||
|
} from "../types/characteristics";
|
||||||
|
import { useComparisonCharacteristics } from "./useComparisonCharacteristics";
|
||||||
|
|
||||||
|
export function useRankings(
|
||||||
|
characteristics: ComputedCharacteristics,
|
||||||
|
): Map<CharacteristicName, CharacteristicRank> {
|
||||||
|
const comparisonCharacteristics = useComparisonCharacteristics();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => calculateRankings(characteristics, comparisonCharacteristics),
|
||||||
|
[characteristics, comparisonCharacteristics],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ import { useMemo } from "react";
|
|||||||
import { api } from "../../../../../api/dynamic";
|
import { api } from "../../../../../api/dynamic";
|
||||||
import type { ThicknessRange } from "../../../../../components/StaticArmor";
|
import type { ThicknessRange } from "../../../../../components/StaticArmor";
|
||||||
import { TankopediaCharacteristics } from "../../../../../components/TankopediaCharacteristics";
|
import { TankopediaCharacteristics } from "../../../../../components/TankopediaCharacteristics";
|
||||||
|
import { TankopediaGameModeAbilities } from "../../../../../components/TankopediaGameModeAbilities";
|
||||||
import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout";
|
import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout";
|
||||||
import { GameModeSection } from "../../../../../components/GameModeSection";
|
|
||||||
import { defaultEqualizer } from "../../../../../config/equalizer";
|
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";
|
||||||
@@ -59,7 +59,7 @@ export const Page = withErrorWrapper(
|
|||||||
<div className={styles.sandbox}>
|
<div className={styles.sandbox}>
|
||||||
{/* {!skeleton && <TankopediaSandbox thicknessRange={thicknessRange} />} */}
|
{/* {!skeleton && <TankopediaSandbox thicknessRange={thicknessRange} />} */}
|
||||||
<TankopediaCharacteristics characteristics={characteristics} />
|
<TankopediaCharacteristics characteristics={characteristics} />
|
||||||
<GameModeSection />
|
<TankopediaGameModeAbilities />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ const { id, locale } = Astro.props;
|
|||||||
|
|
||||||
<BaseLayout {locale} accent="violet">
|
<BaseLayout {locale} accent="violet">
|
||||||
<ColorScale color="amber" alpha />
|
<ColorScale color="amber" alpha />
|
||||||
|
|
||||||
<ColorScale color="red" />
|
<ColorScale color="red" />
|
||||||
|
<ColorScale color="ruby" alpha />
|
||||||
<ColorScale color="orange" />
|
<ColorScale color="orange" />
|
||||||
<ColorScale color="amber" />
|
<ColorScale color="amber" />
|
||||||
<ColorScale color="jade" />
|
<ColorScale color="jade" />
|
||||||
|
<ColorScale color="jade" alpha />
|
||||||
|
<ColorScale color="cyan" alpha />
|
||||||
|
|
||||||
<Page {id} {locale} client:only="react">
|
<Page {id} {locale} client:only="react">
|
||||||
<Page slot="fallback" {id} {locale} skeleton />
|
<Page slot="fallback" {id} {locale} skeleton />
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export type ArmorPiercingLayer =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum TankopediaRelativeAgainst {
|
export enum TankopediaRelativeAgainst {
|
||||||
Class,
|
ClassAndTier,
|
||||||
Tier,
|
Tier,
|
||||||
All,
|
All,
|
||||||
}
|
}
|
||||||
@@ -90,9 +90,10 @@ interface Tankopedia {
|
|||||||
antagonist: TankState;
|
antagonist: TankState;
|
||||||
environment: TankEnvironment;
|
environment: TankEnvironment;
|
||||||
|
|
||||||
|
relative_against: TankopediaRelativeAgainst;
|
||||||
|
|
||||||
modelRequested: boolean;
|
modelRequested: boolean;
|
||||||
shot?: Shot;
|
shot?: Shot;
|
||||||
relativeAgainst: TankopediaRelativeAgainst;
|
|
||||||
editStatic: boolean;
|
editStatic: boolean;
|
||||||
highlightArmor?: {
|
highlightArmor?: {
|
||||||
editingPlate: boolean;
|
editingPlate: boolean;
|
||||||
@@ -133,9 +134,10 @@ export const Tankopedia = new Soapstone<Tankopedia, [TankDefinition]>(
|
|||||||
skills: createDefaultSkills(skills),
|
skills: createDefaultSkills(skills),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
relative_against: TankopediaRelativeAgainst.ClassAndTier,
|
||||||
|
|
||||||
modelRequested: false,
|
modelRequested: false,
|
||||||
|
|
||||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
|
||||||
editStatic: false,
|
editStatic: false,
|
||||||
xpMultiplier: 1,
|
xpMultiplier: 1,
|
||||||
requestedDisplay: TankopediaDisplay.Model,
|
requestedDisplay: TankopediaDisplay.Model,
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { characteristicsGroups } from "../config/characteristicsGroups";
|
||||||
|
import {
|
||||||
|
CharacteristicRanking as CharacteristicRankingDirection,
|
||||||
|
type CharacteristicName,
|
||||||
|
type CharacteristicRenderConfig,
|
||||||
|
type ComputedCharacteristics,
|
||||||
|
} from "../types/characteristics";
|
||||||
|
|
||||||
|
const characteristicConfigs = new Map<
|
||||||
|
CharacteristicName,
|
||||||
|
CharacteristicRenderConfig
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const { order } of characteristicsGroups) {
|
||||||
|
for (const item of order) {
|
||||||
|
if (!("name" in item)) continue;
|
||||||
|
characteristicConfigs.set(item.name, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacteristicRank {
|
||||||
|
rank: number;
|
||||||
|
total: number;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateRankings(
|
||||||
|
characteristics: ComputedCharacteristics,
|
||||||
|
comparisons: ComputedCharacteristics[],
|
||||||
|
) {
|
||||||
|
const rankings = new Map<CharacteristicName, CharacteristicRank>();
|
||||||
|
|
||||||
|
for (const [name, value] of characteristics) {
|
||||||
|
if (typeof value !== "number" || name[0] === "_") continue;
|
||||||
|
|
||||||
|
const config = characteristicConfigs.get(name)!;
|
||||||
|
|
||||||
|
if (config.ranking === CharacteristicRankingDirection.None) continue;
|
||||||
|
|
||||||
|
const lowerIsBetter =
|
||||||
|
config.ranking === CharacteristicRankingDirection.LowerIsBetter;
|
||||||
|
|
||||||
|
const values = comparisons
|
||||||
|
.map((comparison) => comparison.get(name))
|
||||||
|
.filter((other) => typeof other === "number");
|
||||||
|
|
||||||
|
if (values.length === 0) continue;
|
||||||
|
|
||||||
|
const betterCount = values.filter((other) =>
|
||||||
|
lowerIsBetter ? other < value : other > value,
|
||||||
|
).length;
|
||||||
|
|
||||||
|
rankings.set(name, {
|
||||||
|
rank: Math.min(betterCount + 1, values.length),
|
||||||
|
total: values.length,
|
||||||
|
score: (values.length - betterCount) / values.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rankings;
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ import type {
|
|||||||
TurretDefinition,
|
TurretDefinition,
|
||||||
} from "@blitzkit/protos";
|
} from "@blitzkit/protos";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { characteristics } from "../config/characteristics";
|
import type { characteristics } from "../config/characteristics";
|
||||||
|
import type { toys } from "../config/toys";
|
||||||
import type { DuelSide } from "../hooks/useEquipment";
|
import type { DuelSide } from "../hooks/useEquipment";
|
||||||
import type { TankEnvironment } from "../stores/tankopedia";
|
import type { TankEnvironment } from "../stores/tankopedia";
|
||||||
import type { TankState } from "../tankopedia/tankState";
|
import type { TankState } from "../tankopedia/tankState";
|
||||||
@@ -64,6 +65,12 @@ export enum CharacteristicType {
|
|||||||
Number,
|
Number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CharacteristicRanking {
|
||||||
|
HigherIsBetter,
|
||||||
|
LowerIsBetter,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
export type CharacteristicName = keyof typeof characteristics;
|
export type CharacteristicName = keyof typeof characteristics;
|
||||||
export type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
|
export type CharacteristicReturnType = ReturnType<Characteristic["compute"]>;
|
||||||
|
|
||||||
@@ -79,6 +86,7 @@ export interface CharacteristicRenderConfig {
|
|||||||
units?: keyof Strings["units"];
|
units?: keyof Strings["units"];
|
||||||
localize?: boolean;
|
localize?: boolean;
|
||||||
indent?: boolean;
|
indent?: boolean;
|
||||||
|
ranking?: CharacteristicRanking;
|
||||||
strings?: string;
|
strings?: string;
|
||||||
|
|
||||||
render?: (data: {
|
render?: (data: {
|
||||||
@@ -92,9 +100,11 @@ export interface ToyRenderConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LabelRenderConfig {
|
export interface LabelRenderConfig {
|
||||||
label: string;
|
label: ToyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ToyName = keyof typeof toys;
|
||||||
|
|
||||||
export type CharacteristicsGroup = {
|
export type CharacteristicsGroup = {
|
||||||
name: string;
|
name: string;
|
||||||
order: (CharacteristicRenderConfig | LabelRenderConfig | ToyRenderConfig)[];
|
order: (CharacteristicRenderConfig | LabelRenderConfig | ToyRenderConfig)[];
|
||||||
|
|||||||
Reference in new issue
Block a user