mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: cursor
This commit is contained in:
1 parent
097f2792ae
commit
e1b5b21fae
11 files changed
+181
-110
No files matched your search
@@ -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",
|
||||||
|
|||||||
-66
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#assault-cursor {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assault-cursor:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
+107
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+46
-34
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
+4
-1
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user