Fix: gun depression visualizer ignoring turret mount tilt

The gun flexibility visualizer at the bottom of the Tankopedia Firepower section computed depression/elevation purely from the raw gun `pitch.min`/`pitch.max` values, ignoring `initial_turret_rotation.pitch` (the tank's baked-in turret mount tilt). Every other place that reports gun depression/elevation already accounts for this offset; the visualizer was the outlier.

On tanks with a level turret mount the offset is zero, so it was invisible almost everywhere. Minotauro's turret sits pitched 3° forward, which made the visualizer's min/max/pitch stats and wedge shape disagree with the Firepower stats directly above it (e.g. 7°/-18° instead of 10°/15°).

**Changes:**
- `GunFlexibilityVisualizer/index.tsx`: fold the turret's initial pitch offset into `minPitch`, `maxPitch` and the displayed `pitch` stat, and into the drawn limit shape, the pitch dot and the pointer mapping, so the dashed neutrality circle sits at the displayed 0°.
- `GunFlexibilityVisualizer/components/CanvasContent.tsx`: nest the gun mesh inside a turret group carrying the same static tilt as its base rotation, so the mini 3D preview's live drag-pitch composes on top of it, mirroring `useTankTransform.ts`.
This commit is contained in:
Mark Greene committed 2026-10-04 13:38:20 +13:00
1 parent f3e8cdd5e0
commit 98b555fa7e
2 files changed
+51 -16

No files matched your search

@@ -1,6 +1,7 @@
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import type { Group } from "three";
import { degToRad } from "three/src/math/MathUtils.js";
import { api } from "../../../../../../../../core/blitzkit/api";
import {
modelTransformEvent,
@@ -21,6 +22,13 @@ export function CanvasContent() {
const tankModel = modelDefinitions.models[tank.id];
const turretModel = tankModel.turrets[turret.id];
// baked-in turret mount tilt (e.g. Minotauro sits pitched 3deg forward);
// applied as a static base rotation so the live gun pitch composes on top
// of it, mirroring useTankTransform.ts's turretContainer/gunContainer nesting
const initialTurretPitch = -degToRad(
tankModel.initial_turret_rotation?.pitch ?? 0,
);
useEffect(() => {
function handleModelTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>,
@@ -45,10 +53,23 @@ export function CanvasContent() {
</group>
</group>
<ModelChunk only="turret" />
<group
position={[0, turretModel.gun_origin!.y, turretModel.gun_origin!.z]}
rotation={[initialTurretPitch, 0, 0]}
>
<group
position={[
0,
-turretModel.gun_origin!.y,
-turretModel.gun_origin!.z,
]}
>
<ModelChunk only="turret" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
</group>
</group>
</group>
</>
);
@@ -48,13 +48,27 @@ export function GunFlexibilityVisualizer({
const turretModel = tankModel.turrets[turret.id];
const gunModel = turretModel.guns[gun.id];
// turret's own baked-in mount tilt (e.g. Minotauro's turret sits pitched
// 3deg forward on the hull); must be folded into displayed pitch values
// the same way tankCharacteristics.ts and QuickInputs do
const initialTurretPitch = degToRad(
tankModel.initial_turret_rotation?.pitch ?? 0,
);
// radial map is in the tilt-compensated frame so the neutrality circle sits
// at the displayed 0deg
const tiltDeg = radToDeg(initialTurretPitch);
const magTilted = (deg: number) => mag(deg + tiltDeg);
const [yaw, setYaw] = useState(0);
const [pitch, setPitch] = useState(0);
const [minPitch, setMinPitch] = useState(
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max),
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max) -
initialTurretPitch,
);
const [maxPitch, setMaxPitch] = useState(
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min) -
initialTurretPitch,
);
const container = useRef<HTMLDivElement>(null);
@@ -70,7 +84,7 @@ export function GunFlexibilityVisualizer({
let d = "";
// right
const mo = mag(p.max);
const mo = magTilted(p.max);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mo)}`;
@@ -87,7 +101,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.max);
const m = magTilted(f.max);
d += `L ${c(90 - f.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
}
@@ -108,7 +122,7 @@ export function GunFlexibilityVisualizer({
// back
if (!y && b) {
const m = mag(b.max);
const m = magTilted(b.max);
d += `L ${c(-90 - b.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
}
@@ -116,7 +130,7 @@ export function GunFlexibilityVisualizer({
d += `Z`;
// right
const mi = mag(p.min);
const mi = magTilted(p.min);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mi)}`;
@@ -133,7 +147,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.min);
const m = magTilted(f.min);
d += `L ${c(90 - f.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
}
@@ -154,14 +168,14 @@ export function GunFlexibilityVisualizer({
// back
if (!y && b) {
const m = mag(b.min);
const m = magTilted(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 r = mag(radToDeg(-(pitch - initialTurretPitch)));
const dotX = r * Math.sin(yaw);
const dotY = r * Math.cos(yaw);
@@ -251,13 +265,13 @@ export function GunFlexibilityVisualizer({
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));
let pitch = degToRad(magInverse(-t)) + initialTurretPitch;
pitch = clamp(pitch, min[0], max[0]);
setPitch(pitch);
setYaw(yaw);
setMinPitch(min[0]);
setMaxPitch(max[0]);
setMinPitch(min[0] - initialTurretPitch);
setMaxPitch(max[0] - initialTurretPitch);
modelTransformEvent.dispatch({ pitch, yaw });
}}
@@ -376,7 +390,7 @@ export function GunFlexibilityVisualizer({
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
value={literals(strings.common.units.deg, {
value: radToDeg(pitch).toFixed(0),
value: radToDeg(pitch - initialTurretPitch).toFixed(0),
})}
side="top-left"
/>