mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: simplified duel state
This commit is contained in:
1 parent
7314f1d9b1
commit
66016dedf0
13 files changed
+276
-246
No files matched your search
@@ -59,6 +59,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fuzzysort": "^1.0.1",
|
||||
"react-promise-suspense": "^0.3.4",
|
||||
"react-spinners": "^0.13.8",
|
||||
"satori": "^0.10.8",
|
||||
"sharp": "^0.33.2",
|
||||
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { go } from 'fuzzysort';
|
||||
import { produce } from 'immer';
|
||||
import { debounce } from 'lodash';
|
||||
import { use, useRef, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, use, useRef, useState } from 'react';
|
||||
import { ModuleButton } from '../../../../../components/ModuleButton';
|
||||
import { SmallTankIcon } from '../../../../../components/SmallTankIcon';
|
||||
import {
|
||||
@@ -25,23 +26,26 @@ import {
|
||||
tankDefinitions,
|
||||
tankNamesDiacritics,
|
||||
} from '../../../../../core/blitzkrieg/tankDefinitions';
|
||||
import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../stores/tankopedia';
|
||||
import { useTankopedia } from '../../../../../stores/tankopedia';
|
||||
import { Duel } from '../page';
|
||||
|
||||
export function AntagonistBar() {
|
||||
interface AntagonistBarProps {
|
||||
duel: Duel;
|
||||
setDuel: Dispatch<SetStateAction<Duel>>;
|
||||
}
|
||||
|
||||
export function AntagonistBar({
|
||||
duel: { antagonist },
|
||||
setDuel,
|
||||
}: AntagonistBarProps) {
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const mode = useTankopedia((state) => state.mode);
|
||||
const awaitedTankNamesDiacritics = use(tankNamesDiacritics);
|
||||
const [tab, setTab] = useState('search');
|
||||
const searchInput = useRef<HTMLInputElement>(null);
|
||||
const antagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.antagonist;
|
||||
});
|
||||
const [searchResults, setSearchResults] = useState<number[]>([]);
|
||||
|
||||
if (!antagonist || mode !== 'armor') return null;
|
||||
if (mode !== 'armor') return null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -58,10 +62,11 @@ export function AntagonistBar() {
|
||||
key={shell.id}
|
||||
last={index === antagonist.gun.shells.length - 1}
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
draft.antagonist.shell = shell;
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.antagonist.shell = shell;
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -147,17 +152,17 @@ export function AntagonistBar() {
|
||||
key={tank.id}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
|
||||
draft.antagonist.tank = tank;
|
||||
draft.antagonist.turret =
|
||||
tank.turrets.at(-1)!;
|
||||
draft.antagonist.gun =
|
||||
draft.antagonist.turret.guns.at(-1)!;
|
||||
draft.antagonist.shell =
|
||||
draft.antagonist.gun.shells[0];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.antagonist.tank = tank;
|
||||
draft.antagonist.turret =
|
||||
tank.turrets.at(-1)!;
|
||||
draft.antagonist.gun =
|
||||
draft.antagonist.turret.guns.at(-1)!;
|
||||
draft.antagonist.shell =
|
||||
draft.antagonist.gun.shells[0];
|
||||
}),
|
||||
);
|
||||
setSearchResults([]);
|
||||
searchInput.current!.value = '';
|
||||
setTab('configure');
|
||||
@@ -200,15 +205,15 @@ export function AntagonistBar() {
|
||||
}
|
||||
key={turret.id}
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
|
||||
draft.antagonist.turret = turret;
|
||||
draft.antagonist.gun =
|
||||
turret.guns.at(-1)!;
|
||||
draft.antagonist.shell =
|
||||
draft.antagonist.gun.shells[0];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.antagonist.turret = turret;
|
||||
draft.antagonist.gun =
|
||||
turret.guns.at(-1)!;
|
||||
draft.antagonist.shell =
|
||||
draft.antagonist.gun.shells[0];
|
||||
}),
|
||||
);
|
||||
}}
|
||||
selected={antagonist.turret.id === turret.id}
|
||||
tier={turret.tier}
|
||||
@@ -228,11 +233,12 @@ export function AntagonistBar() {
|
||||
index === antagonist.turret.guns.length - 1
|
||||
}
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
draft.antagonist.gun = gun;
|
||||
draft.antagonist.shell = gun.shells[0];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.antagonist.gun = gun;
|
||||
draft.antagonist.shell = gun.shells[0];
|
||||
}),
|
||||
);
|
||||
}}
|
||||
selected={antagonist.gun.id === gun.id}
|
||||
tier={gun.tier}
|
||||
@@ -277,14 +283,14 @@ export function AntagonistBar() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
|
||||
[draft.antagonist, draft.protagonist] = [
|
||||
draft.protagonist,
|
||||
draft.antagonist,
|
||||
];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
[draft.protagonist, draft.antagonist] = [
|
||||
draft.antagonist,
|
||||
draft.protagonist,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ShuffleIcon /> Swap tanks
|
||||
|
||||
@@ -10,6 +10,7 @@ import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../../stores/tankopedia';
|
||||
import { Loader } from '../../../../components/Loader';
|
||||
import { Duel } from '../../page';
|
||||
import { Controls } from '../Control';
|
||||
import { Lighting } from '../Lighting';
|
||||
import { RotationInputs } from '../RotationInputs';
|
||||
@@ -18,16 +19,16 @@ import { Options } from './components/Options';
|
||||
import { TankArmor } from './components/TankArmor';
|
||||
import { TankModel } from './components/TankModel';
|
||||
|
||||
export function TankDisplay() {
|
||||
interface TankDisplayProps {
|
||||
duel: Duel;
|
||||
}
|
||||
|
||||
export function TankDisplay({ duel }: TankDisplayProps) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
const canvasWrapper = useRef<HTMLDivElement>(null);
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const mode = useTankopedia((state) => state.mode);
|
||||
const protagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.protagonist;
|
||||
});
|
||||
|
||||
function handlePointerDown() {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
@@ -56,12 +57,14 @@ export function TankDisplay() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!protagonist) return void 0;
|
||||
if (!duel.protagonist) return void 0;
|
||||
|
||||
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
||||
const tankModelDefinition =
|
||||
awaitedModelDefinitions[duel.protagonist.tank.id];
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonist.turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
tankModelDefinition.turrets[duel.protagonist.turret.id];
|
||||
const gunModelDefinition =
|
||||
turretModelDefinition.guns[duel.protagonist.gun.id];
|
||||
|
||||
mutateTankopedia((draft) => {
|
||||
[draft.model.physical.pitch, draft.model.physical.yaw] =
|
||||
@@ -72,9 +75,7 @@ export function TankDisplay() {
|
||||
turretModelDefinition.yaw,
|
||||
);
|
||||
});
|
||||
}, [protagonist?.gun, protagonist?.turret]);
|
||||
|
||||
if (!protagonist) return null;
|
||||
}, [duel.protagonist.gun, duel.protagonist.turret]);
|
||||
|
||||
return (
|
||||
<Theme radius={isFullScreen ? 'none' : undefined}>
|
||||
@@ -130,15 +131,15 @@ export function TankDisplay() {
|
||||
</Html>
|
||||
}
|
||||
>
|
||||
<TankModel />
|
||||
<TankArmor />
|
||||
<TankModel duel={duel} />
|
||||
<TankArmor duel={duel} />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
<Options canvas={canvasWrapper} isFullScreen={isFullScreen} />
|
||||
|
||||
<RotationInputs />
|
||||
<RotationInputs duel={duel} />
|
||||
</Flex>
|
||||
</Theme>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { Euler, Group, Mesh, Vector3 } from 'three';
|
||||
import { GLTFLoader } from 'three-stdlib';
|
||||
import { degToRad } from 'three/src/math/MathUtils';
|
||||
@@ -7,27 +7,23 @@ import { ArmorMesh } from '../../../../../../../components/ArmorMesh';
|
||||
import { HeadsUpDisplay } from '../../../../../../../components/HeadsUpDisplay';
|
||||
import { X_AXIS, Y_AXIS, Z_AXIS } from '../../../../../../../constants/axis';
|
||||
import { asset } from '../../../../../../../core/blitzkrieg/asset';
|
||||
import {
|
||||
ModelDefinitions,
|
||||
modelDefinitions,
|
||||
} from '../../../../../../../core/blitzkrieg/modelDefinitions';
|
||||
import {
|
||||
ModelTransformEventData,
|
||||
modelTransformEvent,
|
||||
} from '../../../../../../../core/blitzkrieg/modelTransform';
|
||||
import { nameToArmorId } from '../../../../../../../core/blitzkrieg/nameToArmorId';
|
||||
import { resolveArmor } from '../../../../../../../core/blitzkrieg/resolveThickness';
|
||||
import { useModelDefinitions } from '../../../../../../../core/hooks/useModelDefinitions';
|
||||
import { useTankopedia } from '../../../../../../../stores/tankopedia';
|
||||
import { Duel } from '../../../page';
|
||||
|
||||
export const TankArmor = memo(() => {
|
||||
interface TankArmorProps {
|
||||
duel: Duel;
|
||||
}
|
||||
|
||||
export const TankArmor = memo<TankArmorProps>(({ duel }) => {
|
||||
const wrapper = useRef<Group>(null);
|
||||
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
|
||||
ModelDefinitions | undefined
|
||||
>(undefined);
|
||||
const protagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.protagonist;
|
||||
});
|
||||
const awaitedModelDefinitions = useModelDefinitions();
|
||||
const showSpacedArmor = useTankopedia(
|
||||
(state) => state.model.visual.showSpacedArmor,
|
||||
);
|
||||
@@ -35,12 +31,6 @@ export const TankArmor = memo(() => {
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const initialTankopediaState = useTankopedia.getState();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setAwaitedModelDefinitions(await modelDefinitions);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!awaitedModelDefinitions) return;
|
||||
|
||||
@@ -118,25 +108,22 @@ export const TankArmor = memo(() => {
|
||||
return unsubscribe;
|
||||
});
|
||||
|
||||
if (!protagonist) return null;
|
||||
|
||||
const armorGltf = useLoader(
|
||||
GLTFLoader,
|
||||
asset(`3d/tanks/armor/${protagonist.tank.id}.glb`),
|
||||
asset(`3d/tanks/armor/${duel.protagonist.tank.id}.glb`),
|
||||
);
|
||||
const modelGltf = useLoader(
|
||||
GLTFLoader,
|
||||
asset(`3d/tanks/models/${protagonist.tank.id}.glb`),
|
||||
asset(`3d/tanks/models/${duel.protagonist.tank.id}.glb`),
|
||||
);
|
||||
|
||||
if (!awaitedModelDefinitions) return null;
|
||||
|
||||
const armorNodes = Object.values(armorGltf.nodes);
|
||||
const modelNodes = Object.values(modelGltf.nodes);
|
||||
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
||||
const tankModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id];
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonist.turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
tankModelDefinition.turrets[duel.protagonist.turret.id];
|
||||
const gunModelDefinition =
|
||||
turretModelDefinition.guns[duel.protagonist.gun.id];
|
||||
const turretOrigin = new Vector3(
|
||||
tankModelDefinition.turretOrigin[0],
|
||||
tankModelDefinition.turretOrigin[1],
|
||||
@@ -173,6 +160,7 @@ export const TankArmor = memo(() => {
|
||||
|
||||
return (
|
||||
<ArmorMesh
|
||||
duel={duel}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
isSpaced={spaced ?? false}
|
||||
@@ -191,6 +179,7 @@ export const TankArmor = memo(() => {
|
||||
|
||||
return (
|
||||
<ArmorMesh
|
||||
duel={duel}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
isSpaced
|
||||
@@ -223,6 +212,7 @@ export const TankArmor = memo(() => {
|
||||
|
||||
return (
|
||||
<ArmorMesh
|
||||
duel={duel}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin}
|
||||
@@ -252,6 +242,7 @@ export const TankArmor = memo(() => {
|
||||
|
||||
return (
|
||||
<ArmorMesh
|
||||
duel={duel}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
position={turretOrigin.clone().add(gunOrigin)}
|
||||
@@ -272,6 +263,7 @@ export const TankArmor = memo(() => {
|
||||
|
||||
return (
|
||||
<ArmorMesh
|
||||
duel={duel}
|
||||
key={node.uuid}
|
||||
geometry={(node as Mesh).geometry}
|
||||
isSpaced
|
||||
|
||||
@@ -26,17 +26,16 @@ import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle1
|
||||
import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../../../stores/tankopedia';
|
||||
import { Duel } from '../../../page';
|
||||
|
||||
export const TankModel = memo(() => {
|
||||
interface TankModelProps {
|
||||
duel: Duel;
|
||||
}
|
||||
|
||||
export const TankModel = memo(({ duel: { protagonist } }: TankModelProps) => {
|
||||
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
|
||||
ModelDefinitions | undefined
|
||||
>(undefined);
|
||||
const protagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.protagonist;
|
||||
});
|
||||
|
||||
if (!protagonist) return null;
|
||||
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const physical = useTankopedia((state) => state.model.physical);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { produce } from 'immer';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { ModuleButton } from '../../../../../components/ModuleButton';
|
||||
import { Duel } from '../page';
|
||||
|
||||
interface ModulesProps {
|
||||
duel: Duel;
|
||||
setDuel: Dispatch<SetStateAction<Duel>>;
|
||||
}
|
||||
|
||||
export function Modules({ duel, setDuel }: ModulesProps) {
|
||||
return (
|
||||
<Flex gap="2">
|
||||
<Flex>
|
||||
{duel.protagonist.tank.turrets.map((thisTurret, index) => {
|
||||
return (
|
||||
<ModuleButton
|
||||
key={thisTurret.id}
|
||||
selected={duel.protagonist.turret.id === thisTurret.id}
|
||||
type="module"
|
||||
module="turret"
|
||||
tier={thisTurret.tier}
|
||||
first={index === 0}
|
||||
last={index === duel.protagonist.tank.turrets.length - 1}
|
||||
rowChild
|
||||
onClick={() => {
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.protagonist.turret = thisTurret;
|
||||
draft.protagonist.gun = thisTurret.guns.at(-1)!;
|
||||
draft.protagonist.shell = thisTurret.guns.at(-1)!.shells[0];
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
{duel.protagonist.turret.guns.map((thisGun, index) => {
|
||||
return (
|
||||
<ModuleButton
|
||||
key={thisGun.id}
|
||||
type="module"
|
||||
module="gun"
|
||||
tier={thisGun.tier}
|
||||
selected={duel.protagonist.gun.id === thisGun.id}
|
||||
first={index === 0}
|
||||
last={index === duel.protagonist.turret.guns.length - 1}
|
||||
rowChild
|
||||
onClick={() => {
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.protagonist.gun = thisGun;
|
||||
draft.protagonist.shell = thisGun.shells[0];
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -6,16 +6,17 @@ import { modelTransformEvent } from '../../../../../core/blitzkrieg/modelTransfo
|
||||
import mutateTankopedia, {
|
||||
useTankopedia,
|
||||
} from '../../../../../stores/tankopedia';
|
||||
import { Duel } from '../page';
|
||||
|
||||
export function RotationInputs() {
|
||||
interface RotationInputsProps {
|
||||
duel: Duel;
|
||||
}
|
||||
|
||||
export function RotationInputs({ duel: { protagonist } }: RotationInputsProps) {
|
||||
const physical = useTankopedia((state) => state.model.physical);
|
||||
const awaitedModelDefinitions = use(modelDefinitions);
|
||||
const turretYawInput = useRef<HTMLInputElement>(null);
|
||||
const gunPitchInput = useRef<HTMLInputElement>(null);
|
||||
const protagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.protagonist;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
turretYawInput.current!.value = `${-Math.round(
|
||||
@@ -28,8 +29,6 @@ export function RotationInputs() {
|
||||
)}`;
|
||||
}, [physical.pitch]);
|
||||
|
||||
if (!protagonist) return null;
|
||||
|
||||
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonist.turret.id];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ArrowLeftIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { Flag } from '../../../../../components/Flag';
|
||||
import { Duel } from '../page';
|
||||
|
||||
interface TitleProps {
|
||||
duel: Duel;
|
||||
}
|
||||
|
||||
export function Title({ duel }: TitleProps) {
|
||||
return (
|
||||
<Flex justify="between" align="center">
|
||||
<Link
|
||||
href="/tools/tankopedia"
|
||||
style={{ color: 'unset', textDecoration: 'none' }}
|
||||
>
|
||||
<Flex gap="1" align="center">
|
||||
<ArrowLeftIcon />
|
||||
<Text>Back</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<Flag nation={duel.protagonist.tank.nation} />
|
||||
<Heading>{duel.protagonist.tank.name}</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowLeftIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import { use, useEffect } from 'react';
|
||||
import { Flag } from '../../../../components/Flag';
|
||||
import { ModuleButton } from '../../../../components/ModuleButton';
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { produce } from 'immer';
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import PageWrapper from '../../../../components/PageWrapper';
|
||||
import { tankDefinitions } from '../../../../core/blitzkrieg/tankDefinitions';
|
||||
import mutateTankopedia, { useTankopedia } from '../../../../stores/tankopedia';
|
||||
import mutateTankopedia, { DuelMember } from '../../../../stores/tankopedia';
|
||||
import { AntagonistBar } from './components/AntagonistBar';
|
||||
import { TankDisplay } from './components/Model/TankDisplay';
|
||||
import { Modules } from './components/Modules';
|
||||
import { Title } from './components/Title';
|
||||
|
||||
export interface Duel {
|
||||
protagonist: DuelMember;
|
||||
antagonist: DuelMember;
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const id = parseInt(params.id);
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const [duel, setDuel] = useState<Duel>({
|
||||
protagonist: {
|
||||
tank: awaitedTankDefinitions[id],
|
||||
turret: awaitedTankDefinitions[id].turrets.at(-1)!,
|
||||
gun: awaitedTankDefinitions[id].turrets.at(-1)!.guns.at(-1)!,
|
||||
shell: awaitedTankDefinitions[id].turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||
},
|
||||
antagonist: {
|
||||
tank: awaitedTankDefinitions[id],
|
||||
turret: awaitedTankDefinitions[id].turrets.at(-1)!,
|
||||
gun: awaitedTankDefinitions[id].turrets.at(-1)!.guns.at(-1)!,
|
||||
shell: awaitedTankDefinitions[id].turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
mutateTankopedia((draft) => {
|
||||
draft.model.physical.yaw = 0;
|
||||
draft.model.physical.pitch = 0;
|
||||
draft.mode = 'model';
|
||||
draft.areTanksAssigned = true;
|
||||
|
||||
if (!draft.areTanksAssigned) return;
|
||||
|
||||
draft.protagonist = {} as typeof draft.protagonist;
|
||||
draft.protagonist.tank = awaitedTankDefinitions[id];
|
||||
draft.protagonist.turret = draft.protagonist.tank.turrets.at(-1)!;
|
||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||
draft.antagonist = draft.protagonist;
|
||||
});
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === '1') {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
draft.antagonist.shell = draft.antagonist.gun.shells[0];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
draft.antagonist.shell = draft.antagonist.gun.shells[0];
|
||||
}),
|
||||
);
|
||||
} else if (event.key === '2') {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
if (draft.antagonist.gun.shells[1])
|
||||
draft.antagonist.shell = draft.antagonist.gun.shells[1];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
if (draft.antagonist.gun.shells[1]) {
|
||||
draft.antagonist.shell = draft.antagonist.gun.shells[1];
|
||||
}
|
||||
}),
|
||||
);
|
||||
} else if (event.key === '3') {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
if (draft.antagonist.gun.shells[2])
|
||||
draft.antagonist.shell = draft.antagonist.gun.shells[2];
|
||||
});
|
||||
setDuel(
|
||||
produce<Duel>((draft) => {
|
||||
if (draft.antagonist.gun.shells[2]) {
|
||||
draft.antagonist.shell = draft.antagonist.gun.shells[2];
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
mutateTankopedia((draft) => {
|
||||
draft.areTanksAssigned = false;
|
||||
});
|
||||
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
const protagonist = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.protagonist;
|
||||
});
|
||||
|
||||
if (!protagonist) return null;
|
||||
|
||||
return (
|
||||
<PageWrapper color="purple">
|
||||
<Flex gap="8" direction="column">
|
||||
<Flex gap="4" direction="column">
|
||||
<Flex justify="between" align="center">
|
||||
<Link
|
||||
href="/tools/tankopedia"
|
||||
style={{ color: 'unset', textDecoration: 'none' }}
|
||||
>
|
||||
<Flex gap="1" align="center">
|
||||
<ArrowLeftIcon />
|
||||
<Text>Back</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<Flag nation={protagonist.tank.nation} />
|
||||
<Heading>{protagonist.tank.name}</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<TankDisplay />
|
||||
<AntagonistBar />
|
||||
|
||||
<Flex gap="2">
|
||||
<Flex>
|
||||
{protagonist.tank.turrets.map((thisTurret, index) => {
|
||||
return (
|
||||
<ModuleButton
|
||||
key={thisTurret.id}
|
||||
selected={protagonist.turret.id === thisTurret.id}
|
||||
type="module"
|
||||
module="turret"
|
||||
tier={thisTurret.tier}
|
||||
first={index === 0}
|
||||
last={index === protagonist.tank.turrets.length - 1}
|
||||
rowChild
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
|
||||
draft.protagonist.turret = thisTurret;
|
||||
draft.protagonist.gun = thisTurret.guns.at(-1)!;
|
||||
draft.protagonist.shell =
|
||||
draft.protagonist.gun.shells[0];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
{protagonist.turret.guns.map((thisGun, index) => {
|
||||
return (
|
||||
<ModuleButton
|
||||
key={thisGun.id}
|
||||
type="module"
|
||||
module="gun"
|
||||
tier={thisGun.tier}
|
||||
selected={protagonist.gun.id === thisGun.id}
|
||||
first={index === 0}
|
||||
last={index === protagonist.turret.guns.length - 1}
|
||||
rowChild
|
||||
onClick={() => {
|
||||
mutateTankopedia((draft) => {
|
||||
if (!draft.areTanksAssigned) return;
|
||||
|
||||
draft.protagonist.gun = thisGun;
|
||||
draft.protagonist.shell = thisGun.shells[0];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Title duel={duel} />
|
||||
<TankDisplay duel={duel} />
|
||||
<AntagonistBar duel={duel} setDuel={setDuel} />
|
||||
<Modules duel={duel} setDuel={setDuel} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -2,14 +2,15 @@ import { MeshProps } from '@react-three/fiber';
|
||||
import { MeshStandardMaterial } from 'three';
|
||||
import CustomShaderMaterial from 'three-custom-shader-material';
|
||||
import { degToRad } from 'three/src/math/MathUtils';
|
||||
import { Duel } from '../../app/tools/tankopedia/[id]/page';
|
||||
import { canSplash } from '../../core/blitz/canSplash';
|
||||
import { isExplosive } from '../../core/blitz/isExplosive';
|
||||
import { resolveNearPenetration } from '../../core/blitz/resolveNearPenetration';
|
||||
import { useTankopedia } from '../../stores/tankopedia';
|
||||
import fragmentShader from './shaders/fragment.glsl';
|
||||
import vertexShader from './shaders/vertex.glsl';
|
||||
|
||||
interface ArmorMeshProps extends MeshProps {
|
||||
duel: Duel;
|
||||
isSpaced: boolean;
|
||||
isExternalModule?: boolean;
|
||||
thickness: number;
|
||||
@@ -19,14 +20,10 @@ export function ArmorMesh({
|
||||
isSpaced,
|
||||
isExternalModule = false,
|
||||
thickness,
|
||||
duel,
|
||||
...props
|
||||
}: ArmorMeshProps) {
|
||||
const shell = useTankopedia((state) => {
|
||||
if (!state.areTanksAssigned) return;
|
||||
return state.antagonist.shell;
|
||||
});
|
||||
|
||||
if (!shell) return null;
|
||||
const { shell } = duel.antagonist;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import usePromise from 'react-promise-suspense';
|
||||
import { modelDefinitions } from '../blitzkrieg/modelDefinitions';
|
||||
|
||||
export function useModelDefinitions() {
|
||||
return usePromise(async () => await modelDefinitions, []);
|
||||
}
|
||||
@@ -14,8 +14,13 @@ export type TankopediaSortBy = 'tier' | 'name';
|
||||
export type TankopediaSortDirection = 'ascending' | 'descending';
|
||||
export type TankopediaTestTankDisplay = 'include' | 'exclude' | 'only';
|
||||
export type TankopediaMode = 'model' | 'armor';
|
||||
|
||||
type Tankopedia = {
|
||||
export interface DuelMember {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
shell: ShellDefinition;
|
||||
}
|
||||
interface Tankopedia {
|
||||
sort: {
|
||||
by: TankopediaSortBy;
|
||||
direction: TankopediaSortDirection;
|
||||
@@ -39,24 +44,7 @@ type Tankopedia = {
|
||||
};
|
||||
};
|
||||
mode: TankopediaMode;
|
||||
} & (
|
||||
| {
|
||||
areTanksAssigned: true;
|
||||
protagonist: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
shell: ShellDefinition;
|
||||
};
|
||||
antagonist: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
shell: ShellDefinition;
|
||||
};
|
||||
}
|
||||
| { areTanksAssigned: false }
|
||||
);
|
||||
}
|
||||
|
||||
export const useTankopedia = create<Tankopedia>()(
|
||||
subscribeWithSelector<Tankopedia>(() => ({
|
||||
|
||||
@@ -3715,6 +3715,11 @@ [email protected], fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-deep-equal@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==
|
||||
|
||||
[email protected], fast-glob@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
@@ -5264,6 +5269,13 @@ react-merge-refs@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
|
||||
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
|
||||
|
||||
react-promise-suspense@^0.3.4:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz#05d19a75703d71374674840056cfef2fcd38809d"
|
||||
integrity sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==
|
||||
dependencies:
|
||||
fast-deep-equal "^2.0.1"
|
||||
|
||||
react-reconciler@^0.27.0:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.27.0.tgz#360124fdf2d76447c7491ee5f0e04503ed9acf5b"
|
||||
|
||||
Reference in new issue
Block a user