diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 61b24dc17..bf33bbe1d 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -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>; +} + 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'); + } } }), ); diff --git a/src/app/tools/tankopedia/[id]/components/Model/HeroSection.tsx b/src/app/tools/tankopedia/[id]/components/Model/HeroSection.tsx index e371ad648..fa96f803e 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/HeroSection.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/HeroSection.tsx @@ -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'; diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index 99eccfa2c..9883c309b 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -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( const display = TankopediaPersistent.use((state) => state.display); const onScreen = useOnScreen(canvas); - console.log(onScreen); - useImperativeHandle(ref, () => canvas.current!, []); function handlePointerDown() { diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx index 150d56949..e07e81dfd 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/Options.tsx @@ -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 - {antagonistUniqueGuns.length > 1 && ( @@ -201,7 +216,6 @@ export function Options({ thicknessRange, canvas }: OptionsProps) { )} - + + {consumablesList.some((consumable) => consumable.id === 73) && ( + { + 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; + }); + }} + > + Calibrated Shells + + )} )} @@ -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'), diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx index 583852a1f..73cf2280f 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -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; diff --git a/src/app/tools/tankopedia/[id]/components/Thicknesses.tsx b/src/app/tools/tankopedia/[id]/components/Thicknesses.tsx index 6c15ea62c..26345f09c 100644 --- a/src/app/tools/tankopedia/[id]/components/Thicknesses.tsx +++ b/src/app/tools/tankopedia/[id]/components/Thicknesses.tsx @@ -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 ( + + {consumablesList.some((consumable) => consumable.id === 73) && ( + { + 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; + }); + }} + > + Calibrated Shells + + )} ); } diff --git a/src/components/Armor/components/PrimaryArmorScene.tsx b/src/components/Armor/components/PrimaryArmorScene.tsx index 5ab77f777..7b2dc2160 100644 --- a/src/components/Armor/components/PrimaryArmorScene.tsx +++ b/src/components/Armor/components/PrimaryArmorScene.tsx @@ -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 ( { 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 ( @@ -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 ( diff --git a/src/components/Armor/components/SpacedArmorScene.tsx b/src/components/Armor/components/SpacedArmorScene.tsx index 3720c48b2..d432d92d4 100644 --- a/src/components/Armor/components/SpacedArmorScene.tsx +++ b/src/components/Armor/components/SpacedArmorScene.tsx @@ -45,6 +45,10 @@ export const SpacedArmorScene = memo(({ 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(({ 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 ( (({ 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 ( @@ -134,7 +150,13 @@ export const SpacedArmorScene = memo(({ 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 ( diff --git a/src/components/StaticArmor/index.tsx b/src/components/Armor/components/StaticArmor.tsx similarity index 88% rename from src/components/StaticArmor/index.tsx rename to src/components/Armor/components/StaticArmor.tsx index 0dab33d50..61da9c600 100644 --- a/src/components/StaticArmor/index.tsx +++ b/src/components/Armor/components/StaticArmor.tsx @@ -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(({ 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(({ 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(({ 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(({ 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(); diff --git a/src/components/Armor/components/StaticArmorSceneComponent.tsx b/src/components/Armor/components/StaticArmorSceneComponent.tsx index 3eb8dbecc..054d3272a 100644 --- a/src/components/Armor/components/StaticArmorSceneComponent.tsx +++ b/src/components/Armor/components/StaticArmorSceneComponent.tsx @@ -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; diff --git a/src/components/ConsumablesManager.tsx b/src/components/ConsumablesManager.tsx index 1448b1c31..14bf5534d 100644 --- a/src/components/ConsumablesManager.tsx +++ b/src/components/ConsumablesManager.tsx @@ -26,7 +26,7 @@ export function ConsumablesManager({ }: ConsumablesManagerProps) { return ( - {consumables.map((consumable, index) => { + {consumables.map((consumable) => { const isSelected = selected.some((id) => id === consumable.id); return ( diff --git a/src/core/blitzkit/modelDefinitions.ts b/src/core/blitzkit/modelDefinitions.ts index 201b525bc..91572527b 100644 --- a/src/core/blitzkit/modelDefinitions.ts +++ b/src/core/blitzkit/modelDefinitions.ts @@ -7,6 +7,7 @@ export type ModelDefinitions = Record; export interface ModelArmor { thickness: Record; spaced?: number[]; + // dynamic?: Record<`${boolean}`, number[]>; } export interface BoundingBox { diff --git a/src/hooks/useConsumable.ts b/src/hooks/useConsumable.ts deleted file mode 100644 index e66dbf4cd..000000000 --- a/src/hooks/useConsumable.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function useConsumable(id: number, consumables: number[]) { - return consumables.includes(id); -}