diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 99b8d1098..93687ee11 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -10,8 +10,8 @@ import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { ConsumableDefinitions } from '../../src/core/blitzkrieg/consumablesDefinitions'; import { EquipmentDefinitions, + EquipmentPreset, EquipmentRow, - EquipmentRows, } from '../../src/core/blitzkrieg/equipmentDefinitions'; import { ModelArmor, @@ -618,7 +618,7 @@ export async function buildDefinitions(production: boolean) { optionalDevices.root[options.device1].id, ]; }) as EquipmentRow, - ) as EquipmentRows; + ) as EquipmentPreset; }, ); diff --git a/src/app/tools/tankopedia/[id]/components/Control.tsx b/src/app/tools/tankopedia/[id]/components/Control.tsx index b869d6073..349a595b6 100644 --- a/src/app/tools/tankopedia/[id]/components/Control.tsx +++ b/src/app/tools/tankopedia/[id]/components/Control.tsx @@ -1,12 +1,12 @@ import { OrbitControls } from '@react-three/drei'; import { useThree } from '@react-three/fiber'; import { useEffect, useRef } from 'react'; -import usePromise from 'react-promise-suspense'; import { Vector3 } from 'three'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; import { modelDefinitions } from '../../../../../core/blitzkrieg/modelDefinitions'; import { Pose, poseEvent } from '../../../../../core/blitzkrieg/pose'; +import { useAwait } from '../../../../../hooks/useAwait'; import { useTankopediaTemporary } from '../../../../../stores/tankopedia'; import { Duel } from '../page'; @@ -23,10 +23,7 @@ interface ControlsProps { export function Controls({ duel }: ControlsProps) { const camera = useThree((state) => state.camera); const orbitControls = useRef(null); - const awaitedModelDefinitions = usePromise( - async () => await modelDefinitions, - [], - ); + const awaitedModelDefinitions = useAwait(modelDefinitions); const protagonistModelDefinition = awaitedModelDefinitions[duel.protagonist.tank.id]; const antagonistModelDefinition = diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx index 6c61607f3..a246bfbee 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankSandbox.tsx @@ -16,6 +16,7 @@ import { Loader } from '../../../../components/Loader'; import { Duel } from '../../page'; import { Controls } from '../Control'; import { Lighting } from '../Lighting'; +import { QuickEquipments } from '../QuickEquipments'; import { RotationInputs } from '../RotationInputs'; import { SceneProps } from '../SceneProps'; import { Options } from './components/Options'; @@ -69,8 +70,8 @@ export function TankSandbox({ duel }: TankSandboxProps) { ); modelTransformEvent.emit({ pitch, yaw }); - draft.model.physical.pitch = pitch; - draft.model.physical.yaw = yaw; + draft.model.pose.pitch = pitch; + draft.model.pose.yaw = yaw; }); break; @@ -86,8 +87,8 @@ export function TankSandbox({ duel }: TankSandboxProps) { ); modelTransformEvent.emit({ pitch, yaw }); - draft.model.physical.pitch = pitch; - draft.model.physical.yaw = yaw; + draft.model.pose.pitch = pitch; + draft.model.pose.yaw = yaw; }); break; @@ -111,13 +112,12 @@ export function TankSandbox({ duel }: TankSandboxProps) { if (!duel.protagonist) return void 0; mutateTankopediaTemporary((draft) => { - [draft.model.physical.pitch, draft.model.physical.yaw] = - applyPitchYawLimits( - draft.model.physical.pitch, - draft.model.physical.yaw, - gunModelDefinition.pitch, - turretModelDefinition.yaw, - ); + [draft.model.pose.pitch, draft.model.pose.yaw] = applyPitchYawLimits( + draft.model.pose.pitch, + draft.model.pose.yaw, + gunModelDefinition.pitch, + turretModelDefinition.yaw, + ); }); }, [duel.protagonist.gun, duel.protagonist.turret]); @@ -183,6 +183,7 @@ export function TankSandbox({ duel }: TankSandboxProps) { + diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx index 229240c39..d8608ddfb 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ArmorHighlighting.tsx @@ -86,7 +86,7 @@ export function ArmorHighlighting({ duel }: ArmorHighlightingProps) { turretContainer.current?.rotation.copy(turretRotation); } - handleModelTransform(useTankopediaTemporary.getState().model.physical); + handleModelTransform(useTankopediaTemporary.getState().model.pose); modelTransformEvent.on(handleModelTransform); return () => { diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx index 0bd8b95a7..db2267d49 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/ExternalModuleMask.tsx @@ -94,7 +94,7 @@ export const ExternalModuleMask = memo(({ duel }) => { turretContainer.current?.rotation.copy(turretRotation); } - handleModelTransform(useTankopediaTemporary.getState().model.physical); + handleModelTransform(useTankopediaTemporary.getState().model.pose); modelTransformEvent.on(handleModelTransform); return () => { diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx index f554da5e7..aec7a74b2 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor/components/SpacedArmorDepth.tsx @@ -100,7 +100,7 @@ export const SpacedArmorDepth = memo(({ duel }) => { turretContainer.current?.rotation.copy(turretRotation); } - handleModelTransform(useTankopediaTemporary.getState().model.physical); + handleModelTransform(useTankopediaTemporary.getState().model.pose); modelTransformEvent.on(handleModelTransform); return () => { 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 a25ca4c9e..01ffcd21d 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -38,7 +38,7 @@ export const TankModel = memo(({ duel: { protagonist } }: TankModelProps) => { >(undefined); const canvas = useThree((state) => state.gl.domElement); - const physical = useTankopediaTemporary((state) => state.model.physical); + const physical = useTankopediaTemporary((state) => state.model.pose); const hullContainer = useRef(null); const turretContainer = useRef(null); const gunContainer = useRef(null); @@ -220,8 +220,8 @@ export const TankModel = memo(({ duel: { protagonist } }: TankModelProps) => { function handlePointerUp() { mutateTankopediaTemporary((state) => { state.model.visual.controlsEnabled = true; - state.model.physical.pitch = normalizeAnglePI(pitch); - state.model.physical.yaw = normalizeAnglePI(yaw); + state.model.pose.pitch = normalizeAnglePI(pitch); + state.model.pose.yaw = normalizeAnglePI(yaw); }); window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); @@ -283,8 +283,8 @@ export const TankModel = memo(({ duel: { protagonist } }: TankModelProps) => { function handlePointerUp() { mutateTankopediaTemporary((state) => { state.model.visual.controlsEnabled = true; - state.model.physical.pitch = normalizeAnglePI(pitch); - state.model.physical.yaw = normalizeAnglePI(yaw); + state.model.pose.pitch = normalizeAnglePI(pitch); + state.model.pose.yaw = normalizeAnglePI(yaw); }); window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); diff --git a/src/app/tools/tankopedia/[id]/components/QuickEquipments.tsx b/src/app/tools/tankopedia/[id]/components/QuickEquipments.tsx new file mode 100644 index 000000000..52b1fe972 --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/QuickEquipments.tsx @@ -0,0 +1,50 @@ +import { Flex } from '@radix-ui/themes'; +import { ModuleButton } from '../../../../../components/ModuleButton'; +import { + mutateTankopediaTemporary, + useTankopediaTemporary, +} from '../../../../../stores/tankopedia'; + +export function QuickEquipments() { + const equipment = useTankopediaTemporary((state) => state.model.equipment); + + return ( + + { + mutateTankopediaTemporary((draft) => { + draft.model.equipment.calibratedShells = + !draft.model.equipment.calibratedShells; + }); + }} + /> + { + mutateTankopediaTemporary((draft) => { + draft.model.equipment.enhancedArmor = + !draft.model.equipment.enhancedArmor; + }); + }} + /> + + ); +} diff --git a/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx b/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx index 8a6bcfcfa..c3bc501f5 100644 --- a/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx +++ b/src/app/tools/tankopedia/[id]/components/RotationInputs.tsx @@ -14,7 +14,7 @@ interface RotationInputsProps { } export function RotationInputs({ duel: { protagonist } }: RotationInputsProps) { - const physical = useTankopediaTemporary((state) => state.model.physical); + const physical = useTankopediaTemporary((state) => state.model.pose); const awaitedModelDefinitions = use(modelDefinitions); const turretYawInput = useRef(null); const gunPitchInput = useRef(null); @@ -69,8 +69,8 @@ export function RotationInputs({ duel: { protagonist } }: RotationInputsProps) { ); modelTransformEvent.emit({ pitch, yaw }); mutateTankopediaTemporary((state) => { - state.model.physical.pitch = pitch; - state.model.physical.yaw = yaw; + state.model.pose.pitch = pitch; + state.model.pose.yaw = yaw; }); turretYawInput.current!.value = `${Math.round( yaw * (180 / Math.PI), @@ -105,8 +105,8 @@ export function RotationInputs({ duel: { protagonist } }: RotationInputsProps) { ); modelTransformEvent.emit({ pitch, yaw }); mutateTankopediaTemporary((state) => { - state.model.physical.pitch = pitch; - state.model.physical.yaw = yaw; + state.model.pose.pitch = pitch; + state.model.pose.yaw = yaw; }); gunPitchInput.current!.value = `${Math.round( pitch * (180 / Math.PI), diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index 80f79acd3..5de60a15e 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -39,8 +39,8 @@ export default function Page({ params }: { params: { id: string } }) { useEffect(() => { mutateTankopediaTemporary((draft) => { - draft.model.physical.yaw = 0; - draft.model.physical.pitch = 0; + draft.model.pose.yaw = 0; + draft.model.pose.pitch = 0; draft.mode = 'model'; }); diff --git a/src/components/ArmorMesh/components/Armor/index.tsx b/src/components/ArmorMesh/components/Armor/index.tsx index b194c32cd..cbfdc5804 100644 --- a/src/components/ArmorMesh/components/Armor/index.tsx +++ b/src/components/ArmorMesh/components/Armor/index.tsx @@ -25,6 +25,7 @@ export function ArmorMesh({ duel, ...props }: ArmorMeshProps) { + const equipment = useTankopediaTemporary((draft) => draft.model.equipment); const camera = useThree((state) => state.camera); const { shell } = duel.antagonist; const greenPenetration = useTankopediaTemporary( @@ -32,6 +33,12 @@ export function ArmorMesh({ ); const material = useRef(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; useFrame(({ gl, camera }) => { if (material.current) { @@ -75,11 +82,11 @@ export function ArmorMesh({ zFar: { value: camera.far }, greenPenetration: { value: greenPenetration }, maxThickness: { value: maxThickness }, - isExplosive: { value: isExplosive(shell.type) }, + isExplosive: { value: explosionCapable }, canSplash: { value: canSplash(shell.type) }, - thickness: { value: thickness }, + thickness: { value: mutatedThickness }, penetration: { - value: resolveNearPenetration(shell.penetration), + value: penetration, }, caliber: { value: shell.caliber }, ricochetAngle: { value: degToRad(shell.ricochet ?? 90) }, diff --git a/src/core/blitzkrieg/equipmentDefinitions.ts b/src/core/blitzkrieg/equipmentDefinitions.ts index 2a574d22c..4de4f8ff7 100644 --- a/src/core/blitzkrieg/equipmentDefinitions.ts +++ b/src/core/blitzkrieg/equipmentDefinitions.ts @@ -8,11 +8,11 @@ export type EquipmentRow = [ EquipmentOptions, ]; -export type EquipmentRows = [EquipmentRow, EquipmentRow, EquipmentRow]; +export type EquipmentPreset = [EquipmentRow, EquipmentRow, EquipmentRow]; export interface EquipmentDefinitions { presets: { - [key: string]: EquipmentRows; + [key: string]: EquipmentPreset; }; equipments: { [key: number]: { diff --git a/src/hooks/useAppliedEquipments.ts b/src/hooks/useAppliedEquipments.ts new file mode 100644 index 000000000..a65059b76 --- /dev/null +++ b/src/hooks/useAppliedEquipments.ts @@ -0,0 +1,10 @@ +import { EquipmentPreset } from '../core/blitzkrieg/equipmentDefinitions'; +import { EquipmentMatrix } from '../stores/tankopedia'; + +export function useAppliedEquipments( + matrix: EquipmentMatrix, + preset: EquipmentPreset, +) { + const flatMatrix = matrix.flat(); + return preset.flat().map((options, index) => options[flatMatrix[index]]); +} diff --git a/src/hooks/useAwait.ts b/src/hooks/useAwait.ts new file mode 100644 index 000000000..f0d9b2145 --- /dev/null +++ b/src/hooks/useAwait.ts @@ -0,0 +1,8 @@ +import usePromise from 'react-promise-suspense'; + +/** + * A more stable alternative to `use` using `usePromise`. + */ +export function useAwait(promise: Promise) { + return usePromise(async () => await promise, []); +} diff --git a/src/hooks/useEquipmentPreset.ts b/src/hooks/useEquipmentPreset.ts new file mode 100644 index 000000000..73ef7d6c2 --- /dev/null +++ b/src/hooks/useEquipmentPreset.ts @@ -0,0 +1,8 @@ +import { equipmentDefinitions } from '../core/blitzkrieg/equipmentDefinitions'; +import { useAwait } from './useAwait'; + +export function useEquipmentPreset(preset: string) { + const awaitedEquipmentDefinitions = useAwait(equipmentDefinitions); + console.log(awaitedEquipmentDefinitions); + return awaitedEquipmentDefinitions.presets[preset]; +} diff --git a/src/hooks/useModelDefinitions.ts b/src/hooks/useModelDefinitions.ts index 39a4509b5..8f6b8530f 100644 --- a/src/hooks/useModelDefinitions.ts +++ b/src/hooks/useModelDefinitions.ts @@ -1,6 +1,6 @@ -import usePromise from 'react-promise-suspense'; import { modelDefinitions } from '../core/blitzkrieg/modelDefinitions'; +import { useAwait } from './useAwait'; export function useModelDefinitions() { - return usePromise(async () => await modelDefinitions, []); + return useAwait(modelDefinitions); } diff --git a/src/stores/tankopedia.ts b/src/stores/tankopedia.ts index df37008f3..de73ad424 100644 --- a/src/stores/tankopedia.ts +++ b/src/stores/tankopedia.ts @@ -35,19 +35,25 @@ interface TankopediaPersistent { }; } +export type EquipmentMatrix = (0 | 1)[][]; + interface TankopediaTemporary { model: { - physical: { + pose: { yaw: number; pitch: number; }; + equipment: { + enhancedArmor: boolean; + calibratedShells: boolean; + }; visual: { controlsEnabled: boolean; showGrid: boolean; greenPenetration: boolean; }; }; - equipmentMatrix: (0 | 1)[][]; + equipmentMatrix: EquipmentMatrix; mode: TankopediaMode; } @@ -74,10 +80,14 @@ export const useTankopediaPersistent = create()( export const useTankopediaTemporary = create()( subscribeWithSelector(() => ({ model: { - physical: { + pose: { yaw: 0, pitch: 0, }, + equipment: { + enhancedArmor: false, + calibratedShells: false, + }, visual: { controlsEnabled: true, showGrid: true,