feat: simplified duel state

This commit is contained in:
TrèsAbhi committed 2024-01-15 15:02:04 -06:00
1 parent 7314f1d9b1
commit 66016dedf0
13 files changed
+276 -246

No files matched your search

@@ -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>
);
}
+48 -115
View File
@@ -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>