mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: isolated duel
This commit is contained in:
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>
|
||||
|
||||
+7
-13
@@ -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}
|
||||
|
||||
+8
-12
@@ -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,
|
||||
|
||||
+8
-12
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in new issue
Block a user