mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 20:19:49 +02:00
feat: hull rotate
This commit is contained in:
1 parent
cde6904513
commit
e47b1315a9
4 files changed
+418
-374
No files matched your search
+23
-9
@@ -1,23 +1,34 @@
|
|||||||
import type { QuicklimeEvent } from 'quicklime';
|
import type { QuicklimeEvent } from 'quicklime';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import type { Group } from 'three';
|
import type { Group } from 'three';
|
||||||
|
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||||
import {
|
import {
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
type ModelTransformEventData,
|
type ModelTransformEventData,
|
||||||
} from '../../../../../../../../core/blitzkit/modelTransform';
|
} from '../../../../../../../../core/blitzkit/modelTransform';
|
||||||
|
import { Duel } from '../../../../../../../../stores/duel';
|
||||||
import { ModelChunk } from './ModelChunk';
|
import { ModelChunk } from './ModelChunk';
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
export function CanvasContent() {
|
export function CanvasContent() {
|
||||||
const hull = useRef<Group>(null);
|
const hullWrapper = useRef<Group>(null);
|
||||||
const gun = useRef<Group>(null);
|
const gunWrapper = useRef<Group>(null);
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleModelTransformEvent(
|
function handleModelTransformEvent(
|
||||||
event: QuicklimeEvent<ModelTransformEventData>,
|
event: QuicklimeEvent<ModelTransformEventData>,
|
||||||
) {
|
) {
|
||||||
if (!gun.current) return;
|
if (!gunWrapper.current || !hullWrapper.current) return;
|
||||||
|
|
||||||
gun.current.rotation.x = event.data.pitch;
|
gunWrapper.current.rotation.x = event.data.pitch;
|
||||||
|
if (event.data.yaw) hullWrapper.current.rotation.y = event.data.yaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
modelTransformEvent.on(handleModelTransformEvent);
|
modelTransformEvent.on(handleModelTransformEvent);
|
||||||
@@ -26,15 +37,18 @@ export function CanvasContent() {
|
|||||||
modelTransformEvent.off(handleModelTransformEvent);
|
modelTransformEvent.off(handleModelTransformEvent);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<group ref={hull}>
|
<group ref={hullWrapper} position={[0, 0, turretModel.gun_origin.z]}>
|
||||||
<ModelChunk />
|
<group position={[0, 0, -turretModel.gun_origin.z]}>
|
||||||
|
<ModelChunk only="body" />
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group ref={gun}>
|
<ModelChunk only="turret" />
|
||||||
<ModelChunk onlyGun />
|
|
||||||
|
<group ref={gunWrapper}>
|
||||||
|
<ModelChunk only="gun" />
|
||||||
</group>
|
</group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
-343
@@ -1,343 +0,0 @@
|
|||||||
import { literals } from '@blitzkit/i18n';
|
|
||||||
import { Box } from '@radix-ui/themes';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { clamp, degToRad, 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 { modelTransformEvent } from '../../../../../../../../core/blitzkit/modelTransform';
|
|
||||||
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 (1 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1 / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function magInverse(y: number): number {
|
|
||||||
return (1 / ANGLE_COEFFICIENT) * Math.tan(Math.PI * y - Math.PI / 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 [pitch, setPitch] = 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 0 0`;
|
|
||||||
d += `L ${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 0 0`;
|
|
||||||
d += `L ${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`;
|
|
||||||
|
|
||||||
const r = mag(radToDeg(-pitch));
|
|
||||||
const dotX = r * Math.sin(yaw);
|
|
||||||
const dotY = r * Math.cos(yaw);
|
|
||||||
|
|
||||||
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;
|
|
||||||
let 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,
|
|
||||||
);
|
|
||||||
|
|
||||||
yaw = min[1];
|
|
||||||
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
|
||||||
t = clamp(t, 0, 1);
|
|
||||||
let pitch = degToRad(magInverse(-t));
|
|
||||||
pitch = clamp(pitch, min[0], max[0]);
|
|
||||||
|
|
||||||
setPitch(pitch);
|
|
||||||
setYaw(yaw);
|
|
||||||
setMinPitch(min[0]);
|
|
||||||
setMaxPitch(max[0]);
|
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
|
||||||
}}
|
|
||||||
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-5)"
|
|
||||||
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-a11')}, ${Var('gray-a6')})`,
|
|
||||||
transform: 'translateX(-50%)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
position="absolute"
|
|
||||||
left={`${50 * (dotX + 1)}%`}
|
|
||||||
bottom={`${50 * (dotY + 1)}%`}
|
|
||||||
width="0.25rem"
|
|
||||||
height="0.25rem"
|
|
||||||
style={{
|
|
||||||
borderRadius: '50%',
|
|
||||||
backgroundColor: Var('gray-12'),
|
|
||||||
transform: 'translate(-50%, 50%)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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.pitch}
|
|
||||||
value={literals(strings.common.units.deg, [radToDeg(pitch).toFixed(0)])}
|
|
||||||
side="bottom-left"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VisualizerCornerStat
|
|
||||||
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
|
|
||||||
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
|
|
||||||
side="bottom-right"
|
|
||||||
/>
|
|
||||||
</VisualizerCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+29
-20
@@ -17,10 +17,10 @@ const outlineMaterial = new LineBasicMaterial({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onlyGun?: boolean;
|
only: 'gun' | 'turret' | 'body';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModelChunk({ onlyGun = false }: Props) {
|
export function ModelChunk({ only }: Props) {
|
||||||
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);
|
||||||
@@ -43,29 +43,38 @@ export function ModelChunk({ onlyGun = false }: Props) {
|
|||||||
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
|
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
|
||||||
const isCurrentGun =
|
const isCurrentGun =
|
||||||
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`;
|
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`;
|
||||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
const isGun = isCurrentGun || isCurrentMantlet;
|
||||||
const isHull = node.name === 'hull';
|
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
const isCurrentTurret =
|
const isCurrentTurret =
|
||||||
node.name ===
|
node.name ===
|
||||||
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
|
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
|
||||||
const isAnythingElse = isHull || isWheel || isTrack || isCurrentTurret;
|
const isTurret = isCurrentTurret;
|
||||||
|
|
||||||
if ((onlyGun && isVisible) || (!onlyGun && isAnythingElse)) {
|
const isHull = node.name === 'hull';
|
||||||
return jsxTree(node, {
|
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||||
mesh(mesh, props, key) {
|
const isTrack = node.name.startsWith('chassis_track_');
|
||||||
return (
|
const isBody = isHull || isWheel || isTrack;
|
||||||
<>
|
|
||||||
<mesh key={key} {...props} material={surfaceMaterial} />
|
if (
|
||||||
<lineSegments material={outlineMaterial}>
|
(only === 'gun' && !isGun) ||
|
||||||
<edgesGeometry args={[mesh.geometry, 29]} />
|
(only === 'turret' && !isTurret) ||
|
||||||
</lineSegments>
|
(only === 'body' && !isBody)
|
||||||
</>
|
) {
|
||||||
);
|
return null;
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(mesh, props, key) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<mesh key={key} {...props} material={surfaceMaterial} />
|
||||||
|
<lineSegments material={outlineMaterial}>
|
||||||
|
<edgesGeometry args={[mesh.geometry, 29]} />
|
||||||
|
</lineSegments>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|||||||
+366
-2
@@ -1,12 +1,172 @@
|
|||||||
|
import { literals } from '@blitzkit/i18n';
|
||||||
import { Box, Card, Flex } from '@radix-ui/themes';
|
import { Box, Card, Flex } from '@radix-ui/themes';
|
||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { degToRad, 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 { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
|
import { Duel } from '../../../../../../../stores/duel';
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
||||||
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
|
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
||||||
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
||||||
import { FlexibilityCard } from './components/FlexibilityCard';
|
|
||||||
|
const ANGLE_COEFFICIENT = 1 / 10;
|
||||||
|
const CONIC_TRANSITION = degToRad(0);
|
||||||
|
|
||||||
|
const modelDefinition = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
function mag(x: number) {
|
||||||
|
return (1 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function magInverse(y: number): number {
|
||||||
|
return (1 / ANGLE_COEFFICIENT) * Math.tan(Math.PI * y - Math.PI / 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({
|
export function GunFlexibilityVisualizer({
|
||||||
skeleton,
|
skeleton,
|
||||||
}: MaybeSkeletonComponentProps) {
|
}: MaybeSkeletonComponentProps) {
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
|
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 [yaw, setYaw] = useState(0);
|
||||||
|
const [pitch, setPitch] = useState(0);
|
||||||
|
const [minPitch, setMinPitch] = useState(
|
||||||
|
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max),
|
||||||
|
);
|
||||||
|
const [maxPitch, setMaxPitch] = useState(
|
||||||
|
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min),
|
||||||
|
);
|
||||||
|
|
||||||
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
const highlighter = useRef<HTMLDivElement>(null);
|
||||||
|
const pointer = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
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 0 0`;
|
||||||
|
d += `L ${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 0 0`;
|
||||||
|
d += `L ${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`;
|
||||||
|
|
||||||
|
const r = mag(radToDeg(-pitch));
|
||||||
|
const dotX = r * Math.sin(yaw);
|
||||||
|
const dotY = r * Math.cos(yaw);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column-reverse" gap="0">
|
<Flex direction="column-reverse" gap="0">
|
||||||
<Box mt="-4" px="2">
|
<Box mt="-4" px="2">
|
||||||
@@ -20,11 +180,215 @@ export function GunFlexibilityVisualizer({
|
|||||||
left="0"
|
left="0"
|
||||||
>
|
>
|
||||||
{!skeleton && <FlexibilityCanvas />}
|
{!skeleton && <FlexibilityCanvas />}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="33%"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(
|
||||||
|
at 0 50%,
|
||||||
|
|
||||||
|
transparent,
|
||||||
|
${Var('orange-a2')} ${Math.PI / 2 - maxPitch - CONIC_TRANSITION}rad,
|
||||||
|
|
||||||
|
${Var('cyan-a5')} ${Math.PI / 2 - maxPitch}rad,
|
||||||
|
${Var('jade-a5')} ${Math.PI / 2 - minPitch}rad,
|
||||||
|
|
||||||
|
${Var('orange-a2')} ${Math.PI / 2 - minPitch + CONIC_TRANSITION}rad,
|
||||||
|
transparent 180deg
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<FlexibilityCard />
|
<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;
|
||||||
|
let 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
yaw = min[1];
|
||||||
|
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
||||||
|
t = clamp(t, 0, 1);
|
||||||
|
let pitch = degToRad(magInverse(-t));
|
||||||
|
pitch = clamp(pitch, min[0], max[0]);
|
||||||
|
|
||||||
|
setPitch(pitch);
|
||||||
|
setYaw(yaw);
|
||||||
|
setMinPitch(min[0]);
|
||||||
|
setMaxPitch(max[0]);
|
||||||
|
|
||||||
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
|
}}
|
||||||
|
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-5)"
|
||||||
|
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-a11')}, ${Var('gray-a6')})`,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
left={`${50 * (dotX + 1)}%`}
|
||||||
|
bottom={`${50 * (dotY + 1)}%`}
|
||||||
|
width="0.25rem"
|
||||||
|
height="0.25rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: Var('gray-12'),
|
||||||
|
transform: 'translate(-50%, 50%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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.pitch}
|
||||||
|
value={literals(strings.common.units.deg, [
|
||||||
|
radToDeg(pitch).toFixed(0),
|
||||||
|
])}
|
||||||
|
side="bottom-left"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisualizerCornerStat
|
||||||
|
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
|
||||||
|
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
|
||||||
|
side="bottom-right"
|
||||||
|
/>
|
||||||
|
</VisualizerCard>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user