feat: cursor

This commit is contained in:
TrèsAbhi committed 2025-08-01 12:25:09 -05:00
1 parent 097f2792ae
commit e1b5b21fae
11 files changed
+181 -110

No files matched your search

+1 -1
View File
@@ -1083,7 +1083,6 @@
"hull_traverse_speed": "Hull traverse speed", "hull_traverse_speed": "Hull traverse speed",
"camouflage": "Camouflage", "camouflage": "Camouflage",
"reload": "Reload", "reload": "Reload",
"crewCount": "Crew count", "crewCount": "Crew count",
"shellNormalization": "Normalization", "shellNormalization": "Normalization",
"shellRicochet": "Ricochet", "shellRicochet": "Ricochet",
@@ -1097,6 +1096,7 @@
"caliber": "Caliber", "caliber": "Caliber",
"penetration": "Penetration", "penetration": "Penetration",
"damage": "Damage", "damage": "Damage",
"damageWithoutAssault": "Damage without assault shells",
"clipDamage": "Clipping potential", "clipDamage": "Clipping potential",
"moduleDamage": "Module damage", "moduleDamage": "Module damage",
"explosionRadius": "Explosion radius", "explosionRadius": "Explosion radius",
@@ -1,66 +0,0 @@
import { DragHandleDots2Icon } from '@radix-ui/react-icons';
import { Box, Flex, Text } from '@radix-ui/themes';
import { useState } from 'react';
import { Var } from '../../../../../../../../core/radix/var';
export function AssaultCursor() {
const [x, setX] = useState(0.5);
return (
<Box
position="absolute"
top="0"
left={`${x * 100}%`}
height="100%"
width="3pt"
style={{
transform: 'translateX(-50%)',
backgroundColor: Var('accent-11'),
}}
>
<Flex
align="center"
direction="column"
position="absolute"
left="50%"
bottom="100%"
style={{ transform: 'translateX(-50%)', cursor: 'grab' }}
>
<Flex
align="center"
justify="center"
style={{
width: '1.5rem',
height: '1.5rem',
backgroundColor: Var('accent-9'),
borderRadius: `50% 50% ${Var('radius-1')} ${Var('radius-1')}`,
}}
>
<Text style={{ display: 'flex' }} highContrast color="purple">
<DragHandleDots2Icon />
</Text>
</Flex>
<Box
height="0.5rem"
width="2px"
style={{
backgroundColor: Var('accent-9'),
}}
/>
</Flex>
<Box
position="absolute"
left="50%"
top="0"
height="100%"
width="1pt"
style={{
transform: 'translateX(-50%)',
backgroundColor: Var('accent-9'),
}}
/>
</Box>
);
}
@@ -0,0 +1,7 @@
#assault-cursor {
cursor: grab;
}
#assault-cursor:active {
cursor: grabbing;
}
@@ -0,0 +1,107 @@
import { DragHandleDots2Icon } from '@radix-ui/react-icons';
import { Box, Flex, Text } from '@radix-ui/themes';
import { useEffect, useRef, useState } from 'react';
import { clamp } from 'three/src/math/MathUtils.js';
import { fakeLogInverse } from '../../../../../../../../../core/blitzkit/fakeLog';
import { Var } from '../../../../../../../../../core/radix/var';
import { Duel } from '../../../../../../../../../stores/duel';
import './index.css';
interface Props {
maxDistance: number;
maxFakeDistance: number;
}
export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
const [x, setX] = useState(0.5);
const cursor = useRef<HTMLDivElement>(null);
const mutateDuel = Duel.useMutation();
useEffect(() => {
let localX = x;
function handlePointerDown(event: PointerEvent) {
event.preventDefault();
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
const parent = cursor.current?.parentElement;
if (!parent) return;
const width = parent.clientWidth;
setX((state) => {
localX = clamp(state + event.movementX / width, 0, 1);
return localX;
});
}
function handlePointerUp() {
mutateDuel((draft) => {
draft.protagonist.assaultDistance =
fakeLogInverse(localX * maxFakeDistance, 2) * maxDistance;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
cursor.current?.addEventListener('pointerdown', handlePointerDown);
return () => {
cursor.current?.removeEventListener('pointerdown', handlePointerDown);
};
}, []);
return (
<Box
ref={cursor}
position="absolute"
top="0"
left={`${x * 100}%`}
height="100%"
width="2pt"
id="assault-cursor"
style={{
transform: 'translateX(-50%)',
backgroundColor: Var('accent-11'),
}}
>
<Flex
align="center"
direction="column"
position="absolute"
left="50%"
bottom="100%"
style={{ transform: 'translateX(-50%)' }}
>
<Flex
align="center"
justify="center"
style={{
width: '1.5rem',
height: '1.5rem',
backgroundColor: Var('accent-9'),
borderRadius: `50% 50% ${Var('radius-1')} ${Var('radius-1')}`,
}}
>
<Text style={{ display: 'flex' }} highContrast color="purple">
<DragHandleDots2Icon />
</Text>
</Flex>
<Box
height="0.5rem"
width="2pt"
style={{
backgroundColor: Var('accent-11'),
}}
/>
</Flex>
</Box>
);
}
@@ -2,6 +2,7 @@ import type { AssaultRanges } from '@blitzkit/core';
import { literals } from '@blitzkit/i18n'; import { literals } from '@blitzkit/i18n';
import { Box, Card, Flex, Text } from '@radix-ui/themes'; import { Box, Card, Flex, Text } from '@radix-ui/themes';
import { clamp } from 'three/src/math/MathUtils.js'; import { clamp } from 'three/src/math/MathUtils.js';
import { fakeLog } from '../../../../../../../core/blitzkit/fakeLog';
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics'; import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
import { Var } from '../../../../../../../core/radix/var'; import { Var } from '../../../../../../../core/radix/var';
import { useLocale } from '../../../../../../../hooks/useLocale'; import { useLocale } from '../../../../../../../hooks/useLocale';
@@ -13,18 +14,14 @@ interface Props {
} }
export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) { export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
const maxDistance = Math.max( const maxDistance = _ranges.ranges.at(-1)!.distance;
..._ranges.ranges.map((range) => range.distance),
);
const ranges = _ranges.ranges.map((range) => ({ const ranges = _ranges.ranges.map((range) => ({
factor: range.factor, factor: range.factor,
distance: range.distance, distance: range.distance,
fakeDistance: (2 / Math.PI) * Math.atan((4 * range.distance) / maxDistance), fakeDistance: fakeLog(range.distance / maxDistance, 2),
})); }));
const sumFakeDistances = ranges.reduce( const maxFakeDistance = ranges.at(-1)!.fakeDistance;
(total, range) => total + range.fakeDistance,
0,
);
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -51,8 +48,9 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
> >
<Flex flexGrow="1"> <Flex flexGrow="1">
<Box position="relative" width={Var('space-8')}> <Box position="relative" width={Var('space-8')}>
{ranges.map((range) => ( {ranges.map((range, index) => (
<Text <Text
key={index}
style={{ style={{
position: 'absolute', position: 'absolute',
left: 0, left: 0,
@@ -75,10 +73,14 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
const mix = clamp(1.5 * (range.factor - 1) + 1, 0, 1) * 100; const mix = clamp(1.5 * (range.factor - 1) + 1, 0, 1) * 100;
const color0 = `color-mix(in hsl, ${Var('tomato-9')}, ${Var('jade-9')} ${mix}%)`; const color0 = `color-mix(in hsl, ${Var('tomato-9')}, ${Var('jade-9')} ${mix}%)`;
const color1 = `color-mix(in hsl, ${Var('tomato-7')}, ${Var('jade-7')} ${mix}%)`; const color1 = `color-mix(in hsl, ${Var('tomato-7')}, ${Var('jade-7')} ${mix}%)`;
const last = ranges[index - 1];
const lastFakeDistance = last?.fakeDistance ?? 0;
const fakeWidth = range.fakeDistance - lastFakeDistance;
const fraction = fakeWidth / ranges.at(-1)!.fakeDistance;
return ( return (
<Box <Box
width={`${(range.fakeDistance / sumFakeDistances) * 100}%`} width={`${fraction * 100}%`}
height={`${range.factor * 100}%`} height={`${range.factor * 100}%`}
style={{ style={{
background: `linear-gradient(45deg, ${color1}, ${color0})`, background: `linear-gradient(45deg, ${color1}, ${color0})`,
@@ -103,35 +105,45 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
/> />
))} ))}
<AssaultCursor /> <AssaultCursor
maxDistance={maxDistance}
maxFakeDistance={maxFakeDistance}
/>
</Flex> </Flex>
</Flex> </Flex>
<Flex ml="8" height={Var('space-7')}> <Flex ml="8" height={Var('space-7')}>
{ranges.map((range, index) => ( {ranges.map((range, index) => {
<Box const last = ranges[index - 1];
position="relative" const lastFakeDistance = last?.fakeDistance ?? 0;
key={index} const fakeWidth = range.fakeDistance - lastFakeDistance;
width={`${(range.fakeDistance / sumFakeDistances) * 100}%`} const fraction = fakeWidth / ranges.at(-1)!.fakeDistance;
height="100%"
> return (
<Text <Box
size="1" position="relative"
color="gray" key={index}
mt="2" width={`${fraction * 100}%`}
style={{ height="100%"
position: 'absolute',
right: 0,
transform: 'translateX(50%)',
writingMode: 'vertical-rl',
}}
> >
{literals(strings.common.units.m, [ <Text
`${Math.round(range.distance)}`, size="1"
])} color="gray"
</Text> mt="2"
</Box> style={{
))} position: 'absolute',
right: 0,
transform: 'translateX(50%)',
writingMode: 'vertical-rl',
}}
>
{literals(strings.common.units.m, [
`${Math.round(range.distance)}`,
])}
</Text>
</Box>
);
})}
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>
@@ -118,7 +118,7 @@ export function InfoWithDelta({
tank, tank,
track: member.track, track: member.track,
turret: member.turret, turret: member.turret,
assaultDistance: 0, assaultDistance: member.assaultDistance,
}, },
{ {
equipmentDefinitions, equipmentDefinitions,
@@ -24,6 +24,9 @@ export function Characteristics() {
const provisions = Duel.use((state) => state.protagonist.provisions); const provisions = Duel.use((state) => state.protagonist.provisions);
const consumables = Duel.use((state) => state.protagonist.consumables); const consumables = Duel.use((state) => state.protagonist.consumables);
const camouflage = Duel.use((state) => state.protagonist.camouflage); const camouflage = Duel.use((state) => state.protagonist.camouflage);
const assaultDistance = Duel.use(
(state) => state.protagonist.assaultDistance,
);
const equipmentMatrix = Duel.use( const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
); );
@@ -58,7 +61,7 @@ export function Characteristics() {
applyReactiveArmor: false, applyReactiveArmor: false,
applyDynamicArmor: false, applyDynamicArmor: false,
applySpallLiner: false, applySpallLiner: false,
assaultDistance: 0, assaultDistance,
}, },
{ {
tankModelDefinition, tankModelDefinition,
@@ -0,0 +1,7 @@
export function fakeLog(x: number, a = 1) {
return (2 / Math.PI) * Math.atan((Math.PI * a * x) / 2);
}
export function fakeLogInverse(y: number, a = 1) {
return (2 / (Math.PI * a)) * Math.tan((Math.PI * y) / 2);
}
@@ -159,13 +159,12 @@ export function tankCharacteristics(
[applyDynamicArmor, -0.1], [applyDynamicArmor, -0.1],
[applySpallLiner && shell.type === ShellType.HE, -0.2], [applySpallLiner && shell.type === ShellType.HE, -0.2],
); );
const assaultDamageCoefficient = const assaultDamageCoefficient = gun.gun_type!.value.base.assault_ranges
gun.gun_type!.value.base.assault_ranges && ? // && gun.gun_type!.value.base.assault_ranges.types.includes(shell.type)
gun.gun_type!.value.base.assault_ranges.types.includes(shell.type) (gun.gun_type!.value.base.assault_ranges.ranges.find(
? (gun.gun_type!.value.base.assault_ranges.ranges.find( ({ distance }) => distance >= assaultDistance,
({ distance }) => distance >= assaultDistance, )?.factor ?? 0)
)?.factor ?? 0) : 1;
: 1;
const moduleDamageCoefficient = const moduleDamageCoefficient =
coefficient([hasTungsten, 0.15]) * assaultDamageCoefficient; coefficient([hasTungsten, 0.15]) * assaultDamageCoefficient;
const reloadCoefficient = const reloadCoefficient =
@@ -40,5 +40,6 @@ export function tankToDuelMember(
yaw, yaw,
pitch, pitch,
turret, turret,
assaultDistance: 0,
} satisfies DuelMember; } satisfies DuelMember;
} }
@@ -39,6 +39,7 @@ export interface DuelMember {
provisions: number[]; provisions: number[];
camouflage: boolean; camouflage: boolean;
cooldownBooster: number; cooldownBooster: number;
assaultDistance: number;
} }
export interface DuelStore { export interface DuelStore {