feat: some armor rendering

This commit is contained in:
tresabhi committed 2025-03-20 21:48:17 -05:00
1 parent 3a32624823
commit 5b16e22750
13 files changed
+662 -1060

No files matched your search

@@ -1,23 +1,10 @@
import { normalizeAngleRad } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
import { useThree, type ThreeEvent } from '@react-three/fiber'; import { useGLTF } from '@react-three/drei';
import { memo, useRef } from 'react'; import { memo } from 'react';
import { Group, Plane, Vector2, Vector3 } from 'three'; import { Color, Material, MeshBasicMaterial } from 'three';
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits'; import { jsxTree } from '../../../core/blitzkit/jsxTree';
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel'; import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
import { ModelTankWrapper } from './ModelTankWrapper';
import { ArmorType } from './SpacedArmorScene';
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
interface ArmorSceneProps { interface ArmorSceneProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -28,401 +15,87 @@ export interface ThicknessRange {
} }
export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => { export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const wrapper = useRef<Group>(null); // const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null); // const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null); // const gunContainer = useRef<Group>(null);
const protagonist = Duel.use((draft) => draft.protagonist); // const protagonist = Duel.use((draft) => draft.protagonist);
const tank = Duel.use((state) => state.protagonist.tank); // const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track); // const track = Duel.use((state) => state.protagonist.track);
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);
const { gltf: armorGltf } = useArmor(tank.id); // const { gltf: armorGltf } = useArmor(tank.id);
const { gltf: modelGltf } = useModel(tank.id); // const { gltf: modelGltf } = useModel(tank.id);
const armorNodes = Object.values(armorGltf.nodes); // const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes); // const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = useTankModelDefinition(); // const tankModelDefinition = useTankModelDefinition();
const trackModelDefinition = tankModelDefinition.tracks[track.id]; // const trackModelDefinition = tankModelDefinition.tracks[track.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; // const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = // const gunModelDefinition =
turretModelDefinition.guns[gun.gun_type!.value.base.id]; // turretModelDefinition.guns[gun.gun_type!.value.base.id];
const hullOrigin = correctZYTuple(trackModelDefinition.origin); // const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); // const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); // const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
const mutateDuel = Duel.useMutation(); // const mutateDuel = Duel.useMutation();
const canvas = useThree((state) => state.gl.domElement); // const canvas = useThree((state) => state.gl.domElement);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); // const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const duelStore = Duel.useStore(); // const duelStore = Duel.useStore();
const maskOrigin = // const maskOrigin =
gunModelDefinition.mask === undefined // gunModelDefinition.mask === undefined
? undefined // ? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; // : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const showPrimaryArmor = TankopediaPersistent.use( // const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor, // (state) => state.showPrimaryArmor,
); // );
const showSpacedArmor = TankopediaPersistent.use( // const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor, // (state) => state.showSpacedArmor,
); // );
const showExternalModules = TankopediaPersistent.use( // const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules, // (state) => state.showExternalModules,
); // );
const isDynamicArmorActive = Duel.use((state) => // const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73), // state.protagonist.consumables.includes(73),
); // );
useTankTransform(track, turret, turretContainer, gunContainer); // useTankTransform(track, turret, turretContainer, gunContainer);
const tank = Duel.use((state) => state.protagonist.tank);
const armor = TankopediaEphemeral.use((state) => state.armor);
const hullArmor = armor.groups.hull;
const hullGltf = useGLTF(asset(`tanks/${tank.id}/collision/hull.glb`));
return ( return (
<ModelTankWrapper ref={wrapper}> <group>
<group position={hullOrigin}> {jsxTree(hullGltf.scene, {
{armorNodes.map((node) => { mesh(mesh) {
const isHull = node.name.startsWith('hull_'); if (!(mesh.material instanceof Material)) {
const isVisible = isHull; return { visible: false };
const armorId = nameToArmorId(node.name); }
const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor,
armorId,
);
if ( const armorName = mesh.material.name;
!isVisible || const material = new MeshBasicMaterial({});
thickness === undefined || const armorPlate = hullArmor.armors[armorName];
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
)
return null;
return ( switch (armorPlate.type) {
<StaticArmorSceneComponent case 'Armor':
name={node.name} material.color = new Color('#ec6142');
thicknessRange={thicknessRange} material.transparent = true;
key={node.uuid} material.opacity = 0.5;
type={spaced ? ArmorType.Spaced : ArmorType.Primary} break;
thickness={thickness}
node={node}
onPointerDown={(event) => event.stopPropagation()}
/>
);
})}
</group>
{modelNodes.map((node) => { case 'ArmorScreen':
const isWheel = node.name.startsWith('chassis_wheel_'); material.color = new Color('#ff8dcc');
const isTrack = node.name.startsWith('chassis_track_'); material.transparent = true;
const isVisible = isWheel || isTrack; material.opacity = 0.5;
break;
if (!isVisible || !showExternalModules) return null; default:
material.color = new Color('#70b8ff');
break;
}
return ( return { material };
<StaticArmorSceneComponent },
name={node.name}
thicknessRange={thicknessRange}
key={node.uuid}
type={ArmorType.External}
thickness={trackModelDefinition.thickness}
variant="track"
node={node}
onPointerDown={(event) => event.stopPropagation()}
/>
);
})} })}
</group>
<group ref={turretContainer}>
{armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor,
armorId,
);
if (
!isVisible ||
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
) {
return null;
}
const position = new Vector2();
const delta = new Vector2();
let pitch = 0;
let yaw = 0;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
position.set(event.clientX, event.clientY);
yaw = protagonist.yaw;
pitch = protagonist.pitch;
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = await hasEquipment(
124,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
[pitch, yaw] = applyPitchYawLimits(
pitch,
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
function handlePointerUp() {
mutateDuel((draft) => {
draft.protagonist.pitch = normalizeAngleRad(pitch);
draft.protagonist.yaw = normalizeAngleRad(yaw);
});
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
return (
<group key={node.uuid} position={hullOrigin}>
<group position={turretOrigin}>
<StaticArmorSceneComponent
name={node.name}
thicknessRange={thicknessRange}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
thickness={thickness}
node={node}
onPointerDown={onPointerDown}
/>
</group>
</group>
);
})}
<group ref={gunContainer}>
{armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`,
);
const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor,
armorId,
);
if (
!isVisible ||
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) ||
(!isDynamicArmorActive && node.name.includes('state_01'))
)
return null;
const position = new Vector2();
const delta = new Vector2();
let pitch = 0;
let yaw = 0;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.set(event.clientX, event.clientY);
pitch = protagonist.pitch;
yaw = protagonist.yaw;
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = await hasEquipment(
124,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
[pitch, yaw] = applyPitchYawLimits(
pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = true;
});
mutateDuel((draft) => {
draft.protagonist.pitch = normalizeAngleRad(pitch);
draft.protagonist.yaw = normalizeAngleRad(yaw);
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
return (
<group key={node.uuid} position={hullOrigin}>
<group position={turretOrigin.clone().add(gunOrigin)}>
<StaticArmorSceneComponent
name={node.name}
thicknessRange={thicknessRange}
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
thickness={thickness}
node={node}
onPointerDown={onPointerDown}
/>
</group>
</group>
);
})}
{modelNodes.map((node) => {
const gunString = `gun_${gunModelDefinition.model_id
.toString()
.padStart(2, '0')}`;
const isCurrentGun = gunModelDefinition.mask
? node.name.startsWith(gunString)
: node.name === gunString;
const isVisible = isCurrentGun;
if (!isVisible || !showExternalModules) return null;
const position = new Vector2();
const delta = new Vector2();
let pitch = 0;
let yaw = 0;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.set(event.clientX, event.clientY);
pitch = protagonist.pitch;
yaw = protagonist.yaw;
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
);
const hasDownImprovedVerticalStabilizer = await hasEquipment(
124,
duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
[pitch, yaw] = applyPitchYawLimits(
pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = true;
});
mutateDuel((draft) => {
draft.protagonist.pitch = normalizeAngleRad(pitch);
draft.protagonist.yaw = normalizeAngleRad(yaw);
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
return (
<StaticArmorSceneComponent
name={node.name}
thicknessRange={thicknessRange}
key={node.uuid}
type={ArmorType.External}
thickness={gunModelDefinition.thickness}
variant="gun"
node={node}
clip={
maskOrigin === undefined
? undefined
: new Plane(new Vector3(0, 0, -1), -maskOrigin)
}
hullOrigin={hullOrigin}
turretOrigin={turretOrigin}
gunOrigin={gunOrigin}
onPointerDown={onPointerDown}
/>
);
})}
</group>
</group>
</ModelTankWrapper>
); );
}); });
@@ -1,7 +1,7 @@
import { asset } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
interface SmallTankIconProps { interface SmallTankIconProps {
id: number; id: string;
size?: number; size?: number;
} }
@@ -19,7 +19,7 @@ export function SmallTankIcon({ id, size = HEIGHT }: SmallTankIconProps) {
> >
<img <img
alt={`Tank ${id}`} alt={`Tank ${id}`}
src={asset(`icons/tanks/small/${id}.webp`)} src={asset(`api/tanks/${id}/icons/small.png`)}
style={{ style={{
transform: `scale(${(size / HEIGHT) * 100}%)`, transform: `scale(${(size / HEIGHT) * 100}%)`,
transformOrigin: 'top left', transformOrigin: 'top left',
@@ -3,6 +3,7 @@ import {
normalizeBoundingBox, normalizeBoundingBox,
resolveDpm, resolveDpm,
SEARCH_KEYS, SEARCH_KEYS,
Tank,
unionBoundingBox, unionBoundingBox,
type TankDefinition, type TankDefinition,
} from '@blitzkit/core'; } from '@blitzkit/core';
@@ -35,8 +36,8 @@ import { MAX_RECENTLY_VIEWED } from './constants';
type TankSearchProps = Omit<FlexProps, 'onSelect'> & { type TankSearchProps = Omit<FlexProps, 'onSelect'> & {
compact?: boolean; compact?: boolean;
onSelect?: (tank: TankDefinition) => void; onSelect?: (tank: Tank) => void;
onSelectAll?: (tanks: TankDefinition[]) => void; onSelectAll?: (tanks: Tank[]) => void;
}; };
const PREVIEW_COUNT = 20; const PREVIEW_COUNT = 20;
@@ -18,28 +18,28 @@ export function QuickInputs() {
const yawInput = useRef<HTMLInputElement>(null); const yawInput = useRef<HTMLInputElement>(null);
const pitchInput = useRef<HTMLInputElement>(null); const pitchInput = useRef<HTMLInputElement>(null);
const tankModelDefinition = useTankModelDefinition(); const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = // const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id]; // tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = // const gunModelDefinition =
turretModelDefinition.guns[protagonist.gun.gun_type!.value.base.id]; // turretModelDefinition.guns[protagonist.gun.gun_type!.value.base.id];
const initialGunPitch = const initialGunPitch =
tankModelDefinition.initial_turret_rotation?.pitch ?? 0; tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124); const hasDownImprovedVerticalStabilizer = useEquipment(124);
useEffect(() => { // useEffect(() => {
if (!yawInput.current) return; // if (!yawInput.current) return;
yawInput.current.value = radToDeg(protagonist.yaw).toFixed(1); // yawInput.current.value = radToDeg(protagonist.yaw).toFixed(1);
}, [protagonist.yaw]); // }, [protagonist.yaw]);
useEffect(() => { // useEffect(() => {
if (!pitchInput.current) return; // if (!pitchInput.current) return;
pitchInput.current.value = ( // pitchInput.current.value = (
-radToDeg(protagonist.pitch) + initialGunPitch // -radToDeg(protagonist.pitch) + initialGunPitch
).toFixed(1); // ).toFixed(1);
}, [protagonist.pitch]); // }, [protagonist.pitch]);
useEffect(() => { useEffect(() => {
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) { function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
@@ -1,12 +1,10 @@
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from '@blitzkit/i18n/src/literals';
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons'; import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
import { Button, Checkbox, Flex, Text } from '@radix-ui/themes'; import { Button, Checkbox, Flex, Text } from '@radix-ui/themes';
import { Suspense } from 'react';
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from '../../../../../../hooks/useLocale';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor'; import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
import { DynamicArmorSwitcher } from './DynamicArmorSwitcher';
interface ThicknessesProps { interface ThicknessesProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -190,9 +188,9 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
</Button> </Button>
</Flex> </Flex>
<Suspense> {/* <Suspense>
<DynamicArmorSwitcher /> <DynamicArmorSwitcher />
</Suspense> </Suspense> */}
</Flex> </Flex>
); );
} }
@@ -1,37 +1,6 @@
import { import { imgur } from '@blitzkit/core';
asset, import { Box, Flex, SegmentedControl, Tooltip } from '@radix-ui/themes';
imgur, import { useState, type RefObject } from 'react';
resolvePenetrationCoefficient,
TIER_ROMAN_NUMERALS,
uniqueGuns,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n/src/literals';
import {
CameraIcon,
CopyIcon,
DownloadIcon,
EnterFullScreenIcon,
ExitFullScreenIcon,
EyeOpenIcon,
GearIcon,
} from '@radix-ui/react-icons';
import {
Box,
Button,
Checkbox,
Dialog,
DropdownMenu,
Flex,
IconButton,
Popover,
SegmentedControl,
Tabs,
Text,
Tooltip,
} from '@radix-ui/themes';
import { Suspense, useState, type RefObject } from 'react';
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
import { useEquipment } from '../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../hooks/useFullScreen'; import { useFullScreen } from '../../../../../hooks/useFullScreen';
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability'; import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
import { useLocale } from '../../../../../hooks/useLocale'; import { useLocale } from '../../../../../hooks/useLocale';
@@ -44,12 +13,6 @@ import {
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor'; import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../SmallTankIcon';
import { TankSearch } from '../../../../TankSearch';
import { CustomShellButton } from './components/CustomShellButton';
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
import { QuickInputs } from './components/QuickInputs';
import { Thicknesses } from './components/Thicknesses'; import { Thicknesses } from './components/Thicknesses';
interface OptionsProps { interface OptionsProps {
@@ -78,22 +41,22 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
const fullScreenAvailable = useFullscreenAvailability(); const fullScreenAvailable = useFullscreenAvailability();
const developerMode = App.use((state) => state.developerMode); const developerMode = App.use((state) => state.developerMode);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistGun = Duel.use((state) => state.antagonist.gun); // const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell); // const antagonistShell = Duel.use((state) => state.antagonist.shell);
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false); const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
const antagonistTank = Duel.use((state) => state.antagonist.tank); const antagonistTank = Duel.use((state) => state.antagonist.tank);
const hasCalibratedShells = useEquipment(103, true); // const hasCalibratedShells = useEquipment(103, true);
const [tab, setTab] = useState('search'); const [tab, setTab] = useState('search');
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const hasEnhancedArmor = useEquipment(110); // const hasEnhancedArmor = useEquipment(110);
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets); // const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
const mutateTankopediaPersistent = TankopediaPersistent.useMutation(); const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
return ( return (
<> <>
<QuickInputs /> {/* <QuickInputs /> */}
{display === TankopediaDisplay.ShootingRange && ( {display === TankopediaDisplay.ShootingRange && (
<Box <Box
@@ -116,7 +79,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
<Thicknesses thicknessRange={thicknessRange} /> <Thicknesses thicknessRange={thicknessRange} />
)} )}
{display === TankopediaDisplay.DynamicArmor && ( {/* {display === TankopediaDisplay.DynamicArmor && (
<Flex <Flex
gap="2" gap="2"
direction="column" direction="column"
@@ -137,6 +100,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
])} ])}
</Text> </Text>
)} )}
{antagonistUniqueGuns.length > 1 && ( {antagonistUniqueGuns.length > 1 && (
<Popover.Root> <Popover.Root>
<Popover.Trigger> <Popover.Trigger>
@@ -215,6 +179,8 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root>
)} )}
<Flex <Flex
direction="column" direction="column"
style={{ style={{
@@ -321,7 +287,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
<DynamicArmorSwitcher /> <DynamicArmorSwitcher />
</Suspense> </Suspense>
</Flex> </Flex>
)} )} */}
<Flex <Flex
direction="column" direction="column"
@@ -331,7 +297,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
left="50%" left="50%"
style={{ transform: 'translateX(-50%)' }} style={{ transform: 'translateX(-50%)' }}
> >
{display === TankopediaDisplay.DynamicArmor && ( {/* {display === TankopediaDisplay.DynamicArmor && (
<> <>
<Flex <Flex
align="center" align="center"
@@ -380,13 +346,13 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
onSelect={(tank) => { onSelect={(tank) => {
mutateDuel((draft) => { mutateDuel((draft) => {
draft.antagonist.tank = tank; draft.antagonist.tank = tank;
draft.antagonist.engine = tank.engines.at(-1)!; // draft.antagonist.engine = tank.engines.at(-1)!;
draft.antagonist.track = tank.tracks.at(-1)!; // draft.antagonist.track = tank.tracks.at(-1)!;
draft.antagonist.turret = tank.turrets.at(-1)!; // draft.antagonist.turret = tank.turrets.at(-1)!;
draft.antagonist.gun = // draft.antagonist.gun =
draft.antagonist.turret.guns.at(-1)!; // draft.antagonist.turret.guns.at(-1)!;
draft.antagonist.shell = // draft.antagonist.shell =
draft.antagonist.gun.gun_type!.value.base.shells[0]; // draft.antagonist.gun.gun_type!.value.base.shells[0];
}); });
setAntagonistSelectorOpen(false); setAntagonistSelectorOpen(false);
}} }}
@@ -396,7 +362,8 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
</Dialog.Root> </Dialog.Root>
</Flex> </Flex>
</> </>
)} )} */}
<Flex gap="3" align="center" mt="2"> <Flex gap="3" align="center" mt="2">
<SegmentedControl.Root <SegmentedControl.Root
value={`${display}`} value={`${display}`}
@@ -441,7 +408,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
</SegmentedControl.Item> */} </SegmentedControl.Item> */}
</SegmentedControl.Root> </SegmentedControl.Root>
<DropdownMenu.Root> {/* <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<IconButton variant="ghost" color="gray"> <IconButton variant="ghost" color="gray">
<EyeOpenIcon /> <EyeOpenIcon />
@@ -461,9 +428,9 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
{strings.website.tools.tanks.sandbox.poses.default} {strings.website.tools.tanks.sandbox.poses.default}
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root> */}
<DropdownMenu.Root> {/* <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<IconButton variant="ghost" color="gray"> <IconButton variant="ghost" color="gray">
<GearIcon /> <GearIcon />
@@ -549,9 +516,9 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
{strings.website.tools.tanks.sandbox.settings.show_grid} {strings.website.tools.tanks.sandbox.settings.show_grid}
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root> */}
<Popover.Root> {/* <Popover.Root>
<Popover.Trigger> <Popover.Trigger>
<IconButton color="gray" variant="ghost"> <IconButton color="gray" variant="ghost">
<CameraIcon /> <CameraIcon />
@@ -603,9 +570,9 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
</Popover.Close> </Popover.Close>
</Flex> </Flex>
</Popover.Content> </Popover.Content>
</Popover.Root> </Popover.Root> */}
{fullScreenAvailable && ( {/* {fullScreenAvailable && (
<IconButton <IconButton
color="gray" color="gray"
variant="ghost" variant="ghost"
@@ -617,7 +584,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
> >
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />} {isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
</IconButton> </IconButton>
)} )} */}
</Flex> </Flex>
</Flex> </Flex>
</> </>
@@ -1,22 +1,14 @@
import { OrbitControls } from '@react-three/drei'; import { OrbitControls } from '@react-three/drei';
import { invalidate, useThree } from '@react-three/fiber'; import { useThree } from '@react-three/fiber';
import { clamp } from 'lodash-es'; import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react'; import { PerspectiveCamera } from 'three';
import { PerspectiveCamera, Vector3 } from 'three';
import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
import { degToRad } from 'three/src/math/MathUtils.js'; import { degToRad } from 'three/src/math/MathUtils.js';
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { Pose } from '../../../../../../core/blitzkit/pose';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
import { Duel } from '../../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
import { import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
SHOOTING_RANGE_ZOOM_COEFFICIENTS,
ShootingRangeZoom,
TankopediaEphemeral,
} from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
import { aimTarget } from './SceneProps/constants';
const poseDistances: Record<Pose, number> = { const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15, [Pose.HullDown]: 15,
@@ -43,302 +35,302 @@ export function Controls({ naked }: ControlsProps) {
const camera = useThree((state) => state.camera as PerspectiveCamera); const camera = useThree((state) => state.camera as PerspectiveCamera);
const canvas = useThree((state) => state.gl.domElement); const canvas = useThree((state) => state.gl.domElement);
const orbitControls = useRef<OrbitControlsClass>(null); const orbitControls = useRef<OrbitControlsClass>(null);
const protagonistTurret = Duel.use((state) => state.protagonist.turret); // const protagonistTurret = Duel.use((state) => state.protagonist.turret);
const antagonistTurret = Duel.use((state) => state.antagonist.turret); // const antagonistTurret = Duel.use((state) => state.antagonist.turret);
const protagonistTrack = Duel.use((state) => state.protagonist.track); // const protagonistTrack = Duel.use((state) => state.protagonist.track);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistTank = Duel.use((state) => state.antagonist.tank); // const antagonistTank = Duel.use((state) => state.antagonist.tank);
const protagonistGun = Duel.use((state) => state.protagonist.gun); // const protagonistGun = Duel.use((state) => state.protagonist.gun);
const protagonistModelDefinition = // const protagonistModelDefinition =
modelDefinitions.models[protagonistTank.id]; // modelDefinitions.models[protagonistTank.id];
const protagonistTrackModelDefinition = // const protagonistTrackModelDefinition =
modelDefinitions.models[protagonistTank.id].tracks[protagonistTrack.id]; // modelDefinitions.models[protagonistTank.id].tracks[protagonistTrack.id];
const antagonistModelDefinition = modelDefinitions.models[antagonistTank.id]; // const antagonistModelDefinition = modelDefinitions.models[antagonistTank.id];
const protagonistTurretModelDefinition = // const protagonistTurretModelDefinition =
protagonistModelDefinition.turrets[protagonistTurret.id]; // protagonistModelDefinition.turrets[protagonistTurret.id];
const antagonistTurretModelDefinition = // const antagonistTurretModelDefinition =
antagonistModelDefinition.turrets[antagonistTurret.id]; // antagonistModelDefinition.turrets[antagonistTurret.id];
const protagonistGunModelDefinition = // const protagonistGunModelDefinition =
protagonistTurretModelDefinition.guns[ // protagonistTurretModelDefinition.guns[
protagonistGun.gun_type!.value.base.id // protagonistGun.gun_type!.value.base.id
]; // ];
const protagonistHullOrigin = new Vector3( // const protagonistHullOrigin = new Vector3(
protagonistTrackModelDefinition.origin.x, // protagonistTrackModelDefinition.origin.x,
protagonistTrackModelDefinition.origin.y, // protagonistTrackModelDefinition.origin.y,
-protagonistTrackModelDefinition.origin.z, // -protagonistTrackModelDefinition.origin.z,
); // );
const protagonistTurretOrigin = new Vector3( // const protagonistTurretOrigin = new Vector3(
protagonistModelDefinition.turret_origin.x, // protagonistModelDefinition.turret_origin.x,
protagonistModelDefinition.turret_origin.y, // protagonistModelDefinition.turret_origin.y,
-protagonistModelDefinition.turret_origin.z, // -protagonistModelDefinition.turret_origin.z,
); // );
const protagonistGunOrigin = new Vector3( // const protagonistGunOrigin = new Vector3(
protagonistTurretModelDefinition.gun_origin.x, // protagonistTurretModelDefinition.gun_origin.x,
protagonistTurretModelDefinition.gun_origin.y, // protagonistTurretModelDefinition.gun_origin.y,
-protagonistTurretModelDefinition.gun_origin.z, // -protagonistTurretModelDefinition.gun_origin.z,
); // );
const antagonistGunHeight = // const antagonistGunHeight =
protagonistTrackModelDefinition.origin.y + // protagonistTrackModelDefinition.origin.y +
antagonistModelDefinition.turret_origin.y + // antagonistModelDefinition.turret_origin.y +
antagonistTurretModelDefinition.gun_origin.y; // antagonistTurretModelDefinition.gun_origin.y;
const [autoRotate, setAutoRotate] = useState( const [autoRotate, setAutoRotate] = useState(
display !== TankopediaDisplay.ShootingRange && !naked, display !== TankopediaDisplay.ShootingRange && !naked,
); );
const gunHeight = // const gunHeight =
protagonistHullOrigin.y + // protagonistHullOrigin.y +
protagonistTurretOrigin.y + // protagonistTurretOrigin.y +
protagonistGunOrigin.y; // protagonistGunOrigin.y;
const inspectModeInitialPosition = [ // const inspectModeInitialPosition = [
-8, // -8,
gunHeight + (naked ? 10 : 4), // gunHeight + (naked ? 10 : 4),
-13, // -13,
] as const; // ] as const;
const protagonistGunOriginOnlyY = new Vector3( // const protagonistGunOriginOnlyY = new Vector3(
0, // 0,
protagonistTurretModelDefinition.gun_origin.y, // protagonistTurretModelDefinition.gun_origin.y,
0, // 0,
); // );
const shellOrigin = protagonistHullOrigin // const shellOrigin = protagonistHullOrigin
.clone() // .clone()
.add(protagonistTurretOrigin) // .add(protagonistTurretOrigin)
.add(protagonistGunOriginOnlyY); // .add(protagonistGunOriginOnlyY);
useEffect(() => { // useEffect(() => {
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe( // const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
(state) => state.controlsEnabled, // (state) => state.controlsEnabled,
(enabled) => { // (enabled) => {
if (orbitControls.current) orbitControls.current.enabled = enabled; // if (orbitControls.current) orbitControls.current.enabled = enabled;
}, // },
); // );
async function handlePoseEvent(event: Pose) { // async function handlePoseEvent(event: Pose) {
const duel = duelStore.getState(); // const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment( // const hasImprovedVerticalStabilizer = await hasEquipment(
122, // 122,
duel.protagonist.tank.equipment_preset, // duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, // duel.protagonist.equipmentMatrix,
); // );
const hasDownImprovedVerticalStabilizer = await hasEquipment( // const hasDownImprovedVerticalStabilizer = await hasEquipment(
124, // 124,
duel.protagonist.tank.equipment_preset, // duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, // duel.protagonist.equipmentMatrix,
); // );
switch (event) { // switch (event) {
case Pose.HullDown: { // case Pose.HullDown: {
const [pitch] = applyPitchYawLimits( // const [pitch] = applyPitchYawLimits(
-Infinity, // -Infinity,
0, // 0,
protagonistGunModelDefinition.pitch, // protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw, // protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer, // hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, // hasDownImprovedVerticalStabilizer,
); // );
camera.position // camera.position
.set(0, 0, 0) // .set(0, 0, 0)
.add(protagonistHullOrigin) // .add(protagonistHullOrigin)
.add(protagonistTurretOrigin) // .add(protagonistTurretOrigin)
.add(protagonistGunOrigin) // .add(protagonistGunOrigin)
.add( // .add(
new Vector3( // new Vector3(
0, // 0,
poseDistances[event] * Math.sin(pitch), // poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch), // poseDistances[event] * -Math.cos(pitch),
), // ),
); // );
camera.lookAt( // camera.lookAt(
protagonistHullOrigin // protagonistHullOrigin
.clone() // .clone()
.add(protagonistTurretOrigin) // .add(protagonistTurretOrigin)
.add(protagonistGunOrigin), // .add(protagonistGunOrigin),
); // );
orbitControls.current?.target.set(0, antagonistGunHeight, 0); // orbitControls.current?.target.set(0, antagonistGunHeight, 0);
break; // break;
} // }
case Pose.FaceHug: { // case Pose.FaceHug: {
const [pitch] = applyPitchYawLimits( // const [pitch] = applyPitchYawLimits(
0, // 0,
0, // 0,
protagonistGunModelDefinition.pitch, // protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw, // protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer, // hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, // hasDownImprovedVerticalStabilizer,
); // );
camera.position // camera.position
.set(0, 0, 0) // .set(0, 0, 0)
.add(protagonistHullOrigin) // .add(protagonistHullOrigin)
.add(protagonistTurretOrigin) // .add(protagonistTurretOrigin)
.add(protagonistGunOrigin) // .add(protagonistGunOrigin)
.add( // .add(
new Vector3( // new Vector3(
0, // 0,
poseDistances[event] * Math.sin(pitch), // poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch), // poseDistances[event] * -Math.cos(pitch),
), // ),
); // );
orbitControls.current?.target // orbitControls.current?.target
.set(0, 0, 0) // .set(0, 0, 0)
.add(protagonistHullOrigin) // .add(protagonistHullOrigin)
.add(protagonistTurretOrigin) // .add(protagonistTurretOrigin)
.add(protagonistGunOrigin) // .add(protagonistGunOrigin)
.add( // .add(
new Vector3( // new Vector3(
0, // 0,
0.5 * poseDistances[event] * Math.sin(pitch), // 0.5 * poseDistances[event] * Math.sin(pitch),
0.5 * poseDistances[event] * -Math.cos(pitch), // 0.5 * poseDistances[event] * -Math.cos(pitch),
), // ),
); // );
break; // break;
} // }
case Pose.Default: { // case Pose.Default: {
camera.position.set(...inspectModeInitialPosition); // camera.position.set(...inspectModeInitialPosition);
orbitControls.current?.target.set(0, 1.25, 0); // orbitControls.current?.target.set(0, 1.25, 0);
break; // break;
} // }
} // }
invalidate(); // invalidate();
} // }
poseEvent.on(handlePoseEvent); // poseEvent.on(handlePoseEvent);
return () => { // return () => {
unsubscribeTankopediaEphemeral(); // unsubscribeTankopediaEphemeral();
poseEvent.off(handlePoseEvent); // poseEvent.off(handlePoseEvent);
}; // };
}, [camera, protagonistTank.id, antagonistTank.id]); // }, [camera, protagonistTank.id, antagonistTank.id]);
useEffect(() => { // useEffect(() => {
let isInitialIteration = true; // let isInitialIteration = true;
function handleDisturbance() { // function handleDisturbance() {
setAutoRotate(false); // setAutoRotate(false);
} // }
function handleKeyDown(event: KeyboardEvent) { // function handleKeyDown(event: KeyboardEvent) {
if (display !== TankopediaDisplay.ShootingRange || event.repeat) return; // if (display !== TankopediaDisplay.ShootingRange || event.repeat) return;
if (event.key === 'Shift') { // if (event.key === 'Shift') {
mutateTankopediaEphemeral((draft) => { // mutateTankopediaEphemeral((draft) => {
draft.shootingRangeZoom = // draft.shootingRangeZoom =
draft.shootingRangeZoom === ShootingRangeZoom.Arcade // draft.shootingRangeZoom === ShootingRangeZoom.Arcade
? ShootingRangeZoom.Zoom0 // ? ShootingRangeZoom.Zoom0
: ShootingRangeZoom.Arcade; // : ShootingRangeZoom.Arcade;
}); // });
} // }
} // }
function handleWheel(event: WheelEvent) { // function handleWheel(event: WheelEvent) {
if (display !== TankopediaDisplay.ShootingRange) return; // if (display !== TankopediaDisplay.ShootingRange) return;
event.preventDefault(); // event.preventDefault();
mutateTankopediaEphemeral((draft) => { // mutateTankopediaEphemeral((draft) => {
draft.shootingRangeZoom = clamp( // draft.shootingRangeZoom = clamp(
draft.shootingRangeZoom - Math.sign(event.deltaY), // draft.shootingRangeZoom - Math.sign(event.deltaY),
ShootingRangeZoom.Arcade, // ShootingRangeZoom.Arcade,
ShootingRangeZoom.Zoom2, // ShootingRangeZoom.Zoom2,
); // );
}); // });
} // }
function handleScroll(event: Event) { // function handleScroll(event: Event) {
if (display !== TankopediaDisplay.ShootingRange) return; // if (display !== TankopediaDisplay.ShootingRange) return;
event.preventDefault(); // event.preventDefault();
} // }
poseEvent.on(handleDisturbance); // poseEvent.on(handleDisturbance);
canvas.addEventListener('pointerdown', handleDisturbance); // canvas.addEventListener('pointerdown', handleDisturbance);
window.addEventListener('keydown', handleKeyDown); // window.addEventListener('keydown', handleKeyDown);
canvas.addEventListener('wheel', handleWheel); // canvas.addEventListener('wheel', handleWheel);
document.body.addEventListener('scroll', handleScroll); // document.body.addEventListener('scroll', handleScroll);
function updateCamera() { // function updateCamera() {
if (!orbitControls.current) return; // if (!orbitControls.current) return;
orbitControls.current.rotateSpeed = 0.25; // orbitControls.current.rotateSpeed = 0.25;
if (display === TankopediaDisplay.ShootingRange) { // if (display === TankopediaDisplay.ShootingRange) {
const { shootingRangeZoom } = tankopediaEphemeralStore.getState(); // const { shootingRangeZoom } = tankopediaEphemeralStore.getState();
orbitControls.current.enablePan = false; // orbitControls.current.enablePan = false;
orbitControls.current.enableZoom = false; // orbitControls.current.enableZoom = false;
orbitControls.current.rotateSpeed *= // orbitControls.current.rotateSpeed *=
SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom]; // SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
camera.fov = // camera.fov =
ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom]; // ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
if (shootingRangeZoom === ShootingRangeZoom.Arcade) { // if (shootingRangeZoom === ShootingRangeZoom.Arcade) {
orbitControls.current.target.set(0, gunHeight + 3, 0); // orbitControls.current.target.set(0, gunHeight + 3, 0);
if (isInitialIteration) { // if (isInitialIteration) {
isInitialIteration = false; // isInitialIteration = false;
camera.position.set( // camera.position.set(
0, // 0,
gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE), // gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE),
ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE), // ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE),
); // );
} else { // } else {
camera.position // camera.position
.copy(aimTarget) // .copy(aimTarget)
.sub(orbitControls.current.target) // .sub(orbitControls.current.target)
.normalize() // .normalize()
.multiplyScalar(-ARCADE_MODE_DISTANCE) // .multiplyScalar(-ARCADE_MODE_DISTANCE)
.add(orbitControls.current.target); // .add(orbitControls.current.target);
} // }
} else { // } else {
orbitControls.current.target.copy(shellOrigin); // orbitControls.current.target.copy(shellOrigin);
camera.position // camera.position
.copy(aimTarget) // .copy(aimTarget)
.sub(shellOrigin) // .sub(shellOrigin)
.normalize() // .normalize()
.multiplyScalar(-Number.EPSILON) // .multiplyScalar(-Number.EPSILON)
.add(shellOrigin); // .add(shellOrigin);
} // }
} else { // } else {
camera.position.set(...inspectModeInitialPosition); // camera.position.set(...inspectModeInitialPosition);
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0); // orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
orbitControls.current.enablePan = true; // orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true; // orbitControls.current.enableZoom = true;
camera.fov = naked ? 20 : 25; // camera.fov = naked ? 20 : 25;
} // }
camera.updateProjectionMatrix(); // camera.updateProjectionMatrix();
} // }
updateCamera(); // updateCamera();
const unsubscribeDisplay = tankopediaEphemeralStore.subscribe( // const unsubscribeDisplay = tankopediaEphemeralStore.subscribe(
(state) => state.display, // (state) => state.display,
() => { // () => {
handleDisturbance(); // handleDisturbance();
updateCamera(); // updateCamera();
}, // },
); // );
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe( // const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
(state) => state.shootingRangeZoom, // (state) => state.shootingRangeZoom,
updateCamera, // updateCamera,
); // );
return () => { // return () => {
canvas.removeEventListener('pointerdown', handleDisturbance); // canvas.removeEventListener('pointerdown', handleDisturbance);
poseEvent.off(handleDisturbance); // poseEvent.off(handleDisturbance);
unsubscribeDisplay(); // unsubscribeDisplay();
unsubscribeShootingRangeZoom(); // unsubscribeShootingRangeZoom();
window.removeEventListener('keydown', handleKeyDown); // window.removeEventListener('keydown', handleKeyDown);
canvas.removeEventListener('wheel', handleWheel); // canvas.removeEventListener('wheel', handleWheel);
document.body.removeEventListener('scroll', handleScroll); // document.body.removeEventListener('scroll', handleScroll);
}; // };
}, [display === TankopediaDisplay.ShootingRange]); // }, [display === TankopediaDisplay.ShootingRange]);
return ( return (
<OrbitControls <OrbitControls
@@ -2,19 +2,14 @@ import { Environment } from '@react-three/drei';
import { times } from 'lodash-es'; import { times } from 'lodash-es';
import { Euler } from 'three'; import { Euler } from 'three';
import { degToRad } from 'three/src/math/MathUtils.js'; import { degToRad } from 'three/src/math/MathUtils.js';
import { useModel } from '../../../../../../hooks/useModel';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
const LIGHTS_COUNT = 5; const LIGHTS_COUNT = 5;
export function Lighting() { export function Lighting() {
const protagonist = Duel.use((state) => state.protagonist);
const display = TankopediaEphemeral.use((state) => state.display); const display = TankopediaEphemeral.use((state) => state.display);
const { hasPbr } = useModel(protagonist.tank.id); const isBrighterLighting = display !== TankopediaDisplay.StaticArmor;
const isBrighterLighting =
!hasPbr && display !== TankopediaDisplay.StaticArmor;
return ( return (
<> <>
@@ -1,29 +1,13 @@
import { asset, I_HAT, imgur, J_HAT } from '@blitzkit/core'; import { imgur } from '@blitzkit/core';
import { Html } from '@react-three/drei';
import { useFrame, useLoader } from '@react-three/fiber'; import { useFrame, useLoader } from '@react-three/fiber';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { useRef } from 'react'; import { useRef } from 'react';
import { import { MeshStandardMaterial, TextureLoader, Vector2 } from 'three';
Group,
MeshStandardMaterial,
PerspectiveCamera,
TextureLoader,
Vector2,
Vector3,
} from 'three';
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions'; import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions';
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
import { Duel } from '../../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants';
import { TargetCircle } from './components/TargetCircle';
import { aimTarget } from './constants';
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] = const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
await Promise.all([ await Promise.all([
@@ -35,203 +19,203 @@ const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
const emptyVector = new Vector2(); const emptyVector = new Vector2();
export function SceneProps() { export function SceneProps() {
const targetCircleWrapper = useRef<Group>(null); // const targetCircleWrapper = useRef<Group>(null);
const clientTargetCircle = useRef<HTMLDivElement>(null); // const clientTargetCircle = useRef<HTMLDivElement>(null);
const serverTargetCircle = useRef<HTMLDivElement>(null); // const serverTargetCircle = useRef<HTMLDivElement>(null);
const material = useRef<MeshStandardMaterial>(null); const material = useRef<MeshStandardMaterial>(null);
const playground = useRef<Group>(null); // const playground = useRef<Group>(null);
let lastPitch = Duel.use((state) => state.protagonist.pitch); // let lastPitch = Duel.use((state) => state.protagonist.pitch);
let lastYaw = Duel.use((state) => state.protagonist.yaw); // let lastYaw = Duel.use((state) => state.protagonist.yaw);
const display = TankopediaEphemeral.use((state) => state.display); const display = TankopediaEphemeral.use((state) => state.display);
const turret = Duel.use((state) => state.protagonist.turret); // const turret = Duel.use((state) => state.protagonist.turret);
const track = Duel.use((state) => state.protagonist.track); // const track = Duel.use((state) => state.protagonist.track);
const tank = Duel.use((state) => state.protagonist.tank); // const tank = Duel.use((state) => state.protagonist.tank);
const gun = Duel.use((state) => state.protagonist.gun); // const gun = Duel.use((state) => state.protagonist.gun);
const shell = Duel.use((state) => state.protagonist.shell); // const shell = Duel.use((state) => state.protagonist.shell);
const engine = Duel.use((state) => state.protagonist.engine); // const engine = Duel.use((state) => state.protagonist.engine);
const tankModelDefinition = modelDefinitions.models[tank.id]; // const tankModelDefinition = modelDefinitions.models[tank.id];
const trackModelDefinition = // const trackModelDefinition =
modelDefinitions.models[tank.id].tracks[track.id]; // modelDefinitions.models[tank.id].tracks[track.id];
const gunModelDefinition = // const gunModelDefinition =
tankModelDefinition.turrets[turret.id].guns[gun.gun_type!.value.base.id]; // tankModelDefinition.turrets[turret.id].guns[gun.gun_type!.value.base.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; // const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const hullOrigin = new Vector3( // const hullOrigin = new Vector3(
trackModelDefinition.origin.x, // trackModelDefinition.origin.x,
trackModelDefinition.origin.y, // trackModelDefinition.origin.y,
-trackModelDefinition.origin.z, // -trackModelDefinition.origin.z,
); // );
const turretOrigin = new Vector3( // const turretOrigin = new Vector3(
tankModelDefinition.turret_origin.x, // tankModelDefinition.turret_origin.x,
tankModelDefinition.turret_origin.y, // tankModelDefinition.turret_origin.y,
-tankModelDefinition.turret_origin.z, // -tankModelDefinition.turret_origin.z,
); // );
const gunOriginOnlyY = new Vector3(0, turretModelDefinition.gun_origin.y, 0); // const gunOriginOnlyY = new Vector3(0, turretModelDefinition.gun_origin.y, 0);
const shellOrigin = hullOrigin.clone().add(turretOrigin).add(gunOriginOnlyY); // const shellOrigin = hullOrigin.clone().add(turretOrigin).add(gunOriginOnlyY);
const camouflage = Duel.use((state) => state.protagonist.camouflage); // const camouflage = Duel.use((state) => state.protagonist.camouflage);
const crewSkills = TankopediaEphemeral.use((state) => state.skills); // const crewSkills = TankopediaEphemeral.use((state) => state.skills);
const consumables = Duel.use((state) => state.protagonist.consumables); // const consumables = Duel.use((state) => state.protagonist.consumables);
const provisions = Duel.use((state) => state.protagonist.provisions); // const provisions = Duel.use((state) => state.protagonist.provisions);
const equipmentMatrix = Duel.use( // const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix, // (state) => state.protagonist.equipmentMatrix,
); // );
const characteristics = tankCharacteristics( // const characteristics = tankCharacteristics(
{ // {
tank, // tank,
turret, // turret,
gun, // gun,
shell, // shell,
consumables, // consumables,
applyDynamicArmor: false, // applyDynamicArmor: false,
applyReactiveArmor: false, // applyReactiveArmor: false,
applySpallLiner: false, // applySpallLiner: false,
camouflage, // camouflage,
crewSkills, // crewSkills,
engine, // engine,
equipmentMatrix, // equipmentMatrix,
provisions, // provisions,
stockEngine: tank.engines[0], // stockEngine: tank.engines[0],
stockGun: tank.turrets[0].guns[0], // stockGun: tank.turrets[0].guns[0],
stockTurret: tank.turrets[0], // stockTurret: tank.turrets[0],
stockTrack: tank.tracks[0], // stockTrack: tank.tracks[0],
track, // track,
}, // },
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition }, // { equipmentDefinitions, provisionDefinitions, tankModelDefinition },
); // );
let { dispersion } = characteristics; // let { dispersion } = characteristics;
const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed); // const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed);
const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`)); // const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`));
const texture = useLoader(TextureLoader, imgur('C28Z8nU')); const texture = useLoader(TextureLoader, imgur('C28Z8nU'));
const path = new Vector3();
const direction = new Vector3();
texture.anisotropy = 2; texture.anisotropy = 2;
// const path = new Vector3();
// const direction = new Vector3();
useFrame(({ camera }) => { useFrame(({ camera }) => {
if (!material.current) return; if (!material.current) return;
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1); material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
}); });
let lastTime = 0; // let lastTime = 0;
let DEBUG_disable = false; // let DEBUG_disable = false;
useFrame(({ raycaster, camera, clock, gl }) => { // useFrame(({ raycaster, camera, clock, gl }) => {
if ( // if (
display !== TankopediaDisplay.ShootingRange || // display !== TankopediaDisplay.ShootingRange ||
!playground.current || // !playground.current ||
!targetCircleWrapper.current || // !targetCircleWrapper.current ||
DEBUG_disable || // DEBUG_disable ||
!clientTargetCircle.current || // !clientTargetCircle.current ||
!serverTargetCircle.current // !serverTargetCircle.current
) { // ) {
return; // return;
} // }
raycaster.setFromCamera(emptyVector, camera); // raycaster.setFromCamera(emptyVector, camera);
const cameraIntersections = raycaster.intersectObjects( // const cameraIntersections = raycaster.intersectObjects(
playground.current.children, // playground.current.children,
true, // true,
); // );
let length: number; // let length: number;
if (cameraIntersections.length === 0) { // if (cameraIntersections.length === 0) {
length = characteristics.shellRange; // length = characteristics.shellRange;
direction.set(0, 0, -1).applyQuaternion(camera.quaternion); // direction.set(0, 0, -1).applyQuaternion(camera.quaternion);
aimTarget.copy(shellOrigin).addScaledVector(direction, length); // aimTarget.copy(shellOrigin).addScaledVector(direction, length);
} else { // } else {
aimTarget.copy(cameraIntersections[0].point); // aimTarget.copy(cameraIntersections[0].point);
path.copy(aimTarget).sub(shellOrigin); // path.copy(aimTarget).sub(shellOrigin);
length = path.length(); // length = path.length();
direction.copy(path).normalize(); // direction.copy(path).normalize();
} // }
const deltaT = clock.elapsedTime - lastTime; // const deltaT = clock.elapsedTime - lastTime;
lastTime = clock.elapsedTime; // lastTime = clock.elapsedTime;
const desiredPitch = Math.asin(direction.y); // const desiredPitch = Math.asin(direction.y);
const desiredYaw = Math.atan2(-direction.x, -direction.z); // const desiredYaw = Math.atan2(-direction.x, -direction.z);
const deltaPitch = desiredPitch - lastPitch; // const deltaPitch = desiredPitch - lastPitch;
const deltaYaw = desiredYaw - lastYaw; // const deltaYaw = desiredYaw - lastYaw;
const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2); // const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2);
const maxPossibleRotationInTime = gunRotationSpeed * deltaT; // const maxPossibleRotationInTime = gunRotationSpeed * deltaT;
const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle); // const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle);
const clampedDeltaPitch = // const clampedDeltaPitch =
deltaAngle === 0 ? 0 : deltaPitch * (clampedDeltaAngle / deltaAngle); // deltaAngle === 0 ? 0 : deltaPitch * (clampedDeltaAngle / deltaAngle);
const clampedDeltaYaw = // const clampedDeltaYaw =
deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle); // deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle);
const [pitch, yaw] = applyPitchYawLimits( // const [pitch, yaw] = applyPitchYawLimits(
lastPitch + clampedDeltaPitch, // lastPitch + clampedDeltaPitch,
lastYaw + clampedDeltaYaw, // lastYaw + clampedDeltaYaw,
gunModelDefinition.pitch, // gunModelDefinition.pitch,
turretModelDefinition.yaw, // turretModelDefinition.yaw,
); // );
const actualDeltaPitch = pitch - lastPitch; // const actualDeltaPitch = pitch - lastPitch;
const actualDeltaYaw = yaw - lastYaw; // const actualDeltaYaw = yaw - lastYaw;
const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw); // const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw);
const rotationSpeed = deltaRotation / deltaT; // const rotationSpeed = deltaRotation / deltaT;
const turretTraversePenalty = // const turretTraversePenalty =
characteristics.dispersionTurretTraversing * radToDeg(rotationSpeed); // characteristics.dispersionTurretTraversing * radToDeg(rotationSpeed);
const penalty = Math.sqrt(1 + turretTraversePenalty ** 2); // const penalty = Math.sqrt(1 + turretTraversePenalty ** 2);
if (penalty === 1) { // if (penalty === 1) {
const dispersionMod = dispersion - characteristics.dispersion; // const dispersionMod = dispersion - characteristics.dispersion;
const dispersionModDischarged = // const dispersionModDischarged =
dispersionMod * Math.exp(-deltaT / characteristics.aimTime) - // dispersionMod * Math.exp(-deltaT / characteristics.aimTime) -
dispersionMod; // dispersionMod;
dispersion += dispersionModDischarged; // dispersion += dispersionModDischarged;
} else { // } else {
dispersion = Math.max(characteristics.dispersion * penalty, dispersion); // dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
} // }
modelTransformEvent.emit({ pitch, yaw }); // modelTransformEvent.emit({ pitch, yaw });
lastPitch = pitch; // lastPitch = pitch;
lastYaw = yaw; // lastYaw = yaw;
const gunDirection = new Vector3(0, 0, -1) // const gunDirection = new Vector3(0, 0, -1)
.applyAxisAngle(I_HAT, pitch) // .applyAxisAngle(I_HAT, pitch)
.applyAxisAngle(J_HAT, yaw); // .applyAxisAngle(J_HAT, yaw);
raycaster.set(shellOrigin, gunDirection); // raycaster.set(shellOrigin, gunDirection);
const gunIntersections = raycaster.intersectObjects( // const gunIntersections = raycaster.intersectObjects(
playground.current.children, // playground.current.children,
true, // true,
); // );
let gunTarget: Vector3; // let gunTarget: Vector3;
if (gunIntersections.length === 0) { // if (gunIntersections.length === 0) {
gunTarget = shellOrigin // gunTarget = shellOrigin
.clone() // .clone()
.add(gunDirection.multiplyScalar(characteristics.shellRange)); // .add(gunDirection.multiplyScalar(characteristics.shellRange));
} else { // } else {
gunTarget = gunIntersections[0].point; // gunTarget = gunIntersections[0].point;
} // }
targetCircleWrapper.current.position.copy(gunTarget); // targetCircleWrapper.current.position.copy(gunTarget);
const distanceToTarget = gunTarget.distanceTo(shellOrigin); // const distanceToTarget = gunTarget.distanceTo(shellOrigin);
const dispersionRadius = dispersion * (distanceToTarget / 100); // const dispersionRadius = dispersion * (distanceToTarget / 100);
const dispersionSize = 2 * dispersionRadius; // const dispersionSize = 2 * dispersionRadius;
const targetDistanceFromCamera = gunTarget.distanceTo(camera.position); // const targetDistanceFromCamera = gunTarget.distanceTo(camera.position);
const angularSize = // const angularSize =
2 * Math.atan(dispersionSize / (2 * targetDistanceFromCamera)); // 2 * Math.atan(dispersionSize / (2 * targetDistanceFromCamera));
const verticalScreenSize = // const verticalScreenSize =
2 * Math.tan(degToRad((camera as PerspectiveCamera).fov) / 2); // 2 * Math.tan(degToRad((camera as PerspectiveCamera).fov) / 2);
const screenSpaceSize = // const screenSpaceSize =
gl.domElement.clientHeight * (angularSize / verticalScreenSize); // gl.domElement.clientHeight * (angularSize / verticalScreenSize);
const targetCircleDotsSize = `${screenSpaceSize}px`; // const targetCircleDotsSize = `${screenSpaceSize}px`;
clientTargetCircle.current.style.width = targetCircleDotsSize; // clientTargetCircle.current.style.width = targetCircleDotsSize;
clientTargetCircle.current.style.height = targetCircleDotsSize; // clientTargetCircle.current.style.height = targetCircleDotsSize;
serverTargetCircle.current.style.width = targetCircleDotsSize; // serverTargetCircle.current.style.width = targetCircleDotsSize;
serverTargetCircle.current.style.height = targetCircleDotsSize; // serverTargetCircle.current.style.height = targetCircleDotsSize;
}); // });
return ( return (
<> <>
@@ -242,7 +226,7 @@ export function SceneProps() {
</mesh> </mesh>
)} )}
{display === TankopediaDisplay.ShootingRange && ( {/* {display === TankopediaDisplay.ShootingRange && (
<> <>
<group ref={targetCircleWrapper}> <group ref={targetCircleWrapper}>
<Html center occlude={false} zIndexRange={[0, 0]}> <Html center occlude={false} zIndexRange={[0, 0]}>
@@ -272,7 +256,7 @@ export function SceneProps() {
</group> </group>
</group> </group>
</> </>
)} )} */}
</> </>
); );
} }
@@ -18,6 +18,8 @@ import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper'; import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
export function TankModel() { export function TankModel() {
return null;
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
const protagonist = Duel.use((draft) => draft.protagonist); const protagonist = Duel.use((draft) => draft.protagonist);
@@ -1,19 +1,7 @@
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core'; import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { import { forwardRef, Suspense, useImperativeHandle, useRef } from 'react';
forwardRef,
Suspense,
useEffect,
useImperativeHandle,
useRef,
} from 'react';
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
import { useEquipment } from '../../../../../hooks/useEquipment';
import { useOnScreen } from '../../../../../hooks/useOnScreen'; import { useOnScreen } from '../../../../../hooks/useOnScreen';
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
@@ -40,16 +28,16 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange, naked }, ref) => { ({ thicknessRange, naked }, ref) => {
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122); // const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124); // const hasDownImprovedVerticalStabilizer = useEquipment(124);
const protagonistGun = Duel.use((state) => state.protagonist.gun); // const protagonistGun = Duel.use((state) => state.protagonist.gun);
const protagonistTurret = Duel.use((state) => state.protagonist.turret); // const protagonistTurret = Duel.use((state) => state.protagonist.turret);
const mutateDuel = Duel.useMutation(); // const mutateDuel = Duel.useMutation();
const tankModelDefinition = useTankModelDefinition(); // const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition = // const turretModelDefinition =
tankModelDefinition.turrets[protagonistTurret.id]; // tankModelDefinition.turrets[protagonistTurret.id];
const gunModelDefinition = // const gunModelDefinition =
turretModelDefinition.guns[protagonistGun.gun_type!.value.base.id]; // turretModelDefinition.guns[protagonistGun.gun_type!.value.base.id];
const display = TankopediaEphemeral.use((state) => state.display); const display = TankopediaEphemeral.use((state) => state.display);
const hideTankModelUnderArmor = TankopediaPersistent.use( const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor, (state) => state.hideTankModelUnderArmor,
@@ -72,97 +60,97 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
} }
useEffect(() => { // useEffect(() => {
function handlePoseEvent(pose: Pose) { // function handlePoseEvent(pose: Pose) {
switch (pose) { // switch (pose) {
case Pose.HullDown: { // case Pose.HullDown: {
mutateDuel((draft) => { // mutateDuel((draft) => {
const [pitch, yaw] = applyPitchYawLimits( // const [pitch, yaw] = applyPitchYawLimits(
-Infinity, // -Infinity,
0, // 0,
gunModelDefinition.pitch, // gunModelDefinition.pitch,
turretModelDefinition.yaw, // turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, // hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, // hasDownImprovedVerticalStabilizer,
); // );
modelTransformEvent.emit({ pitch, yaw }); // modelTransformEvent.emit({ pitch, yaw });
draft.protagonist.pitch = pitch; // draft.protagonist.pitch = pitch;
draft.protagonist.yaw = yaw; // draft.protagonist.yaw = yaw;
}); // });
break; // break;
} // }
case Pose.FaceHug: { // case Pose.FaceHug: {
mutateDuel((draft) => { // mutateDuel((draft) => {
const [pitch, yaw] = applyPitchYawLimits( // const [pitch, yaw] = applyPitchYawLimits(
Infinity, // Infinity,
0, // 0,
gunModelDefinition.pitch, // gunModelDefinition.pitch,
turretModelDefinition.yaw, // turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, // hasImprovedVerticalStabilizer,
); // );
modelTransformEvent.emit({ pitch, yaw }); // modelTransformEvent.emit({ pitch, yaw });
draft.protagonist.pitch = pitch; // draft.protagonist.pitch = pitch;
draft.protagonist.yaw = yaw; // draft.protagonist.yaw = yaw;
}); // });
break; // break;
} // }
case Pose.Default: // case Pose.Default:
mutateDuel((draft) => { // mutateDuel((draft) => {
const [pitch, yaw] = applyPitchYawLimits( // const [pitch, yaw] = applyPitchYawLimits(
0, // 0,
0, // 0,
gunModelDefinition.pitch, // gunModelDefinition.pitch,
turretModelDefinition.yaw, // turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, // hasImprovedVerticalStabilizer,
); // );
modelTransformEvent.emit({ pitch, yaw }); // modelTransformEvent.emit({ pitch, yaw });
draft.protagonist.pitch = pitch; // draft.protagonist.pitch = pitch;
draft.protagonist.yaw = yaw; // draft.protagonist.yaw = yaw;
}); // });
break; // break;
} // }
} // }
poseEvent.on(handlePoseEvent); // poseEvent.on(handlePoseEvent);
return () => { // return () => {
poseEvent.off(handlePoseEvent); // poseEvent.off(handlePoseEvent);
}; // };
}); // });
useEffect(() => { // useEffect(() => {
mutateDuel((draft) => { // mutateDuel((draft) => {
[draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits( // [draft.protagonist.pitch, draft.protagonist.yaw] = applyPitchYawLimits(
draft.protagonist.pitch, // draft.protagonist.pitch,
draft.protagonist.yaw, // draft.protagonist.yaw,
gunModelDefinition.pitch, // gunModelDefinition.pitch,
turretModelDefinition.yaw, // turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, // hasImprovedVerticalStabilizer,
); // );
}); // });
}, [protagonistGun, protagonistTurret]); // }, [protagonistGun, protagonistTurret]);
useEffect(() => { // useEffect(() => {
if (display !== TankopediaDisplay.DynamicArmor) { // if (display !== TankopediaDisplay.DynamicArmor) {
mutateTankopediaEphemeral((draft) => { // mutateTankopediaEphemeral((draft) => {
draft.shot = undefined; // draft.shot = undefined;
}); // });
} // }
if (display !== TankopediaDisplay.StaticArmor) { // if (display !== TankopediaDisplay.StaticArmor) {
mutateTankopediaEphemeral((draft) => { // mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined; // draft.highlightArmor = undefined;
}); // });
} // }
}, [display]); // }, [display]);
return ( return (
<Canvas <Canvas
@@ -1,5 +1,5 @@
import { Box, Flex, Heading, Text } from '@radix-ui/themes'; import { Box, Flex, Heading, Text } from '@radix-ui/themes';
import { useRef } from 'react'; import { Suspense, useRef } from 'react';
import { NAVBAR_HEIGHT } from '../../../constants/navbar'; import { NAVBAR_HEIGHT } from '../../../constants/navbar';
import { Var } from '../../../core/radix/var'; import { Var } from '../../../core/radix/var';
import { useFullScreen } from '../../../hooks/useFullScreen'; import { useFullScreen } from '../../../hooks/useFullScreen';
@@ -7,6 +7,8 @@ import { useLocale } from '../../../hooks/useLocale';
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 type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
import { Options } from './components/Options';
import { TankSandbox } from './components/TankSandbox';
import { TankSandboxLoader } from './components/TankSandboxLoader'; import { TankSandboxLoader } from './components/TankSandboxLoader';
export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) { export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
@@ -137,12 +139,12 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
<Box width="100%" height="100%"> <Box width="100%" height="100%">
{skeleton && <TankSandboxLoader id={protagonist.id} />} {skeleton && <TankSandboxLoader id={protagonist.id} />}
{/* <Suspense fallback={<TankSandboxLoader id={protagonist.id} />}> <Suspense fallback={<TankSandboxLoader id={protagonist.id} />}>
<TankSandbox ref={canvas} thicknessRange={thicknessRange} /> <TankSandbox ref={canvas} thicknessRange={thicknessRange} />
</Suspense> */} </Suspense>
</Box> </Box>
{/* <Options canvas={canvas} thicknessRange={thicknessRange} /> */} <Options canvas={canvas} thicknessRange={thicknessRange} />
</Box> </Box>
</Box> </Box>
</Flex> </Flex>
@@ -134,7 +134,7 @@ export const TankopediaEphemeral = createContextualStore((armor: TankArmor) => {
armor, armor,
controlsEnabled: true, controlsEnabled: true,
xpMultiplier: 1, xpMultiplier: 1,
display: TankopediaDisplay.Model, display: TankopediaDisplay.StaticArmor,
})), })),
); );
}); });