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%", "percentage": "%s1%",
"ghost_factory": "Ghost Factory", "ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail" "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 { 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 { Duel } from '../../../../../../../stores/duel';
import { VisualizerCard } from '../VisualizerCard'; import { VisualizerCard } from '../VisualizerCard';
import { VisualizerCornerStat } from '../VisualizerCornerStat';
const ANGLE_COEFFICIENT = 1 / 10; const ANGLE_COEFFICIENT = 1 / 10;
@@ -17,6 +28,17 @@ function c(thetaDeg: number, m = 1) {
} }
export function GunFlexibilityVisualizer() { 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 tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret); const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
@@ -66,7 +88,6 @@ export function GunFlexibilityVisualizer() {
} else if (b) { } else if (b) {
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`; d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
} else { } else {
console.log('this should log 0');
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`; d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
} }
@@ -125,7 +146,76 @@ export function GunFlexibilityVisualizer() {
d += `Z`; d += `Z`;
return ( 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 <svg
style={{ style={{
position: 'absolute', position: 'absolute',
@@ -138,15 +228,15 @@ export function GunFlexibilityVisualizer() {
> >
<path <path
fillRule="evenodd" fillRule="evenodd"
fill="var(--gray-5)" fill="var(--gray-6)"
stroke="var(--gray-7)" stroke="var(--gray-8)"
strokeWidth="1px" strokeWidth="1px"
vectorEffect="non-scaling-stroke" vectorEffect="non-scaling-stroke"
d={d} d={d}
/> />
<path <path
stroke="var(--gray-10)" stroke="var(--gray-11)"
fill="transparent" fill="transparent"
strokeWidth="1px" strokeWidth="1px"
strokeDasharray="0.5rem 0.5rem" 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" 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> </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> </VisualizerCard>
); );
} }
@@ -1,6 +1,6 @@
import { asset, isExplosive } from '@blitzkit/core'; import { asset, isExplosive } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n'; 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 { useRef, useState } from 'react';
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js'; import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { Var } from '../../../../../../../core/radix/var'; import { Var } from '../../../../../../../core/radix/var';
@@ -8,6 +8,7 @@ import { useLocale } from '../../../../../../../hooks/useLocale';
import { Duel } from '../../../../../../../stores/duel'; import { Duel } from '../../../../../../../stores/duel';
import type { StatsAcceptorProps } from '../TraverseVisualizer'; import type { StatsAcceptorProps } from '../TraverseVisualizer';
import { VisualizerCard } from '../VisualizerCard'; import { VisualizerCard } from '../VisualizerCard';
import { VisualizerCornerStat } from '../VisualizerCornerStat';
import { ShellSwitcher } from './components/ShellSwitcher'; import { ShellSwitcher } from './components/ShellSwitcher';
export function RicochetVisualizer({ stats }: StatsAcceptorProps) { export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
@@ -157,85 +158,51 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
/> />
</Box> </Box>
<Flex <VisualizerCornerStat
style={{ userSelect: 'none' }} label={strings.website.tools.tankopedia.visualizers.ricochet.angle}
gap="4" value={literals(strings.common.units.deg, [
position="absolute" radToDeg(Math.abs(angle)).toFixed(0),
top="0" ])}
left="0" side="top-left"
p="3" />
width="100%" <VisualizerCornerStat
justify="between" label={
> strings.website.tools.tankopedia.visualizers.ricochet.normalization
<Flex gap="2"> }
<Text color="gray" size="1"> value={
{strings.website.tools.tankopedia.visualizers.ricochet.angle} doesRicochet
</Text> ? '-'
<Code size="1" variant="ghost"> : literals(strings.common.units.deg, [
{literals(strings.common.units.deg, [ radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(0),
radToDeg(Math.abs(angle)).toFixed(0), ])
])} }
</Code> side="top-right"
</Flex> />
<VisualizerCornerStat
<Flex gap="2"> label={
<Text color="gray" size="1"> strings.website.tools.tankopedia.visualizers.ricochet
{ .effective_angle
strings.website.tools.tankopedia.visualizers.ricochet }
.normalization value={
} doesRicochet
</Text> ? '-'
<Code size="1" variant="ghost"> : literals(strings.common.units.deg, [
{doesRicochet radToDeg(Math.abs(effectiveAngle)).toFixed(0),
? '-' ])
: literals(strings.common.units.deg, [ }
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed( side="bottom-left"
0, />
), <VisualizerCornerStat
])} label={strings.website.tools.tankopedia.visualizers.ricochet.armor}
</Code> value={
</Flex> doesRicochet
</Flex> ? '-'
: literals(strings.common.units.percentage, [
<Flex armorPercentage.toFixed(0),
style={{ userSelect: 'none' }} ])
gap="4" }
position="absolute" side="bottom-right"
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>
{explodes && ( {explodes && (
<Flex <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); newPitch = clamp(newPitch, lowerPitch, upperPitch);
return [newPitch, newYaw]; return [newPitch, newYaw] as const;
} }
@@ -1,12 +1,13 @@
import type { EquipmentMatrix } from '../../stores/duel'; import type { EquipmentMatrix } from '../../stores/duel';
import { awaitableEquipmentDefinitions } from '../awaitables/equipmentDefinitions'; import { awaitableEquipmentDefinitions } from '../awaitables/equipmentDefinitions';
export async function hasEquipment( const equipmentDefinitions = await awaitableEquipmentDefinitions;
export function hasEquipment(
id: number, id: number,
equipmentPreset: string, equipmentPreset: string,
equipmentMatrix: EquipmentMatrix, equipmentMatrix: EquipmentMatrix,
) { ) {
const equipmentDefinitions = await awaitableEquipmentDefinitions;
const preset = equipmentDefinitions.presets[equipmentPreset]; const preset = equipmentDefinitions.presets[equipmentPreset];
return preset.slots.some((slot, index) => { return preset.slots.some((slot, index) => {
const row = Math.floor(index / 3); const row = Math.floor(index / 3);