mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-22 11:17:05 +02:00
feat: prot and anta
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
@@ -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')}
|
||||
|
||||
@@ -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',
|
||||
}));
|
||||
|
||||
|
||||
Reference in new issue
Block a user