mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +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 id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toUniqueId } from '@blitzkit/core';
|
||||
import { NodeIO } from '@gltf-transform/core';
|
||||
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 { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
||||
@@ -32,6 +32,7 @@ export async function tankModels() {
|
||||
if (tankKey.includes('tutorial_bot')) continue;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
|
||||
+6
-2
@@ -21,6 +21,7 @@ import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
||||
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
||||
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
||||
import { Info } from './Info';
|
||||
@@ -30,7 +31,10 @@ import { RicochetVisualizer } from './RicochetVisualizer';
|
||||
import { StatsTableWrapper } from './StatsTableWrapper';
|
||||
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
||||
|
||||
export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
export function Firepower({
|
||||
stats,
|
||||
skeleton,
|
||||
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||
@@ -405,7 +409,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<GunFlexibilityVisualizer />
|
||||
<GunFlexibilityVisualizer skeleton={skeleton} />
|
||||
</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 } from '@radix-ui/themes';
|
||||
import { useRef, useState } from 'react';
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
||||
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`;
|
||||
import { Box, Flex } from '@radix-ui/themes';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
||||
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
||||
import { FlexibilityCard } from './components/FlexibilityCard';
|
||||
|
||||
export function GunFlexibilityVisualizer({
|
||||
skeleton,
|
||||
}: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<VisualizerCard
|
||||
ref={container}
|
||||
onPointerMove={(event) => {
|
||||
if (!container.current || !highlighter.current || !pointer.current)
|
||||
return;
|
||||
<Flex direction="column" gap="4">
|
||||
<FlexibilityCard />
|
||||
|
||||
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);
|
||||
<Box position="relative" style={{ aspectRatio: '2 / 1' }}>
|
||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||
{!skeleton && <FlexibilityCanvas />}
|
||||
</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
|
||||
position="absolute"
|
||||
width="100%"
|
||||
height="100%"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
opacity: 2 ** -3,
|
||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
||||
boxShadow: 'inset 0 -1rem 1rem var(--gray-1)',
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+3
-2
@@ -8,6 +8,7 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||
import { Crew } from './components/Crew';
|
||||
import { Firepower } from './components/Firepower';
|
||||
import { Maneuverability } from './components/Maneuverability';
|
||||
@@ -18,7 +19,7 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
]);
|
||||
|
||||
export function Characteristics() {
|
||||
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
@@ -93,7 +94,7 @@ export function Characteristics() {
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
width={{ initial: '100%', sm: 'auto' }}
|
||||
>
|
||||
<Firepower stats={stats} />
|
||||
<Firepower skeleton={skeleton} stats={stats} />
|
||||
|
||||
<Flex
|
||||
gap="8"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
import { Characteristics } from './components/Characteristics';
|
||||
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
||||
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
||||
@@ -9,7 +10,9 @@ import { Modules } from './components/Modules';
|
||||
import { Provisions } from './components/Provisions';
|
||||
import { Skills } from './components/Skills';
|
||||
|
||||
export function CharacteristicsSection() {
|
||||
export function CharacteristicsSection({
|
||||
skeleton,
|
||||
}: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<Flex direction="column" gap="3">
|
||||
<Flex
|
||||
@@ -64,7 +67,7 @@ export function CharacteristicsSection() {
|
||||
<CompareOptions />
|
||||
</Flex>
|
||||
|
||||
<Characteristics />
|
||||
<Characteristics skeleton={skeleton} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -25,5 +25,6 @@ export function useModel(id: number) {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return cache[id];
|
||||
}
|
||||
@@ -54,9 +54,9 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
|
||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||
<TechTreeSection skeleton={skeleton} />
|
||||
)}
|
||||
<CharacteristicsSection />
|
||||
<CharacteristicsSection skeleton={skeleton} />
|
||||
<GameModeSection />
|
||||
{guide && <GuideSection skeleton={skeleton} guide={guide} />}
|
||||
{guide && <GuideSection guide={guide} />}
|
||||
<VideoSection skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
</Duel.Provider>
|
||||
|
||||
Reference in new issue
Block a user