mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: embedded canvas
This commit is contained in:
1 parent
40f5042896
commit
c243ebd19a
10 files changed
+382
-293
No files matched your search
@@ -62,6 +62,7 @@ export async function tankIcons() {
|
|||||||
|
|
||||||
const nationVehicleId = tank.id;
|
const nationVehicleId = tank.id;
|
||||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { toUniqueId } from '@blitzkit/core';
|
import { toUniqueId } from '@blitzkit/core';
|
||||||
import { NodeIO } from '@gltf-transform/core';
|
import { NodeIO } from '@gltf-transform/core';
|
||||||
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
||||||
import { readdir, writeFile } from 'fs/promises';
|
import { readdir } from 'fs/promises';
|
||||||
import { extractModel } from '../core/blitz/extractModel';
|
import { extractModel } from '../core/blitz/extractModel';
|
||||||
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
||||||
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
||||||
@@ -32,6 +32,7 @@ export async function tankModels() {
|
|||||||
if (tankKey.includes('tutorial_bot')) continue;
|
if (tankKey.includes('tutorial_bot')) continue;
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||||
);
|
);
|
||||||
|
|||||||
+6
-2
@@ -21,6 +21,7 @@ import { useEquipment } from '../../../../../../hooks/useEquipment';
|
|||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
||||||
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
||||||
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
||||||
import { Info } from './Info';
|
import { Info } from './Info';
|
||||||
@@ -30,7 +31,10 @@ import { RicochetVisualizer } from './RicochetVisualizer';
|
|||||||
import { StatsTableWrapper } from './StatsTableWrapper';
|
import { StatsTableWrapper } from './StatsTableWrapper';
|
||||||
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
||||||
|
|
||||||
export function Firepower({ stats }: StatsAcceptorProps) {
|
export function Firepower({
|
||||||
|
stats,
|
||||||
|
skeleton,
|
||||||
|
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||||
@@ -405,7 +409,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<GunFlexibilityVisualizer />
|
<GunFlexibilityVisualizer skeleton={skeleton} />
|
||||||
</StatsTableWrapper>
|
</StatsTableWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import { Canvas } from '@react-three/fiber';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||||
|
import { useModel } from '../../../../../../../../hooks/useModel';
|
||||||
|
import { Duel } from '../../../../../../../../stores/duel';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function FlexibilityCanvas() {
|
||||||
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
|
||||||
|
const { gltf } = useModel(tank.id);
|
||||||
|
|
||||||
|
const zoom = 60;
|
||||||
|
const originY = tankModel.turret_origin.z + turretModel.gun_origin.z + 1.5;
|
||||||
|
const originZ = tankModel.turret_origin.y + turretModel.gun_origin.y;
|
||||||
|
|
||||||
|
console.log(originY, originZ);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
frameloop="demand"
|
||||||
|
orthographic
|
||||||
|
camera={{
|
||||||
|
zoom,
|
||||||
|
position: [10, originZ, -originY],
|
||||||
|
rotation: [0, Math.PI / 2, 0],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ambientLight intensity={2} />
|
||||||
|
<directionalLight position={[1, 1, 1]} intensity={8} />
|
||||||
|
|
||||||
|
{/* <mesh>
|
||||||
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
|
<meshNormalMaterial />
|
||||||
|
</mesh> */}
|
||||||
|
|
||||||
|
<group rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
|
<primitive object={gltf.scene} />
|
||||||
|
</group>
|
||||||
|
</Canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
+298
@@ -0,0 +1,298 @@
|
|||||||
|
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,
|
||||||
|
applyPitchYawLimits,
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
const modelDefinition = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
function mag(x: number) {
|
||||||
|
return ((2 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function c(thetaDeg: number, m = 1) {
|
||||||
|
const theta = thetaDeg * (Math.PI / 180);
|
||||||
|
return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlexibilityCard() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
const tankModel = modelDefinition.models[tank.id];
|
||||||
|
const turretModel = tankModel.turrets[turret.id];
|
||||||
|
const gunModel = turretModel.guns[gun.gun_type!.value.base.id];
|
||||||
|
|
||||||
|
const p = gunModel.pitch;
|
||||||
|
const y = turretModel.yaw;
|
||||||
|
const f = p.front;
|
||||||
|
const b = p.back;
|
||||||
|
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
||||||
|
|
||||||
|
let d = '';
|
||||||
|
|
||||||
|
// right
|
||||||
|
const mo = mag(p.max);
|
||||||
|
if (y) {
|
||||||
|
d += `M ${c(90 - y.max, mo)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `M ${c(-90 + b.range / 2 + t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `M ${c(-90, mo)}`;
|
||||||
|
}
|
||||||
|
if (f) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// front
|
||||||
|
if (f) {
|
||||||
|
const m = mag(f.max);
|
||||||
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (f) {
|
||||||
|
d += `L ${c(90 + f.range / 2 + t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `L ${c(90, mo)}`;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
if (!y && b) {
|
||||||
|
const m = mag(b.max);
|
||||||
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += `Z`;
|
||||||
|
|
||||||
|
// right
|
||||||
|
const mi = mag(p.min);
|
||||||
|
if (y) {
|
||||||
|
d += `M ${c(90 - y.max, mi)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `M ${c(-90 + b.range / 2 + t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `M ${c(-90, mi)}`;
|
||||||
|
}
|
||||||
|
if (f) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// front
|
||||||
|
if (f) {
|
||||||
|
const m = mag(f.min);
|
||||||
|
d += `L ${c(90 - f.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// left
|
||||||
|
if (f) {
|
||||||
|
d += `L ${c(90 + f.range / 2 + t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `L ${c(90, mi)}`;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`;
|
||||||
|
} else if (b) {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`;
|
||||||
|
} else {
|
||||||
|
d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// back
|
||||||
|
if (!y && b) {
|
||||||
|
const m = mag(b.min);
|
||||||
|
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||||
|
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += `Z`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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]);
|
||||||
|
}}
|
||||||
|
mb="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',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 'calc(100%)',
|
||||||
|
height: 'calc(100%)',
|
||||||
|
}}
|
||||||
|
viewBox="-1 -1 2 2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
fill="var(--gray-6)"
|
||||||
|
stroke="var(--gray-8)"
|
||||||
|
strokeWidth="1px"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
d={d}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
stroke="var(--gray-11)"
|
||||||
|
fill="transparent"
|
||||||
|
strokeWidth="1px"
|
||||||
|
strokeDasharray="0.5rem 0.5rem"
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+18
-284
@@ -1,297 +1,31 @@
|
|||||||
import { literals } from '@blitzkit/i18n';
|
import { Box, Flex } from '@radix-ui/themes';
|
||||||
import { Box } from '@radix-ui/themes';
|
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
||||||
import { useRef, useState } from 'react';
|
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { FlexibilityCard } from './components/FlexibilityCard';
|
||||||
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
|
|
||||||
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;
|
|
||||||
|
|
||||||
const modelDefinition = await awaitableModelDefinitions;
|
|
||||||
|
|
||||||
function mag(x: number) {
|
|
||||||
return ((2 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function c(thetaDeg: number, m = 1) {
|
|
||||||
const theta = thetaDeg * (Math.PI / 180);
|
|
||||||
return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const tankModel = modelDefinition.models[tank.id];
|
|
||||||
const turretModel = tankModel.turrets[turret.id];
|
|
||||||
const gunModel = turretModel.guns[gun.gun_type!.value.base.id];
|
|
||||||
|
|
||||||
const p = gunModel.pitch;
|
|
||||||
const y = turretModel.yaw;
|
|
||||||
const f = p.front;
|
|
||||||
const b = p.back;
|
|
||||||
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
|
||||||
|
|
||||||
let d = '';
|
|
||||||
|
|
||||||
// right
|
|
||||||
const mo = mag(p.max);
|
|
||||||
if (y) {
|
|
||||||
d += `M ${c(90 - y.max, mo)}`;
|
|
||||||
} else if (b) {
|
|
||||||
d += `M ${c(-90 + b.range / 2 + t, mo)}`;
|
|
||||||
} else {
|
|
||||||
d += `M ${c(-90, mo)}`;
|
|
||||||
}
|
|
||||||
if (f) {
|
|
||||||
d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`;
|
|
||||||
} else {
|
|
||||||
d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// front
|
|
||||||
if (f) {
|
|
||||||
const m = mag(f.max);
|
|
||||||
d += `L ${c(90 - f.range / 2, m)}`;
|
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// left
|
|
||||||
if (f) {
|
|
||||||
d += `L ${c(90 + f.range / 2 + t, mo)}`;
|
|
||||||
} else {
|
|
||||||
d += `L ${c(90, mo)}`;
|
|
||||||
}
|
|
||||||
if (y) {
|
|
||||||
d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`;
|
|
||||||
} else if (b) {
|
|
||||||
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
|
|
||||||
} else {
|
|
||||||
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// back
|
|
||||||
if (!y && b) {
|
|
||||||
const m = mag(b.max);
|
|
||||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
d += `Z`;
|
|
||||||
|
|
||||||
// right
|
|
||||||
const mi = mag(p.min);
|
|
||||||
if (y) {
|
|
||||||
d += `M ${c(90 - y.max, mi)}`;
|
|
||||||
} else if (b) {
|
|
||||||
d += `M ${c(-90 + b.range / 2 + t, mi)}`;
|
|
||||||
} else {
|
|
||||||
d += `M ${c(-90, mi)}`;
|
|
||||||
}
|
|
||||||
if (f) {
|
|
||||||
d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`;
|
|
||||||
} else {
|
|
||||||
d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// front
|
|
||||||
if (f) {
|
|
||||||
const m = mag(f.min);
|
|
||||||
d += `L ${c(90 - f.range / 2, m)}`;
|
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// left
|
|
||||||
if (f) {
|
|
||||||
d += `L ${c(90 + f.range / 2 + t, mi)}`;
|
|
||||||
} else {
|
|
||||||
d += `L ${c(90, mi)}`;
|
|
||||||
}
|
|
||||||
if (y) {
|
|
||||||
d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`;
|
|
||||||
} else if (b) {
|
|
||||||
d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`;
|
|
||||||
} else {
|
|
||||||
d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// back
|
|
||||||
if (!y && b) {
|
|
||||||
const m = mag(b.min);
|
|
||||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
|
||||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
d += `Z`;
|
|
||||||
|
|
||||||
|
export function GunFlexibilityVisualizer({
|
||||||
|
skeleton,
|
||||||
|
}: MaybeSkeletonComponentProps) {
|
||||||
return (
|
return (
|
||||||
<VisualizerCard
|
<Flex direction="column" gap="4">
|
||||||
ref={container}
|
<FlexibilityCard />
|
||||||
onPointerMove={(event) => {
|
|
||||||
if (!container.current || !highlighter.current || !pointer.current)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const rect = container.current.getBoundingClientRect();
|
<Box position="relative" style={{ aspectRatio: '2 / 1' }}>
|
||||||
const u = event.clientX - rect.left - rect.width / 2;
|
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||||
const v = event.clientY - rect.top - rect.height / 2;
|
{!skeleton && <FlexibilityCanvas />}
|
||||||
const yaw = Math.atan2(u, -v);
|
</Box>
|
||||||
|
|
||||||
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
|
<Box
|
||||||
|
position="absolute"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
boxShadow: 'inset 0 -1rem 1rem var(--gray-1)',
|
||||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Flex>
|
||||||
<svg
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: 'calc(100%)',
|
|
||||||
height: 'calc(100%)',
|
|
||||||
}}
|
|
||||||
viewBox="-1 -1 2 2"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
fill="var(--gray-6)"
|
|
||||||
stroke="var(--gray-8)"
|
|
||||||
strokeWidth="1px"
|
|
||||||
vectorEffect="non-scaling-stroke"
|
|
||||||
d={d}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
stroke="var(--gray-11)"
|
|
||||||
fill="transparent"
|
|
||||||
strokeWidth="1px"
|
|
||||||
strokeDasharray="0.5rem 0.5rem"
|
|
||||||
vectorEffect="non-scaling-stroke"
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+3
-2
@@ -8,6 +8,7 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
|
|||||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||||
import { Crew } from './components/Crew';
|
import { Crew } from './components/Crew';
|
||||||
import { Firepower } from './components/Firepower';
|
import { Firepower } from './components/Firepower';
|
||||||
import { Maneuverability } from './components/Maneuverability';
|
import { Maneuverability } from './components/Maneuverability';
|
||||||
@@ -18,7 +19,7 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
|||||||
awaitableProvisionDefinitions,
|
awaitableProvisionDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function Characteristics() {
|
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||||
@@ -93,7 +94,7 @@ export function Characteristics() {
|
|||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: 'center', sm: 'start' }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: '100%', sm: 'auto' }}
|
||||||
>
|
>
|
||||||
<Firepower stats={stats} />
|
<Firepower skeleton={skeleton} stats={stats} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from '@radix-ui/themes';
|
||||||
|
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||||
import { Characteristics } from './components/Characteristics';
|
import { Characteristics } from './components/Characteristics';
|
||||||
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
||||||
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
||||||
@@ -9,7 +10,9 @@ import { Modules } from './components/Modules';
|
|||||||
import { Provisions } from './components/Provisions';
|
import { Provisions } from './components/Provisions';
|
||||||
import { Skills } from './components/Skills';
|
import { Skills } from './components/Skills';
|
||||||
|
|
||||||
export function CharacteristicsSection() {
|
export function CharacteristicsSection({
|
||||||
|
skeleton,
|
||||||
|
}: MaybeSkeletonComponentProps) {
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="3">
|
<Flex direction="column" gap="3">
|
||||||
<Flex
|
<Flex
|
||||||
@@ -64,7 +67,7 @@ export function CharacteristicsSection() {
|
|||||||
<CompareOptions />
|
<CompareOptions />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Characteristics />
|
<Characteristics skeleton={skeleton} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -25,5 +25,6 @@ export function useModel(id: number) {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return cache[id];
|
return cache[id];
|
||||||
}
|
}
|
||||||
@@ -54,9 +54,9 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
|
|||||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||||
<TechTreeSection skeleton={skeleton} />
|
<TechTreeSection skeleton={skeleton} />
|
||||||
)}
|
)}
|
||||||
<CharacteristicsSection />
|
<CharacteristicsSection skeleton={skeleton} />
|
||||||
<GameModeSection />
|
<GameModeSection />
|
||||||
{guide && <GuideSection skeleton={skeleton} guide={guide} />}
|
{guide && <GuideSection guide={guide} />}
|
||||||
<VideoSection skeleton={skeleton} />
|
<VideoSection skeleton={skeleton} />
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
</Duel.Provider>
|
</Duel.Provider>
|
||||||
|
|||||||
Reference in new issue
Block a user