feat: cursor

This commit is contained in:
TrèsAbhi committed 2025-08-09 13:30:53 -05:00
1 parent 21130b29f8
commit 40f5042896
6 files changed
+230 -89

No files matched your search

+6
View File
@@ -913,6 +913,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"depression": "Depression",
"elevation": "Elevation"
}
},
@@ -1,7 +1,18 @@
import { literals } from '@blitzkit/i18n';
import { Box } from '@radix-ui/themes';
import { useRef, useState } from 'react';
import { radToDeg } from 'three/src/math/MathUtils.js';
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
import { DEFAULT_PITCH_TRANSITION } from '../../../../../../../core/blitz/applyPitchYawLimits';
import {
applyPitchYawLimits,
DEFAULT_PITCH_TRANSITION,
} from '../../../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment';
import { Var } from '../../../../../../../core/radix/var';
import { useLocale } from '../../../../../../../hooks/useLocale';
import { Duel } from '../../../../../../../stores/duel';
import { VisualizerCard } from '../VisualizerCard';
import { VisualizerCornerStat } from '../VisualizerCornerStat';
const ANGLE_COEFFICIENT = 1 / 10;
@@ -17,6 +28,17 @@ function c(thetaDeg: number, m = 1) {
}
export function GunFlexibilityVisualizer() {
const { strings } = useLocale();
const [yaw, setYaw] = useState(0);
const [minPitch, setMinPitch] = useState(0);
const [maxPitch, setMaxPitch] = useState(0);
const container = useRef<HTMLDivElement>(null);
const highlighter = useRef<HTMLDivElement>(null);
const pointer = useRef<HTMLDivElement>(null);
const duelStore = Duel.useStore();
const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
@@ -66,7 +88,6 @@ export function GunFlexibilityVisualizer() {
} else if (b) {
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
} else {
console.log('this should log 0');
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
}
@@ -125,7 +146,76 @@ export function GunFlexibilityVisualizer() {
d += `Z`;
return (
<VisualizerCard>
<VisualizerCard
ref={container}
onPointerMove={(event) => {
if (!container.current || !highlighter.current || !pointer.current)
return;
const rect = container.current.getBoundingClientRect();
const u = event.clientX - rect.left - rect.width / 2;
const v = event.clientY - rect.top - rect.height / 2;
const yaw = Math.atan2(u, -v);
const {
equipmentMatrix,
tank: { equipment_preset },
} = duelStore.getState().protagonist;
const hasImprovedVerticalStabilizer = hasEquipment(
122,
equipment_preset,
equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = hasEquipment(
124,
equipment_preset,
equipmentMatrix,
);
const min = applyPitchYawLimits(
-Math.PI,
yaw,
p,
y,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
const max = applyPitchYawLimits(
Math.PI,
yaw,
p,
y,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
setYaw(min[1]);
setMinPitch(min[0]);
setMaxPitch(max[0]);
}}
>
<Box
ref={highlighter}
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
style={{
borderRadius: Var('radius-2'),
overflow: 'hidden',
}}
>
<Box
width="100%"
height="100%"
style={{
opacity: 2 ** -3,
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
}}
/>
</Box>
<svg
style={{
position: 'absolute',
@@ -138,15 +228,15 @@ export function GunFlexibilityVisualizer() {
>
<path
fillRule="evenodd"
fill="var(--gray-5)"
stroke="var(--gray-7)"
fill="var(--gray-6)"
stroke="var(--gray-8)"
strokeWidth="1px"
vectorEffect="non-scaling-stroke"
d={d}
/>
<path
stroke="var(--gray-10)"
stroke="var(--gray-11)"
fill="transparent"
strokeWidth="1px"
strokeDasharray="0.5rem 0.5rem"
@@ -154,6 +244,54 @@ export function GunFlexibilityVisualizer() {
d="M 0.5 0 A 0.5 0.5 0 1 1 -0.5 0 A 0.5 0.5 0 1 1 0.5 0 Z"
/>
</svg>
<Box
ref={pointer}
position="absolute"
top="50%"
left="50%"
width="0"
height="0"
style={{
transform: `translate(-50%, -50%) rotate(${yaw + Math.PI}rad)`,
}}
>
<Box
width="1pt"
height="14rem"
style={{
borderRadius: Var('radius-1'),
background: `linear-gradient(${Var('gray-a6')}, ${Var('gray-a11')})`,
transform: 'translateX(-50%)',
}}
/>
</Box>
<VisualizerCornerStat
label={
strings.website.tools.tankopedia.visualizers.flexibility.elevation
}
value={literals(strings.common.units.deg, [
radToDeg(maxPitch).toFixed(0),
])}
side="top-left"
/>
<VisualizerCornerStat
label={
strings.website.tools.tankopedia.visualizers.flexibility.depression
}
value={literals(strings.common.units.deg, [
radToDeg(-minPitch).toFixed(0),
])}
side="top-right"
/>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
side="bottom-left"
/>
</VisualizerCard>
);
}
@@ -1,6 +1,6 @@
import { asset, isExplosive } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import { Box, Code, Flex, Text } from '@radix-ui/themes';
import { Box, Flex, Text } from '@radix-ui/themes';
import { useRef, useState } from 'react';
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { Var } from '../../../../../../../core/radix/var';
@@ -8,6 +8,7 @@ import { useLocale } from '../../../../../../../hooks/useLocale';
import { Duel } from '../../../../../../../stores/duel';
import type { StatsAcceptorProps } from '../TraverseVisualizer';
import { VisualizerCard } from '../VisualizerCard';
import { VisualizerCornerStat } from '../VisualizerCornerStat';
import { ShellSwitcher } from './components/ShellSwitcher';
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
@@ -157,85 +158,51 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
/>
</Box>
<Flex
style={{ userSelect: 'none' }}
gap="4"
position="absolute"
top="0"
left="0"
p="3"
width="100%"
justify="between"
>
<Flex gap="2">
<Text color="gray" size="1">
{strings.website.tools.tankopedia.visualizers.ricochet.angle}
</Text>
<Code size="1" variant="ghost">
{literals(strings.common.units.deg, [
radToDeg(Math.abs(angle)).toFixed(0),
])}
</Code>
</Flex>
<Flex gap="2">
<Text color="gray" size="1">
{
strings.website.tools.tankopedia.visualizers.ricochet
.normalization
}
</Text>
<Code size="1" variant="ghost">
{doesRicochet
? '-'
: literals(strings.common.units.deg, [
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(
0,
),
])}
</Code>
</Flex>
</Flex>
<Flex
style={{ userSelect: 'none' }}
gap="4"
position="absolute"
bottom="0"
left="0"
p="3"
width="100%"
justify="between"
>
<Flex gap="2">
<Text color="gray" size="1">
{
strings.website.tools.tankopedia.visualizers.ricochet
.effective_angle
}
</Text>
<Code size="1" variant="ghost">
{doesRicochet
? '-'
: literals(strings.common.units.deg, [
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
])}
</Code>
</Flex>
<Flex gap="2">
<Text color="gray" size="1">
{strings.website.tools.tankopedia.visualizers.ricochet.armor}
</Text>
<Code size="1" variant="ghost">
{doesRicochet
? '-'
: literals(strings.common.units.percentage, [
armorPercentage.toFixed(0),
])}
</Code>
</Flex>
</Flex>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.ricochet.angle}
value={literals(strings.common.units.deg, [
radToDeg(Math.abs(angle)).toFixed(0),
])}
side="top-left"
/>
<VisualizerCornerStat
label={
strings.website.tools.tankopedia.visualizers.ricochet.normalization
}
value={
doesRicochet
? '-'
: literals(strings.common.units.deg, [
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(0),
])
}
side="top-right"
/>
<VisualizerCornerStat
label={
strings.website.tools.tankopedia.visualizers.ricochet
.effective_angle
}
value={
doesRicochet
? '-'
: literals(strings.common.units.deg, [
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
])
}
side="bottom-left"
/>
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.ricochet.armor}
value={
doesRicochet
? '-'
: literals(strings.common.units.percentage, [
armorPercentage.toFixed(0),
])
}
side="bottom-right"
/>
{explodes && (
<Flex
@@ -0,0 +1,29 @@
import { Code, Flex, Text } from '@radix-ui/themes';
interface Props {
label: string;
value: string;
side: `${'top' | 'bottom'}-${'left' | 'right'}`;
}
export function VisualizerCornerStat({ label, value, side }: Props) {
return (
<Flex
gap="2"
position="absolute"
top={side === 'top-left' || side === 'top-right' ? '3' : undefined}
bottom={
side === 'bottom-left' || side === 'bottom-right' ? '3' : undefined
}
left={side === 'top-left' || side === 'bottom-left' ? '3' : undefined}
right={side === 'top-right' || side === 'bottom-right' ? '3' : undefined}
>
<Text color="gray" size="1">
{label}
</Text>
<Code size="1" variant="ghost">
{value}
</Code>
</Flex>
);
}
@@ -86,5 +86,5 @@ export function applyPitchYawLimits(
newPitch = clamp(newPitch, lowerPitch, upperPitch);
return [newPitch, newYaw];
return [newPitch, newYaw] as const;
}
@@ -1,12 +1,13 @@
import type { EquipmentMatrix } from '../../stores/duel';
import { awaitableEquipmentDefinitions } from '../awaitables/equipmentDefinitions';
export async function hasEquipment(
const equipmentDefinitions = await awaitableEquipmentDefinitions;
export function hasEquipment(
id: number,
equipmentPreset: string,
equipmentMatrix: EquipmentMatrix,
) {
const equipmentDefinitions = await awaitableEquipmentDefinitions;
const preset = equipmentDefinitions.presets[equipmentPreset];
return preset.slots.some((slot, index) => {
const row = Math.floor(index / 3);