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