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 { Html } from '@react-three/drei';
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 { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { theme } from '../../../../../../stitches.config';
import mutateTankopedia, {
TankopediaMode,
@@ -19,19 +21,20 @@ import { RotationInputs } from '../RotationInputs';
import { SceneProps } from '../SceneProps';
import { TankModel } from './components/TankModel';
interface TankDisplayProps {
tankId: number;
turretId: number;
gunId: number;
}
export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
export function TankDisplay() {
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 hullContainer = useRef<Group>(null);
const showGrid = useTankopedia((state) => state.model.showGrid);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
if (!protagonist) return null;
function handlePointerDown() {
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 (
<Theme radius={isFullScreen ? 'none' : undefined}>
<Card
@@ -111,12 +130,7 @@ export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
</Html>
}
>
<TankModel
gunId={gunId}
tankId={tankId}
turretId={turretId}
ref={hullContainer}
/>
<TankModel ref={hullContainer} />
</Suspense>
</Canvas>
</div>
@@ -159,7 +173,7 @@ export function TankDisplay({ tankId, turretId, gunId }: TankDisplayProps) {
</Button>
</Flex>
<RotationInputs gunId={gunId} tankId={tankId} turretId={turretId} />
<RotationInputs />
</Flex>
</Theme>
</Card>
@@ -20,127 +20,127 @@ import { GunContainer } from '../../GunContainer';
import { HullContainer } from '../../HullContainer';
import { TurretContainer } from '../../TurretContainer';
interface TankModelProps {
tankId: number;
turretId: number;
gunId: number;
}
export const TankModel = forwardRef<Group>((_props, ref) => {
// "TypeError: dispatcher.use is not a function"
// const awaitedModelDefinitions = use(modelDefinitions);
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined
>(undefined);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
export const TankModel = forwardRef<Group, TankModelProps>(
({ 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);
if (!protagonist) return null;
useImperativeHandle(ref, () => hullContainer.current!);
useEffect(() => {
(async () => {
setAwaitedModelDefinitions(await modelDefinitions);
})();
}, []);
const gltf = useLoader(GLTFLoader, `/test/${protagonist.tank.id}.glb`);
const model = useTankopedia((state) => state.model);
const hullContainer = useRef<Group>(null);
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];
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);
if (!awaitedModelDefinitions) return null;
return (
<HullContainer
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
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)}
yaw={model.hullYaw}
ref={hullContainer}
model={turretModelDefinition.model}
onYawStart={() =>
mutateTankopedia((draft) => {
draft.model.controlsEnabled = false;
mutateTankopedia((state) => {
state.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;
yawLimits={turretModelDefinition.yaw}
pitchLimits={gunModelDefinition.pitch}
pitch={model.gunPitch}
onYawEnd={(pitch, yaw) => {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = pitch;
state.model.turretYaw = yaw;
});
}}
gunContainer={gunContainer}
turretOrigin={turretOrigin}
yaw={model.turretYaw}
>
<TurretContainer
<GunContainer
ref={gunContainer}
initialTurretRotation={tankModelDefinition.turretRotation}
gunOrigin={gunOrigin}
ref={turretContainer}
objects={Object.values(gltf.nodes)}
model={turretModelDefinition.model}
onYawStart={() =>
onPitchStart={() => {
mutateTankopedia((state) => {
state.model.controlsEnabled = false;
})
}
yawLimits={turretModelDefinition.yaw}
});
}}
pitchLimits={gunModelDefinition.pitch}
pitch={model.gunPitch}
onYawEnd={(pitch, yaw) => {
turretContainer={turretContainer}
yaw={model.turretYaw}
onPitchEnd={(pitch, yaw) => {
mutateTankopedia((state) => {
state.model.controlsEnabled = true;
state.model.gunPitch = pitch;
state.model.turretYaw = yaw;
});
}}
gunContainer={gunContainer}
yawLimits={turretModelDefinition.yaw}
gunOrigin={gunOrigin}
pitch={model.gunPitch}
model={gunModelDefinition.model}
objects={Object.values(gltf.nodes)}
turretOrigin={turretOrigin}
yaw={model.turretYaw}
>
<GunContainer
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>
);
},
);
/>
</TurretContainer>
</HullContainer>
);
});
@@ -7,26 +7,23 @@ import mutateTankopedia, {
useTankopedia,
} from '../../../../../stores/tankopedia';
interface RotationInputsProps {
tankId: number;
gunId: number;
turretId: number;
}
export function RotationInputs({
tankId,
gunId,
turretId,
}: RotationInputsProps) {
export function RotationInputs() {
const model = useTankopedia((state) => state.model);
const awaitedModelDefinitions = use(modelDefinitions);
const turretYawInput = useRef<HTMLInputElement>(null);
const hullYawInput = useRef<HTMLInputElement>(null);
const gunPitchInput = useRef<HTMLInputElement>(null);
const protagonist = useTankopedia((state) => {
if (!state.areTanksAssigned) return;
return state.protagonist;
});
const tankModelDefinition = awaitedModelDefinitions[tankId];
const turretModelDefinition = tankModelDefinition.turrets[turretId];
const gunModelDefinition = turretModelDefinition.guns[gunId];
if (!protagonist) return null;
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
useEffect(() => {
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';
import {
ArrowLeftIcon,
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 { ArrowLeftIcon } from '@radix-ui/react-icons';
import { Button, Flex, Heading, Text, Theme, Tooltip } from '@radix-ui/themes';
import Link from 'next/link';
import { use, useRef, useState } from 'react';
import { Vector3 } from 'three';
import { use, useEffect } from 'react';
import { Flag } from '../../../../components/Flag';
import { ModuleButtons } from '../../../../components/ModuleButton';
import PageWrapper from '../../../../components/PageWrapper';
import { asset } from '../../../../core/blitzkrieg/asset';
import {
TIER_ROMAN_NUMERALS,
tankDefinitions,
tankNamesDiacritics,
} from '../../../../core/blitzkrieg/tankDefinitions';
import { useTankopedia } from '../../../../stores/tankopedia';
import mutateTankopedia, { useTankopedia } from '../../../../stores/tankopedia';
import { TankDisplay } from './components/Model/TankDisplay';
const X_AXIS = new Vector3(1, 0, 0);
import { VersusBar } from './components/VersusBar';
export default function Page({ params }: { params: { id: string } }) {
const id = parseInt(params.id);
const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTankNamesDiacritics = use(tankNamesDiacritics);
const tank = awaitedTankDefinitions[id];
const [turret, setTurret] = useState(tank.turrets.at(-1)!);
const [gun, setGun] = useState(turret.guns.at(-1)!);
const [crew, setCrew] = useState(100);
const versusTankSearchInput = useRef<HTMLInputElement>(null);
const [versusTankSearchResults, setVersusTankSearchResults] = useState<
number[]
>([]);
const [versusTank, setVersusTank] = useState(tank);
const [versusTurret, setVersusTurret] = useState(versusTank.turrets.at(-1)!);
const [versusGun, setVersusGun] = useState(versusTurret.guns.at(-1)!);
const [versusTankTab, setVersusTankTab] = useState('search');
const mode = useTankopedia((state) => state.mode);
useEffect(() => {
mutateTankopedia((draft) => {
draft.model.hullYaw = 0;
draft.model.turretYaw = 0;
draft.model.gunPitch = 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.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 (
<PageWrapper color="purple">
@@ -71,254 +67,33 @@ export default function Page({ params }: { params: { id: string } }) {
</Link>
<Flex gap="2" align="center">
<Flag nation={tank.nation} />
<Heading>{tank.name}</Heading>
<Flag nation={protagonist.tank.nation} />
<Heading>{protagonist.tank.name}</Heading>
</Flex>
</Flex>
<TankDisplay gunId={gun.id} tankId={tank.id} turretId={turret.id} />
{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>
)}
<TankDisplay />
<VersusBar />
<Theme radius="small">
<Flex gap="4">
<Flex gap="1">
{tank.turrets.map((thisTurret) => {
{protagonist.tank.turrets.map((thisTurret) => {
return (
<Tooltip content={thisTurret.name} key={thisTurret.id}>
<Button
onClick={() => {
setTurret(thisTurret);
setGun(thisTurret.guns.at(-1)!);
mutateTankopedia((draft) => {
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
src={asset('icons/modules/turret.webp')}
@@ -333,12 +108,19 @@ export default function Page({ params }: { params: { id: string } }) {
</Flex>
<Flex gap="1">
{turret.guns.map((thisGun) => {
{protagonist.turret.guns.map((thisGun) => {
return (
<Tooltip content={thisGun.name} key={thisGun.id}>
<Button
onClick={() => setGun(thisGun)}
variant={gun.id === thisGun.id ? 'solid' : 'soft'}
onClick={() => {
mutateTankopedia((draft) => {
if (!draft.areTanksAssigned) return;
draft.protagonist.gun = thisGun;
});
}}
variant={
protagonist.gun.id === thisGun.id ? 'solid' : 'soft'
}
>
<img
src={asset('icons/modules/gun.webp')}
+23 -2
View File
@@ -1,7 +1,12 @@
import { produce } from 'immer';
import { create } from 'zustand';
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 TankopediaSortDirection = 'ascending' | 'descending';
@@ -28,7 +33,22 @@ type Tankopedia = {
showGrid: boolean;
};
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>()(() => ({
sort: {
@@ -49,6 +69,7 @@ export const useTankopedia = create<Tankopedia>()(() => ({
controlsEnabled: true,
showGrid: true,
},
areTanksAssigned: false,
mode: 'model',
}));