feat: start of dynamic armor

This commit is contained in:
TrèsAbhi committed 2024-08-28 21:16:25 -05:00
1 parent 86e369af19
commit cd0769675e
13 files changed
+300 -45

No files matched your search

+75
View File
@@ -46,6 +46,11 @@ import { DATA } from './constants';
import { Avatar } from './skillIcons';
import { TankParameters } from './tankIcons';
function ensureDynamicArmor(armor: ModelArmor) {
if (!armor.dynamic) armor.dynamic = { false: [], true: [] };
return armor as ModelArmor & Required<Pick<ModelArmor, 'dynamic'>>;
}
export interface BlitzStrings {
[key: string]: string;
}
@@ -210,6 +215,27 @@ interface VehicleDefinitions {
};
};
};
extras?: {
armorsStatesController?: {
state: [
{
id: 0;
armors: string;
type: 'default';
enabled: true;
factorsModifiers: '';
default: '';
},
{
id: 1;
armors: string;
type: 'default';
enabled: false;
factorsModifiers: '';
},
];
};
};
}
export interface VehicleDefinitionList {
[key: string]: {
@@ -1242,6 +1268,55 @@ export async function definitions() {
Object.values(tankDefinitions[tankId].tracks).forEach((track) => {
track.xp = trackXps.get(track.id);
});
if (tankDefinition.root.extras?.armorsStatesController) {
const [falsePlates, truePlates] =
tankDefinition.root.extras.armorsStatesController.state.map(
(state) => state.armors.split(' '),
);
function applyDynamicArmor(plates: string[], type: `${boolean}`) {
plates.forEach((plate) => {
const [name, armorIndexString] = plate.split('.');
const armorIndex = Number(armorIndexString.split('_')[1]);
if (name === 'hull') {
ensureDynamicArmor(modelDefinitions[tankId].armor).dynamic[
type
].push(armorIndex);
} else {
const [module, moduleIndexString] = name.split('_');
const moduleIndex = Number(moduleIndexString);
if (module === 'turret') {
const turret = Object.values(
modelDefinitions[tankId].turrets,
).find((turret) => turret.model === moduleIndex);
if (!turret) throw new Error('Turret not found');
ensureDynamicArmor(turret.armor).dynamic[type].push(
armorIndex,
);
} else if (module === 'gun') {
Object.values(modelDefinitions[tankId].turrets).forEach(
(turret) => {
const gun = Object.values(turret.guns).find(
(gun) => gun.model === moduleIndex,
);
if (!gun) throw new Error('Gun not found');
ensureDynamicArmor(gun.armor).dynamic[type].push(
armorIndex,
);
},
);
} else
throw new Error(`Unknown dynamic armor module ${module}`);
}
});
}
applyDynamicArmor(falsePlates, 'false');
applyDynamicArmor(truePlates, 'true');
}
}
}),
);
@@ -2,8 +2,8 @@ import { ChevronLeftIcon, MixIcon } from '@radix-ui/react-icons';
import { Box, Button, Flex, Heading } from '@radix-ui/themes';
import Link from 'next/link';
import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react';
import { ThicknessRange } from '../../../../../../components/Armor/components/StaticArmor';
import { classIcons } from '../../../../../../components/ClassIcon';
import { ThicknessRange } from '../../../../../../components/StaticArmor';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { tankDefinitions } from '../../../../../../core/blitzkit/tankDefinitions';
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
@@ -12,7 +12,7 @@ import { ShotDisplay } from '../../../../../../components/Armor/components/ShotD
import {
StaticArmor,
ThicknessRange,
} from '../../../../../../components/StaticArmor';
} from '../../../../../../components/Armor/components/StaticArmor';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
@@ -48,8 +48,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const display = TankopediaPersistent.use((state) => state.display);
const onScreen = useOnScreen(canvas);
console.log(onScreen);
useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() {
@@ -22,13 +22,15 @@ import {
Tooltip,
} from '@radix-ui/themes';
import { invalidate } from '@react-three/fiber';
import { RefObject, useEffect, useState } from 'react';
import { RefObject, use, useEffect, useState } from 'react';
import { ThicknessRange } from '../../../../../../../components/Armor/components/StaticArmor';
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../../../../components/SmallTankIcon';
import { ThicknessRange } from '../../../../../../../components/StaticArmor';
import { resolveNearPenetration } from '../../../../../../../core/blitz/resolveNearPenetration';
import { resolvePenetrationCoefficient } from '../../../../../../../core/blitz/resolvePenetrationCoefficient';
import { asset } from '../../../../../../../core/blitzkit/asset';
import { checkConsumableProvisionInclusivity } from '../../../../../../../core/blitzkit/checkConsumableProvisionInclusivity';
import { consumableDefinitions } from '../../../../../../../core/blitzkit/consumableDefinitions';
import { imgur } from '../../../../../../../core/blitzkit/imgur';
import { Pose, poseEvent } from '../../../../../../../core/blitzkit/pose';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../core/blitzkit/tankDefinitions/constants';
@@ -77,15 +79,29 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
(state) => state.model.visual.environment,
);
const developerMode = App.useDeferred(false, (state) => state.developerMode);
const protagonist = Duel.use((state) => state.protagonist.tank);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const protagonistGun = Duel.use((state) => state.protagonist.gun);
const protagonistConsumables = Duel.use(
(state) => state.protagonist.consumables,
);
const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell);
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
const antagonistTank = Duel.use((state) => state.antagonist.tank);
const hasCalibratedShells = useEquipment(103, true);
const hasDynamicArmor = protagonistConsumables.includes(73);
const [tab, setTab] = useState('search');
const mutateDuel = Duel.useMutation();
const hasEnhancedArmor = useEquipment(110);
const awaitedConsumableDefinitions = use(consumableDefinitions);
const consumablesList = Object.values(awaitedConsumableDefinitions).filter(
(consumable) =>
checkConsumableProvisionInclusivity(
consumable,
protagonistTank,
protagonistGun,
),
);
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
useEffect(() => setDisplay(displayRaw), [displayRaw]);
@@ -141,7 +157,6 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
).toFixed(0)}
mm
</Text>
{antagonistUniqueGuns.length > 1 && (
<Popover.Root>
<Popover.Trigger>
@@ -201,7 +216,6 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
</Popover.Content>
</Popover.Root>
)}
<Flex
direction="column"
style={{
@@ -303,6 +317,46 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
/>
</IconButton>
</Flex>
{consumablesList.some((consumable) => consumable.id === 73) && (
<IconButton
color={hasDynamicArmor ? undefined : 'gray'}
variant="soft"
size={{ initial: '2', sm: '3' }}
onClick={() => {
invalidate();
mutateDuel((draft) => {
if (draft.protagonist.consumables.includes(73)) {
draft.protagonist.consumables =
draft.protagonist.consumables.filter((id) => id !== 73);
} else {
if (
draft.protagonist.consumables.length ===
protagonistTank.consumables
) {
draft.protagonist.consumables[
protagonistTank.consumables - 1
] = 73;
} else {
draft.protagonist.consumables.push(73);
}
}
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
});
}}
>
<img
alt="Calibrated Shells"
src={asset('icons/consumables/73.webp')}
style={{
width: '50%',
height: '50%',
}}
/>
</IconButton>
)}
</Flex>
)}
@@ -323,7 +377,10 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
const anchor = document.createElement('a');
anchor.setAttribute('download', `${protagonist.name}.png`);
anchor.setAttribute(
'download',
`${protagonistTank.name}.png`,
);
anchor.setAttribute(
'href',
canvas.current.toDataURL('image/png'),
@@ -46,6 +46,8 @@ export const TankModel = memo(() => {
if (!isVisible) return null;
console.log(node.name);
function translateTexture(offset: number) {
const mesh = node.children[0] as Mesh;
const material = mesh?.material as MeshStandardMaterial;
@@ -1,6 +1,19 @@
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
import { Box, Button, Checkbox, Flex, Text } from '@radix-ui/themes';
import { ThicknessRange } from '../../../../../components/StaticArmor';
import {
Box,
Button,
Checkbox,
Flex,
IconButton,
Text,
} from '@radix-ui/themes';
import { invalidate } from '@react-three/fiber';
import { use } from 'react';
import { ThicknessRange } from '../../../../../components/Armor/components/StaticArmor';
import { asset } from '../../../../../core/blitzkit/asset';
import { checkConsumableProvisionInclusivity } from '../../../../../core/blitzkit/checkConsumableProvisionInclusivity';
import { consumableDefinitions } from '../../../../../core/blitzkit/consumableDefinitions';
import * as Duel from '../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../stores/tankopediaEphemeral';
import * as TankopediaPersistent from '../../../../../stores/tankopediaPersistent';
@@ -18,10 +31,26 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.model.visual.showPrimaryArmor,
);
const mutateDuel = Duel.useMutation();
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const protagonistGun = Duel.use((state) => state.protagonist.gun);
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const awaitedConsumableDefinitions = use(consumableDefinitions);
const protagonistConsumables = Duel.use(
(state) => state.protagonist.consumables,
);
const hasDynamicArmor = protagonistConsumables.includes(73);
const consumablesList = Object.values(awaitedConsumableDefinitions).filter(
(consumable) =>
checkConsumableProvisionInclusivity(
consumable,
protagonistTank,
protagonistGun,
),
);
return (
<Flex
@@ -169,6 +198,46 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
<Checkbox size="2" checked={editStatic} />
</Flex>
</Flex>
{consumablesList.some((consumable) => consumable.id === 73) && (
<IconButton
color={hasDynamicArmor ? undefined : 'gray'}
variant="soft"
size={{ initial: '2', sm: '3' }}
onClick={() => {
invalidate();
mutateDuel((draft) => {
if (draft.protagonist.consumables.includes(73)) {
draft.protagonist.consumables =
draft.protagonist.consumables.filter((id) => id !== 73);
} else {
if (
draft.protagonist.consumables.length ===
protagonistTank.consumables
) {
draft.protagonist.consumables[
protagonistTank.consumables - 1
] = 73;
} else {
draft.protagonist.consumables.push(73);
}
}
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
});
}}
>
<img
alt="Calibrated Shells"
src={asset('icons/consumables/73.webp')}
style={{
width: '50%',
height: '50%',
}}
/>
</IconButton>
)}
</Flex>
);
}
@@ -28,6 +28,10 @@ export const PrimaryArmorScene = memo(() => {
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
const turretOrigin = correctZYTuple(tankModelDefinition.turretOrigin);
const gunOrigin = correctZYTuple(turretModelDefinition.gunOrigin);
const protagonistConsumables = Duel.use(
(state) => state.protagonist.consumables,
);
const hasDynamicArmor = protagonistConsumables.includes(73);
useTankTransform(protagonist, turretContainer, gunContainer);
@@ -43,7 +47,14 @@ export const PrimaryArmorScene = memo(() => {
armorId,
);
if (!isVisible || spaced || thickness === undefined) return null;
if (
!isVisible ||
spaced ||
thickness === undefined ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
return (
<PrimaryArmorSceneComponent
@@ -67,7 +78,14 @@ export const PrimaryArmorScene = memo(() => {
armorId,
);
if (!isVisible || spaced || thickness === undefined) return null;
if (
!isVisible ||
spaced ||
thickness === undefined ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
return (
<group position={hullOrigin}>
@@ -94,7 +112,14 @@ export const PrimaryArmorScene = memo(() => {
armorId,
);
if (!isVisible || spaced || thickness === undefined) return null;
if (
!isVisible ||
spaced ||
thickness === undefined ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
return (
<group position={hullOrigin}>
@@ -45,6 +45,10 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
gunModelDefinition.mask === undefined
? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const protagonistConsumables = Duel.use(
(state) => state.protagonist.consumables,
);
const hasDynamicArmor = protagonistConsumables.includes(73);
useTankTransform(protagonist, turretContainer, gunContainer);
@@ -60,7 +64,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
armorId,
);
if (!isVisible || thickness === undefined) return null;
if (
!isVisible ||
thickness === undefined ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
return (
<SpacedArmorSceneComponent
@@ -105,7 +115,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
armorId,
);
if (!isVisible || thickness === undefined) return null;
if (
!isVisible ||
thickness === undefined ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
return (
<group position={hullOrigin}>
@@ -134,7 +150,13 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
armorId,
);
if (!isVisible || thickness === undefined) return null;
if (
!isVisible ||
thickness === undefined ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
return (
<group position={hullOrigin}>
@@ -1,23 +1,23 @@
import { ThreeEvent, useThree } from '@react-three/fiber';
import { memo, useRef } from 'react';
import { Group, Plane, Vector2, Vector3 } from 'three';
import { applyPitchYawLimits } from '../../core/blitz/applyPitchYawLimits';
import { correctZYTuple } from '../../core/blitz/correctZYTuple';
import { hasEquipment } from '../../core/blitzkit/hasEquipment';
import { modelTransformEvent } from '../../core/blitzkit/modelTransform';
import { nameToArmorId } from '../../core/blitzkit/nameToArmorId';
import { resolveArmor } from '../../core/blitzkit/resolveThickness';
import { normalizeAngleRad } from '../../core/math/normalizeAngleRad';
import { useArmor } from '../../hooks/useArmor';
import { useModel } from '../../hooks/useModel';
import { useTankModelDefinition } from '../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../hooks/useTankTransform';
import * as Duel from '../../stores/duel';
import * as TankopediaEphemeral from '../../stores/tankopediaEphemeral';
import * as TankopediaPersistent from '../../stores/tankopediaPersistent';
import { ModelTankWrapper } from '../Armor/components/ModelTankWrapper';
import { ArmorType } from '../Armor/components/SpacedArmorScene';
import { StaticArmorSceneComponent } from '../Armor/components/StaticArmorSceneComponent';
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits';
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { normalizeAngleRad } from '../../../core/math/normalizeAngleRad';
import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import * as Duel from '../../../stores/duel';
import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral';
import * as TankopediaPersistent from '../../../stores/tankopediaPersistent';
import { ModelTankWrapper } from './ModelTankWrapper';
import { ArmorType } from './SpacedArmorScene';
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
interface ArmorSceneProps {
thicknessRange: ThicknessRange;
@@ -64,6 +64,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
const showExternalModules = TankopediaPersistent.use(
(state) => state.model.visual.showExternalModules,
);
const protagonistConsumables = Duel.use(
(state) => state.protagonist.consumables,
);
const hasDynamicArmor = protagonistConsumables.includes(73);
useTankTransform(protagonist, turretContainer, gunContainer);
@@ -83,7 +87,9 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
!isVisible ||
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced)
(!showSpacedArmor && spaced) ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
@@ -138,7 +144,9 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
!isVisible ||
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced)
(!showSpacedArmor && spaced) ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
) {
return null;
}
@@ -231,10 +239,11 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
!isVisible ||
thickness === undefined ||
(!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced)
) {
(!showSpacedArmor && spaced) ||
(hasDynamicArmor && node.name.includes('state_01')) ||
(!hasDynamicArmor && node.name.includes('state_00'))
)
return null;
}
const position = new Vector2();
const delta = new Vector2();
@@ -23,12 +23,12 @@ import {
} from '../../../core/blitzkit/modelTransform';
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
import * as TankopediaEphemeral from '../../../stores/tankopediaEphemeral';
import { ThicknessRange } from '../../StaticArmor';
import { ArmorType } from './SpacedArmorScene';
import {
ArmorUserData,
ExternalModuleVariant,
} from './SpacedArmorSceneComponent';
import { ThicknessRange } from './StaticArmor';
type StaticArmorSceneComponentProps = {
name: string;
+1 -1
View File
@@ -26,7 +26,7 @@ export function ConsumablesManager({
}: ConsumablesManagerProps) {
return (
<Flex wrap="wrap" gap="2" {...props}>
{consumables.map((consumable, index) => {
{consumables.map((consumable) => {
const isSelected = selected.some((id) => id === consumable.id);
return (
+1
View File
@@ -7,6 +7,7 @@ export type ModelDefinitions = Record<number, ModelDefinition>;
export interface ModelArmor {
thickness: Record<number, number>;
spaced?: number[];
// dynamic?: Record<`${boolean}`, number[]>;
}
export interface BoundingBox {
-3
View File
@@ -1,3 +0,0 @@
export function useConsumable(id: number, consumables: number[]) {
return consumables.includes(id);
}