mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
traverse toy
This commit is contained in:
1 parent
de80519f06
commit
cae63a70b0
15 files changed
+344
-34
No files matched your search
@@ -20,14 +20,6 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tanks": {
|
"tanks": {
|
||||||
"characteristics": {
|
|
||||||
"compare": {
|
|
||||||
"label": "Compare against",
|
|
||||||
"class": "Tier {tier} {class}",
|
|
||||||
"tier": "All tier {tier}",
|
|
||||||
"all": "All tanks"
|
|
||||||
},
|
|
||||||
|
|
||||||
"toys": {
|
"toys": {
|
||||||
"ricochet": {
|
"ricochet": {
|
||||||
"angle": "Angle",
|
"angle": "Angle",
|
||||||
@@ -35,9 +27,23 @@
|
|||||||
"effective": "Effective",
|
"effective": "Effective",
|
||||||
"armor": "Armor",
|
"armor": "Armor",
|
||||||
"no_ricochet": "Explosive shells don't ricochet"
|
"no_ricochet": "Explosive shells don't ricochet"
|
||||||
|
},
|
||||||
|
|
||||||
|
"traverse": {
|
||||||
|
"effective": "Effective",
|
||||||
|
"hull": "Hull",
|
||||||
|
"turret": "Turret"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"characteristics": {
|
||||||
|
"compare": {
|
||||||
|
"label": "Compare against",
|
||||||
|
"class": "Tier {tier} {class}",
|
||||||
|
"tier": "All tier {tier}",
|
||||||
|
"all": "All tanks"
|
||||||
|
},
|
||||||
|
|
||||||
"titles": {
|
"titles": {
|
||||||
"gun": "Gun",
|
"gun": "Gun",
|
||||||
"clip": "Clip",
|
"clip": "Clip",
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,19 @@
|
|||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
.control {
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
accent-color: var(--accent-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&,
|
||||||
|
* {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useId, type ReactNode } from "react";
|
||||||
|
import { Text } from "../Text";
|
||||||
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
|
export interface CheckboxProps {
|
||||||
|
checked: boolean;
|
||||||
|
onCheckedChange(checked: boolean): void;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Checkbox({
|
||||||
|
checked,
|
||||||
|
onCheckedChange,
|
||||||
|
children,
|
||||||
|
}: CheckboxProps) {
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.checkbox}>
|
||||||
|
<input
|
||||||
|
className={styles.control}
|
||||||
|
type="checkbox"
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onChange={(event) => onCheckedChange(event.target.checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{children && (
|
||||||
|
<label htmlFor={id}>
|
||||||
|
<Text lowContrast>{children}</Text>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,23 +1,53 @@
|
|||||||
.options {
|
.options {
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
border-radius: var(--radius-max);
|
border-radius: var(--radius-max);
|
||||||
|
|
||||||
background-color: var(--gray-3);
|
&[data-size="minor"] {
|
||||||
|
.option {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
background-color: var(--accent-3);
|
|
||||||
outline: var(--outline-focus);
|
outline: var(--outline-focus);
|
||||||
border-radius: var(--radius-max);
|
border-radius: var(--radius-max);
|
||||||
|
|
||||||
transition: var(--duration-brief);
|
transition: var(--duration-brief);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-variant="solid"] {
|
||||||
|
background-color: var(--gray-3);
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
background-color: var(--accent-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-variant="underline"] {
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
background: linear-gradient(transparent, var(--accent-a2));
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
border-bottom: var(--space-1) solid var(--accent-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
export interface SegmentedControlOption<Value extends string | number> {
|
export interface SegmentedControlOption {
|
||||||
value: Value;
|
value: string;
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SegmentedControlProps<Value extends string | number> {
|
export interface SegmentedControlProps {
|
||||||
options: readonly SegmentedControlOption<Value>[];
|
options: readonly SegmentedControlOption[];
|
||||||
value: Value;
|
value: string;
|
||||||
onValueChange(value: Value): void;
|
onValueChange(value: string): void;
|
||||||
|
size?: "regular" | "minor";
|
||||||
|
variant?: "solid" | "underline";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SegmentedControl<Value extends string | number>({
|
export function SegmentedControl({
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
}: SegmentedControlProps<Value>) {
|
size = "regular",
|
||||||
|
variant = "solid",
|
||||||
|
}: SegmentedControlProps) {
|
||||||
const selectedIndex = options.findIndex((option) => option.value === value);
|
const selectedIndex = options.findIndex((option) => option.value === value);
|
||||||
const segmentWidth = 100 / options.length;
|
const segmentWidth = 100 / options.length;
|
||||||
|
|
||||||
@@ -24,7 +28,11 @@ export function SegmentedControl<Value extends string | number>({
|
|||||||
<div
|
<div
|
||||||
className={styles.options}
|
className={styles.options}
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
style={{ gridTemplateColumns: `repeat(${options.length}, 1fr)` }}
|
data-size={size}
|
||||||
|
data-variant={variant}
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${options.length}, minmax(0, 1fr))`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.indicator}
|
className={styles.indicator}
|
||||||
|
|||||||
+1
-1
@@ -296,7 +296,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
opacity: 2 ** -3,
|
||||||
backgroundImage: `url(/media/random/armor-hash.png)`,
|
backgroundImage: `url(/media/toys/armor-hash.png)`,
|
||||||
boxShadow: "inset 0 0 4rem 1rem black",
|
boxShadow: "inset 0 0 4rem 1rem black",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+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(/media/random/armor-hash.png)`,
|
backgroundImage: `url(/media/toys/armor-hash.png)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -198,20 +198,23 @@ function CompareOptions() {
|
|||||||
|
|
||||||
const options = useRef([
|
const options = useRef([
|
||||||
{
|
{
|
||||||
value: TankopediaRelativeAgainst.ClassAndTier,
|
value: `${TankopediaRelativeAgainst.ClassAndTier}`,
|
||||||
|
relativeAgainst: TankopediaRelativeAgainst.ClassAndTier,
|
||||||
label: literals(strings.tanks.characteristics.compare.class, {
|
label: literals(strings.tanks.characteristics.compare.class, {
|
||||||
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
||||||
class: strings.common.tank_class_short_plural_lowercase[tank.class],
|
class: strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TankopediaRelativeAgainst.Tier,
|
value: `${TankopediaRelativeAgainst.Tier}`,
|
||||||
|
relativeAgainst: TankopediaRelativeAgainst.Tier,
|
||||||
label: literals(strings.tanks.characteristics.compare.tier, {
|
label: literals(strings.tanks.characteristics.compare.tier, {
|
||||||
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
tier: TIER_ROMAN_NUMERALS[tank.tier],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TankopediaRelativeAgainst.All,
|
value: `${TankopediaRelativeAgainst.All}`,
|
||||||
|
relativeAgainst: TankopediaRelativeAgainst.All,
|
||||||
label: strings.tanks.characteristics.compare.all,
|
label: strings.tanks.characteristics.compare.all,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -221,10 +224,10 @@ function CompareOptions() {
|
|||||||
<Text lowContrast>{strings.tanks.characteristics.compare.label}</Text>
|
<Text lowContrast>{strings.tanks.characteristics.compare.label}</Text>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
options={options.current}
|
options={options.current}
|
||||||
value={relativeAgainst}
|
value={`${relativeAgainst}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
Tankopedia.mutate((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.relative_against = value;
|
draft.relative_against = Number(value);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0.125;
|
opacity: 0.125;
|
||||||
background-image: url("/media/random/armor-hash.png");
|
background-image: url("/media/toys/armor-hash.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
.zones {
|
.zones {
|
||||||
@@ -149,3 +149,76 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background: radial-gradient(var(--black-a7), var(--black-a11));
|
background: radial-gradient(var(--black-a7), var(--black-a11));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.traverse-visualizer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: inherit;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.traverse-hull {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 12rem;
|
||||||
|
height: 12rem;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
&[data-turretless="true"] {
|
||||||
|
width: 22rem;
|
||||||
|
height: 22rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.traverse-hull-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(0 0 var(--space-4) var(--black-a7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.traverse-turret {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 27rem;
|
||||||
|
height: 27rem;
|
||||||
|
object-fit: contain;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
filter: drop-shadow(0 0 var(--space-4) var(--black-a7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.traverse-toggles {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-4);
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-5);
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 var(--space-5);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.traverse-terrain {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.traverse-effective {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(var(--space-2) + 3rem);
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--gray-11);
|
||||||
|
font-size: var(--font-size-minor);
|
||||||
|
white-space: nowrap;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { alias, isExplosive } from "@blitzkit/core";
|
import { alias, isExplosive } from "@blitzkit/core";
|
||||||
import { literals } from "@blitzkit/i18n";
|
import { literals } from "@blitzkit/i18n";
|
||||||
import {
|
import {
|
||||||
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
@@ -10,7 +11,10 @@ import { clamp, degToRad, radToDeg } from "three/src/math/MathUtils.js";
|
|||||||
import type { ToyProps } from "../../config/toys";
|
import type { ToyProps } from "../../config/toys";
|
||||||
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
import { DuelModule, useDuel } from "../../hooks/useDuel";
|
||||||
import { useStrings } from "../../hooks/useStrings";
|
import { useStrings } from "../../hooks/useStrings";
|
||||||
|
import type { ComputedCharacteristics } from "../../types/characteristics";
|
||||||
|
import { Checkbox } from "../Checkbox";
|
||||||
import { Code } from "../Code";
|
import { Code } from "../Code";
|
||||||
|
import { SegmentedControl } from "../SegmentedControl";
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
export function RicochetToy({ characteristics }: ToyProps) {
|
export function RicochetToy({ characteristics }: ToyProps) {
|
||||||
@@ -78,7 +82,6 @@ export function RicochetToy({ characteristics }: ToyProps) {
|
|||||||
<div className={styles["shell-path-line"]} />
|
<div className={styles["shell-path-line"]} />
|
||||||
<img
|
<img
|
||||||
className={styles.shell}
|
className={styles.shell}
|
||||||
alt=""
|
|
||||||
src={alias("api", `/icons/shells/${shell.icon}.webp`)}
|
src={alias("api", `/icons/shells/${shell.icon}.webp`)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,14 +98,14 @@ export function RicochetToy({ characteristics }: ToyProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CornerStat
|
<CornerStat
|
||||||
label={strings.tanks.characteristics.toys.ricochet.angle}
|
label={strings.tanks.toys.ricochet.angle}
|
||||||
value={literals(strings.common.units.deg, {
|
value={literals(strings.common.units.deg, {
|
||||||
value: radToDeg(Math.abs(angle)).toFixed(0),
|
value: radToDeg(Math.abs(angle)).toFixed(0),
|
||||||
})}
|
})}
|
||||||
position={CornerStatPosition.TopLeft}
|
position={CornerStatPosition.TopLeft}
|
||||||
/>
|
/>
|
||||||
<CornerStat
|
<CornerStat
|
||||||
label={strings.tanks.characteristics.toys.ricochet.normalization}
|
label={strings.tanks.toys.ricochet.normalization}
|
||||||
value={
|
value={
|
||||||
doesRicochet
|
doesRicochet
|
||||||
? "-"
|
? "-"
|
||||||
@@ -115,7 +118,7 @@ export function RicochetToy({ characteristics }: ToyProps) {
|
|||||||
position={CornerStatPosition.TopRight}
|
position={CornerStatPosition.TopRight}
|
||||||
/>
|
/>
|
||||||
<CornerStat
|
<CornerStat
|
||||||
label={strings.tanks.characteristics.toys.ricochet.effective}
|
label={strings.tanks.toys.ricochet.effective}
|
||||||
value={
|
value={
|
||||||
doesRicochet
|
doesRicochet
|
||||||
? "-"
|
? "-"
|
||||||
@@ -126,7 +129,7 @@ export function RicochetToy({ characteristics }: ToyProps) {
|
|||||||
position={CornerStatPosition.BottomLeft}
|
position={CornerStatPosition.BottomLeft}
|
||||||
/>
|
/>
|
||||||
<CornerStat
|
<CornerStat
|
||||||
label={strings.tanks.characteristics.toys.ricochet.armor}
|
label={strings.tanks.toys.ricochet.armor}
|
||||||
value={
|
value={
|
||||||
doesRicochet
|
doesRicochet
|
||||||
? "-"
|
? "-"
|
||||||
@@ -139,7 +142,7 @@ export function RicochetToy({ characteristics }: ToyProps) {
|
|||||||
|
|
||||||
{explodes && (
|
{explodes && (
|
||||||
<div className={styles["explosive-overlay"]}>
|
<div className={styles["explosive-overlay"]}>
|
||||||
{strings.tanks.characteristics.toys.ricochet.no_ricochet}
|
{strings.tanks.toys.ricochet.no_ricochet}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -169,3 +172,135 @@ function CornerStat({ label, value, position }: CornerStatProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const terrains = ["hard", "medium", "soft"] as const;
|
||||||
|
type Terrain = (typeof terrains)[number];
|
||||||
|
|
||||||
|
interface TraverseToyProps {
|
||||||
|
characteristics: ComputedCharacteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TraverseToy({ characteristics }: TraverseToyProps) {
|
||||||
|
const hull = useRef<HTMLDivElement>(null);
|
||||||
|
const turret = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const [terrain, setTerrain] = useState<Terrain>("hard");
|
||||||
|
const [rotateHull, setRotateHull] = useState(true);
|
||||||
|
const [rotateTurret, setRotateTurret] = useState(true);
|
||||||
|
|
||||||
|
const turretAngle = useRef(0);
|
||||||
|
const hullAngle = useRef(0);
|
||||||
|
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
const hullSpeed = characteristics.get(
|
||||||
|
`hull_traverse_speed_${terrain}`,
|
||||||
|
) as number;
|
||||||
|
const turretTraverseSpeed = characteristics.get(
|
||||||
|
"turret_traverse_speed",
|
||||||
|
) as number;
|
||||||
|
|
||||||
|
const turretSpeed = rotateTurret ? turretTraverseSpeed : 0;
|
||||||
|
const hullSpeedEffective = rotateHull ? hullSpeed : 0;
|
||||||
|
|
||||||
|
const isTurretless =
|
||||||
|
characteristics.has("azimuth_left") || characteristics.has("azimuth_right");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancel = false;
|
||||||
|
const turretSpeedRad = (turretSpeed * Math.PI) / 180;
|
||||||
|
const hullSpeedRad = (hullSpeedEffective * Math.PI) / 180;
|
||||||
|
let lastT = Date.now() / 1000;
|
||||||
|
|
||||||
|
function frame() {
|
||||||
|
const t = Date.now() / 1000;
|
||||||
|
const dt = t - lastT;
|
||||||
|
lastT = t;
|
||||||
|
|
||||||
|
if (turret.current) {
|
||||||
|
turretAngle.current =
|
||||||
|
(turretAngle.current + turretSpeedRad * dt) % (2 * Math.PI);
|
||||||
|
turret.current.style.transform = `translate(-50%, -50%) rotate(${-turretAngle.current}rad)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hull.current) {
|
||||||
|
hullAngle.current =
|
||||||
|
(hullAngle.current + hullSpeedRad * dt) % (2 * Math.PI);
|
||||||
|
hull.current.style.transform = `translate(-50%, -50%) rotate(${-hullAngle.current}rad)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancel) requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancel = true;
|
||||||
|
};
|
||||||
|
}, [hullSpeedEffective, turretSpeed]);
|
||||||
|
|
||||||
|
const terrainOptions = terrains.map((terrain) => ({
|
||||||
|
value: terrain,
|
||||||
|
label:
|
||||||
|
strings.tanks.characteristics.items[`hull_traverse_speed_${terrain}`],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["traverse-visualizer"]}>
|
||||||
|
<div
|
||||||
|
ref={hull}
|
||||||
|
className={styles["traverse-hull"]}
|
||||||
|
data-turretless={isTurretless}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
className={styles["traverse-hull-image"]}
|
||||||
|
src={`/media/toys/hull${isTurretless ? "-only" : ""}.png`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isTurretless && (
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
ref={turret}
|
||||||
|
className={styles["traverse-turret"]}
|
||||||
|
src="/media/toys/turret.png"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["traverse-toggles"]}>
|
||||||
|
{!isTurretless && (
|
||||||
|
<>
|
||||||
|
<Checkbox checked={rotateHull} onCheckedChange={setRotateHull}>
|
||||||
|
{strings.tanks.toys.traverse.hull}
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox checked={rotateTurret} onCheckedChange={setRotateTurret}>
|
||||||
|
{strings.tanks.toys.traverse.turret}
|
||||||
|
</Checkbox>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["traverse-terrain"]}>
|
||||||
|
<SegmentedControl
|
||||||
|
options={terrainOptions}
|
||||||
|
value={terrain}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const nextTerrain = terrains.find((terrain) => terrain === value);
|
||||||
|
if (nextTerrain) setTerrain(nextTerrain);
|
||||||
|
}}
|
||||||
|
variant="underline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["traverse-effective"]}>
|
||||||
|
<span>{strings.tanks.toys.traverse.effective}</span>
|
||||||
|
<Code size="minor">
|
||||||
|
{literals(strings.units.deg_s, {
|
||||||
|
value: (turretSpeed + hullSpeedEffective).toFixed(1),
|
||||||
|
})}
|
||||||
|
</Code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { RicochetToy } from "../components/TankopediaToys";
|
import { RicochetToy, TraverseToy } from "../components/TankopediaToys";
|
||||||
import type { ComputedCharacteristics } from "../types/characteristics";
|
import type { ComputedCharacteristics } from "../types/characteristics";
|
||||||
|
|
||||||
export interface ToyProps {
|
export interface ToyProps {
|
||||||
@@ -8,4 +8,5 @@ export interface ToyProps {
|
|||||||
|
|
||||||
export const toys: Record<string, (props: ToyProps) => ReactNode> = {
|
export const toys: Record<string, (props: ToyProps) => ReactNode> = {
|
||||||
ricochet: RicochetToy,
|
ricochet: RicochetToy,
|
||||||
|
traverse: TraverseToy,
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user