mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
reload visualizer
This commit is contained in:
1 parent
1c4ef7c129
commit
3d8cfc3aeb
10 files changed
+450
-9
No files matched your search
@@ -33,6 +33,10 @@
|
|||||||
"effective": "Effective",
|
"effective": "Effective",
|
||||||
"hull": "Hull",
|
"hull": "Hull",
|
||||||
"turret": "Turret"
|
"turret": "Turret"
|
||||||
|
},
|
||||||
|
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
@@ -86,5 +86,9 @@
|
|||||||
&[data-variant="surface"],
|
&[data-variant="surface"],
|
||||||
&[data-variant="outline"] {
|
&[data-variant="outline"] {
|
||||||
outline: var(--space-1) solid var(--accent-6);
|
outline: var(--space-1) solid var(--accent-6);
|
||||||
|
|
||||||
|
&[data-high-contrast="true"] {
|
||||||
|
outline: var(--space-1) solid var(--accent-11);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ export interface ButtonProps extends ComponentProps<"button"> {
|
|||||||
radius?: `${1 | 2 | 3}` | "max";
|
radius?: `${1 | 2 | 3}` | "max";
|
||||||
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
|
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
|
||||||
size?: "regular" | "minor" | "major";
|
size?: "regular" | "minor" | "major";
|
||||||
highContrast?: boolean; // TODO: add high contrast
|
highContrast?: boolean;
|
||||||
|
|
||||||
listChild?: boolean;
|
listChild?: boolean;
|
||||||
parentListChild?: boolean;
|
parentListChild?: boolean;
|
||||||
@@ -40,6 +40,7 @@ export function Button({
|
|||||||
listChild,
|
listChild,
|
||||||
parentListChild,
|
parentListChild,
|
||||||
disabled,
|
disabled,
|
||||||
|
highContrast,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
@@ -52,6 +53,7 @@ export function Button({
|
|||||||
data-size={size}
|
data-size={size}
|
||||||
data-list-child={listChild}
|
data-list-child={listChild}
|
||||||
data-parent-list-child={parentListChild}
|
data-parent-list-child={parentListChild}
|
||||||
|
data-high-contrast={highContrast}
|
||||||
data-disabled={disabled}
|
data-disabled={disabled}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
|
|||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maskImage:
|
maskImage:
|
||||||
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
|
'url(/media/toys/shell.png)',
|
||||||
maskSize: 'contain',
|
maskSize: 'contain',
|
||||||
maskRepeat: 'no-repeat',
|
maskRepeat: 'no-repeat',
|
||||||
maskPosition: 'center',
|
maskPosition: 'center',
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ export function Target() {
|
|||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
'url(/assets/images/tankopedia/visualizers/reload/background.jpg)',
|
'url(/media/toys/background.jpg)',
|
||||||
backgroundSize: '25rem',
|
backgroundSize: '25rem',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -222,3 +222,167 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reload {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: inherit;
|
||||||
|
|
||||||
|
.backdrop,
|
||||||
|
.target {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: url("/media/toys/background.jpg");
|
||||||
|
background-size: 25rem;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(var(--space-2)) contrast(1.2) brightness(0.5) saturate(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vignette {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(transparent 50%, var(--black-a8) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow,
|
||||||
|
.ring {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 65%;
|
||||||
|
height: 65%;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: var(--duration-brief);
|
||||||
|
|
||||||
|
&[data-shot="true"] {
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow {
|
||||||
|
filter: blur(var(--space-4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ring {
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: var(--orange-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.core {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
mask-image: radial-gradient(transparent 25%, black 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caret {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 10%;
|
||||||
|
height: 10%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remaining-time,
|
||||||
|
.total-time {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remaining-time {
|
||||||
|
right: calc(50% + var(--space-6));
|
||||||
|
transform: translate(50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-time {
|
||||||
|
left: calc(50% + var(--space-6));
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shells {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-track {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 90%;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--jade-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--space-1);
|
||||||
|
mask-image: url("/media/toys/shell.png");
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell-fill {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--jade-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: transparent;
|
||||||
|
transition: var(--duration-brief);
|
||||||
|
|
||||||
|
&[data-shot="true"] {
|
||||||
|
background: var(--white-a3);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip {
|
||||||
|
position: absolute;
|
||||||
|
top: 68%;
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { alias, isExplosive } from "@blitzkit/core";
|
import { alias, isExplosive } from "@blitzkit/core";
|
||||||
import { literals } from "@blitzkit/i18n";
|
import { literals } from "@blitzkit/i18n";
|
||||||
|
import { DoubleArrowRightIcon } from "@radix-ui/react-icons";
|
||||||
|
import { times } from "lodash-es";
|
||||||
import {
|
import {
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
@@ -12,9 +16,11 @@ 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 type { ComputedCharacteristics } from "../../types/characteristics";
|
||||||
|
import { Button } from "../Button";
|
||||||
import { Checkbox } from "../Checkbox";
|
import { Checkbox } from "../Checkbox";
|
||||||
import { Code } from "../Code";
|
import { Code } from "../Code";
|
||||||
import { SegmentedControl } from "../SegmentedControl";
|
import { SegmentedControl } from "../SegmentedControl";
|
||||||
|
import { ShellBorder } from "../Tankopedia/CharacteristicsSection/components/Characteristics/components/ReloadVisualizer/components/ShellBorder";
|
||||||
import styles from "./index.module.css";
|
import styles from "./index.module.css";
|
||||||
|
|
||||||
export function RicochetToy({ characteristics }: ToyProps) {
|
export function RicochetToy({ characteristics }: ToyProps) {
|
||||||
@@ -247,11 +253,7 @@ export function TraverseToy({ characteristics }: TraverseToyProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.traverse}>
|
<div className={styles.traverse}>
|
||||||
<div
|
<div ref={hull} className={styles.hull} data-turretless={isTurretless}>
|
||||||
ref={hull}
|
|
||||||
className={styles.hull}
|
|
||||||
data-turretless={isTurretless}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
draggable="false"
|
draggable="false"
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
@@ -304,3 +306,263 @@ export function TraverseToy({ characteristics }: TraverseToyProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RELOAD_PRECISION = 1;
|
||||||
|
|
||||||
|
interface ReloadData {
|
||||||
|
reloads: number[];
|
||||||
|
shells: number;
|
||||||
|
intraClip: number;
|
||||||
|
burstShells: number;
|
||||||
|
burstInterShell?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReloadToy({ characteristics }: ToyProps) {
|
||||||
|
const gun = useDuel("protagonist", DuelModule.Gun);
|
||||||
|
const strings = useStrings();
|
||||||
|
|
||||||
|
const reloadData = useMemo<ReloadData>(() => {
|
||||||
|
const reloadCoefficient = characteristics.get(
|
||||||
|
"_reload_coefficient",
|
||||||
|
) as number;
|
||||||
|
const intraClip =
|
||||||
|
gun.gun_type!.$case === "regular"
|
||||||
|
? 0
|
||||||
|
: (characteristics.get("intra_clip") as number);
|
||||||
|
|
||||||
|
let reloads: number[];
|
||||||
|
let shells: number;
|
||||||
|
|
||||||
|
switch (gun.gun_type!.$case) {
|
||||||
|
case "regular":
|
||||||
|
reloads = [characteristics.get("reload") as number];
|
||||||
|
shells = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_loader":
|
||||||
|
reloads = [characteristics.get("reload") as number];
|
||||||
|
shells = gun.gun_type!.value.shell_count;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "auto_reloader":
|
||||||
|
reloads = gun.gun_type!.value.shell_reloads.map(
|
||||||
|
(reload) => reload * reloadCoefficient,
|
||||||
|
);
|
||||||
|
shells = gun.gun_type!.value.shell_count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reloads,
|
||||||
|
shells,
|
||||||
|
intraClip,
|
||||||
|
burstShells: gun.burst?.count ?? 1,
|
||||||
|
burstInterShell: gun.burst?.interval,
|
||||||
|
};
|
||||||
|
}, [characteristics, gun]);
|
||||||
|
|
||||||
|
const glow = useRef<HTMLDivElement>(null);
|
||||||
|
const ring = useRef<HTMLDivElement>(null);
|
||||||
|
const core = useRef<HTMLDivElement>(null);
|
||||||
|
const remainingTime = useRef<HTMLSpanElement>(null);
|
||||||
|
const totalTime = useRef<HTMLSpanElement>(null);
|
||||||
|
const flash = useRef<HTMLDivElement>(null);
|
||||||
|
const shellBars = useRef<Array<HTMLDivElement | null>>([]);
|
||||||
|
const state = useRef(0);
|
||||||
|
|
||||||
|
const shoot = useCallback(() => {
|
||||||
|
for (const element of [flash.current, ring.current, glow.current]) {
|
||||||
|
if (!element) continue;
|
||||||
|
|
||||||
|
element.dataset.shot = "true";
|
||||||
|
|
||||||
|
void element.offsetWidth;
|
||||||
|
delete element.dataset.shot;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancel = false;
|
||||||
|
let lastTime = performance.now() / 1000;
|
||||||
|
let lastState = state.current;
|
||||||
|
|
||||||
|
state.current = 0;
|
||||||
|
|
||||||
|
function frame() {
|
||||||
|
const reloadThreshold = reloadData.reloads.length;
|
||||||
|
const now = performance.now() / 1000;
|
||||||
|
const deltaTime = now - lastTime;
|
||||||
|
|
||||||
|
let reload = 0;
|
||||||
|
let shells: number[];
|
||||||
|
let interval = 0;
|
||||||
|
|
||||||
|
if (state.current < reloadThreshold) {
|
||||||
|
const currentReload = reloadData.reloads[Math.floor(state.current)];
|
||||||
|
|
||||||
|
if (currentReload === undefined) {
|
||||||
|
throw new Error("Missing reload duration for current reload stage");
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = currentReload;
|
||||||
|
state.current = Math.min(
|
||||||
|
reloadThreshold,
|
||||||
|
state.current + deltaTime / interval,
|
||||||
|
);
|
||||||
|
reload = state.current % 1;
|
||||||
|
shells = Array(reloadData.shells).fill(reload);
|
||||||
|
|
||||||
|
if (reloadThreshold > 1) {
|
||||||
|
shells = shells.map((_, index) =>
|
||||||
|
Math.max(0, state.current - reloadData.shells + index + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.current >= reloadThreshold) shoot();
|
||||||
|
} else if (state.current < reloadData.shells + reloadThreshold - 1) {
|
||||||
|
if (
|
||||||
|
reloadData.burstShells === 1 ||
|
||||||
|
Math.floor(state.current) % reloadData.burstShells === 0
|
||||||
|
) {
|
||||||
|
interval = reloadData.intraClip;
|
||||||
|
} else {
|
||||||
|
if (reloadData.burstInterShell === undefined) {
|
||||||
|
throw new Error("Missing burst interval for firing sequence");
|
||||||
|
}
|
||||||
|
|
||||||
|
interval = reloadData.burstInterShell;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.current = Math.min(
|
||||||
|
reloadData.shells + reloadThreshold,
|
||||||
|
state.current + deltaTime / interval,
|
||||||
|
);
|
||||||
|
reload = state.current % 1;
|
||||||
|
|
||||||
|
shells =
|
||||||
|
reloadThreshold > 1
|
||||||
|
? times(reloadData.shells, (index) =>
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(index + 1 + reloadThreshold - state.current),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: times(reloadData.shells, (index) =>
|
||||||
|
Number(index > state.current - 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastState % 1 > state.current % 1) shoot();
|
||||||
|
} else {
|
||||||
|
state.current = 0;
|
||||||
|
shells = Array(reloadData.shells).fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interval > 0 && totalTime.current) {
|
||||||
|
totalTime.current.textContent = interval.toFixed(RELOAD_PRECISION);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interval > 0 && remainingTime.current) {
|
||||||
|
remainingTime.current.textContent = ((1 - reload) * interval).toFixed(
|
||||||
|
RELOAD_PRECISION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, bar] of shellBars.current.entries()) {
|
||||||
|
if (bar) bar.style.width = `${(shells[index] ?? 0) * 100}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ring.current && glow.current && core.current) {
|
||||||
|
const angle = reload * 2 * Math.PI;
|
||||||
|
|
||||||
|
ring.current.style.background = `conic-gradient(var(--amber-9) 0 ${angle}rad, var(--orange-7) ${angle}rad)`;
|
||||||
|
core.current.style.background = `conic-gradient(var(--amber-6) 0 ${angle}rad, var(--orange-3) ${angle}rad)`;
|
||||||
|
glow.current.style.background = `conic-gradient(var(--amber-9) 0 ${angle}rad, transparent ${angle}rad)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cancel) requestAnimationFrame(frame);
|
||||||
|
|
||||||
|
lastTime = now;
|
||||||
|
lastState = state.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancel = true;
|
||||||
|
};
|
||||||
|
}, [reloadData, shoot]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.reload}>
|
||||||
|
<div className={styles.backdrop} />
|
||||||
|
<div className={styles.vignette} />
|
||||||
|
|
||||||
|
<div className={styles.glow} ref={glow} />
|
||||||
|
<div className={styles.ring} ref={ring}>
|
||||||
|
<div className={styles.core}>
|
||||||
|
<div className={styles.target} />
|
||||||
|
<div className={styles.fill} ref={core} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className={styles.caret}
|
||||||
|
src="/assets/images/tankopedia/visualizers/reload/caret.png"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Code
|
||||||
|
variant="ghost"
|
||||||
|
className={styles["remaining-time"]}
|
||||||
|
ref={remainingTime}
|
||||||
|
>
|
||||||
|
{(0).toFixed(RELOAD_PRECISION)}
|
||||||
|
</Code>
|
||||||
|
<Code variant="ghost" className={styles["total-time"]} ref={totalTime}>
|
||||||
|
{(0).toFixed(RELOAD_PRECISION)}
|
||||||
|
</Code>
|
||||||
|
|
||||||
|
{reloadData.shells > 1 && (
|
||||||
|
<div className={styles.shells} aria-hidden="true">
|
||||||
|
{Array.from({ length: reloadData.shells }, (_, index) => (
|
||||||
|
<div
|
||||||
|
className={styles["shell-track"]}
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
transform: `rotate(${135 - 7 * (index - (reloadData.shells - 1) / 2)}deg)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.shell}>
|
||||||
|
<ShellBorder className={styles.border} />
|
||||||
|
<div className={styles.mask}>
|
||||||
|
<div
|
||||||
|
className={styles["shell-fill"]}
|
||||||
|
ref={(element) => {
|
||||||
|
shellBars.current[index] = element;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.flash} ref={flash} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.skip}
|
||||||
|
size="minor"
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
aria-label={strings.tanks.toys.reload.skip}
|
||||||
|
onClick={() => {
|
||||||
|
state.current = Math.floor(state.current + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.tanks.toys.reload.skip}
|
||||||
|
<DoubleArrowRightIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { RicochetToy, TraverseToy } from "../components/TankopediaToys";
|
import {
|
||||||
|
ReloadToy,
|
||||||
|
RicochetToy,
|
||||||
|
TraverseToy,
|
||||||
|
} from "../components/TankopediaToys";
|
||||||
import type { ComputedCharacteristics } from "../types/characteristics";
|
import type { ComputedCharacteristics } from "../types/characteristics";
|
||||||
|
|
||||||
export interface ToyProps {
|
export interface ToyProps {
|
||||||
@@ -9,4 +13,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,
|
traverse: TraverseToy,
|
||||||
|
reload: ReloadToy,
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user