feat: isolated duel

This commit is contained in:
TrèsAbhi committed 2024-01-22 18:04:13 -06:00
1 parent fce5f19100
commit 0b5c2e42fb
15 files changed
+218 -249

No files matched your search

@@ -9,25 +9,17 @@ import {
Text, Text,
Tooltip, Tooltip,
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import { produce } from 'immer'; import { useState } from 'react';
import { Dispatch, SetStateAction, useState } from 'react';
import { ModuleButton } from '../../../../../../components/ModuleButton'; import { ModuleButton } from '../../../../../../components/ModuleButton';
import { SmallTankIcon } from '../../../../../../components/SmallTankIcon'; import { SmallTankIcon } from '../../../../../../components/SmallTankIcon';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { SHELL_NAMES } from '../../../../../../core/blitzkrieg/tankDefinitions'; import { SHELL_NAMES } from '../../../../../../core/blitzkrieg/tankDefinitions';
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
import { TankSearch } from '../../../components/TankSearch'; import { TankSearch } from '../../../components/TankSearch';
import { Duel } from '../../page';
interface AntagonistBarProps { export function AntagonistBar() {
duel: Duel; const antagonist = useDuel((state) => state.antagonist!);
setDuel: Dispatch<SetStateAction<Duel>>;
}
export function AntagonistBar({
duel: { antagonist },
setDuel,
}: AntagonistBarProps) {
const mode = useTankopediaTemporary((state) => state.mode); const mode = useTankopediaTemporary((state) => state.mode);
const [tab, setTab] = useState('search'); const [tab, setTab] = useState('search');
const [antagonistSelectorOpen, setAntagonistSelectorVisible] = const [antagonistSelectorOpen, setAntagonistSelectorVisible] =
@@ -50,11 +42,9 @@ export function AntagonistBar({
key={shell.id} key={shell.id}
last={index === antagonist.gun.shells.length - 1} last={index === antagonist.gun.shells.length - 1}
onClick={() => { onClick={() => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.antagonist!.shell = shell;
draft.antagonist.shell = shell; });
}),
);
}} }}
/> />
); );
@@ -109,16 +99,14 @@ export function AntagonistBar({
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.antagonist!.tank = tank;
draft.antagonist.tank = tank; draft.antagonist!.turret = tank.turrets.at(-1)!;
draft.antagonist.turret = tank.turrets.at(-1)!; draft.antagonist!.gun =
draft.antagonist.gun = draft.antagonist!.turret.guns.at(-1)!;
draft.antagonist.turret.guns.at(-1)!; draft.antagonist!.shell =
draft.antagonist.shell = draft.antagonist!.gun.shells[0];
draft.antagonist.gun.shells[0]; });
}),
);
setAntagonistSelectorVisible(false); setAntagonistSelectorVisible(false);
}} }}
/> />
@@ -144,15 +132,13 @@ export function AntagonistBar({
} }
key={turret.id} key={turret.id}
onClick={() => { onClick={() => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.antagonist!.turret = turret;
draft.antagonist.turret = turret; draft.antagonist!.gun =
draft.antagonist.gun = turret.guns.at(-1)!;
turret.guns.at(-1)!; draft.antagonist!.shell =
draft.antagonist.shell = draft.antagonist!.gun.shells[0];
draft.antagonist.gun.shells[0]; });
}),
);
}} }}
selected={antagonist.turret.id === turret.id} selected={antagonist.turret.id === turret.id}
tier={turret.tier} tier={turret.tier}
@@ -172,12 +158,10 @@ export function AntagonistBar({
index === antagonist.turret.guns.length - 1 index === antagonist.turret.guns.length - 1
} }
onClick={() => { onClick={() => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.antagonist!.gun = gun;
draft.antagonist.gun = gun; draft.antagonist!.shell = gun.shells[0];
draft.antagonist.shell = gun.shells[0]; });
}),
);
}} }}
selected={antagonist.gun.id === gun.id} selected={antagonist.gun.id === gun.id}
tier={gun.tier} tier={gun.tier}
@@ -226,14 +210,12 @@ export function AntagonistBar({
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { [draft.protagonist, draft.antagonist] = [
[draft.protagonist, draft.antagonist] = [ draft.antagonist,
draft.antagonist, draft.protagonist,
draft.protagonist, ];
]; });
}),
);
}} }}
> >
<ShuffleIcon /> Swap tanks <ShuffleIcon /> Swap tanks
@@ -1,23 +1,18 @@
import { Flex, Heading } from '@radix-ui/themes'; import { Flex, Heading } from '@radix-ui/themes';
import { produce } from 'immer'; import { use } from 'react';
import { Dispatch, SetStateAction, use } from 'react';
import { ModuleButton } from '../../../../../components/ModuleButton'; import { ModuleButton } from '../../../../../components/ModuleButton';
import { equipmentDefinitions } from '../../../../../core/blitzkrieg/equipmentDefinitions'; import { equipmentDefinitions } from '../../../../../core/blitzkrieg/equipmentDefinitions';
import { mutateDuel, useDuel } from '../../../../../stores/duel';
import { import {
mutateTankopediaTemporary, mutateTankopediaTemporary,
useTankopediaTemporary, useTankopediaTemporary,
} from '../../../../../stores/tankopedia'; } from '../../../../../stores/tankopedia';
import { Duel } from '../page';
interface ConfigureProps { export function Configure() {
duel: Duel; const protagonist = useDuel((state) => state.protagonist!);
setDuel: Dispatch<SetStateAction<Duel>>;
}
export function Configure({ duel, setDuel }: ConfigureProps) {
const awaitedEquipmentDefinitions = use(equipmentDefinitions); const awaitedEquipmentDefinitions = use(equipmentDefinitions);
const equipmentRows = const equipmentRows =
awaitedEquipmentDefinitions.presets[duel.protagonist.tank.equipment]; awaitedEquipmentDefinitions.presets[protagonist.tank.equipment];
const equipmentMatrix = useTankopediaTemporary( const equipmentMatrix = useTankopediaTemporary(
(state) => state.equipmentMatrix, (state) => state.equipmentMatrix,
); );
@@ -32,26 +27,24 @@ export function Configure({ duel, setDuel }: ConfigureProps) {
<Flex gap="2"> <Flex gap="2">
<Flex> <Flex>
{duel.protagonist.tank.turrets.map((thisTurret, index) => { {protagonist.tank.turrets.map((thisTurret, index) => {
return ( return (
<ModuleButton <ModuleButton
key={thisTurret.id} key={thisTurret.id}
selected={duel.protagonist.turret.id === thisTurret.id} selected={protagonist.turret.id === thisTurret.id}
type="module" type="module"
module="turret" module="turret"
tier={thisTurret.tier} tier={thisTurret.tier}
first={index === 0} first={index === 0}
last={index === duel.protagonist.tank.turrets.length - 1} last={index === protagonist.tank.turrets.length - 1}
rowChild rowChild
onClick={() => { onClick={() => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.protagonist!.turret = thisTurret;
draft.protagonist.turret = thisTurret; draft.protagonist!.gun = thisTurret.guns.at(-1)!;
draft.protagonist.gun = thisTurret.guns.at(-1)!; draft.protagonist!.shell =
draft.protagonist.shell = thisTurret.guns.at(-1)!.shells[0];
thisTurret.guns.at(-1)!.shells[0]; });
}),
);
}} }}
/> />
); );
@@ -59,24 +52,22 @@ export function Configure({ duel, setDuel }: ConfigureProps) {
</Flex> </Flex>
<Flex> <Flex>
{duel.protagonist.turret.guns.map((thisGun, index) => { {protagonist.turret.guns.map((thisGun, index) => {
return ( return (
<ModuleButton <ModuleButton
key={thisGun.id} key={thisGun.id}
type="module" type="module"
module="gun" module="gun"
tier={thisGun.tier} tier={thisGun.tier}
selected={duel.protagonist.gun.id === thisGun.id} selected={protagonist.gun.id === thisGun.id}
first={index === 0} first={index === 0}
last={index === duel.protagonist.turret.guns.length - 1} last={index === protagonist.turret.guns.length - 1}
rowChild rowChild
onClick={() => { onClick={() => {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.protagonist!.gun = thisGun;
draft.protagonist.gun = thisGun; draft.protagonist!.shell = thisGun.shells[0];
draft.protagonist.shell = thisGun.shells[0]; });
}),
);
}} }}
/> />
); );
@@ -7,8 +7,8 @@ import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimi
import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions';
import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose'; import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose';
import { useAwait } from '../../../../../hooks/useAwait'; import { useAwait } from '../../../../../hooks/useAwait';
import { useDuel } from '../../../../../stores/duel';
import { useTankopediaPersistent } from '../../../../../stores/tankopedia'; import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
import { Duel } from '../page';
const poseDistances: Record<Pose, number> = { const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15, [Pose.HullDown]: 15,
@@ -16,24 +16,21 @@ const poseDistances: Record<Pose, number> = {
[Pose.Default]: -1, [Pose.Default]: -1,
}; };
interface ControlsProps { export function Controls() {
duel: Duel;
}
export function Controls({ duel }: ControlsProps) {
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const orbitControls = useRef<OrbitControlsClass>(null); const orbitControls = useRef<OrbitControlsClass>(null);
const awaitedModelDefinitions = useAwait(modelDefinitions); const awaitedModelDefinitions = useAwait(modelDefinitions);
const protagonist = useDuel((state) => state.protagonist!);
const antagonist = useDuel((state) => state.antagonist!);
const protagonistModelDefinition = const protagonistModelDefinition =
awaitedModelDefinitions[duel.protagonist.tank.id]; awaitedModelDefinitions[protagonist.tank.id];
const antagonistModelDefinition = const antagonistModelDefinition = awaitedModelDefinitions[antagonist.tank.id];
awaitedModelDefinitions[duel.antagonist.tank.id];
const protagonistTurretModelDefinition = const protagonistTurretModelDefinition =
protagonistModelDefinition.turrets[duel.protagonist.turret.id]; protagonistModelDefinition.turrets[protagonist.turret.id];
const antagonistTurretModelDefinition = const antagonistTurretModelDefinition =
antagonistModelDefinition.turrets[duel.antagonist.turret.id]; antagonistModelDefinition.turrets[antagonist.turret.id];
const protagonistGunModelDefinition = const protagonistGunModelDefinition =
protagonistTurretModelDefinition.guns[duel.protagonist.gun.id]; protagonistTurretModelDefinition.guns[protagonist.gun.id];
const protagonistHullOrigin = new Vector3( const protagonistHullOrigin = new Vector3(
protagonistModelDefinition.hullOrigin[0], protagonistModelDefinition.hullOrigin[0],
protagonistModelDefinition.hullOrigin[1], protagonistModelDefinition.hullOrigin[1],
@@ -148,7 +145,7 @@ export function Controls({ duel }: ControlsProps) {
unsubscribeTankopedia(); unsubscribeTankopedia();
poseEvent.off(handlePoseEvent); poseEvent.off(handlePoseEvent);
}; };
}, [camera, duel.protagonist.tank.id, duel.antagonist.tank.id]); }, [camera, protagonist.tank.id, antagonist.tank.id]);
return ( return (
<OrbitControls <OrbitControls
@@ -1,12 +1,9 @@
import { useModel } from '../../../../../hooks/useModel'; import { useModel } from '../../../../../hooks/useModel';
import { Duel } from '../page'; import { useDuel } from '../../../../../stores/duel';
interface LightingProps { export function Lighting() {
duel: Duel; const protagonist = useDuel((state) => state.protagonist!);
} const { hasPbr } = useModel(protagonist.tank.id);
export function Lighting({ duel }: LightingProps) {
const { hasPbr } = useModel(duel.protagonist.tank.id);
return ( return (
<> <>
@@ -5,12 +5,12 @@ import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawL
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose'; import { Pose, poseEvent } from '../../../../../../core/blitzkrieg/pose';
import { useDuel } from '../../../../../../stores/duel';
import { import {
TankopediaMode, TankopediaMode,
mutateTankopediaTemporary, mutateTankopediaTemporary,
useTankopediaTemporary, useTankopediaTemporary,
} from '../../../../../../stores/tankopedia'; } from '../../../../../../stores/tankopedia';
import { Duel } from '../../page';
import { Controls } from '../Control'; import { Controls } from '../Control';
import { Lighting } from '../Lighting'; import { Lighting } from '../Lighting';
import { RotationInputs } from '../QuickInputs'; import { RotationInputs } from '../QuickInputs';
@@ -20,21 +20,17 @@ import { Options } from './components/Options';
import { TankArmor } from './components/TankArmor'; import { TankArmor } from './components/TankArmor';
import { TankModel } from './components/TankModel'; import { TankModel } from './components/TankModel';
interface TankSandboxProps { export function TankSandbox() {
duel: Duel;
}
export function TankSandbox({ duel }: TankSandboxProps) {
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const awaitedModelDefinitions = use(modelDefinitions); 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 = useTankopediaTemporary((state) => state.mode); const mode = useTankopediaTemporary((state) => state.mode);
const tankModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id]; const protagonist = useDuel((state) => state.protagonist!);
const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id];
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[duel.protagonist.turret.id]; tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
turretModelDefinition.guns[duel.protagonist.gun.id];
function handlePointerDown() { function handlePointerDown() {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointermove', handlePointerMove);
@@ -119,8 +115,6 @@ export function TankSandbox({ duel }: TankSandboxProps) {
}); });
useEffect(() => { useEffect(() => {
if (!duel.protagonist) return void 0;
mutateTankopediaTemporary((draft) => { mutateTankopediaTemporary((draft) => {
[draft.model.pose.pitch, draft.model.pose.yaw] = applyPitchYawLimits( [draft.model.pose.pitch, draft.model.pose.yaw] = applyPitchYawLimits(
draft.model.pose.pitch, draft.model.pose.pitch,
@@ -129,7 +123,7 @@ export function TankSandbox({ duel }: TankSandboxProps) {
turretModelDefinition.yaw, turretModelDefinition.yaw,
); );
}); });
}, [duel.protagonist.gun, duel.protagonist.turret]); }, [protagonist.gun, protagonist.turret]);
return ( return (
<Theme radius={isFullScreen ? 'none' : undefined}> <Theme radius={isFullScreen ? 'none' : undefined}>
@@ -170,21 +164,20 @@ export function TankSandbox({ duel }: TankSandboxProps) {
camera={{ fov: 25 }} camera={{ fov: 25 }}
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
> >
<Controls duel={duel} /> <Controls />
<SceneProps /> <SceneProps />
<Suspense fallback={<ModelLoader />}> <Suspense fallback={<ModelLoader />}>
<Lighting duel={duel} /> <Lighting />
<TankModel duel={duel} /> <TankModel />
<TankArmor duel={duel} /> <TankArmor />
{/* <SpacedArmorDepth duel={duel} /> */}
</Suspense> </Suspense>
</Canvas> </Canvas>
</div> </div>
<Options canvas={canvasWrapper} isFullScreen={isFullScreen} /> <Options canvas={canvasWrapper} isFullScreen={isFullScreen} />
<RotationInputs duel={duel} /> <RotationInputs />
</Flex> </Flex>
</Theme> </Theme>
</Card> </Card>
@@ -11,14 +11,11 @@ import { nameToArmorId } from '../../../../../../../../../core/blitzkrieg/nameTo
import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness'; import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolveThickness';
import { useArmor } from '../../../../../../../../../hooks/useArmor'; import { useArmor } from '../../../../../../../../../hooks/useArmor';
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
import { useDuel } from '../../../../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
import { Duel } from '../../../../../page';
interface ArmorHighlightingProps { export function ArmorHighlighting() {
duel: Duel; const protagonist = useDuel((state) => state.protagonist!);
}
export function ArmorHighlighting({ duel }: ArmorHighlightingProps) {
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions(); const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
@@ -88,13 +85,12 @@ export function ArmorHighlighting({ duel }: ArmorHighlightingProps) {
return unsubscribe; return unsubscribe;
}); });
const armorGltf = useArmor(duel.protagonist.tank.id); const armorGltf = useArmor(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes); const armorNodes = Object.values(armorGltf.nodes);
const tankModelDefinition = modelDefinitions[duel.protagonist.tank.id]; const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[duel.protagonist.turret.id]; tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
turretModelDefinition.guns[duel.protagonist.gun.id];
const hullOrigin = new Vector3( const hullOrigin = new Vector3(
tankModelDefinition.hullOrigin[0], tankModelDefinition.hullOrigin[0],
tankModelDefinition.hullOrigin[1], tankModelDefinition.hullOrigin[1],
@@ -137,7 +133,6 @@ export function ArmorHighlighting({ duel }: ArmorHighlightingProps) {
return ( return (
<ArmorMesh <ArmorMesh
maxThickness={maxThickness} maxThickness={maxThickness}
duel={duel}
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
thickness={thickness} thickness={thickness}
@@ -162,7 +157,6 @@ export function ArmorHighlighting({ duel }: ArmorHighlightingProps) {
return ( return (
<ArmorMesh <ArmorMesh
maxThickness={maxThickness} maxThickness={maxThickness}
duel={duel}
key={node.uuid} key={node.uuid}
geometry={(node as Mesh).geometry} geometry={(node as Mesh).geometry}
position={turretOrigin} position={turretOrigin}
@@ -12,12 +12,8 @@ import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolve
import { useArmor } from '../../../../../../../../../hooks/useArmor'; import { useArmor } from '../../../../../../../../../hooks/useArmor';
import { useModel } from '../../../../../../../../../hooks/useModel'; import { useModel } from '../../../../../../../../../hooks/useModel';
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
import { useDuel } from '../../../../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
import { Duel } from '../../../../../page';
interface ExternalModuleMaskProps {
duel: Duel;
}
/** /**
* When rendered, generates a mask and thickness buffer for external modules. * When rendered, generates a mask and thickness buffer for external modules.
@@ -26,7 +22,8 @@ interface ExternalModuleMaskProps {
* - B: no data * - B: no data
* - A: 1 means is armor; 0 is background * - A: 1 means is armor; 0 is background
*/ */
export const ExternalModuleMask = memo<ExternalModuleMaskProps>(({ duel }) => { export const ExternalModuleMask = memo(() => {
const protagonist = useDuel((state) => state.protagonist!);
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions(); const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
@@ -117,16 +114,15 @@ export const ExternalModuleMask = memo<ExternalModuleMaskProps>(({ duel }) => {
return unsubscribe; return unsubscribe;
}); });
const armorGltf = useArmor(duel.protagonist.tank.id); const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(duel.protagonist.tank.id); const { gltf: modelGltf } = useModel(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes); const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes); const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[duel.protagonist.tank.id]; const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[duel.protagonist.turret.id]; tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
turretModelDefinition.guns[duel.protagonist.gun.id];
const maxThickness = Math.max( const maxThickness = Math.max(
tankModelDefinition.trackThickness, tankModelDefinition.trackThickness,
gunModelDefinition.barrelThickness, gunModelDefinition.barrelThickness,
@@ -12,12 +12,8 @@ import { resolveArmor } from '../../../../../../../../../core/blitzkrieg/resolve
import { useArmor } from '../../../../../../../../../hooks/useArmor'; import { useArmor } from '../../../../../../../../../hooks/useArmor';
import { useModel } from '../../../../../../../../../hooks/useModel'; import { useModel } from '../../../../../../../../../hooks/useModel';
import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions'; import { useModelDefinitions } from '../../../../../../../../../hooks/useModelDefinitions';
import { useDuel } from '../../../../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../../../../stores/tankopedia';
import { Duel } from '../../../../../page';
interface SpacedArmorDepthProps {
duel: Duel;
}
/** /**
* When rendered, this component provides the depth buffer for the armor which * When rendered, this component provides the depth buffer for the armor which
@@ -30,7 +26,8 @@ interface SpacedArmorDepthProps {
* - B: no data * - B: no data
* - A: 1 means is armor; 0 is background * - A: 1 means is armor; 0 is background
*/ */
export const SpacedArmorDepth = memo<SpacedArmorDepthProps>(({ duel }) => { export const SpacedArmorDepth = memo(() => {
const protagonist = useDuel((state) => state.protagonist!);
const wrapper = useRef<Group>(null); const wrapper = useRef<Group>(null);
const modelDefinitions = useModelDefinitions(); const modelDefinitions = useModelDefinitions();
const turretContainer = useRef<Group>(null); const turretContainer = useRef<Group>(null);
@@ -123,16 +120,15 @@ export const SpacedArmorDepth = memo<SpacedArmorDepthProps>(({ duel }) => {
return unsubscribe; return unsubscribe;
}); });
const armorGltf = useArmor(duel.protagonist.tank.id); const armorGltf = useArmor(protagonist.tank.id);
const { gltf: modelGltf } = useModel(duel.protagonist.tank.id); const { gltf: modelGltf } = useModel(protagonist.tank.id);
const armorNodes = Object.values(armorGltf.nodes); const armorNodes = Object.values(armorGltf.nodes);
const modelNodes = Object.values(modelGltf.nodes); const modelNodes = Object.values(modelGltf.nodes);
const tankModelDefinition = modelDefinitions[duel.protagonist.tank.id]; const tankModelDefinition = modelDefinitions[protagonist.tank.id];
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[duel.protagonist.turret.id]; tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
turretModelDefinition.guns[duel.protagonist.gun.id];
const maxThickness = Math.max( const maxThickness = Math.max(
tankModelDefinition.trackThickness, tankModelDefinition.trackThickness,
gunModelDefinition.barrelThickness, gunModelDefinition.barrelThickness,
@@ -3,29 +3,24 @@ import { memo } from 'react';
import { DepthTexture, Scene } from 'three'; import { DepthTexture, Scene } from 'three';
import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh'; import { externalModuleMaskRenderTarget } from '../../../../../../../../components/ArmorMesh';
import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth'; import { spacedArmorDepthRenderTarget } from '../../../../../../../../components/ArmorMesh/components/SpacedArmorDepth';
import { Duel } from '../../../../page';
import { ArmorHighlighting } from './components/ArmorHighlighting'; import { ArmorHighlighting } from './components/ArmorHighlighting';
import { ExternalModuleMask } from './components/ExternalModuleMask'; import { ExternalModuleMask } from './components/ExternalModuleMask';
import { SpacedArmorDepth } from './components/SpacedArmorDepth'; import { SpacedArmorDepth } from './components/SpacedArmorDepth';
interface TankArmorProps { export const TankArmor = memo(() => {
duel: Duel;
}
export const TankArmor = memo<TankArmorProps>(({ duel }) => {
const externalModuleMaskScene = new Scene(); const externalModuleMaskScene = new Scene();
const spacedArmorDepthScene = new Scene(); const spacedArmorDepthScene = new Scene();
const armorHighlightingScene = new Scene(); const armorHighlightingScene = new Scene();
const externalModuleMaskPortal = createPortal( const externalModuleMaskPortal = createPortal(
<ExternalModuleMask duel={duel} />, <ExternalModuleMask />,
externalModuleMaskScene, externalModuleMaskScene,
); );
const spacedArmorDepthPortal = createPortal( const spacedArmorDepthPortal = createPortal(
<SpacedArmorDepth duel={duel} />, <SpacedArmorDepth />,
spacedArmorDepthScene, spacedArmorDepthScene,
); );
const armorHighlightingPortal = createPortal( const armorHighlightingPortal = createPortal(
<ArmorHighlighting duel={duel} />, <ArmorHighlighting />,
armorHighlightingScene, armorHighlightingScene,
); );
@@ -22,21 +22,17 @@ import {
import { resolveJsxTree } from '../../../../../../../core/blitzkrieg/resolveJsxTree'; import { resolveJsxTree } from '../../../../../../../core/blitzkrieg/resolveJsxTree';
import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180'; import { normalizeAnglePI } from '../../../../../../../core/math/normalizeAngle180';
import { useModel } from '../../../../../../../hooks/useModel'; import { useModel } from '../../../../../../../hooks/useModel';
import { useDuel } from '../../../../../../../stores/duel';
import mutateTankopediaPersistent, { import mutateTankopediaPersistent, {
mutateTankopediaTemporary, mutateTankopediaTemporary,
useTankopediaTemporary, useTankopediaTemporary,
} from '../../../../../../../stores/tankopedia'; } from '../../../../../../../stores/tankopedia';
import { Duel } from '../../../page';
interface TankModelProps { export const TankModel = memo(() => {
duel: Duel;
}
export const TankModel = memo(({ duel: { protagonist } }: TankModelProps) => {
const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState<
ModelDefinitions | undefined ModelDefinitions | undefined
>(undefined); >(undefined);
const protagonist = useDuel((draft) => draft.protagonist!);
const canvas = useThree((state) => state.gl.domElement); const canvas = useThree((state) => state.gl.domElement);
const physical = useTankopediaTemporary((state) => state.model.pose); const physical = useTankopediaTemporary((state) => state.model.pose);
const hullContainer = useRef<Group>(null); const hullContainer = useRef<Group>(null);
@@ -3,19 +3,16 @@ import { use, useEffect, useRef } from 'react';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform'; import { modelTransformEvent } from '../../../../../../core/blitzkrieg/modelTransform';
import { useDuel } from '../../../../../../stores/duel';
import mutateTankopediaPersistent, { import mutateTankopediaPersistent, {
mutateTankopediaTemporary, mutateTankopediaTemporary,
useTankopediaPersistent, useTankopediaPersistent,
useTankopediaTemporary, useTankopediaTemporary,
} from '../../../../../../stores/tankopedia'; } from '../../../../../../stores/tankopedia';
import { Duel } from '../../page';
import { QuickEquipmentButton } from './components/QuickEquipmentButton'; import { QuickEquipmentButton } from './components/QuickEquipmentButton';
interface RotationInputsProps { export function RotationInputs() {
duel: Duel; const protagonist = useDuel((state) => state.protagonist!);
}
export function RotationInputs({ duel: { protagonist } }: RotationInputsProps) {
const physical = useTankopediaTemporary((state) => state.model.pose); const physical = useTankopediaTemporary((state) => state.model.pose);
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const turretYawInput = useRef<HTMLInputElement>(null); const turretYawInput = useRef<HTMLInputElement>(null);
@@ -1,13 +1,11 @@
import { ArrowLeftIcon } from '@radix-ui/react-icons'; import { ArrowLeftIcon } from '@radix-ui/react-icons';
import { Flex, Heading, Link, Text } from '@radix-ui/themes'; import { Flex, Heading, Link, Text } from '@radix-ui/themes';
import { Flag } from '../../../../../components/Flag'; import { Flag } from '../../../../../components/Flag';
import { Duel } from '../page'; import { useDuel } from '../../../../../stores/duel';
interface TitleProps { export function Title() {
duel: Duel; const protagonist = useDuel((state) => state.protagonist!);
}
export function Title({ duel }: TitleProps) {
return ( return (
<Flex justify="between" align="center"> <Flex justify="between" align="center">
<Link <Link
@@ -21,8 +19,8 @@ export function Title({ duel }: TitleProps) {
</Link> </Link>
<Flex gap="2" align="center"> <Flex gap="2" align="center">
<Flag nation={duel.protagonist.tank.nation} /> <Flag nation={protagonist.tank.nation} />
<Heading>{duel.protagonist.tank.name}</Heading> <Heading>{protagonist.tank.name}</Heading>
</Flex> </Flex>
</Flex> </Flex>
); );
+45 -52
View File
@@ -1,43 +1,40 @@
'use client'; 'use client';
import { Flex } from '@radix-ui/themes'; import { Flex } from '@radix-ui/themes';
import { produce } from 'immer'; import { use, useEffect } from 'react';
import { use, useEffect, useState } from 'react';
import PageWrapper from '../../../../components/PageWrapper'; import PageWrapper from '../../../../components/PageWrapper';
import { tankDefinitions } from '../../../../core/blitzkrieg/tankDefinitions'; import { tankDefinitions } from '../../../../core/blitzkrieg/tankDefinitions';
import { import { mutateDuel, useDuel } from '../../../../stores/duel';
DuelMember, import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
mutateTankopediaTemporary,
} from '../../../../stores/tankopedia';
import { AntagonistBar } from './components/AntagonistBar'; import { AntagonistBar } from './components/AntagonistBar';
import { Configure } from './components/Configure'; import { Configure } from './components/Configure';
import { TankSandbox } from './components/Model/TankSandbox'; import { TankSandbox } from './components/Model/TankSandbox';
import { Title } from './components/Title'; import { Title } from './components/Title';
export interface Duel {
protagonist: DuelMember;
antagonist: DuelMember;
}
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 [duel, setDuel] = useState<Duel>({ const assigned = useDuel((state) => state.assigned);
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(() => { useEffect(() => {
mutateDuel((draft) => {
draft.assigned = true;
draft.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],
};
draft.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],
};
});
mutateTankopediaTemporary((draft) => { mutateTankopediaTemporary((draft) => {
draft.model.pose.yaw = 0; draft.model.pose.yaw = 0;
draft.model.pose.pitch = 0; draft.model.pose.pitch = 0;
@@ -48,27 +45,21 @@ export default function Page({ params }: { params: { id: string } }) {
if (document.activeElement instanceof HTMLInputElement) return; if (document.activeElement instanceof HTMLInputElement) return;
if (event.key === '1') { if (event.key === '1') {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { draft.antagonist!.shell = draft.antagonist!.gun.shells[0];
draft.antagonist.shell = draft.antagonist.gun.shells[0]; });
}),
);
} else if (event.key === '2') { } else if (event.key === '2') {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { if (draft.antagonist!.gun.shells[1]) {
if (draft.antagonist.gun.shells[1]) { draft.antagonist!.shell = draft.antagonist!.gun.shells[1];
draft.antagonist.shell = draft.antagonist.gun.shells[1]; }
} });
}),
);
} else if (event.key === '3') { } else if (event.key === '3') {
setDuel( mutateDuel((draft) => {
produce<Duel>((draft) => { if (draft.antagonist!.gun.shells[2]) {
if (draft.antagonist.gun.shells[2]) { draft.antagonist!.shell = draft.antagonist!.gun.shells[2];
draft.antagonist.shell = draft.antagonist.gun.shells[2]; }
} });
}),
);
} }
} }
@@ -81,15 +72,17 @@ export default function Page({ params }: { params: { id: string } }) {
return ( return (
<PageWrapper color="purple"> <PageWrapper color="purple">
<Flex gap="8" direction="column"> {assigned && (
<Flex gap="4" direction="column"> <Flex gap="8" direction="column">
<Title duel={duel} /> <Flex gap="4" direction="column">
<TankSandbox duel={duel} /> <Title />
<AntagonistBar duel={duel} setDuel={setDuel} /> <TankSandbox />
</Flex> <AntagonistBar />
</Flex>
<Configure duel={duel} setDuel={setDuel} /> <Configure />
</Flex> </Flex>
)}
</PageWrapper> </PageWrapper>
); );
} }
@@ -1,12 +1,12 @@
import { MeshProps, useFrame, useThree } from '@react-three/fiber'; import { MeshProps, useFrame, useThree } from '@react-three/fiber';
import { useRef } from 'react'; import { useEffect, useRef } from 'react';
import { MeshStandardMaterial, ShaderMaterial, Vector2 } from 'three'; import { MeshStandardMaterial, ShaderMaterial, Vector2 } from 'three';
import ThreeCustomShaderMaterial from 'three-custom-shader-material'; import ThreeCustomShaderMaterial from 'three-custom-shader-material';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils';
import { Duel } from '../../../../app/tools/tankopedia/[id]/page';
import { canSplash } from '../../../../core/blitz/canSplash'; import { canSplash } from '../../../../core/blitz/canSplash';
import { isExplosive } from '../../../../core/blitz/isExplosive'; import { isExplosive } from '../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { useDuel } from '../../../../stores/duel';
import { useTankopediaPersistent } from '../../../../stores/tankopedia'; import { useTankopediaPersistent } from '../../../../stores/tankopedia';
import { externalModuleMaskRenderTarget } from '../ExternalModuleMask'; import { externalModuleMaskRenderTarget } from '../ExternalModuleMask';
import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth'; import { spacedArmorDepthRenderTarget } from '../SpacedArmorDepth';
@@ -14,7 +14,6 @@ import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl'; import vertexShader from './shaders/vertex.glsl';
interface ArmorMeshProps extends MeshProps { interface ArmorMeshProps extends MeshProps {
duel: Duel;
thickness: number; thickness: number;
maxThickness: number; maxThickness: number;
} }
@@ -22,23 +21,43 @@ interface ArmorMeshProps extends MeshProps {
export function ArmorMesh({ export function ArmorMesh({
thickness, thickness,
maxThickness, maxThickness,
duel,
...props ...props
}: ArmorMeshProps) { }: ArmorMeshProps) {
const equipment = useTankopediaPersistent((draft) => draft.model.equipment); const initialEquipment = useTankopediaPersistent.getState().model.equipment;
const camera = useThree((state) => state.camera); const camera = useThree((state) => state.camera);
const { shell } = duel.antagonist; const shell = useDuel((state) => state.antagonist!.shell);
const greenPenetration = useTankopediaPersistent( const greenPenetration = useTankopediaPersistent(
(state) => state.model.visual.greenPenetration, (state) => state.model.visual.greenPenetration,
); );
const material = useRef<ShaderMaterial>(null); const material = useRef<ShaderMaterial>(null);
const resolution = new Vector2(); const resolution = new Vector2();
const explosionCapable = isExplosive(shell.type); const explosionCapable = isExplosive(shell.type);
let mutatedThickness = thickness;
let penetration = resolveNearPenetration(shell.penetration);
if (equipment.calibratedShells) penetration *= explosionCapable ? 1.1 : 1.05; useEffect(() => {
if (equipment.enhancedArmor) mutatedThickness *= 1.04; function updateQuickEquipments() {
const equipment = useTankopediaPersistent.getState().model.equipment;
const nearPenetration = resolveNearPenetration(shell.penetration);
if (material.current) {
material.current.uniforms.thickness.value =
thickness * (equipment.enhancedArmor ? 1.04 : 1);
material.current.uniforms.penetration.value =
nearPenetration *
(equipment.calibratedShells ? (explosionCapable ? 1.1 : 1.05) : 1);
}
}
const unsubscribes = [
useTankopediaPersistent.subscribe(
(state) => state.model.equipment,
updateQuickEquipments,
),
];
return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
});
useFrame(({ gl, camera }) => { useFrame(({ gl, camera }) => {
if (material.current) { if (material.current) {
@@ -84,9 +103,17 @@ export function ArmorMesh({
maxThickness: { value: maxThickness }, maxThickness: { value: maxThickness },
isExplosive: { value: explosionCapable }, isExplosive: { value: explosionCapable },
canSplash: { value: canSplash(shell.type) }, canSplash: { value: canSplash(shell.type) },
thickness: { value: mutatedThickness }, thickness: {
value: thickness * (initialEquipment.enhancedArmor ? 1.04 : 1),
},
penetration: { penetration: {
value: penetration, value:
resolveNearPenetration(shell.penetration) *
(initialEquipment.calibratedShells
? explosionCapable
? 1.1
: 1.05
: 1),
}, },
caliber: { value: shell.caliber }, caliber: { value: shell.caliber },
ricochetAngle: { value: degToRad(shell.ricochet ?? 90) }, ricochetAngle: { value: degToRad(shell.ricochet ?? 90) },
+17
View File
@@ -0,0 +1,17 @@
import { produce } from 'immer';
import { create } from 'zustand';
import { DuelMember } from './tankopedia';
export interface Duel {
assigned: boolean;
protagonist?: DuelMember;
antagonist?: DuelMember;
}
export const useDuel = create<Duel>()(() => ({
assigned: false,
}));
export function mutateDuel(recipe: (draft: Duel) => void) {
useDuel.setState(produce(recipe));
}