mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-13 20:56:11 +01:00
shot card
This commit is contained in:
17 files changed
+373
-1158
No files matched your search
@@ -50,6 +50,31 @@
|
|||||||
"reset": "Reset"
|
"reset": "Reset"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"shot_card": {
|
||||||
|
"element": {
|
||||||
|
"primary": "Primary",
|
||||||
|
"spaced": "Spaced",
|
||||||
|
"external": "External",
|
||||||
|
"gap": "Gap",
|
||||||
|
"gun": "Gun",
|
||||||
|
"track": "Tracks"
|
||||||
|
},
|
||||||
|
|
||||||
|
"status": {
|
||||||
|
"penetration": "{damage}hp penetration",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"ricochet": "Ricochet",
|
||||||
|
"splash": "{damage}hp splash"
|
||||||
|
},
|
||||||
|
|
||||||
|
"stats": {
|
||||||
|
"thickness_and_angle": "{thickness}mm {angle}°",
|
||||||
|
"nominal": "{thickness}mm nominal",
|
||||||
|
"ricochet_loss": "{loss}% penetration",
|
||||||
|
"ricochet_divider": "Ricochet (-25% penetration)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"dynamic_armor": "Dynamic armor",
|
"dynamic_armor": "Dynamic armor",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { LocaleProvider, useLocale } from "../hooks/useLocale";
|
|||||||
import { useStrings } from "../hooks/useStrings";
|
import { useStrings } from "../hooks/useStrings";
|
||||||
import { App } from "../stores/app";
|
import { App } from "../stores/app";
|
||||||
import { Tankopedia } from "../stores/tankopedia";
|
import { Tankopedia } from "../stores/tankopedia";
|
||||||
import { layerTypeNames } from "./ShotDisplayCard";
|
import { layerTypeNames } from "./ShotCard";
|
||||||
import { ArmorType } from "./SpacedArmorScene";
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export function ArmorPlateDisplay() {
|
export function ArmorPlateDisplay() {
|
||||||
@@ -102,11 +102,11 @@ export function ArmorPlateDisplay() {
|
|||||||
|
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const { protagonist } = Duel.state;
|
const { protagonist } = draft;
|
||||||
const tank = draft.model;
|
const tank = protagonist.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret];
|
||||||
const gun = turret.guns[protagonist.gun.id];
|
const gun = turret.guns[protagonist.gun];
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith("hull_")) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor!.thickness[index] = thickness;
|
tank.armor!.thickness[index] = thickness;
|
||||||
@@ -155,18 +155,17 @@ export function ArmorPlateDisplay() {
|
|||||||
|
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const initialState = Tankopedia.initial;
|
const initialState = Tankopedia.initial;
|
||||||
const { protagonist } = Duel.state;
|
const { protagonist } = draft;
|
||||||
const tank = draft.model;
|
const tank = protagonist.model;
|
||||||
const initialTank = initialState.model;
|
const initialTank = initialState.protagonist.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret];
|
||||||
const initialTurret =
|
const initialTurret =
|
||||||
initialTank.turrets[protagonist.turret.id];
|
initialTank.turrets[protagonist.turret];
|
||||||
const gun = turret.guns[protagonist.gun.id];
|
const gun = turret.guns[protagonist.gun];
|
||||||
const initialGun =
|
const initialGun = initialTurret.guns[protagonist.gun];
|
||||||
initialTurret.guns[protagonist.gun.id];
|
const track = tank.tracks[protagonist.track];
|
||||||
const track = tank.tracks[protagonist.track.id];
|
|
||||||
const initialTrack =
|
const initialTrack =
|
||||||
initialTank.tracks[protagonist.track.id];
|
initialTank.tracks[protagonist.track];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith("hull_")) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor!.thickness[index] =
|
tank.armor!.thickness[index] =
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
.cardWrapper {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
/* keeps the card clear of the tracer, on the side it isn't heading toward */
|
||||||
|
&[data-going-up="true"] {
|
||||||
|
transform: translateY(50%);
|
||||||
|
margin-top: 4rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-going-up="false"] {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.point {
|
||||||
|
opacity: 0.75;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
|
||||||
|
padding: var(--space-4);
|
||||||
|
border-radius: var(--space-3);
|
||||||
|
border: 1px solid var(--gray-a4);
|
||||||
|
background-color: var(--gray-2);
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers {
|
||||||
|
display: table;
|
||||||
|
|
||||||
|
/* column-gap doesn't apply to tables; border-spacing also pads the outer
|
||||||
|
edges, so pull them back in */
|
||||||
|
border-spacing: var(--space-3) var(--space-2);
|
||||||
|
margin: 0 calc(-1 * var(--space-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
display: table-cell;
|
||||||
|
|
||||||
|
&[data-align="right"] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
/* bleed to the card edges */
|
||||||
|
margin: 0 calc(-1 * var(--space-4));
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--gray-a4);
|
||||||
|
}
|
||||||
|
}
|
||||||
+220
-36
@@ -1,5 +1,5 @@
|
|||||||
import { J_HAT, K_HAT } from "@blitzkit/core";
|
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||||
import { Box, Flex, Text } from "@radix-ui/themes";
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { Html } from "@react-three/drei";
|
import { Html } from "@react-three/drei";
|
||||||
import { useFrame } from "@react-three/fiber";
|
import { useFrame } from "@react-three/fiber";
|
||||||
import { clamp } from "lodash-es";
|
import { clamp } from "lodash-es";
|
||||||
@@ -16,37 +16,236 @@ import {
|
|||||||
Path,
|
Path,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
} from "three";
|
} from "three";
|
||||||
import { isHalloween } from "../core/blitzkit/isHalloween";
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { LocaleProvider, useLocale } from "../hooks/useLocale";
|
import { isHalloween } from "../../core/blitzkit/isHalloween";
|
||||||
|
import { LocaleProvider, useLocale } from "../../hooks/useLocale";
|
||||||
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
import {
|
import {
|
||||||
Tankopedia,
|
Tankopedia,
|
||||||
|
type Shot,
|
||||||
|
type ShotLayer,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
type ShotStatus,
|
type ShotStatus,
|
||||||
} from "../stores/tankopedia";
|
} from "../../stores/tankopedia";
|
||||||
import { ShotDisplayCard } from "./ShotDisplayCard";
|
import { classNames } from "../../ui/classNames";
|
||||||
|
import { ArmorType } from "../SpacedArmorScene";
|
||||||
|
import { Text, type TextProps } from "../Text";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
export const shotStatusColors: Record<
|
export const shotStatusColors: Record<ShotStatus, TextProps["color"]> = {
|
||||||
ShotStatus,
|
|
||||||
ComponentProps<typeof Text>["color"]
|
|
||||||
> = {
|
|
||||||
blocked: "red",
|
blocked: "red",
|
||||||
penetration: "green",
|
penetration: "green",
|
||||||
ricochet: "yellow",
|
ricochet: "yellow",
|
||||||
splash: "orange",
|
splash: "orange",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const layerTypeNames = {
|
||||||
|
[ArmorType.Primary]: "primary",
|
||||||
|
[ArmorType.Spaced]: "spaced",
|
||||||
|
[ArmorType.External]: "external",
|
||||||
|
null: "gap",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface LayerEntryProps {
|
||||||
|
layerIndex?: string;
|
||||||
|
color: TextProps["color"];
|
||||||
|
layerName: string;
|
||||||
|
layer: ShotLayer;
|
||||||
|
angle?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LayerEntry({
|
||||||
|
layerIndex,
|
||||||
|
color,
|
||||||
|
layerName,
|
||||||
|
layer,
|
||||||
|
angle,
|
||||||
|
}: LayerEntryProps) {
|
||||||
|
const locale = useLocale();
|
||||||
|
const strings = useStrings();
|
||||||
|
const { stats } = strings.tanks.shot_card;
|
||||||
|
const format = (value: number) => Math.round(value).toLocaleString(locale);
|
||||||
|
const mm = (value: number) =>
|
||||||
|
literals(strings.common.units.mm, { value: format(value) });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.cell}>
|
||||||
|
<Text size="minor" lowContrast>
|
||||||
|
{layerIndex}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cell}>
|
||||||
|
<Text color={color}>{layerName}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cell}>
|
||||||
|
<Text color={color}>
|
||||||
|
{layer.type === null && mm(layer.distance * 1000)}
|
||||||
|
{layer.type === ArmorType.External && mm(layer.thickness)}
|
||||||
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) &&
|
||||||
|
(angle === undefined
|
||||||
|
? mm(layer.thicknessAngled)
|
||||||
|
: literals(stats.thickness_and_angle, {
|
||||||
|
thickness: format(layer.thicknessAngled),
|
||||||
|
angle: radToDeg(angle).toFixed(0),
|
||||||
|
}))}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.cell} data-align="right">
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text lowContrast>
|
||||||
|
{literals(stats.ricochet_loss, {
|
||||||
|
loss: Math.max(-100, -50 * layer.distance).toFixed(0),
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
|
<Text lowContrast>
|
||||||
|
{literals(stats.nominal, { thickness: format(layer.thickness) })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LayersProps {
|
||||||
|
layers: ShotLayer[];
|
||||||
|
status: Shot["in"]["status"];
|
||||||
|
containsGaps: boolean;
|
||||||
|
/** Offset applied to displayed layer numbers (for post-ricochet layers). */
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Layers({ layers, status, containsGaps, offset = 0 }: LayersProps) {
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.layers}>
|
||||||
|
{layers.map((layer, index) => {
|
||||||
|
const shiftedIndex = index + offset;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayerEntry
|
||||||
|
key={index}
|
||||||
|
layer={layer}
|
||||||
|
layerName={
|
||||||
|
strings.tanks.shot_card.element[
|
||||||
|
layer.type === ArmorType.External
|
||||||
|
? layer.variant
|
||||||
|
: layerTypeNames[`${layer.type}`]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
layerIndex={
|
||||||
|
layer.type === null
|
||||||
|
? undefined
|
||||||
|
: `${(containsGaps ? shiftedIndex / 2 : shiftedIndex) + 1}.`
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
status === "splash"
|
||||||
|
? shotStatusColors.splash
|
||||||
|
: shotStatusColors[layer.status]
|
||||||
|
}
|
||||||
|
angle={
|
||||||
|
layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced
|
||||||
|
? layer.angle
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShotCardHtmlProps extends ComponentProps<"div"> {
|
||||||
|
shot: Shot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShotCardHtml({ shot, className, ...props }: ShotCardHtmlProps) {
|
||||||
|
const locale = useLocale();
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
if (!shot) return null;
|
||||||
|
|
||||||
|
const { status, stats } = strings.tanks.shot_card;
|
||||||
|
const damage = Math.round(shot.damage).toLocaleString(locale);
|
||||||
|
const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0;
|
||||||
|
const penetrationChance = shot.penetrationChance !== undefined && (
|
||||||
|
<Text lowContrast weight="bold">
|
||||||
|
{literals(strings.common.units.percentage, {
|
||||||
|
value: (shot.penetrationChance * 100).toFixed(0),
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.card, className)} {...props}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Text color={shotStatusColors[shot.in.status]} weight="bold">
|
||||||
|
{literals(status[shot.in.status], { damage })}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{!hasOutLayers && penetrationChance}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Layers
|
||||||
|
layers={shot.in.layers}
|
||||||
|
status={shot.in.status}
|
||||||
|
containsGaps={shot.containsGaps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shot.out && hasOutLayers && (
|
||||||
|
<>
|
||||||
|
<div className={styles.divider}>
|
||||||
|
<Text size="minor" lowContrast>
|
||||||
|
{stats.ricochet_divider}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<Text color={shotStatusColors[shot.out.status]} weight="bold">
|
||||||
|
{literals(status[shot.out.status], { damage })}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{penetrationChance}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Layers
|
||||||
|
layers={shot.out.layers}
|
||||||
|
status={shot.out.status}
|
||||||
|
containsGaps={shot.containsGaps}
|
||||||
|
offset={shot.in.layers.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SURFACE_POINT_SIZE = 12;
|
const SURFACE_POINT_SIZE = 12;
|
||||||
export const SHOT_DISPLAY_LENGTH_INFINITY = 4;
|
const SHOT_DISPLAY_LENGTH_INFINITY = 4;
|
||||||
const TRACER_THICK = 1 / 32;
|
const TRACER_THICK = 1 / 32;
|
||||||
const TRACER_THIN = TRACER_THICK / 2;
|
const TRACER_THIN = TRACER_THICK / 2;
|
||||||
|
|
||||||
export function ShotDisplay() {
|
export function ShotCard() {
|
||||||
const shot = Tankopedia.use((state) => state.shot);
|
const shot = Tankopedia.use((state) => state.shot);
|
||||||
const inTracer = useRef<Mesh>(null);
|
const inTracer = useRef<Mesh>(null);
|
||||||
const outTracer = useRef<Mesh>(null);
|
const outTracer = useRef<Mesh>(null);
|
||||||
const splashRadiusWrapper = useRef<Group>(null);
|
const splashRadiusWrapper = useRef<Group>(null);
|
||||||
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
|
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
|
||||||
const { locale } = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shot?.splashRadius !== undefined) {
|
if (shot?.splashRadius !== undefined) {
|
||||||
@@ -140,20 +339,11 @@ export function ShotDisplay() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Html position={inLast.point} center>
|
<Html position={inLast.point} center>
|
||||||
<Box
|
<div className={styles.cardWrapper} data-going-up={tracerGoingUp}>
|
||||||
style={{
|
|
||||||
pointerEvents: "none",
|
|
||||||
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
|
||||||
}}
|
|
||||||
mb={tracerGoingUp ? "0" : "9"}
|
|
||||||
pb={tracerGoingUp ? "0" : "4"}
|
|
||||||
mt={tracerGoingUp ? "9" : "0"}
|
|
||||||
pt={tracerGoingUp ? "4" : "0"}
|
|
||||||
>
|
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<ShotDisplayCard shot={shot} />
|
<ShotCardHtml shot={shot} />
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
</Box>
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
|
|
||||||
{[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => {
|
{[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => {
|
||||||
@@ -173,21 +363,15 @@ export function ShotDisplay() {
|
|||||||
transform: "translateY(-50%)",
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<div
|
||||||
|
className={styles.point}
|
||||||
style={{
|
style={{
|
||||||
|
width: SURFACE_POINT_SIZE,
|
||||||
|
height: SURFACE_POINT_SIZE,
|
||||||
marginLeft: -SURFACE_POINT_SIZE / 2,
|
marginLeft: -SURFACE_POINT_SIZE / 2,
|
||||||
|
backgroundColor: shotStatusColor,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: SURFACE_POINT_SIZE,
|
|
||||||
height: SURFACE_POINT_SIZE,
|
|
||||||
backgroundColor: shotStatusColor,
|
|
||||||
opacity: 0.75,
|
|
||||||
borderRadius: SURFACE_POINT_SIZE / 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import { literals } from "@blitzkit/i18n/src/literals";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Flex,
|
|
||||||
Inset,
|
|
||||||
Separator,
|
|
||||||
Text,
|
|
||||||
type CardProps,
|
|
||||||
} from "@radix-ui/themes";
|
|
||||||
import type { ComponentProps } from "react";
|
|
||||||
import { radToDeg } from "three/src/math/MathUtils.js";
|
|
||||||
import { useLocale } from "../hooks/useLocale";
|
|
||||||
import type { Shot, ShotLayer } from "../stores/tankopedia";
|
|
||||||
import { shotStatusColors } from "./ShotDisplay";
|
|
||||||
import { ArmorType } from "./SpacedArmorScene";
|
|
||||||
|
|
||||||
export const layerTypeNames = {
|
|
||||||
[ArmorType.Primary]: "primary",
|
|
||||||
[ArmorType.Spaced]: "spaced",
|
|
||||||
[ArmorType.External]: "external",
|
|
||||||
null: "gap",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function LayerEntry({
|
|
||||||
layerIndex,
|
|
||||||
shotStatusColor,
|
|
||||||
layerName,
|
|
||||||
layer,
|
|
||||||
angle,
|
|
||||||
}: {
|
|
||||||
layerIndex?: string;
|
|
||||||
shotStatusColor: ComponentProps<typeof Text>["color"];
|
|
||||||
layerName: string;
|
|
||||||
layer: ShotLayer;
|
|
||||||
angle?: number;
|
|
||||||
}) {
|
|
||||||
const { locale, strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<Text size="1" color="gray">
|
|
||||||
{layerIndex}
|
|
||||||
</Text>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Text size="2" color={shotStatusColor}>
|
|
||||||
{layerName}
|
|
||||||
</Text>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{layer.type === null && (
|
|
||||||
<Text size="2" color={shotStatusColor}>
|
|
||||||
{literals(strings.common.units.mm, {
|
|
||||||
value: Math.round(layer.distance * 1000).toLocaleString(locale),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{layer.type === ArmorType.External && (
|
|
||||||
<Text size="2" color={shotStatusColor}>
|
|
||||||
{literals(strings.common.units.mm, {
|
|
||||||
value: Math.round(layer.thickness).toLocaleString(locale),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{(layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced) && (
|
|
||||||
<Text size="2" color={shotStatusColor}>
|
|
||||||
{angle === undefined
|
|
||||||
? literals(strings.common.units.mm, {
|
|
||||||
value: Math.round(layer.thicknessAngled).toLocaleString(
|
|
||||||
locale
|
|
||||||
),
|
|
||||||
})
|
|
||||||
: literals(
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
|
||||||
.stats.thickness_and_angle,
|
|
||||||
{
|
|
||||||
thickness: Math.round(layer.thicknessAngled).toLocaleString(
|
|
||||||
locale
|
|
||||||
),
|
|
||||||
angle: radToDeg(angle ?? 0).toFixed(0),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{layer.type === null && (
|
|
||||||
<Text size="2" color="gray">
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
|
||||||
.ricochet_loss,
|
|
||||||
{ loss: Math.max(-100, -50 * layer.distance).toFixed(0) }
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced) && (
|
|
||||||
<Text size="2" color="gray">
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
|
||||||
.nominal,
|
|
||||||
{ thickness: Math.round(layer.thickness).toLocaleString(locale) }
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShotDisplayCardProps extends CardProps {
|
|
||||||
shot: Shot;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|
||||||
const { locale, strings } = useLocale();
|
|
||||||
|
|
||||||
if (!shot) return null;
|
|
||||||
|
|
||||||
const outTitleColor = shot.out
|
|
||||||
? shotStatusColors[shot.out.status]
|
|
||||||
: undefined;
|
|
||||||
const inTitleColor = shotStatusColors[shot.in.status];
|
|
||||||
const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0;
|
|
||||||
const penetrationChance = shot.penetrationChance !== undefined && (
|
|
||||||
<Text color="gray" weight="bold">
|
|
||||||
{literals(strings.common.units.percentage, {
|
|
||||||
value: (shot.penetrationChance * 100).toFixed(0),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card variant="classic" {...props}>
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
<Flex direction="column" gap="1">
|
|
||||||
<Flex justify="between" align="center" gap="4">
|
|
||||||
<Text color={inTitleColor} weight="bold">
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
|
||||||
.status[shot.in.status],
|
|
||||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{!hasOutLayers && penetrationChance}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<table
|
|
||||||
style={{
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
borderSpacing: "var(--space-1) 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{shot.in.layers.map((layer, index) => {
|
|
||||||
const layerName =
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
|
||||||
.element[
|
|
||||||
layer.type === ArmorType.External
|
|
||||||
? layer.variant
|
|
||||||
: layerTypeNames[`${layer.type}`]
|
|
||||||
];
|
|
||||||
const layerIndex =
|
|
||||||
layer.type === null
|
|
||||||
? undefined
|
|
||||||
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
|
||||||
const shotStatusColor =
|
|
||||||
shot.in.status === "splash"
|
|
||||||
? shotStatusColors.splash
|
|
||||||
: shotStatusColors[layer.status];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayerEntry
|
|
||||||
key={index}
|
|
||||||
layer={layer}
|
|
||||||
layerIndex={layerIndex}
|
|
||||||
layerName={layerName}
|
|
||||||
shotStatusColor={shotStatusColor}
|
|
||||||
angle={
|
|
||||||
layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced
|
|
||||||
? layer.angle
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</table>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{shot.out && shot.out.layers.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Inset side="x">
|
|
||||||
<Flex align="center" gap="2">
|
|
||||||
<Separator
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text size="1" color="gray">
|
|
||||||
ricochet (-25% penetration)
|
|
||||||
</Text>
|
|
||||||
<Separator
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Inset>
|
|
||||||
|
|
||||||
<Flex justify="between" align="center" gap="4">
|
|
||||||
<Text color={outTitleColor} weight="bold">
|
|
||||||
{literals(
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
|
||||||
.status[shot.out.status],
|
|
||||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{penetrationChance}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex direction="column">
|
|
||||||
{shot.out.layers.map((layer, index) => {
|
|
||||||
const layerName =
|
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
|
||||||
.element[
|
|
||||||
layer.type === ArmorType.External
|
|
||||||
? layer.variant
|
|
||||||
: layerTypeNames[`${layer.type}`]
|
|
||||||
];
|
|
||||||
const shiftedIndex = index + shot.out!.layers.length;
|
|
||||||
const layerIndex =
|
|
||||||
layer.type === null
|
|
||||||
? undefined
|
|
||||||
: `${
|
|
||||||
(shot.containsGaps ? shiftedIndex / 2 : shiftedIndex) +
|
|
||||||
1
|
|
||||||
}.`;
|
|
||||||
const shotStatusColor =
|
|
||||||
shot.out!.status === "splash"
|
|
||||||
? shotStatusColors.splash
|
|
||||||
: shotStatusColors[layer.status];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayerEntry
|
|
||||||
key={index}
|
|
||||||
layer={layer}
|
|
||||||
layerIndex={layerIndex}
|
|
||||||
layerName={layerName}
|
|
||||||
shotStatusColor={shotStatusColor}
|
|
||||||
angle={
|
|
||||||
layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced
|
|
||||||
? layer.angle
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
import { degToRad } from "three/src/math/MathUtils.js";
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { defaultEqualizer } from "../config/equalizer";
|
import { defaultEqualizer } from "../config/equalizer";
|
||||||
import { jsxTree } from "../core/blitzkit/jsxTree";
|
import { jsxTree } from "../core/blitzkit/jsxTree";
|
||||||
|
import {
|
||||||
|
SPALL_LINER_HE_DAMAGE_DELTA,
|
||||||
|
SPALL_LINER_PROVISION_ID,
|
||||||
|
} from "../core/blitzkit/spallLiner";
|
||||||
import { discardClippingPlane } from "../core/three/discardClippingPlane";
|
import { discardClippingPlane } from "../core/three/discardClippingPlane";
|
||||||
import { DuelModule, getDuel } from "../hooks/useDuel";
|
import { DuelModule, getDuel } from "../hooks/useDuel";
|
||||||
import { hasEquipment } from "../hooks/useEquipment";
|
import { hasEquipment } from "../hooks/useEquipment";
|
||||||
@@ -240,8 +244,8 @@ export function SpacedArmorSceneComponent({
|
|||||||
shell.type === ShellType.SHELL_TYPE_HE
|
shell.type === ShellType.SHELL_TYPE_HE
|
||||||
? "blocked"
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? "blocked"
|
? "blocked"
|
||||||
: "penetration",
|
: "penetration",
|
||||||
variant: layer.variant,
|
variant: layer.variant,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -292,8 +296,8 @@ export function SpacedArmorSceneComponent({
|
|||||||
layer.type !== ArmorType.Primary
|
layer.type !== ArmorType.Primary
|
||||||
? "blocked"
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? "blocked"
|
? "blocked"
|
||||||
: "penetration",
|
: "penetration",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -415,7 +419,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
shot.damage *= antagonistEqualizer.damage;
|
shot.damage *= antagonistEqualizer.damage;
|
||||||
|
|
||||||
const hasSpallLiner = Duel.state.protagonist.provisions.includes(
|
const hasSpallLiner = Tankopedia.state.protagonist.provisions.includes(
|
||||||
SPALL_LINER_PROVISION_ID,
|
SPALL_LINER_PROVISION_ID,
|
||||||
);
|
);
|
||||||
if (hasSpallLiner && shell.type === ShellType.SHELL_TYPE_HE) {
|
if (hasSpallLiner && shell.type === ShellType.SHELL_TYPE_HE) {
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
import { type FlexProps, Flex } from "@radix-ui/themes";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
|
|
||||||
type ConfigurationChildWrapperProps = FlexProps & {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ConfigurationChildWrapper({
|
|
||||||
children,
|
|
||||||
style,
|
|
||||||
...props
|
|
||||||
}: ConfigurationChildWrapperProps) {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
gap="2"
|
|
||||||
direction="column"
|
|
||||||
align={{ initial: "center", sm: "start" }}
|
|
||||||
style={{ width: "100%", ...style }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-95
@@ -1,95 +0,0 @@
|
|||||||
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
|
||||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Popover,
|
|
||||||
Text,
|
|
||||||
} from "@radix-ui/themes";
|
|
||||||
import { api } from "../../../../api/dynamic";
|
|
||||||
import { useEquipment } from "../../../../hooks/useEquipment";
|
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../stores/duel";
|
|
||||||
import { ConsumablesManager } from "../../../ConsumablesManager";
|
|
||||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
|
||||||
|
|
||||||
const consumableDefinitions = await api.consumables();
|
|
||||||
|
|
||||||
export function Consumables() {
|
|
||||||
const protagonist = Duel.use((state) => state.protagonist);
|
|
||||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
|
||||||
const consumablesList = Object.values(
|
|
||||||
consumableDefinitions.consumables,
|
|
||||||
).filter((consumable) =>
|
|
||||||
checkConsumableProvisionInclusivity(
|
|
||||||
consumable,
|
|
||||||
protagonist.tank,
|
|
||||||
protagonist.gun,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const cooldownBooster = Duel.use(
|
|
||||||
(state) => state.protagonist.cooldownBooster,
|
|
||||||
);
|
|
||||||
const hasConsumableDeliverySystem = useEquipment("protagonist", 118);
|
|
||||||
const hasHighEndConsumables = useEquipment("protagonist", 101);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigurationChildWrapper>
|
|
||||||
<Flex gap="4" align="center">
|
|
||||||
<Flex gap="2" align="center">
|
|
||||||
<Heading size="4">
|
|
||||||
{strings.website.tools.tankopedia.configuration.consumables.title}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Popover.Root>
|
|
||||||
<Popover.Trigger>
|
|
||||||
<IconButton variant="ghost">
|
|
||||||
<InfoCircledIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Popover.Trigger>
|
|
||||||
|
|
||||||
<Popover.Content>
|
|
||||||
<Text>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.configuration.consumables
|
|
||||||
.info
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
color="red"
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.consumables = [];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.consumables.clear}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<ConsumablesManager
|
|
||||||
consumables={consumablesList}
|
|
||||||
selected={consumables}
|
|
||||||
cooldownBooster={cooldownBooster}
|
|
||||||
disabled={protagonist.tank.max_consumables === consumables.length}
|
|
||||||
hasConsumableDeliverySystem={hasConsumableDeliverySystem}
|
|
||||||
hasHighEndConsumables={hasHighEndConsumables}
|
|
||||||
timers
|
|
||||||
onConsumablesChange={(consumables) => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.consumables = consumables;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
justify={{ initial: "center", sm: "start" }}
|
|
||||||
/>
|
|
||||||
</ConfigurationChildWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-53
@@ -1,53 +0,0 @@
|
|||||||
import { Button, Flex, Heading } from "@radix-ui/themes";
|
|
||||||
import { api } from "../../../../api/dynamic";
|
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../stores/duel";
|
|
||||||
import { EquipmentManager } from "../../../EquipmentManager";
|
|
||||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
|
||||||
|
|
||||||
const equipmentDefinitions = await api.equipment();
|
|
||||||
|
|
||||||
export function Equipment() {
|
|
||||||
const protagonist = Duel.use((state) => state.protagonist);
|
|
||||||
const equipmentPreset =
|
|
||||||
equipmentDefinitions.presets[protagonist.tank.equipment_preset];
|
|
||||||
const equipmentMatrix = Duel.use(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigurationChildWrapper>
|
|
||||||
<Flex gap="4" align="center">
|
|
||||||
<Heading size="4">
|
|
||||||
{strings.website.tools.tankopedia.configuration.equipment.title}
|
|
||||||
</Heading>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
color="red"
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.equipmentMatrix.forEach((row) => {
|
|
||||||
row.forEach((_, index) => {
|
|
||||||
row[index] = 0;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.equipment.clear}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<EquipmentManager
|
|
||||||
matrix={equipmentMatrix}
|
|
||||||
preset={equipmentPreset}
|
|
||||||
onChange={(matrix) => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.equipmentMatrix = matrix;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ConfigurationChildWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-65
@@ -1,65 +0,0 @@
|
|||||||
import { alias } from "@blitzkit/core";
|
|
||||||
import { Flex, Heading } from "@radix-ui/themes";
|
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../stores/duel";
|
|
||||||
import { GenericTankComponentButton } from "../../../ModuleButtons/GenericTankComponentButton";
|
|
||||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
|
||||||
|
|
||||||
export function Miscellaneous() {
|
|
||||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
|
||||||
const cooldownBooster = Duel.use(
|
|
||||||
(state) => state.protagonist.cooldownBooster,
|
|
||||||
);
|
|
||||||
const { tank } = Duel.use((state) => state.protagonist);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigurationChildWrapper>
|
|
||||||
<Heading size="4">
|
|
||||||
{strings.website.tools.tankopedia.configuration.miscellaneous.title}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Flex gap="2" align="center">
|
|
||||||
{!tank.fixed_camouflage && (
|
|
||||||
<GenericTankComponentButton
|
|
||||||
icon={alias("api", "/icons/camo.webp")}
|
|
||||||
selected={camouflage}
|
|
||||||
iconStyles={{
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(calc(-50% + 4px), calc(-50% + 4px))",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.camouflage = !camouflage;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<GenericTankComponentButton
|
|
||||||
icon={alias("api", "/icons/boosters/equipment.webp")}
|
|
||||||
selected={cooldownBooster > 0}
|
|
||||||
banner={
|
|
||||||
cooldownBooster === 0
|
|
||||||
? undefined
|
|
||||||
: cooldownBooster === 1
|
|
||||||
? "#afb0abc0"
|
|
||||||
: cooldownBooster === 2
|
|
||||||
? "#5f72cbc0"
|
|
||||||
: "#9b3cc0c0"
|
|
||||||
}
|
|
||||||
iconStyles={{
|
|
||||||
transform: "translate(-50%, -50%) scale(0.8)",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.cooldownBooster++;
|
|
||||||
if (draft.protagonist.cooldownBooster === 4)
|
|
||||||
draft.protagonist.cooldownBooster = 0;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-446
@@ -1,446 +0,0 @@
|
|||||||
import {
|
|
||||||
alias,
|
|
||||||
formatCompact,
|
|
||||||
tankIcon,
|
|
||||||
romanize,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import {
|
|
||||||
EngineDefinition,
|
|
||||||
GunDefinition,
|
|
||||||
ModuleType,
|
|
||||||
TrackDefinition,
|
|
||||||
TurretDefinition,
|
|
||||||
type TankDefinition,
|
|
||||||
type Unlock,
|
|
||||||
} from "@blitzkit/protos";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ChevronDownIcon,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
} from "@radix-ui/themes";
|
|
||||||
import { api } from "../../../../api/dynamic";
|
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../stores/duel";
|
|
||||||
import { LinkI18n } from "../../../LinkI18n";
|
|
||||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
|
||||||
|
|
||||||
type ModuleDefinition =
|
|
||||||
| TurretDefinition
|
|
||||||
| GunDefinition
|
|
||||||
| TrackDefinition
|
|
||||||
| EngineDefinition;
|
|
||||||
|
|
||||||
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
|
||||||
if ("gun_type" in raw) {
|
|
||||||
return raw;
|
|
||||||
} else
|
|
||||||
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModuleWithUnlocks {
|
|
||||||
unlocks: Unlock[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleTypeTypeIconName: Record<ModuleType, string> = {
|
|
||||||
[ModuleType.MODULE_TYPE_VEHICLE]: "",
|
|
||||||
[ModuleType.MODULE_TYPE_ENGINE]: "engine",
|
|
||||||
[ModuleType.MODULE_TYPE_TRACKS]: "chassis",
|
|
||||||
[ModuleType.MODULE_TYPE_TURRET]: "turret",
|
|
||||||
[ModuleType.MODULE_TYPE_GUN]: "gun",
|
|
||||||
};
|
|
||||||
|
|
||||||
function ModuleButton({
|
|
||||||
tier,
|
|
||||||
selected,
|
|
||||||
unlock,
|
|
||||||
top,
|
|
||||||
tooltip,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
unlock: Unlock;
|
|
||||||
tier: number;
|
|
||||||
selected: boolean;
|
|
||||||
top: boolean;
|
|
||||||
tooltip?: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
const isTank = unlock.type === ModuleType.MODULE_TYPE_VEHICLE;
|
|
||||||
const { locale } = useLocale();
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<IconButton
|
|
||||||
size="4"
|
|
||||||
radius="small"
|
|
||||||
variant={selected ? "surface" : "soft"}
|
|
||||||
color={top ? "amber" : selected ? undefined : "gray"}
|
|
||||||
onClick={onClick}
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width: isTank ? 72 : undefined,
|
|
||||||
height: isTank ? 72 : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
size="1"
|
|
||||||
color="gray"
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: isTank ? 8 : 4,
|
|
||||||
top: isTank ? 8 : 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{romanize(tier)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{unlock.cost!.value > 0 && (
|
|
||||||
<Flex
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "50%",
|
|
||||||
bottom: 4,
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}}
|
|
||||||
align="center"
|
|
||||||
gap={isTank ? "1" : undefined}
|
|
||||||
>
|
|
||||||
<Text size="1" color="gray">
|
|
||||||
{formatCompact(locale, unlock.cost!.value)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt={unlock.cost!.type}
|
|
||||||
src={alias("api", `/icons/currencies/${unlock.cost!.type}.webp`)}
|
|
||||||
style={{
|
|
||||||
width: 12,
|
|
||||||
height: 12,
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt={ModuleType[unlock.type]}
|
|
||||||
src={
|
|
||||||
isTank
|
|
||||||
? tankIcon(unlock.id)
|
|
||||||
: alias(
|
|
||||||
"api",
|
|
||||||
`/icons/modules/${moduleTypeTypeIconName[unlock.type]}.webp`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
width: isTank ? 64 : 32,
|
|
||||||
height: isTank ? 64 : 32,
|
|
||||||
objectFit: "contain",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
const node = isTank ? (
|
|
||||||
<LinkI18n
|
|
||||||
locale={locale}
|
|
||||||
href={`/tanks/${tankDefinitions.tanks[unlock.id].slug}`}
|
|
||||||
>
|
|
||||||
{button}
|
|
||||||
</LinkI18n>
|
|
||||||
) : (
|
|
||||||
button
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tooltip) return node;
|
|
||||||
|
|
||||||
return <Tooltip content={tooltip}>{node}</Tooltip>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tankDefinitions = await api.tanks();
|
|
||||||
|
|
||||||
export function Modules() {
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const hasUpgrades =
|
|
||||||
tank.turrets.length > 1 ||
|
|
||||||
tank.turrets[0].guns.length > 1 ||
|
|
||||||
tank.engines.length > 1 ||
|
|
||||||
tank.tracks.length > 1;
|
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
|
||||||
const engine = Duel.use((state) => state.protagonist.engine);
|
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
|
||||||
const turret0 = tank.turrets[0];
|
|
||||||
const gun0 = turret0.guns[0];
|
|
||||||
const engine0 = tank.engines[0];
|
|
||||||
const track0 = tank.tracks[0];
|
|
||||||
const topTurret = tank.turrets.at(-1)!;
|
|
||||||
const topGun = topTurret.guns.at(-1)!;
|
|
||||||
const topEngine = tank.engines.at(-1)!;
|
|
||||||
const topTrack = tank.tracks.at(-1)!;
|
|
||||||
const { strings, unwrap } = useLocale();
|
|
||||||
|
|
||||||
function setByUnlock(unlock: Unlock) {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
if (unlock.type === ModuleType.MODULE_TYPE_TURRET) {
|
|
||||||
draft.protagonist.turret = draft.protagonist.tank.turrets.find(
|
|
||||||
(turret) => turret.id === unlock.id,
|
|
||||||
)!;
|
|
||||||
|
|
||||||
// Check if current gun exists in new turret
|
|
||||||
const gunInNewTurret = draft.protagonist.turret.guns.find(
|
|
||||||
(gun) => gun.id === draft.protagonist.gun.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (gunInNewTurret) {
|
|
||||||
// Gun exists - get it FROM this turret
|
|
||||||
draft.protagonist.gun = gunInNewTurret;
|
|
||||||
draft.protagonist.shell = gunInNewTurret.shells[0];
|
|
||||||
} else {
|
|
||||||
// Gun doesn't exist - select top gun from new turret
|
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
|
||||||
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
|
||||||
}
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_GUN) {
|
|
||||||
const gunInTurret = draft.protagonist.turret.guns.find(
|
|
||||||
(gun) => gun.id === unlock.id,
|
|
||||||
);
|
|
||||||
if (gunInTurret) {
|
|
||||||
draft.protagonist.gun = gunInTurret;
|
|
||||||
draft.protagonist.shell = gunInTurret.shells[0];
|
|
||||||
} else {
|
|
||||||
// TODO: warn somehow?
|
|
||||||
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
|
||||||
turret.guns.some((gun) => gun.id === unlock.id),
|
|
||||||
)!;
|
|
||||||
const gunInSuitableTurret = suitableTurret.guns.find(
|
|
||||||
(gun) => gun.id === unlock.id,
|
|
||||||
)!;
|
|
||||||
|
|
||||||
draft.protagonist.turret = suitableTurret;
|
|
||||||
draft.protagonist.gun = gunInSuitableTurret;
|
|
||||||
draft.protagonist.shell = gunInSuitableTurret.shells[0];
|
|
||||||
}
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_ENGINE) {
|
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
|
||||||
(engine) => engine.id === unlock.id,
|
|
||||||
)!;
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_TRACKS) {
|
|
||||||
draft.protagonist.track = draft.protagonist.tank.tracks.find(
|
|
||||||
(track) => track.id === unlock.id,
|
|
||||||
)!;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function tree(type: ModuleType, unlocks: Unlock[]) {
|
|
||||||
return (
|
|
||||||
<Flex gap="2">
|
|
||||||
{unlocks.map((unlock, index) => {
|
|
||||||
const first = index === 0;
|
|
||||||
const last = index === unlocks.length - 1;
|
|
||||||
const central = !first && !last;
|
|
||||||
let module: ModuleDefinition | TankDefinition | undefined = undefined;
|
|
||||||
|
|
||||||
if (unlock.type === ModuleType.MODULE_TYPE_TRACKS) {
|
|
||||||
module = tank.tracks.find((track) => track.id === unlock.id)!;
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_ENGINE) {
|
|
||||||
module = tank.engines.find((engine) => engine.id === unlock.id)!;
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_TURRET) {
|
|
||||||
module = tank.turrets.find((turret) => turret.id === unlock.id)!;
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_GUN) {
|
|
||||||
module = tank.turrets
|
|
||||||
.find((turret) =>
|
|
||||||
turret.guns.some((gun) => gun.id === unlock.id),
|
|
||||||
)!
|
|
||||||
.guns.find((gun) => gun.id === unlock.id)!;
|
|
||||||
} else if (unlock.type === ModuleType.MODULE_TYPE_VEHICLE) {
|
|
||||||
module = tankDefinitions.tanks[unlock.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (module === undefined) return null;
|
|
||||||
|
|
||||||
const tooltip = unwrap(module.name!);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
key={unlock.id}
|
|
||||||
align="center"
|
|
||||||
direction="column"
|
|
||||||
gap="2"
|
|
||||||
position="relative"
|
|
||||||
>
|
|
||||||
{unlock.cost!.value > 0 && (
|
|
||||||
<>
|
|
||||||
{unlocks.length > 1 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: "currentcolor",
|
|
||||||
width: `calc(${central ? 100 : 50}% + ${
|
|
||||||
central ? 8 : 4
|
|
||||||
}px)`,
|
|
||||||
height: 1,
|
|
||||||
position: "absolute",
|
|
||||||
right: first ? 0 : undefined,
|
|
||||||
left: last ? 0 : undefined,
|
|
||||||
transform: `translateX(${first ? 4 : last ? -4 : 0}px)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
align="center"
|
|
||||||
style={{
|
|
||||||
marginBottom: -7,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: "currentcolor",
|
|
||||||
width: 1,
|
|
||||||
height: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ChevronDownIcon
|
|
||||||
style={{
|
|
||||||
transform: "translateY(-7px)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ModuleButton
|
|
||||||
unlock={unlock}
|
|
||||||
tier={extractModule(module).tier}
|
|
||||||
tooltip={tooltip}
|
|
||||||
selected={
|
|
||||||
(unlock.type === ModuleType.MODULE_TYPE_TURRET
|
|
||||||
? turret.id
|
|
||||||
: unlock.type === ModuleType.MODULE_TYPE_GUN
|
|
||||||
? gun.id
|
|
||||||
: unlock.type === ModuleType.MODULE_TYPE_ENGINE
|
|
||||||
? engine.id
|
|
||||||
: unlock.type === ModuleType.MODULE_TYPE_TRACKS
|
|
||||||
? track.id
|
|
||||||
: -1) === unlock.id
|
|
||||||
}
|
|
||||||
top={
|
|
||||||
unlock.type === ModuleType.MODULE_TYPE_TURRET
|
|
||||||
? extractModule(module).id == topTurret.id
|
|
||||||
: unlock.type === ModuleType.MODULE_TYPE_GUN
|
|
||||||
? extractModule(module).id === topGun.id
|
|
||||||
: unlock.type === ModuleType.MODULE_TYPE_ENGINE
|
|
||||||
? extractModule(module).id === topEngine.id
|
|
||||||
: unlock.type === ModuleType.MODULE_TYPE_TRACKS
|
|
||||||
? extractModule(module).id === topTrack.id
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
onClick={() => setByUnlock(unlock)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(extractModule(module) as ModuleWithUnlocks).unlocks &&
|
|
||||||
(extractModule(module) as ModuleWithUnlocks).unlocks!.length >
|
|
||||||
1 && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: "currentcolor",
|
|
||||||
width: 1,
|
|
||||||
height: 8,
|
|
||||||
marginBottom: -8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(extractModule(module) as ModuleWithUnlocks).unlocks &&
|
|
||||||
tree(
|
|
||||||
type,
|
|
||||||
(extractModule(module) as ModuleWithUnlocks).unlocks!,
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigurationChildWrapper>
|
|
||||||
<Flex gap="4" align="center">
|
|
||||||
<Heading size="4">
|
|
||||||
{strings.website.tools.tankopedia.configuration.modules.title}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{hasUpgrades && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
color="red"
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
|
||||||
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines[0];
|
|
||||||
draft.protagonist.track = draft.protagonist.tank.tracks[0];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.modules.stock}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.turret =
|
|
||||||
draft.protagonist.tank.turrets.at(-1)!;
|
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
|
||||||
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
|
||||||
draft.protagonist.engine =
|
|
||||||
draft.protagonist.tank.engines.at(-1)!;
|
|
||||||
draft.protagonist.track =
|
|
||||||
draft.protagonist.tank.tracks.at(-1)!;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.modules.upgrade}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex gap="2" wrap="wrap" gapY="6">
|
|
||||||
{tree(ModuleType.MODULE_TYPE_ENGINE, [
|
|
||||||
{
|
|
||||||
cost: { type: "xp", value: 0 },
|
|
||||||
id: engine0.id,
|
|
||||||
type: ModuleType.MODULE_TYPE_ENGINE,
|
|
||||||
},
|
|
||||||
])}
|
|
||||||
{tree(ModuleType.MODULE_TYPE_TRACKS, [
|
|
||||||
{
|
|
||||||
cost: { type: "xp", value: 0 },
|
|
||||||
id: track0.id,
|
|
||||||
type: ModuleType.MODULE_TYPE_TRACKS,
|
|
||||||
},
|
|
||||||
])}
|
|
||||||
{tree(ModuleType.MODULE_TYPE_TURRET, [
|
|
||||||
{
|
|
||||||
cost: { type: "xp", value: 0 },
|
|
||||||
id: turret0.id,
|
|
||||||
type: ModuleType.MODULE_TYPE_TURRET,
|
|
||||||
},
|
|
||||||
])}
|
|
||||||
{tree(ModuleType.MODULE_TYPE_GUN, [
|
|
||||||
{
|
|
||||||
cost: { type: "xp", value: 0 },
|
|
||||||
id: gun0.id,
|
|
||||||
type: ModuleType.MODULE_TYPE_GUN,
|
|
||||||
},
|
|
||||||
])}
|
|
||||||
</Flex>
|
|
||||||
</ConfigurationChildWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
import { availableProvisions } from "@blitzkit/core";
|
|
||||||
import { Button, Flex, Heading } from "@radix-ui/themes";
|
|
||||||
import { api } from "../../../../api/dynamic";
|
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
|
||||||
import { Duel } from "../../../../stores/duel";
|
|
||||||
import { ProvisionsManager } from "../../../ProvisionsManager";
|
|
||||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
|
||||||
|
|
||||||
const provisionDefinitions = await api.provisions();
|
|
||||||
|
|
||||||
export function Provisions() {
|
|
||||||
const { tank, gun } = Duel.use((state) => state.protagonist);
|
|
||||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
|
||||||
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigurationChildWrapper>
|
|
||||||
<Flex gap="4" align="center">
|
|
||||||
<Heading size="4">
|
|
||||||
{strings.website.tools.tankopedia.configuration.provisions.title}
|
|
||||||
</Heading>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
color="red"
|
|
||||||
onClick={() => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.provisions = [];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.provisions.clear}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<ProvisionsManager
|
|
||||||
provisions={provisionsList.map((provision) => provision.id)}
|
|
||||||
selected={provisions}
|
|
||||||
disabled={tank.max_provisions === provisions.length}
|
|
||||||
onChange={(provisions) => {
|
|
||||||
Duel.mutate((draft) => {
|
|
||||||
draft.protagonist.provisions = provisions;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ConfigurationChildWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-82
@@ -1,82 +0,0 @@
|
|||||||
import { MAX_SKILL_LEVEL } from "@blitzkit/core";
|
|
||||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Heading,
|
|
||||||
IconButton,
|
|
||||||
Popover,
|
|
||||||
Text,
|
|
||||||
} from "@radix-ui/themes";
|
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
|
||||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
|
||||||
import { CrewSkillManager } from "../../../CrewSkillManager";
|
|
||||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
|
||||||
|
|
||||||
export function Skills() {
|
|
||||||
const skillLevels = Tankopedia.use((state) => state.skills);
|
|
||||||
const { strings } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigurationChildWrapper>
|
|
||||||
<Flex gap="4" align="center">
|
|
||||||
<Flex gap="2" align="center">
|
|
||||||
<Heading size="4">
|
|
||||||
{strings.website.tools.tankopedia.configuration.skills.title}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Popover.Root>
|
|
||||||
<Popover.Trigger>
|
|
||||||
<IconButton variant="ghost">
|
|
||||||
<InfoCircledIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Popover.Trigger>
|
|
||||||
|
|
||||||
<Popover.Content>
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
<Text>
|
|
||||||
{strings.website.tools.tankopedia.configuration.skills.info}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
color="red"
|
|
||||||
onClick={() => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
|
||||||
draft.skills[skill] = 0;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.skills.clear}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
|
||||||
draft.skills[skill] = MAX_SKILL_LEVEL;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.configuration.skills.maximize}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<CrewSkillManager
|
|
||||||
skillLevels={skillLevels}
|
|
||||||
onChange={(skills) => {
|
|
||||||
Tankopedia.mutate((draft) => {
|
|
||||||
draft.skills = skills;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ConfigurationChildWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ import { CanvasControl } from "../CanvasControl";
|
|||||||
import { InitialCameraAligner } from "../InitialCameraAligner";
|
import { InitialCameraAligner } from "../InitialCameraAligner";
|
||||||
import { Lighting, transitionEvent } from "../Lighting";
|
import { Lighting, transitionEvent } from "../Lighting";
|
||||||
import { SceneProps } from "../SceneProps";
|
import { SceneProps } from "../SceneProps";
|
||||||
import { ShotDisplay } from "../ShotDisplay";
|
import { ShotCard } from "../ShotCard";
|
||||||
import { SmartCanvas } from "../SmartCanvas";
|
import { SmartCanvas } from "../SmartCanvas";
|
||||||
import { TankDynamicArmor } from "../TankDynamicArmor";
|
import { TankDynamicArmor } from "../TankDynamicArmor";
|
||||||
import { TankModel } from "../TankModel";
|
import { TankModel } from "../TankModel";
|
||||||
@@ -75,7 +75,7 @@ export function TankopediaSandbox({
|
|||||||
gunModelDefinition.pitch!,
|
gunModelDefinition.pitch!,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
@@ -89,7 +89,7 @@ export function TankopediaSandbox({
|
|||||||
0,
|
0,
|
||||||
gunModelDefinition.pitch!,
|
gunModelDefinition.pitch!,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
@@ -103,7 +103,7 @@ export function TankopediaSandbox({
|
|||||||
0,
|
0,
|
||||||
gunModelDefinition.pitch!,
|
gunModelDefinition.pitch!,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
@@ -131,7 +131,7 @@ export function TankopediaSandbox({
|
|||||||
modelTransformEvent.last!.yaw,
|
modelTransformEvent.last!.yaw,
|
||||||
gunModelDefinition.pitch!,
|
gunModelDefinition.pitch!,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
@@ -191,7 +191,7 @@ function Content({ thicknessRange }: { thicknessRange: ThicknessRange }) {
|
|||||||
const { hasPbr } = useModel(tank.id);
|
const { hasPbr } = useModel(tank.id);
|
||||||
|
|
||||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||||
(state) => state.hideTankModelUnderArmor,
|
(state) => state.hideTankModelUnderArmor
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -208,7 +208,7 @@ function Content({ thicknessRange }: { thicknessRange: ThicknessRange }) {
|
|||||||
<TankStaticArmor thicknessRange={thicknessRange} />
|
<TankStaticArmor thicknessRange={thicknessRange} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ShotDisplay />
|
<ShotCard />
|
||||||
<ArmorPlateDisplay />
|
<ArmorPlateDisplay />
|
||||||
<CanvasAutoClear />
|
<CanvasAutoClear />
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ export function TankopediaSkinSwitcher() {
|
|||||||
hasSkinAnimations(tank.id, loadedSkin) ||
|
hasSkinAnimations(tank.id, loadedSkin) ||
|
||||||
hasSkinAnimations(tank.id, loadedAttachment);
|
hasSkinAnimations(tank.id, loadedAttachment);
|
||||||
|
|
||||||
console.log("hasAnimations", hasAnimations);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["skin-switcher"]} data-visible={isVisible}>
|
<div className={styles["skin-switcher"]} data-visible={isVisible}>
|
||||||
{groups.map((group, index) => (
|
{groups.map((group, index) => (
|
||||||
|
|||||||
@@ -23,10 +23,18 @@ export function Text({
|
|||||||
}: TextProps) {
|
}: TextProps) {
|
||||||
const scale = color === "gray" && lowContrast ? "white" : color;
|
const scale = color === "gray" && lowContrast ? "white" : color;
|
||||||
const isBW = color === "black" || color === "white";
|
const isBW = color === "black" || color === "white";
|
||||||
let power: string | number = isBW ? "a12" : 12;
|
let power = color === "gray" ? "12" : "11";
|
||||||
|
|
||||||
if (lowContrast) {
|
if (lowContrast) {
|
||||||
power = color === "gray" ? "a9" : isBW ? "a11" : 11;
|
power = "5";
|
||||||
|
|
||||||
|
if (color === "gray") {
|
||||||
|
power = "a9";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBW) {
|
||||||
|
power = "a11";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,9 +44,7 @@ export function Text({
|
|||||||
data-size={size}
|
data-size={size}
|
||||||
data-weight={weight}
|
data-weight={weight}
|
||||||
data-align={align === "left" ? undefined : align}
|
data-align={align === "left" ? undefined : align}
|
||||||
style={{
|
style={{ color: `var(--${scale}-${power})` }}
|
||||||
color: `var(--${scale}-${power})`,
|
|
||||||
}}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const { id, locale } = Astro.props;
|
|||||||
<ColorScale color="jade" />
|
<ColorScale color="jade" />
|
||||||
<ColorScale color="jade" alpha />
|
<ColorScale color="jade" alpha />
|
||||||
<ColorScale color="cyan" alpha />
|
<ColorScale color="cyan" alpha />
|
||||||
|
<ColorScale color="green" />
|
||||||
|
|
||||||
<Page {id} {locale} client:only="react">
|
<Page {id} {locale} client:only="react">
|
||||||
<Page slot="fallback" {id} {locale} skeleton />
|
<Page slot="fallback" {id} {locale} skeleton />
|
||||||
|
|||||||
Reference in new issue
Block a user