mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
Compare commits
2
Commits
1c4ef7c129
...
bc6257a0c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc6257a0c3 | ||
|
|
3d8cfc3aeb |
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -101,6 +101,24 @@ export const characteristics = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reloads: {
|
||||||
|
type: CharacteristicType.NumberList,
|
||||||
|
|
||||||
|
should_render({ gun }) {
|
||||||
|
return gun.gun_type!.$case === "auto_reloader";
|
||||||
|
},
|
||||||
|
|
||||||
|
compute({ gun, characteristic }) {
|
||||||
|
if (gun.gun_type!.$case !== "auto_reloader") return [];
|
||||||
|
|
||||||
|
const reloadCoefficient = characteristic("_reload_coefficient") as number;
|
||||||
|
|
||||||
|
return gun.gun_type!.value.shell_reloads.map(
|
||||||
|
(reload) => reload * reloadCoefficient,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
intra_clip: {
|
intra_clip: {
|
||||||
type: CharacteristicType.Number,
|
type: CharacteristicType.Number,
|
||||||
|
|
||||||
@@ -195,8 +213,6 @@ export const characteristics = {
|
|||||||
|
|
||||||
// module_damage_range: unavailable,
|
// module_damage_range: unavailable,
|
||||||
|
|
||||||
// reloads: unavailable,
|
|
||||||
|
|
||||||
clip_size: {
|
clip_size: {
|
||||||
type: CharacteristicType.Number,
|
type: CharacteristicType.Number,
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ export const characteristicsGroups: CharacteristicsGroup[] = [
|
|||||||
units: "s",
|
units: "s",
|
||||||
ranking: CharacteristicRanking.LowerIsBetter,
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
},
|
},
|
||||||
{ name: "reloads", decimals: 1, units: "s" },
|
{
|
||||||
|
name: "reloads",
|
||||||
|
decimals: 1,
|
||||||
|
units: "s",
|
||||||
|
ranking: CharacteristicRanking.LowerIsBetter,
|
||||||
|
},
|
||||||
|
|
||||||
{ toy: "reload" },
|
{ toy: "reload" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { literalsArray } from "@blitzkit/i18n/src/literals";
|
import { literalsArray } from "@blitzkit/i18n/src/literals";
|
||||||
import { type ReactNode } from "react";
|
import { Fragment, type ReactNode } from "react";
|
||||||
import { Text } from "../components/Text";
|
import { Text } from "../components/Text";
|
||||||
|
import { characteristics } from "../config/characteristics";
|
||||||
import type {
|
import type {
|
||||||
CharacteristicRenderConfig,
|
CharacteristicRenderConfig,
|
||||||
CharacteristicReturnType,
|
CharacteristicReturnType,
|
||||||
} from "../types/characteristics";
|
} from "../types/characteristics";
|
||||||
|
import { CharacteristicType } from "../types/characteristics";
|
||||||
import { useLocale } from "./useLocale";
|
import { useLocale } from "./useLocale";
|
||||||
import { useStrings } from "./useStrings";
|
import { useStrings } from "./useStrings";
|
||||||
|
|
||||||
@@ -21,39 +23,61 @@ export function useCharacteristicRenderer() {
|
|||||||
if (config.render)
|
if (config.render)
|
||||||
return config.render({ output: characteristic, strings });
|
return config.render({ output: characteristic, strings });
|
||||||
|
|
||||||
if (typeof characteristic === "number") {
|
function renderNumber(
|
||||||
if (Number.isFinite(characteristic)) {
|
value: number,
|
||||||
let value: ReactNode = characteristic;
|
units: CharacteristicRenderConfig["units"],
|
||||||
|
): ReactNode {
|
||||||
|
if (!Number.isFinite(value)) return `${value < 0 ? "-" : ""}∞`;
|
||||||
|
|
||||||
|
let rendered: ReactNode = value;
|
||||||
|
|
||||||
if (config.decimals !== undefined) {
|
if (config.decimals !== undefined) {
|
||||||
value *= 10 ** config.decimals;
|
rendered *= 10 ** config.decimals;
|
||||||
value = Math.round(value);
|
rendered = Math.round(rendered);
|
||||||
value /= 10 ** config.decimals;
|
rendered /= 10 ** config.decimals;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.localize) {
|
if (config.localize) {
|
||||||
value = value.toLocaleString(locale);
|
rendered = rendered.toLocaleString(locale);
|
||||||
} else {
|
} else {
|
||||||
value = value.toFixed(config.decimals);
|
rendered = rendered.toFixed(config.decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.units !== undefined) {
|
if (units !== undefined) {
|
||||||
value = literalsArray(strings.units[config.units], {
|
rendered = literalsArray(strings.units[units], {
|
||||||
value: <Text>{value}</Text>,
|
value: <Text>{rendered}</Text>,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${characteristic < 0 ? "-" : ""}∞`;
|
switch (characteristics[config.name].type) {
|
||||||
}
|
case CharacteristicType.Enum:
|
||||||
|
return characteristic as string;
|
||||||
|
|
||||||
if (typeof characteristic === "string") {
|
case CharacteristicType.Number:
|
||||||
return characteristic;
|
return renderNumber(characteristic as number, config.units);
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Unknown characteristic type");
|
case CharacteristicType.NumberList: {
|
||||||
|
const values = (characteristic as number[]).map((value, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{index > 0 && ", "}
|
||||||
|
{renderNumber(value, undefined)}
|
||||||
|
</Fragment>
|
||||||
|
));
|
||||||
|
|
||||||
|
if (config.units === undefined) return values;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{literalsArray(strings.units[config.units], {
|
||||||
|
value: <Text>{values}</Text>,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderCharacteristic;
|
return renderCharacteristic;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { characteristics as characteristicDefinitions } from "../config/characteristics";
|
||||||
import { characteristicsGroups } from "../config/characteristicsGroups";
|
import { characteristicsGroups } from "../config/characteristicsGroups";
|
||||||
import {
|
import {
|
||||||
CharacteristicRanking as CharacteristicRankingDirection,
|
CharacteristicRanking as CharacteristicRankingDirection,
|
||||||
|
CharacteristicType,
|
||||||
type CharacteristicName,
|
type CharacteristicName,
|
||||||
type CharacteristicRenderConfig,
|
type CharacteristicRenderConfig,
|
||||||
|
type CharacteristicReturnType,
|
||||||
type ComputedCharacteristics,
|
type ComputedCharacteristics,
|
||||||
} from "../types/characteristics";
|
} from "../types/characteristics";
|
||||||
|
|
||||||
@@ -24,6 +27,35 @@ export interface CharacteristicRank {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rankingValue(
|
||||||
|
name: CharacteristicName,
|
||||||
|
value?: CharacteristicReturnType,
|
||||||
|
) {
|
||||||
|
if (value === undefined) return;
|
||||||
|
|
||||||
|
switch (characteristicDefinitions[name].type) {
|
||||||
|
case CharacteristicType.Enum:
|
||||||
|
return;
|
||||||
|
|
||||||
|
case CharacteristicType.Number:
|
||||||
|
return value as number;
|
||||||
|
|
||||||
|
case CharacteristicType.NumberList: {
|
||||||
|
const values = value as number[];
|
||||||
|
|
||||||
|
if (values.length === 0) return;
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (const current of values) {
|
||||||
|
total += current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total / values.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function calculateRankings(
|
export function calculateRankings(
|
||||||
characteristics: ComputedCharacteristics,
|
characteristics: ComputedCharacteristics,
|
||||||
comparisons: ComputedCharacteristics[],
|
comparisons: ComputedCharacteristics[],
|
||||||
@@ -31,7 +63,9 @@ export function calculateRankings(
|
|||||||
const rankings = new Map<CharacteristicName, CharacteristicRank>();
|
const rankings = new Map<CharacteristicName, CharacteristicRank>();
|
||||||
|
|
||||||
for (const [name, value] of characteristics) {
|
for (const [name, value] of characteristics) {
|
||||||
if (typeof value !== "number" || name[0] === "_") continue;
|
const currentValue = rankingValue(name, value);
|
||||||
|
|
||||||
|
if (currentValue === undefined || name[0] === "_") continue;
|
||||||
|
|
||||||
const config = characteristicConfigs.get(name)!;
|
const config = characteristicConfigs.get(name)!;
|
||||||
|
|
||||||
@@ -41,13 +75,13 @@ export function calculateRankings(
|
|||||||
config.ranking === CharacteristicRankingDirection.LowerIsBetter;
|
config.ranking === CharacteristicRankingDirection.LowerIsBetter;
|
||||||
|
|
||||||
const values = comparisons
|
const values = comparisons
|
||||||
.map((comparison) => comparison.get(name))
|
.map((comparison) => rankingValue(name, comparison.get(name)))
|
||||||
.filter((other) => typeof other === "number");
|
.filter((other) => other !== undefined);
|
||||||
|
|
||||||
if (values.length === 0) continue;
|
if (values.length === 0) continue;
|
||||||
|
|
||||||
const betterCount = values.filter((other) =>
|
const betterCount = values.filter((other) =>
|
||||||
lowerIsBetter ? other < value : other > value,
|
lowerIsBetter ? other < currentValue : other > currentValue,
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
rankings.set(name, {
|
rankings.set(name, {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export type Characteristic = {
|
|||||||
type: CharacteristicType.Number;
|
type: CharacteristicType.Number;
|
||||||
compute(context: CharacteristicContext): number;
|
compute(context: CharacteristicContext): number;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: CharacteristicType.NumberList;
|
||||||
|
compute(context: CharacteristicContext): number[];
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CharacteristicScript = (
|
export type CharacteristicScript = (
|
||||||
@@ -63,6 +67,7 @@ export type CrewStatRole = "commander" | "loader" | "gunner" | "driver";
|
|||||||
export enum CharacteristicType {
|
export enum CharacteristicType {
|
||||||
Enum,
|
Enum,
|
||||||
Number,
|
Number,
|
||||||
|
NumberList,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CharacteristicRanking {
|
export enum CharacteristicRanking {
|
||||||
|
|||||||
Reference in new issue
Block a user