feat: prot and anta

This commit is contained in:
TrèsAbhi committed 2024-01-07 13:13:27 -06:00
1 parent 5208cf8678
commit 88a6ab2d95
6 files changed
+480 -412

No files matched your search

@@ -6,8 +6,10 @@ import {
import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes'; import { Button, Card, Flex, Tabs, Theme } from '@radix-ui/themes';
import { Html } from '@react-three/drei'; import { Html } from '@react-three/drei';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { Suspense, useEffect, useRef, useState } from 'react'; import { Suspense, use, useEffect, useRef, useState } from 'react';
import { Group } from 'three'; import { Group } from 'three';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { theme } from '../../../../../../stitches.config'; import { theme } from '../../../../../../stitches.config';
import mutateTankopedia, { import mutateTankopedia, {
TankopediaMode, TankopediaMode,
@@ -19,19 +21,20 @@ import { RotationInputs } from '../RotationInputs';
import { SceneProps } from '../SceneProps'; import { SceneProps } from '../SceneProps';
import { TankModel } from './components/TankModel'; import { TankModel } from './components/TankModel';
interface TankDisplayProps { export function TankDisplay() {
tankId: number;
turretId: number;
gunId: number;
}
export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const awaitedModelDefinitions = use(modelDefinitions);
const canvasWrapper = useRef<HTMLDivElement>(null); const canvasWrapper = useRef<HTMLDivElement>(null);
const [isFullScreen, setIsFullScreen] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false);
const mode = useTankopedia((state) => state.mode); const mode = useTankopedia((state) => state.mode);
const hullContainer = useRef<Group>(null); const hullContainer = useRef<Group>(null);
const showGrid = useTankopedia((state) => state.model.showGrid); const showGrid = useTankopedia((state) => state.model.showGrid);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
if (!protagonist) return null;
function handlePointerDown() { function handlePointerDown() {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
@@ -59,6 +62,22 @@ export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
}; };
}); });
useEffect(() => {
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
mutateTankopedia((draft) => {
[draft.model.gunPitch, draft.model.turretYaw] = applyPitchYawLimits(
draft.model.gunPitch,
draft.model.turretYaw,
gunModelDefinition.pitch,
turretModelDefinition.yaw,
);
});
}, [protagonist.gun, protagonist.turret]);
return ( return (
<Theme radius={isFullScreen ? 'none' : undefined}> <Theme radius={isFullScreen ? 'none' : undefined}>
<Card <Card
@@ -111,12 +130,7 @@ export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
</Html> </Html>
} }
> >
<TankModel <TankModel ref={hullContainer} />
gunId={gunId}
tankId={tankId}
turretId={turretId}
ref={hullContainer}
/>
</Suspense> </Suspense>
</Canvas> </Canvas>
</div> </div>
@@ -159,7 +173,7 @@ export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
</Button> </Button>
</Flex> </Flex>
<RotationInputs gunId={gunId} tankId={tankId} turretId={turretId} /> <RotationInputs />
</Flex> </Flex>
</Theme> </Theme>
</Card> </Card>
@@ -20,127 +20,127 @@ import { GunContainer } from '../../GunContainer';
import { HullContainer } from '../../HullContainer'; import { HullContainer } from '../../HullContainer';
import { TurretContainer } from '../../TurretContainer'; import { TurretContainer } from '../../TurretContainer';
interface TankModelProps { export const TankModel = forwardRef<Group>((_props, ref) => {
tankId: number; // "TypeError: dispatcher.use is not a function"
turretId: number; // const awaitedModelDefinitions = use(modelDefinitions);
gunId: number; const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
} ModelDefinitions | undefined
>(undefined);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
export const TankModel = forwardRef<Group, TankModelProps>( if (!protagonist) return null;
({ tankId, turretId, gunId }, ref) => {
// "TypeError: dispatcher.use is not a function"
// const awaitedModelDefinitions = use(modelDefinitions);
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined
>(undefined);
const gltf = useLoader(GLTFLoader, `/test/${tankId}.glb`);
const model = useTankopedia((state) => state.model);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
useImperativeHandle(ref, () => hullContainer.current!); const gltf = useLoader(GLTFLoader, `/test/${protagonist.tank.id}.glb`);
useEffect(() => { const model = useTankopedia((state) => state.model);
(async () => { const hullContainer = useRef<Group>(null);
setAwaitedModelDefinitions(await modelDefinitions); const turretContainer = useRef<Group>(null);
})(); const gunContainer = useRef<Group>(null);
}, []);
if (!awaitedModelDefinitions) return null; useImperativeHandle(ref, () => hullContainer.current!);
useEffect(() => {
(async () => {
setAwaitedModelDefinitions(await modelDefinitions);
})();
}, []);
const tankModelDefinition = awaitedModelDefinitions[tankId]; if (!awaitedModelDefinitions) return null;
const turretModelDefinition = tankModelDefinition.turrets[turretId];
const gunModelDefinition = turretModelDefinition.guns[gunId];
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
return ( const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
<HullContainer const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const turretOrigin = new Vector3(
tankModelDefinition.turretOrigin[0],
tankModelDefinition.turretOrigin[1],
-tankModelDefinition.turretOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
const gunOrigin = new Vector3(
turretModelDefinition.gunOrigin[0],
turretModelDefinition.gunOrigin[1],
-turretModelDefinition.gunOrigin[2],
).applyAxisAngle(X_AXIS, Math.PI / 2);
return (
<HullContainer
objects={Object.values(gltf.nodes)}
yaw={model.hullYaw}
ref={hullContainer}
onYawStart={() =>
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
})
}
onYawEnd={(yaw) => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
draft.model.hullYaw = yaw;
});
}}
onTrackStart={() => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
});
}}
onTrackEnd={() => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
});
}}
>
<TurretContainer
initialTurretRotation={tankModelDefinition.turretRotation}
gunOrigin={gunOrigin}
ref={turretContainer}
objects={Object.values(gltf.nodes)} objects={Object.values(gltf.nodes)}
yaw={model.hullYaw} model={turretModelDefinition.model}
ref={hullContainer}
onYawStart={() => onYawStart={() =>
mutateTankopedia((draft) => { mutateTankopedia((state) => {
draft.model.controlsEnabled = false; state.model.controlsEnabled = false;
}) })
} }
onYawEnd={(yaw) => { yawLimits={turretModelDefinition.yaw}
mutateTankopedia((draft) => { pitchLimits={gunModelDefinition.pitch}
draft.model.controlsEnabled = true; pitch={model.gunPitch}
draft.model.hullYaw = yaw; onYawEnd={(pitch, yaw) => {
}); mutateTankopedia((state) => {
}} state.model.controlsEnabled = true;
onTrackStart={() => { state.model.gunPitch = pitch;
mutateTankopedia((draft) => { state.model.turretYaw = yaw;
draft.model.controlsEnabled = false;
});
}}
onTrackEnd={() => {
mutateTankopedia((draft) => {
draft.model.controlsEnabled = true;
}); });
}} }}
gunContainer={gunContainer}
turretOrigin={turretOrigin}
yaw={model.turretYaw}
> >
<TurretContainer <GunContainer
ref={gunContainer}
initialTurretRotation={tankModelDefinition.turretRotation} initialTurretRotation={tankModelDefinition.turretRotation}
gunOrigin={gunOrigin} onPitchStart={() => {
ref={turretContainer}
objects={Object.values(gltf.nodes)}
model={turretModelDefinition.model}
onYawStart={() =>
mutateTankopedia((state) => { mutateTankopedia((state) => {
state.model.controlsEnabled = false; state.model.controlsEnabled = false;
}) });
} }}
yawLimits={turretModelDefinition.yaw}
pitchLimits={gunModelDefinition.pitch} pitchLimits={gunModelDefinition.pitch}
pitch={model.gunPitch} turretContainer={turretContainer}
onYawEnd={(pitch, yaw) => { yaw={model.turretYaw}
onPitchEnd={(pitch, yaw) => {
mutateTankopedia((state) => { mutateTankopedia((state) => {
state.model.controlsEnabled = true; state.model.controlsEnabled = true;
state.model.gunPitch = pitch; state.model.gunPitch = pitch;
state.model.turretYaw = yaw; state.model.turretYaw = yaw;
}); });
}} }}
gunContainer={gunContainer} yawLimits={turretModelDefinition.yaw}
gunOrigin={gunOrigin}
pitch={model.gunPitch}
model={gunModelDefinition.model}
objects={Object.values(gltf.nodes)}
turretOrigin={turretOrigin} turretOrigin={turretOrigin}
yaw={model.turretYaw} />
> </TurretContainer>
<GunContainer </HullContainer>
ref={gunContainer} );
initialTurretRotation={tankModelDefinition.turretRotation} });
onPitchStart={() => {
mutateTankopedia((state) => {
state.model.controlsEnabled = false;
});
}}
pitchLimits={gunModelDefinition.pitch}
turretContainer={turretContainer}
yaw={model.turretYaw}
onPitchEnd={(pitch, yaw) => {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = pitch;
state.model.turretYaw = yaw;
});
}}
yawLimits={turretModelDefinition.yaw}
gunOrigin={gunOrigin}
pitch={model.gunPitch}
model={gunModelDefinition.model}
objects={Object.values(gltf.nodes)}
turretOrigin={turretOrigin}
/>
</TurretContainer>
</HullContainer>
);
},
);
@@ -7,26 +7,23 @@ import mutateTankopedia, {
useTankopedia, useTankopedia,
} from '../../../../../stores/tankopedia'; } from '../../../../../stores/tankopedia';
interface RotationInputsProps { export function RotationInputs() {
tankId: number;
gunId: number;
turretId: number;
}
export function RotationInputs({
tankId,
gunId,
turretId,
}: RotationInputsProps) {
const model = useTankopedia((state) => state.model); const model = useTankopedia((state) => state.model);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const turretYawInput = useRef<HTMLInputElement>(null); const turretYawInput = useRef<HTMLInputElement>(null);
const hullYawInput = useRef<HTMLInputElement>(null); const hullYawInput = useRef<HTMLInputElement>(null);
const gunPitchInput = useRef<HTMLInputElement>(null); const gunPitchInput = useRef<HTMLInputElement>(null);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
const tankModelDefinition = awaitedModelDefinitions[tankId]; if (!protagonist) return null;
const turretModelDefinition = tankModelDefinition.turrets[turretId];
const gunModelDefinition = turretModelDefinition.guns[gunId]; const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
useEffect(() => { useEffect(() => {
hullYawInput.current!.value = `${-Math.round( hullYawInput.current!.value = `${-Math.round(
@@ -0,0 +1,254 @@
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
import {
Button,
Card,
Checkbox,
Dialog,
Flex,
Heading,
Tabs,
Text,
TextField,
Tooltip,
} from '@radix-ui/themes';
import { go } from 'fuzzysort';
import { debounce } from 'lodash';
import { use, useRef, useState } from 'react';
import { ModuleButtons } from '../../../../../components/ModuleButton';
import { asset } from '../../../../../core/blitzkrieg/asset';
import {
tankDefinitions,
tankNamesDiacritics,
} from '../../../../../core/blitzkrieg/tankDefinitions';
import mutateTankopedia, {
useTankopedia,
} from '../../../../../stores/tankopedia';
export function VersusBar() {
const awaitedTankDefinitions = use(tankDefinitions);
const mode = useTankopedia((state) => state.mode);
const awaitedTankNamesDiacritics = use(tankNamesDiacritics);
const [versusTankTab, setVersusTankTab] = useState('search');
const versusTankSearchInput = useRef<HTMLInputElement>(null);
const antagonist = useTankopedia((state) => {
// goofy ahh typescript discriminator hack
if (!state.areTanksAssigned) return;
return state.antagonist;
});
const [versusTankSearchResults, setVersusTankSearchResults] = useState<
number[]
>([]);
if (!antagonist || mode !== 'armor') return null;
return (
<Card>
<Flex align="center" justify="between" gap="2">
<Flex align="center" gap="4">
<Text>Versus</Text>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">
{antagonist.tank.name}
<CaretRightIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Tabs.Root value={versusTankTab} onValueChange={setVersusTankTab}>
<Flex gap="4" direction="column">
<Tabs.List>
<Tabs.Trigger value="search">Search</Tabs.Trigger>
<Tabs.Trigger value="configure">Configure</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="search">
<Flex
direction="column"
gap="4"
style={{ flex: 1 }}
justify="center"
>
<TextField.Root>
<TextField.Slot>
<MagnifyingGlassIcon />
</TextField.Slot>
<TextField.Input
ref={versusTankSearchInput}
placeholder="Search tank..."
onChange={debounce(() => {
setVersusTankSearchResults(
go(
versusTankSearchInput.current!.value,
awaitedTankNamesDiacritics,
{ key: 'combined', limit: 8 },
).map((item) => item.obj.id),
);
}, 500)}
/>
</TextField.Root>
<Flex direction="column" gap="2">
{(versusTankSearchResults.length > 0 ||
versusTankSearchInput.current?.value) && (
<Flex direction="column" gap="2">
{versusTankSearchResults.map((id) => (
<Button
key={id}
variant="ghost"
onClick={() => {
const thisTank = awaitedTankDefinitions[id];
mutateTankopedia((draft) => {
if (!draft.areTanksAssigned) return;
draft.antagonist.tank = thisTank;
draft.antagonist.turret =
thisTank.turrets.at(-1)!;
draft.antagonist.gun =
draft.antagonist.turret.guns.at(-1)!;
});
setVersusTankSearchResults([]);
versusTankSearchInput.current!.value = '';
setVersusTankTab('configure');
}}
>
{awaitedTankDefinitions[id].name}
</Button>
))}
{versusTankSearchResults.length === 0 &&
(versusTankSearchInput.current
? versusTankSearchInput.current.value.length > 0
: false) && (
<Button disabled variant="ghost">
No search results
</Button>
)}
</Flex>
)}
</Flex>
</Flex>
</Tabs.Content>
<Tabs.Content value="configure">
<Flex direction="column" gap="4">
<Flex direction="column" gap="2" style={{ flex: 1 }}>
<Heading size="4">Configuration</Heading>
<Flex gap="2" wrap="wrap">
<Flex>
{antagonist.tank.turrets.map((turret, index) => (
<Tooltip content={turret.name} key={turret.id}>
<ModuleButtons
key={turret.id}
onClick={() => {
mutateTankopedia((draft) => {
if (!draft.areTanksAssigned) return;
draft.antagonist.turret = turret;
draft.antagonist.gun =
turret.guns.at(-1)!;
});
}}
selected={antagonist.turret.id === turret.id}
tier={turret.tier}
type="turret"
style={{
margin: -0.5,
borderTopLeftRadius:
index === 0 ? undefined : 0,
borderBottomLeftRadius:
index === 0 ? undefined : 0,
borderTopRightRadius:
index ===
antagonist.tank.turrets.length - 1
? undefined
: 0,
borderBottomRightRadius:
index ===
antagonist.tank.turrets.length - 1
? undefined
: 0,
}}
/>
</Tooltip>
))}
</Flex>
<Flex>
{antagonist.turret.guns.map((gun, index) => (
<Tooltip content={gun.name} key={gun.id}>
<ModuleButtons
onClick={() => {
mutateTankopedia((draft) => {
if (!draft.areTanksAssigned) return;
draft.antagonist.gun = gun;
});
}}
selected={antagonist.gun.id === gun.id}
tier={gun.tier}
type="gun"
style={{
margin: -0.5,
borderTopLeftRadius:
index === 0 ? undefined : 0,
borderBottomLeftRadius:
index === 0 ? undefined : 0,
borderTopRightRadius:
index ===
antagonist.turret.guns.length - 1
? undefined
: 0,
borderBottomRightRadius:
index ===
antagonist.turret.guns.length - 1
? undefined
: 0,
}}
/>
</Tooltip>
))}
</Flex>
</Flex>
</Flex>
<Flex direction="column" gap="2" style={{ flex: 1 }}>
<Heading size="4">Properties</Heading>
<ul>
<li>Penetration</li>
</ul>
</Flex>
</Flex>
</Tabs.Content>
</Flex>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
</Flex>
<Flex gap="2" align="center">
<Checkbox defaultChecked />
<Text>Calibrated</Text>
</Flex>
<Flex gap="1">
<Button variant="solid" radius="small">
<img src={asset('icons/shells/ap.webp')} width={24} height={24} />
</Button>
<Button variant="soft" radius="small" color="gray">
<img
src={asset('icons/shells/hc_premium.webp')}
width={24}
height={24}
/>
</Button>
<Button variant="soft" radius="small" color="gray">
<img src={asset('icons/shells/he.webp')} width={24} height={24} />
</Button>
</Flex>
</Flex>
</Card>
);
}
+62 -280
View File
@@ -1,59 +1,55 @@
'use client'; 'use client';
import { import { ArrowLeftIcon } from '@radix-ui/react-icons';
ArrowLeftIcon, import { Button, Flex, Heading, Text, Theme, Tooltip } from '@radix-ui/themes';
CaretRightIcon,
MagnifyingGlassIcon,
} from '@radix-ui/react-icons';
import {
Button,
Card,
Checkbox,
Dialog,
Flex,
Heading,
Tabs,
Text,
TextField,
Theme,
Tooltip,
} from '@radix-ui/themes';
import { go } from 'fuzzysort';
import { debounce } from 'lodash';
import Link from 'next/link'; import Link from 'next/link';
import { use, useRef, useState } from 'react'; import { use, useEffect } from 'react';
import { Vector3 } from 'three';
import { Flag } from '../../../../components/Flag'; import { Flag } from '../../../../components/Flag';
import { ModuleButtons } from '../../../../components/ModuleButton';
import PageWrapper from '../../../../components/PageWrapper'; import PageWrapper from '../../../../components/PageWrapper';
import { asset } from '../../../../core/blitzkrieg/asset'; import { asset } from '../../../../core/blitzkrieg/asset';
import { import {
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
tankDefinitions, tankDefinitions,
tankNamesDiacritics,
} from '../../../../core/blitzkrieg/tankDefinitions'; } from '../../../../core/blitzkrieg/tankDefinitions';
import { useTankopedia } from '../../../../stores/tankopedia'; import mutateTankopedia, { useTankopedia } from '../../../../stores/tankopedia';
import { TankDisplay } from './components/Model/TankDisplay'; import { TankDisplay } from './components/Model/TankDisplay';
import { VersusBar } from './components/VersusBar';
const X_AXIS = new Vector3(1, 0, 0);
export default function Page({ params }: { params: { id: string } }) { export default function Page({ params }: { params: { id: string } }) {
const id = parseInt(params.id); const id = parseInt(params.id);
const awaitedTankDefinitions = use(tankDefinitions); const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTankNamesDiacritics = use(tankNamesDiacritics);
const tank = awaitedTankDefinitions[id]; useEffect(() => {
const [turret, setTurret] = useState(tank.turrets.at(-1)!); mutateTankopedia((draft) => {
const [gun, setGun] = useState(turret.guns.at(-1)!); draft.model.hullYaw = 0;
const [crew, setCrew] = useState(100); draft.model.turretYaw = 0;
const versusTankSearchInput = useRef<HTMLInputElement>(null); draft.model.gunPitch = 0;
const [versusTankSearchResults, setVersusTankSearchResults] = useState< draft.mode = 'model';
number[] draft.areTanksAssigned = true;
>([]);
const [versusTank, setVersusTank] = useState(tank); if (!draft.areTanksAssigned) return;
const [versusTurret, setVersusTurret] = useState(versusTank.turrets.at(-1)!);
const [versusGun, setVersusGun] = useState(versusTurret.guns.at(-1)!); draft.protagonist = {} as typeof draft.protagonist;
const [versusTankTab, setVersusTankTab] = useState('search'); draft.protagonist.tank = awaitedTankDefinitions[id];
const mode = useTankopedia((state) => state.mode); draft.protagonist.turret = draft.protagonist.tank.turrets.at(-1)!;
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
draft.antagonist = draft.protagonist;
});
return () => {
mutateTankopedia((draft) => {
draft.areTanksAssigned = false;
});
};
}, [id]);
const protagonist = useTankopedia((state) => {
// goofy ahh typescript discriminator hack
if (!state.areTanksAssigned) return;
return state.protagonist;
});
if (!protagonist) return null;
return ( return (
<PageWrapper color="purple"> <PageWrapper color="purple">
@@ -71,254 +67,33 @@ export default function Page({ params }: { params: { id: string } }) {
</Link> </Link>
<Flex gap="2" align="center"> <Flex gap="2" align="center">
<Flag nation={tank.nation} /> <Flag nation={protagonist.tank.nation} />
<Heading>{tank.name}</Heading> <Heading>{protagonist.tank.name}</Heading>
</Flex> </Flex>
</Flex> </Flex>
<TankDisplay gunId={gun.id} tankId={tank.id} turretId={turret.id} /> <TankDisplay />
<VersusBar />
{mode === 'armor' && (
<Card>
<Flex align="center" justify="between" gap="2">
<Flex align="center" gap="4">
<Text>Versus</Text>
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">
{versusTank.name}
<CaretRightIcon />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Tabs.Root
value={versusTankTab}
onValueChange={setVersusTankTab}
>
<Flex gap="4" direction="column">
<Tabs.List>
<Tabs.Trigger value="search">Search</Tabs.Trigger>
<Tabs.Trigger value="configure">
Configure
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="search">
<Flex
direction="column"
gap="4"
style={{ flex: 1 }}
justify="center"
>
<TextField.Root>
<TextField.Slot>
<MagnifyingGlassIcon />
</TextField.Slot>
<TextField.Input
ref={versusTankSearchInput}
placeholder="Search tank..."
onChange={debounce(() => {
setVersusTankSearchResults(
go(
versusTankSearchInput.current!.value,
awaitedTankNamesDiacritics,
{ key: 'combined', limit: 8 },
).map((item) => item.obj.id),
);
}, 500)}
/>
</TextField.Root>
<Flex direction="column" gap="2">
{(versusTankSearchResults.length > 0 ||
versusTankSearchInput.current?.value) && (
<Flex direction="column" gap="2">
{versusTankSearchResults.map((id) => (
<Button
key={id}
variant="ghost"
onClick={() => {
const thisTank =
awaitedTankDefinitions[id];
setVersusTank(thisTank);
setVersusTurret(
thisTank.turrets.at(-1)!,
);
setVersusGun(
thisTank.turrets
.at(-1)!
.guns.at(-1)!,
);
setVersusTankSearchResults([]);
versusTankSearchInput.current!.value =
'';
setVersusTankTab('configure');
}}
>
{awaitedTankDefinitions[id].name}
</Button>
))}
{versusTankSearchResults.length === 0 &&
(versusTankSearchInput.current
? versusTankSearchInput.current.value
.length > 0
: false) && (
<Button disabled variant="ghost">
No search results
</Button>
)}
</Flex>
)}
</Flex>
</Flex>
</Tabs.Content>
<Tabs.Content value="configure">
<Flex direction="column" gap="4">
<Flex
direction="column"
gap="2"
style={{ flex: 1 }}
>
<Heading size="4">Configuration</Heading>
<Flex gap="2" wrap="wrap">
<Flex>
{versusTank.turrets.map((turret, index) => (
<Tooltip
content={turret.name}
key={turret.id}
>
<ModuleButtons
key={id}
onClick={() => {
setVersusTurret(turret);
setVersusGun(turret.guns.at(-1)!);
}}
selected={versusTurret.id === id}
tier={turret.tier}
type="turret"
style={{
margin: -0.5,
borderTopLeftRadius:
index === 0 ? undefined : 0,
borderBottomLeftRadius:
index === 0 ? undefined : 0,
borderTopRightRadius:
index ===
versusTank.turrets.length - 1
? undefined
: 0,
borderBottomRightRadius:
index ===
versusTank.turrets.length - 1
? undefined
: 0,
}}
/>
</Tooltip>
))}
</Flex>
<Flex>
{versusTurret.guns.map((gun, index) => (
<Tooltip content={gun.name} key={gun.id}>
<ModuleButtons
key={id}
onClick={() => setVersusGun(gun)}
selected={versusGun.id === id}
tier={gun.tier}
type="gun"
style={{
margin: -0.5,
borderTopLeftRadius:
index === 0 ? undefined : 0,
borderBottomLeftRadius:
index === 0 ? undefined : 0,
borderTopRightRadius:
index ===
versusTurret.guns.length - 1
? undefined
: 0,
borderBottomRightRadius:
index ===
versusTurret.guns.length - 1
? undefined
: 0,
}}
/>
</Tooltip>
))}
</Flex>
</Flex>
</Flex>
<Flex
direction="column"
gap="2"
style={{ flex: 1 }}
>
<Heading size="4">Properties</Heading>
<ul>
<li>Penetration</li>
</ul>
</Flex>
</Flex>
</Tabs.Content>
</Flex>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
</Flex>
<Flex gap="2" align="center">
<Checkbox defaultChecked />
<Text>Calibrated</Text>
</Flex>
<Flex gap="1">
<Button variant="solid" radius="small">
<img
src={asset('icons/shells/ap.webp')}
width={24}
height={24}
/>
</Button>
<Button variant="soft" radius="small" color="gray">
<img
src={asset('icons/shells/hc_premium.webp')}
width={24}
height={24}
/>
</Button>
<Button variant="soft" radius="small" color="gray">
<img
src={asset('icons/shells/he.webp')}
width={24}
height={24}
/>
</Button>
</Flex>
</Flex>
</Card>
)}
<Theme radius="small"> <Theme radius="small">
<Flex gap="4"> <Flex gap="4">
<Flex gap="1"> <Flex gap="1">
{tank.turrets.map((thisTurret) => { {protagonist.tank.turrets.map((thisTurret) => {
return ( return (
<Tooltip content={thisTurret.name} key={thisTurret.id}> <Tooltip content={thisTurret.name} key={thisTurret.id}>
<Button <Button
onClick={() => { onClick={() => {
setTurret(thisTurret); mutateTankopedia((draft) => {
setGun(thisTurret.guns.at(-1)!); if (!draft.areTanksAssigned) return;
draft.protagonist.turret = thisTurret;
draft.protagonist.gun = thisTurret.guns.at(-1)!;
});
}} }}
variant={turret.id === thisTurret.id ? 'solid' : 'soft'} variant={
protagonist.turret.id === thisTurret.id
? 'solid'
: 'soft'
}
> >
<img <img
src={asset('icons/modules/turret.webp')} src={asset('icons/modules/turret.webp')}
@@ -333,12 +108,19 @@ export default function Page({ params }: { params: { id: string } }) {
</Flex> </Flex>
<Flex gap="1"> <Flex gap="1">
{turret.guns.map((thisGun) => { {protagonist.turret.guns.map((thisGun) => {
return ( return (
<Tooltip content={thisGun.name} key={thisGun.id}> <Tooltip content={thisGun.name} key={thisGun.id}>
<Button <Button
onClick={() => setGun(thisGun)} onClick={() => {
variant={gun.id === thisGun.id ? 'solid' : 'soft'} mutateTankopedia((draft) => {
if (!draft.areTanksAssigned) return;
draft.protagonist.gun = thisGun;
});
}}
variant={
protagonist.gun.id === thisGun.id ? 'solid' : 'soft'
}
> >
<img <img
src={asset('icons/modules/gun.webp')} src={asset('icons/modules/gun.webp')}
+23 -2
View File
@@ -1,7 +1,12 @@
import { produce } from 'immer'; import { produce } from 'immer';
import { create } from 'zustand'; import { create } from 'zustand';
import { TankType, TreeTypeString } from '../components/Tanks'; import { TankType, TreeTypeString } from '../components/Tanks';
import { Tier } from '../core/blitzkrieg/tankDefinitions'; import {
GunDefinition,
TankDefinition,
Tier,
TurretDefinition,
} from '../core/blitzkrieg/tankDefinitions';
export type TankopediaSortBy = 'tier' | 'name'; export type TankopediaSortBy = 'tier' | 'name';
export type TankopediaSortDirection = 'ascending' | 'descending'; export type TankopediaSortDirection = 'ascending' | 'descending';
@@ -28,7 +33,22 @@ type Tankopedia = {
showGrid: boolean; showGrid: boolean;
}; };
mode: TankopediaMode; mode: TankopediaMode;
}; } & (
| {
areTanksAssigned: true;
protagonist: {
tank: TankDefinition;
turret: TurretDefinition;
gun: GunDefinition;
};
antagonist: {
tank: TankDefinition;
turret: TurretDefinition;
gun: GunDefinition;
};
}
| { areTanksAssigned: false }
);
export const useTankopedia = create<Tankopedia>()(() => ({ export const useTankopedia = create<Tankopedia>()(() => ({
sort: { sort: {
@@ -49,6 +69,7 @@ export const useTankopedia = create<Tankopedia>()(() => ({
controlsEnabled: true, controlsEnabled: true,
showGrid: true, showGrid: true,
}, },
areTanksAssigned: false,
mode: 'model', mode: 'model',
})); }));