From bd412cdaa25e0aee6365d1628a00928c920eff21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:16:12 -0500 Subject: [PATCH] feat: generalized tank stats --- .../[id]/components/Characteristics/index.tsx | 390 ++++----------- .../tankopedia/[id]/components/Control.tsx | 7 +- .../components/Model/components/TankModel.tsx | 14 +- .../CoreArmorSceneComponent/index.tsx | 21 +- .../SpacedArmorSubExternal/index.tsx | 21 +- .../components/SpacedArmorSubSpaced/index.tsx | 14 +- .../SpacedArmorSceneComponent/index.tsx | 13 +- src/core/blitzkrieg/hasEquipment.ts | 11 +- src/hooks/useConsumable.ts | 5 +- src/hooks/useProvisions.ts | 9 - src/hooks/useTankCharacteristics.ts | 459 ++++++++++++++++++ 11 files changed, 633 insertions(+), 331 deletions(-) delete mode 100644 src/hooks/useProvisions.ts create mode 100644 src/hooks/useTankCharacteristics.ts diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx index 64ddf7135..83ee39d11 100644 --- a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx @@ -14,25 +14,17 @@ import { lerp } from 'three/src/math/MathUtils'; import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { isExplosive } from '../../../../../../core/blitz/isExplosive'; -import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; import { coefficient } from '../../../../../../core/blitzkrieg/coefficient'; -import { degressiveStat } from '../../../../../../core/blitzkrieg/degressiveStat'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; -import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox'; -import { progressiveStat } from '../../../../../../core/blitzkrieg/progressiveStat'; -import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm'; -import { sum } from '../../../../../../core/blitzkrieg/sum'; +import { provisionDefinitions } from '../../../../../../core/blitzkrieg/provisionDefinitions'; import { CREW_MEMBER_NAMES, GUN_TYPE_NAMES, SHELL_NAMES, } from '../../../../../../core/blitzkrieg/tankDefinitions/constants'; -import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox'; -import { useConsumable } from '../../../../../../hooks/useConsumable'; import { useEquipment } from '../../../../../../hooks/useEquipment'; import { useFullScreen } from '../../../../../../hooks/useFullScreen'; -import { useProvision } from '../../../../../../hooks/useProvision'; -import { useProvisions } from '../../../../../../hooks/useProvisions'; +import { useTankCharacteristics } from '../../../../../../hooks/useTankCharacteristics'; import { mutateDuel, useDuel } from '../../../../../../stores/duel'; import { useTankopediaTemporary } from '../../../../../../stores/tankopedia'; import { Info } from './components/Info'; @@ -40,22 +32,31 @@ import { InfoWithDelta } from './components/InfoWithDelta'; export function Characteristics() { const awaitedModelDefinitions = use(modelDefinitions); + const awaitedProvisionDefinitions = use(provisionDefinitions); const crewSkills = useTankopediaTemporary((state) => state.skills); - const { tank, turret, gun, engine, track, shell } = useDuel( - (state) => state.protagonist!, - ); + const penetrationDistanceInput = useRef(null); const isFullScreen = useFullScreen(); const hasImprovedVentilation = useEquipment(102); - const provisions = useProvisions(); const crewMastery = useDuel((state) => state.protagonist!.crewMastery); + const [penetrationDistance, setPenetrationDistance] = useState(250); + + const provisions = useDuel((state) => state.protagonist!.provisions); const provisionCrewBonus = provisions.reduce( (total, provision) => - provision.crew ? total + provision.crew / 100 : total, + awaitedProvisionDefinitions[provision].crew + ? total + awaitedProvisionDefinitions[provision].crew! / 100 + : total, 0, ) + (hasImprovedVentilation ? 0.05 : 0); const commanderMastery = crewMastery + provisionCrewBonus; const crewMemberMastery = commanderMastery * 1.1; + const consumables = useDuel((state) => state.protagonist!.consumables); + const camouflage = useDuel((state) => state.protagonist!.camouflage); + const equipmentMatrix = useDuel((state) => state.protagonist!.equipment); + const { tank, turret, gun, engine, track, shell } = useDuel( + (state) => state.protagonist!, + ); const stockEngine = tank.engines[0]; const stockTrack = tank.tracks[0]; const stockTurret = tank.turrets[0]; @@ -63,187 +64,36 @@ export function Characteristics() { const tankModelDefinition = awaitedModelDefinitions[tank.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id]; - const size = normalizeBoundingBox( - unionBoundingBox( - tankModelDefinition.boundingBox, - turretModelDefinition.boundingBox, - ), - ); - const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0; - const weight = - tank.weight + engine.weight + track.weight + turret.weight + gun.weight; - const weightTons = weight / 1000; - const stockWeight = - tank.weight + - stockEngine.weight + - stockTrack.weight + - stockTurret.weight + - stockGun.weight; - const [penetrationDistance, setPenetrationDistance] = useState(250); - const camouflage = useDuel((state) => state.protagonist!.camouflage); - const hasRammer = useEquipment(100); - const hasCalibratedShells = useEquipment(103); - const hasEnhancedGunLayingDrive = useEquipment(104); - const hasSupercharger = useEquipment(107); - const hasVerticalStabilizer = useEquipment(105); - const hasRefinedGun = useEquipment(106); - const hasImprovedAssembly = useEquipment(111); - const hasImprovedOptics = useEquipment(114); - const hasImprovedControl = useEquipment(116); - const hasEngineAccelerator = useEquipment(117); - const hasCamouflageNet = useEquipment(115); - const hasImprovedVerticalStabilizer = useEquipment(122); - const hasImprovedSuspension = useEquipment(123); - const penetrationDistanceInput = useRef(null); - const hasEnginePowerBoost = useConsumable(14); - const hasImprovedEnginePowerBoost = useConsumable(27); - const hasAdrenaline = useConsumable(18); - const hasTungsten = useConsumable(45); - const hasReticleCalibration = useConsumable(28); - const hasShellReloadBoost = useConsumable(29); - const hasStandardFuel = useProvision(18); - const hasImprovedFuel = useProvision(19); - const hasProtectiveKit = useProvision(22); - const hasSandbagArmor = useProvision(28); - const hasEnhancedSandbagArmor = useProvision(29); - const hasImprovedGunPowder = useProvision(46); - const hasGearOil = useProvision(44); - const hasImprovedGearOil = useProvision(45); + const stats = useTankCharacteristics({ + tank, + camouflage, + consumables, + crewMastery, + crewSkills, + engine, + equipmentMatrix, + gun, + provisions, + shell, + turret, + track, + stockEngine, + stockGun, + stockTrack, + stockTurret, + }); - const healthCoefficient = coefficient( - [hasSandbagArmor, 0.03], - [hasEnhancedSandbagArmor, 0.06], - [hasImprovedAssembly, 0.06], - ); - const reloadCoefficient = - (coefficient([hasRammer, -0.07]) * - coefficient([true, degressiveStat(crewMemberMastery)])) / - coefficient([hasAdrenaline && gun.type === 'regular', 0.2]); + const hasSupercharger = useEquipment(107); + const hasCalibratedShells = useEquipment(103); + const hasImprovedVerticalStabilizer = useEquipment(122); const penetrationCoefficient = coefficient([ hasCalibratedShells, isExplosive(shell.type) ? 0.1 : 0.05, ]); - const damageCoefficient = coefficient( - // TODO: add max rolls - [hasTungsten, 0.15], - ); - const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]); - const shellVelocityCoefficient = coefficient( - [hasSupercharger, 0.3], - [hasImprovedGunPowder, 0.3], - ); const penetrationLossOverDistanceCoefficient = coefficient([ hasSupercharger, -0.5, ]); - const aimTimeCoefficient = - coefficient( - [hasEnhancedGunLayingDrive, -0.1], - [hasReticleCalibration, -0.4], - ) * coefficient([true, degressiveStat(crewMemberMastery)]); - const dispersionStillCoefficient = - coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) * - coefficient([true, degressiveStat(crewMemberMastery)]); - const dispersionDamagedCoefficient = coefficient([ - hasVerticalStabilizer, - -0.15, - ]); - const dispersionHullTraverseCoefficient = coefficient( - [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_turn / 100], - ); - const dispersionAfterShotCoefficient = coefficient( - [hasVerticalStabilizer, -0.15], - [true, -crewSkills.soft_recoil / 100], - ); - const dispersionMovingCoefficient = coefficient( - [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_driving / 100], - ); - const dispersionTurretTraverseCoefficient = coefficient( - [hasVerticalStabilizer, -0.15], - [true, -crewSkills.smooth_turret * (2 / 100)], - ); - const enginePowerCoefficient = coefficient( - [ - hasEngineAccelerator && - (tank.class === 'lightTank' || tank.class === 'mediumTank'), - 0.05, - ], - [ - hasEngineAccelerator && - (tank.class === 'heavyTank' || tank.class === 'AT-SPG'), - 0.07, - ], - [hasEnginePowerBoost, 0.2], - [hasImprovedEnginePowerBoost, 0.4], - [hasStandardFuel, 0.03], - [hasImprovedFuel, 0.1], - [hasGearOil, 0.03], - [hasImprovedGearOil, 0.06], - ); - const turretTraverseCoefficient = - coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) * - coefficient([true, progressiveStat(crewMemberMastery)]); - const hullTraverseCoefficient = - coefficient( - [hasImprovedControl, 0.1], - [hasImprovedEnginePowerBoost, 0.05], - [true, crewSkills.virtuoso / 100], - ) * coefficient([true, progressiveStat(crewMemberMastery)]); - const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]); - const viewRangeCoefficient = - coefficient([ - hasImprovedOptics, - tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1, - ]) * coefficient([true, progressiveStat(commanderMastery)]); - const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); - - const speedForwardsSum = sum( - [hasImprovedEnginePowerBoost, 5], - [hasGearOil, 2], - [hasImprovedGearOil, 4], - ); - const speedBackwardsSum = sum( - [hasImprovedEnginePowerBoost, 10], - [hasGearOil, 2], - [hasImprovedGearOil, 4], - ); - const camouflageSumMoving = sum( - [ - hasCamouflageNet, - tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05, - ], - [ - camouflage, - tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02, - ], - ); - const camouflageSumStill = sum( - [ - hasCamouflageNet, - 2 * - (tank.class === 'heavyTank' - ? 0.03 - : tank.class === 'AT-SPG' - ? 0.07 - : 0.05), - ], - [ - camouflage, - tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02, - ], - [true, crewSkills.camouflage * (3 / 100)], - ); - - const resolvedEnginePower = engine.power * enginePowerCoefficient; - const dpm = resolveDpm( - gun, - shell, - damageCoefficient, - reloadCoefficient, - intraClipCoefficient, - ); useEffect(() => { if (penetrationDistanceInput.current) { @@ -294,19 +144,12 @@ export function Characteristics() { {GUN_TYPE_NAMES[gun.type]} - {dpm} + {stats.dpm} {gun.type === 'autoReloader' && ( <> - {gun.reload.at(-1)! < gun.reload.at(-2)! - ? ((damageCoefficient * shell.damage.armor) / - (gun.reload.at(-1)! * reloadCoefficient + - gun.intraClip * intraClipCoefficient)) * - 60 - : ((damageCoefficient * shell.damage.armor) / - (gun.reload[0] * reloadCoefficient)) * - 60} + {stats.dpmMaximum!} - {gun.reload.at(-1)! < gun.reload.at(-2)! - ? ((damageCoefficient * shell.damage.armor) / - (gun.reload.at(-1)! * reloadCoefficient + - gun.intraClip * intraClipCoefficient)) * - (60 - - (gun.reload.slice(0, -1).length - 1) * - gun.intraClip * - intraClipCoefficient) + - damageCoefficient * - shell.damage.armor * - gun.reload.slice(0, -1).length - : ((damageCoefficient * shell.damage.armor) / - (gun.reload[0] * reloadCoefficient + - gun.intraClip * intraClipCoefficient)) * - (60 - - (gun.reload.slice(1).length - 1) * - gun.intraClip * - intraClipCoefficient) + - damageCoefficient * - shell.damage.armor * - gun.reload.slice(1).length} + {stats.dpmEffective!} )} + {gun.type !== 'regular' && ( + {stats.shells} + )} + {gun.type === 'autoReloader' ? ( <> - {gun.reload.length} - {gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1} + {stats.mostOptimalShellIndex} - {gun.reload.map((reload, index) => ( + {stats.shellReloads!.map((reload, index) => ( - {reload * reloadCoefficient} + {reload} ))} @@ -375,7 +201,7 @@ export function Characteristics() { unit="s" deltaType="lowerIsBetter" > - {gun.reload * reloadCoefficient} + {stats.shellReload!} )} {gun.type === 'autoLoader' || @@ -387,14 +213,14 @@ export function Characteristics() { unit="s" deltaType="lowerIsBetter" > - {gun.intraClip * intraClipCoefficient} + {stats.intraClip!} ))} - {shell.caliber} + {stats.caliber} - {resolveNearPenetration(shell.penetration) * penetrationCoefficient} + {stats.penetration} {typeof shell.penetration !== 'number' && ( <> @@ -446,18 +272,18 @@ export function Characteristics() { )} - {shell.damage.armor * damageCoefficient} + {stats.damage} - {shell.damage.module * damageCoefficient} + {stats.moduleDamage} {isExplosive(shell.type) && ( - {shell.explosionRadius!} + {stats.explosionRadius!} )} - {shell.speed * shellVelocityCoefficient} + {stats.shellVelocity} - {gun.aimTime * aimTimeCoefficient} + {stats.aimTime} - {gun.dispersion.base * dispersionStillCoefficient} + {stats.dispersion} - {track.dispersion.move * dispersionMovingCoefficient} + {stats.dispersionMoving} - {track.dispersion.traverse * dispersionHullTraverseCoefficient} + {stats.dispersionHullTraversing} - {gun.dispersion.traverse * dispersionTurretTraverseCoefficient} + {stats.dispersionTurretTraversing} - {gun.dispersion.shot * dispersionAfterShotCoefficient} + {stats.dispersionShooting} - {gun.dispersion.damaged * dispersionDamagedCoefficient} + {stats.dispersionGunDamaged} - {gunModelDefinition.pitch.max + - gunDefaultPitch + - (hasImprovedVerticalStabilizer ? 3 : 0)} + {stats.gunDepression} - {-gunModelDefinition.pitch.min - - gunDefaultPitch + - (hasImprovedVerticalStabilizer ? 3 : 0)} + {stats.gunElevation} {gunModelDefinition.pitch.front && ( <> - {gunModelDefinition.pitch.front.max} + {stats.gunFrontalDepression!} - {-gunModelDefinition.pitch.front.min} + {stats.gunFrontalElevation!} )} {gunModelDefinition.pitch.back && ( <> - {gunModelDefinition.pitch.back.max} + {stats.gunRearDepression!} - {-gunModelDefinition.pitch.back.min} + {stats.gunRearElevation!} )} {turretModelDefinition.yaw && ( <> - {-turretModelDefinition.yaw.min} + {stats.azimuthLeft!} - {turretModelDefinition.yaw.max} + {stats.azimuthRight!} )} @@ -570,29 +392,23 @@ export function Characteristics() { Maneuverability - {tank.speed.forwards + speedForwardsSum} + {stats.speedForwards} - {tank.speed.backwards + speedBackwardsSum} + {stats.speedBackwards} - {resolvedEnginePower} + {stats.enginePower} - {resolvedEnginePower / - weightTons / - (track.resistance.hard * resistanceCoefficient)} + {stats.powerToWeightRatioHardTerrain} - {resolvedEnginePower / - weightTons / - (track.resistance.medium * resistanceCoefficient)} + {stats.powerToWeightRatioMediumTerrain} - {resolvedEnginePower / - weightTons / - (track.resistance.soft * resistanceCoefficient)} + {stats.powerToWeightRatioSoftTerrain} - {weightTons} + {stats.weight} - {turret.traverseSpeed * turretTraverseCoefficient} + {stats.turretTraverseSpeed} - {(resolvedEnginePower / stockEngine.power) * - track.traverseSpeed * - hullTraverseCoefficient * - (track.resistance.hard / - (track.resistance.hard * resistanceCoefficient)) * - (stockWeight / weight)} + {stats.hullTraverseHardTerrain} - {(resolvedEnginePower / stockEngine.power) * - track.traverseSpeed * - hullTraverseCoefficient * - (track.resistance.hard / - (track.resistance.medium * resistanceCoefficient)) * - (stockWeight / weight)} + {stats.hullTraverseMediumTerrain} - {(resolvedEnginePower / stockEngine.power) * - track.traverseSpeed * - hullTraverseCoefficient * - (track.resistance.hard / - (track.resistance.soft * resistanceCoefficient)) * - (stockWeight / weight)} + {stats.hullTraverseSoftTerrain} Survivability - {(tank.health + turret.health) * healthCoefficient} + {stats.health} - - {Math.round(engine.fireChance * fireChanceCoefficient * 100)} + + {stats.fireChance * 100} - {turret.viewRange * viewRangeCoefficient} + {stats.viewRange} - {(tank.camouflage.still + camouflageSumStill) * 100} + {stats.camouflageStill * 100} - {(tank.camouflage.moving + camouflageSumMoving) * 100} + {stats.camouflageMoving * 100} - {(tank.camouflage.still * gun.camouflageLoss + camouflageSumStill) * - 100} + {stats.camouflageShootingStill * 100} - {(tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving) * - 100} + {stats.camouflageShootingMoving * 100} - {(tank.camouflage.onFire * tank.camouflage.still + - camouflageSumStill) * - 100} + {stats.camouflageCaughtOnFire * 100} - {size[0].toFixed(2)} x {size[2].toFixed(2)} x {size[1].toFixed(2)} + {stats.tankSize.map((component) => component.toFixed(2)).join(' x ')} diff --git a/src/app/tools/tankopedia/[id]/components/Control.tsx b/src/app/tools/tankopedia/[id]/components/Control.tsx index 956c9815f..919be6fc9 100644 --- a/src/app/tools/tankopedia/[id]/components/Control.tsx +++ b/src/app/tools/tankopedia/[id]/components/Control.tsx @@ -69,7 +69,12 @@ export function Controls() { ); async function handlePoseEvent(event: Pose) { - const hasImprovedVerticalStabilizer = await hasEquipment(122); + const duel = useDuel.getState(); + const hasImprovedVerticalStabilizer = await hasEquipment( + 122, + duel.protagonist!.tank.equipment, + duel.protagonist!.equipment, + ); switch (event) { case Pose.HullDown: { 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 ac88d0f86..24e2e193e 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx @@ -162,7 +162,12 @@ export const TankModel = memo(() => { } } async function handlePointerMove(event: PointerEvent) { - const hasImprovedVerticalStabilizer = await hasEquipment(122); + const duel = useDuel.getState(); + const hasImprovedVerticalStabilizer = await hasEquipment( + 122, + duel.protagonist!.tank.equipment, + duel.protagonist!.equipment, + ); [pitch, yaw] = applyPitchYawLimits( pitch, yaw + event.movementX * (Math.PI / canvas.width), @@ -223,7 +228,12 @@ export const TankModel = memo(() => { window.addEventListener('pointerup', handlePointerUp); } async function handlePointerMove(event: PointerEvent) { - const hasImprovedVerticalStabilizer = await hasEquipment(122); + const duel = useDuel.getState(); + const hasImprovedVerticalStabilizer = await hasEquipment( + 122, + duel.protagonist!.tank.equipment, + duel.protagonist!.equipment, + ); [pitch, yaw] = applyPitchYawLimits( pitch - event.movementY * (Math.PI / canvas.height), yaw + event.movementX * (Math.PI / canvas.width), diff --git a/src/components/Armor/components/CoreArmorSceneComponent/index.tsx b/src/components/Armor/components/CoreArmorSceneComponent/index.tsx index 8ef484fb0..28bafad51 100644 --- a/src/components/Armor/components/CoreArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/CoreArmorSceneComponent/index.tsx @@ -84,18 +84,29 @@ export function CoreArmorSceneComponent({ async function handleProtagonistEquipmentChange( equipment: EquipmentMatrix, ) { - const hasEnhancedArmor = await hasEquipment(110, false, equipment); + const duel = useDuel.getState(); + const hasEnhancedArmor = await hasEquipment( + 110, + duel.protagonist!.tank.equipment, + equipment, + ); material.uniforms.thickness.value = hasEnhancedArmor ? thickness * 1.04 : thickness; } async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { - const shell = useDuel.getState().antagonist!.shell; - const penetration = resolveNearPenetration(shell.penetration); - const hasCalibratedShells = await hasEquipment(103, true, equipment); + const duel = useDuel.getState(); + const penetration = resolveNearPenetration( + duel.antagonist!.shell.penetration, + ); + const hasCalibratedShells = await hasEquipment( + 103, + duel.antagonist!.tank.equipment, + equipment, + ); material.uniforms.penetration.value = hasCalibratedShells - ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) + ? penetration * (isExplosive(duel.antagonist!.shell.type) ? 1.1 : 1.05) : penetration; } diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx index 4145225bb..81d4c510b 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx @@ -50,17 +50,28 @@ export function SpacedArmorSubExternal({ async function handleProtagonistEquipmentChange( equipment: EquipmentMatrix, ) { - const hasEnhancedArmor = await hasEquipment(110, false, equipment); + const duel = useDuel.getState(); + const hasEnhancedArmor = await hasEquipment( + 110, + duel.protagonist!.tank.equipment, + equipment, + ); material.uniforms.thickness.value = hasEnhancedArmor ? thickness * 1.04 : thickness; } async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { - const shell = useDuel.getState().antagonist!.shell; - const penetration = resolveNearPenetration(shell.penetration); - const hasCalibratedShells = await hasEquipment(103, true, equipment); + const duel = useDuel.getState(); + const penetration = resolveNearPenetration( + duel.antagonist!.shell.penetration, + ); + const hasCalibratedShells = await hasEquipment( + 103, + duel.antagonist!.tank.equipment, + equipment, + ); material.uniforms.penetration.value = hasCalibratedShells - ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) + ? penetration * (isExplosive(duel.antagonist!.shell.type) ? 1.1 : 1.05) : penetration; } diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx index 241648e9b..0563515e8 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx @@ -62,15 +62,25 @@ export function SpacedArmorSubSpaced({ async function handleProtagonistEquipmentChange( equipment: EquipmentMatrix, ) { - const hasEnhancedArmor = await hasEquipment(110, false, equipment); + const duel = useDuel.getState(); + const hasEnhancedArmor = await hasEquipment( + 110, + duel.protagonist!.tank.equipment, + equipment, + ); material.uniforms.thickness.value = hasEnhancedArmor ? thickness * 1.04 : thickness; } async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { + const duel = useDuel.getState(); const shell = useDuel.getState().antagonist!.shell; const penetration = resolveNearPenetration(shell.penetration); - const hasCalibratedShells = await hasEquipment(103, true, equipment); + const hasCalibratedShells = await hasEquipment( + 103, + duel.antagonist!.tank.equipment, + equipment, + ); material.uniforms.penetration.value = hasCalibratedShells ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) : penetration; diff --git a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 227857266..385e14d93 100644 --- a/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -89,8 +89,17 @@ export function SpacedArmorSceneComponent({ async onClick(event) { event.stopPropagation(); - const hasCalibratedShells = await hasEquipment(103, true); - const hasEnhancedArmor = await hasEquipment(110); + const duel = useDuel.getState(); + const hasCalibratedShells = await hasEquipment( + 103, + duel.antagonist!.tank.equipment, + duel.antagonist!.equipment, + ); + const hasEnhancedArmor = await hasEquipment( + 110, + duel.protagonist!.tank.equipment, + duel.protagonist!.equipment, + ); const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1; const shell = useDuel.getState().antagonist!.shell; const explosive = isExplosive(shell.type); diff --git a/src/core/blitzkrieg/hasEquipment.ts b/src/core/blitzkrieg/hasEquipment.ts index d0ddf713f..c3a73f9ff 100644 --- a/src/core/blitzkrieg/hasEquipment.ts +++ b/src/core/blitzkrieg/hasEquipment.ts @@ -1,16 +1,13 @@ -import { useDuel } from '../../stores/duel'; +import { EquipmentMatrix } from '../../stores/duel'; import { equipmentDefinitions } from './equipmentDefinitions'; export async function hasEquipment( id: number, - antagonist = false, - equipmentMatrix = useDuel.getState()[ - antagonist ? 'antagonist' : 'protagonist' - ]!.equipment, + equipmentPreset: string, + equipmentMatrix: EquipmentMatrix, ) { const awaitedEquipmentDefinitions = await equipmentDefinitions; - const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!; - const presetRows = awaitedEquipmentDefinitions.presets[member.tank.equipment]; + const presetRows = awaitedEquipmentDefinitions.presets[equipmentPreset]; return presetRows.some((equipmentRow, rowIndex) => { return equipmentRow.some((equipment, columnIndex) => { const choice = equipmentMatrix[rowIndex][columnIndex]; diff --git a/src/hooks/useConsumable.ts b/src/hooks/useConsumable.ts index 758cb60ec..e66dbf4cd 100644 --- a/src/hooks/useConsumable.ts +++ b/src/hooks/useConsumable.ts @@ -1,6 +1,3 @@ -import { useDuel } from '../stores/duel'; - -export function useConsumable(id: number) { - const consumables = useDuel((state) => state.protagonist!.consumables); +export function useConsumable(id: number, consumables: number[]) { return consumables.includes(id); } diff --git a/src/hooks/useProvisions.ts b/src/hooks/useProvisions.ts deleted file mode 100644 index 3ebcb4c97..000000000 --- a/src/hooks/useProvisions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { use } from 'react'; -import { provisionDefinitions } from '../core/blitzkrieg/provisionDefinitions'; -import { useDuel } from '../stores/duel'; - -export function useProvisions() { - const provisions = useDuel((state) => state.protagonist!.provisions); - const awaitedProvisionDefinitions = use(provisionDefinitions); - return provisions.map((id) => awaitedProvisionDefinitions[id]); -} diff --git a/src/hooks/useTankCharacteristics.ts b/src/hooks/useTankCharacteristics.ts new file mode 100644 index 000000000..c9c750681 --- /dev/null +++ b/src/hooks/useTankCharacteristics.ts @@ -0,0 +1,459 @@ +import { use } from 'react'; +import { isExplosive } from '../core/blitz/isExplosive'; +import { resolveNearPenetration } from '../core/blitz/resolveNearPenetration'; +import { coefficient } from '../core/blitzkrieg/coefficient'; +import { degressiveStat } from '../core/blitzkrieg/degressiveStat'; +import { equipmentDefinitions } from '../core/blitzkrieg/equipmentDefinitions'; +import { modelDefinitions } from '../core/blitzkrieg/modelDefinitions'; +import { normalizeBoundingBox } from '../core/blitzkrieg/normalizeBoundingBox'; +import { progressiveStat } from '../core/blitzkrieg/progressiveStat'; +import { provisionDefinitions } from '../core/blitzkrieg/provisionDefinitions'; +import { resolveDpm } from '../core/blitzkrieg/resolveDpm'; +import { sum } from '../core/blitzkrieg/sum'; +import { + EngineDefinition, + GunDefinition, + ShellDefinition, + TankDefinition, + TrackDefinition, + TurretDefinition, +} from '../core/blitzkrieg/tankDefinitions'; +import { unionBoundingBox } from '../core/blitzkrieg/unionBoundingBox'; +import { EquipmentMatrix } from '../stores/duel'; + +export function useTankCharacteristics({ + tank, + turret, + gun, + shell, + consumables, + equipmentMatrix, + crewMastery, + provisions, + engine, + crewSkills, + camouflage, + track, + stockEngine, + stockTrack, + stockTurret, + stockGun, +}: { + tank: TankDefinition; + turret: TurretDefinition; + gun: GunDefinition; + shell: ShellDefinition; + track: TrackDefinition; + engine: EngineDefinition; + stockEngine: EngineDefinition; + stockTrack: TrackDefinition; + stockTurret: TurretDefinition; + stockGun: GunDefinition; + consumables: number[]; + equipmentMatrix: EquipmentMatrix; + crewMastery: number; + provisions: number[]; + crewSkills: Record; + camouflage: boolean; +}) { + const awaitedModelDefinitions = use(modelDefinitions); + const awaitedEquipmentDefinitions = use(equipmentDefinitions); + const awaitedProvisionDefinitions = use(provisionDefinitions); + const presetRows = awaitedEquipmentDefinitions.presets[tank.equipment]; + const tankModelDefinition = awaitedModelDefinitions[tank.id]; + const turretModelDefinition = tankModelDefinition.turrets[turret.id]; + const gunModelDefinition = turretModelDefinition.guns[gun.id]; + + function equipment(id: number) { + return presetRows.some((equipmentRow, rowIndex) => { + return equipmentRow.some((equipment, columnIndex) => { + const choice = equipmentMatrix[rowIndex][columnIndex]; + if (choice === 0) return false; + const equipped = equipment[choice === -1 ? 0 : 1]; + return equipped === id; + }); + }); + } + function consumable(id: number) { + return consumables.includes(id); + } + function provision(id: number) { + return provisions.includes(id); + } + + const hasRammer = equipment(100); + const hasImprovedVentilation = equipment(102); + const hasCalibratedShells = equipment(103); + const hasEnhancedGunLayingDrive = equipment(104); + const hasSupercharger = equipment(107); + const hasVerticalStabilizer = equipment(105); + const hasRefinedGun = equipment(106); + const hasImprovedAssembly = equipment(111); + const hasImprovedOptics = equipment(114); + const hasImprovedControl = equipment(116); + const hasEngineAccelerator = equipment(117); + const hasCamouflageNet = equipment(115); + const hasImprovedVerticalStabilizer = equipment(122); + const hasImprovedSuspension = equipment(123); + + const hasTungsten = consumable(45); + const hasAdrenaline = consumable(18); + const hasShellReloadBoost = consumable(29); + const hasEnginePowerBoost = consumable(14); + const hasImprovedEnginePowerBoost = consumable(27); + const hasReticleCalibration = consumable(28); + + const hasStandardFuel = provision(18); + const hasImprovedFuel = provision(19); + const hasProtectiveKit = provision(22); + const hasSandbagArmor = provision(28); + const hasEnhancedSandbagArmor = provision(29); + const hasImprovedGunPowder = provision(46); + const hasGearOil = provision(44); + const hasImprovedGearOil = provision(45); + + const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0; + const provisionCrewBonus = + provisions.reduce( + (total, provision) => + awaitedProvisionDefinitions[provision].crew + ? total + awaitedProvisionDefinitions[provision].crew! / 100 + : total, + 0, + ) + (hasImprovedVentilation ? 0.05 : 0); + const commanderMastery = crewMastery + provisionCrewBonus; + const crewMemberMastery = commanderMastery * 1.1; + + const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]); + const damageCoefficient = coefficient([hasTungsten, 0.15]); + const reloadCoefficient = + (coefficient([hasRammer, -0.07]) * + coefficient([true, degressiveStat(crewMemberMastery)])) / + coefficient([hasAdrenaline && gun.type === 'regular', 0.2]); + const penetrationCoefficient = coefficient([ + hasCalibratedShells, + isExplosive(shell.type) ? 0.1 : 0.05, + ]); + const healthCoefficient = coefficient( + [hasSandbagArmor, 0.03], + [hasEnhancedSandbagArmor, 0.06], + [hasImprovedAssembly, 0.06], + ); + const shellVelocityCoefficient = coefficient( + [hasSupercharger, 0.3], + [hasImprovedGunPowder, 0.3], + ); + const penetrationLossOverDistanceCoefficient = coefficient([ + hasSupercharger, + -0.5, + ]); + const aimTimeCoefficient = + coefficient( + [hasEnhancedGunLayingDrive, -0.1], + [hasReticleCalibration, -0.4], + ) * coefficient([true, degressiveStat(crewMemberMastery)]); + const dispersionStillCoefficient = + coefficient([hasRefinedGun, -0.1], [hasReticleCalibration, -0.4]) * + coefficient([true, degressiveStat(crewMemberMastery)]); + const dispersionDamagedCoefficient = coefficient([ + hasVerticalStabilizer, + -0.15, + ]); + const dispersionHullTraverseCoefficient = coefficient( + [hasVerticalStabilizer, -0.15], + [true, -crewSkills.smooth_turn / 100], + ); + const dispersionAfterShotCoefficient = coefficient( + [hasVerticalStabilizer, -0.15], + [true, -crewSkills.soft_recoil / 100], + ); + const dispersionMovingCoefficient = coefficient( + [hasVerticalStabilizer, -0.15], + [true, -crewSkills.smooth_driving / 100], + ); + const dispersionTurretTraverseCoefficient = coefficient( + [hasVerticalStabilizer, -0.15], + [true, -crewSkills.smooth_turret * (2 / 100)], + ); + const enginePowerCoefficient = coefficient( + [ + hasEngineAccelerator && + (tank.class === 'lightTank' || tank.class === 'mediumTank'), + 0.05, + ], + [ + hasEngineAccelerator && + (tank.class === 'heavyTank' || tank.class === 'AT-SPG'), + 0.07, + ], + [hasEnginePowerBoost, 0.2], + [hasImprovedEnginePowerBoost, 0.4], + [hasStandardFuel, 0.03], + [hasImprovedFuel, 0.1], + [hasGearOil, 0.03], + [hasImprovedGearOil, 0.06], + ); + const turretTraverseCoefficient = + coefficient([hasStandardFuel, 0.03], [hasImprovedFuel, 0.1]) * + coefficient([true, progressiveStat(crewMemberMastery)]); + const hullTraverseCoefficient = + coefficient( + [hasImprovedControl, 0.1], + [hasImprovedEnginePowerBoost, 0.05], + [true, crewSkills.virtuoso / 100], + ) * coefficient([true, progressiveStat(crewMemberMastery)]); + const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]); + const viewRangeCoefficient = + coefficient([ + hasImprovedOptics, + tank.class === 'AT-SPG' ? 0.05 : tank.class === 'heavyTank' ? 0.07 : 0.1, + ]) * coefficient([true, progressiveStat(commanderMastery)]); + const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); + + const speedForwardsSum = sum( + [hasImprovedEnginePowerBoost, 5], + [hasGearOil, 2], + [hasImprovedGearOil, 4], + ); + const speedBackwardsSum = sum( + [hasImprovedEnginePowerBoost, 10], + [hasGearOil, 2], + [hasImprovedGearOil, 4], + ); + const camouflageSumMoving = sum( + [ + hasCamouflageNet, + tank.class === 'heavyTank' ? 0.03 : tank.class === 'AT-SPG' ? 0.07 : 0.05, + ], + [ + camouflage, + tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02, + ], + ); + const camouflageSumStill = sum( + [ + hasCamouflageNet, + 2 * + (tank.class === 'heavyTank' + ? 0.03 + : tank.class === 'AT-SPG' + ? 0.07 + : 0.05), + ], + [ + camouflage, + tank.class === 'AT-SPG' ? 0.04 : tank.class === 'heavyTank' ? 0.03 : 0.02, + ], + [true, crewSkills.camouflage * (3 / 100)], + ); + + const size = normalizeBoundingBox( + unionBoundingBox( + tankModelDefinition.boundingBox, + turretModelDefinition.boundingBox, + ), + ); + const weightKg = + tank.weight + engine.weight + track.weight + turret.weight + gun.weight; + const weightTons = weightKg / 1000; + const stockWeight = + tank.weight + + stockEngine.weight + + stockTrack.weight + + stockTurret.weight + + stockGun.weight; + const resolvedEnginePower = engine.power * enginePowerCoefficient; + const dpm = resolveDpm( + gun, + shell, + damageCoefficient, + reloadCoefficient, + intraClipCoefficient, + ); + const dpmMaximum = + gun.type === 'autoReloader' + ? gun.reload.at(-1)! < gun.reload.at(-2)! + ? ((damageCoefficient * shell.damage.armor) / + (gun.reload.at(-1)! * reloadCoefficient + + gun.intraClip * intraClipCoefficient)) * + 60 + : ((damageCoefficient * shell.damage.armor) / + (gun.reload[0] * reloadCoefficient)) * + 60 + : undefined; + const dpmEffective = + gun.type === 'autoReloader' + ? gun.reload.at(-1)! < gun.reload.at(-2)! + ? ((damageCoefficient * shell.damage.armor) / + (gun.reload.at(-1)! * reloadCoefficient + + gun.intraClip * intraClipCoefficient)) * + (60 - + (gun.reload.slice(0, -1).length - 1) * + gun.intraClip * + intraClipCoefficient) + + damageCoefficient * + shell.damage.armor * + gun.reload.slice(0, -1).length + : ((damageCoefficient * shell.damage.armor) / + (gun.reload[0] * reloadCoefficient + + gun.intraClip * intraClipCoefficient)) * + (60 - + (gun.reload.slice(1).length - 1) * + gun.intraClip * + intraClipCoefficient) + + damageCoefficient * shell.damage.armor * gun.reload.slice(1).length + : undefined; + const shells = gun.type === 'regular' ? 1 : gun.shells.length; + const mostOptimalShellIndex = + gun.type === 'autoReloader' + ? gun.reload.at(-1)! < gun.reload.at(-2)! + ? gun.reload.length + : 1 + : undefined; + const shellReloads = + gun.type === 'autoReloader' + ? gun.reload.map((reload) => reload * reloadCoefficient) + : undefined; + const shellReload = + gun.type === 'autoReloader' ? undefined : gun.reload * reloadCoefficient; + const intraClip = + gun.type === 'regular' ? undefined : gun.intraClip * intraClipCoefficient; + const caliber = shell.caliber; + const penetration = + resolveNearPenetration(shell.penetration) * penetrationCoefficient; + const damage = shell.damage.armor * damageCoefficient; + const moduleDamage = shell.damage.module * damageCoefficient; + const explosionRadius = shell.explosionRadius; + const shellVelocity = shell.speed * shellVelocityCoefficient; + const aimTime = gun.aimTime * aimTimeCoefficient; + const dispersion = gun.dispersion.base * dispersionStillCoefficient; + const dispersionMoving = track.dispersion.move * dispersionMovingCoefficient; + const dispersionHullTraversing = + track.dispersion.traverse * dispersionHullTraverseCoefficient; + const dispersionTurretTraversing = + gun.dispersion.traverse * dispersionTurretTraverseCoefficient; + const dispersionShooting = + gun.dispersion.shot * dispersionAfterShotCoefficient; + const dispersionGunDamaged = + gun.dispersion.damaged * dispersionDamagedCoefficient; + const gunDepression = + gunModelDefinition.pitch.max + + gunDefaultPitch + + (hasImprovedVerticalStabilizer ? 3 : 0); + const gunElevation = + -gunModelDefinition.pitch.min - + gunDefaultPitch + + (hasImprovedVerticalStabilizer ? 3 : 0); + const gunFrontalDepression = gunModelDefinition.pitch.front?.max; + const gunFrontalElevation = gunModelDefinition.pitch.front + ? -gunModelDefinition.pitch.front.min + : undefined; + const gunRearDepression = gunModelDefinition.pitch.back?.max; + const gunRearElevation = gunModelDefinition.pitch.back + ? -gunModelDefinition.pitch.back.min + : undefined; + const azimuthLeft = turretModelDefinition.yaw + ? -turretModelDefinition.yaw.min + : undefined; + const azimuthRight = turretModelDefinition.yaw?.max; + const speedForwards = tank.speed.forwards + speedForwardsSum; + const speedBackwards = tank.speed.backwards + speedBackwardsSum; + const enginePower = resolvedEnginePower; + const powerToWeightRatioHardTerrain = + resolvedEnginePower / + weightTons / + (track.resistance.hard * resistanceCoefficient); + const powerToWeightRatioMediumTerrain = + resolvedEnginePower / + weightTons / + (track.resistance.medium * resistanceCoefficient); + const powerToWeightRatioSoftTerrain = + resolvedEnginePower / + weightTons / + (track.resistance.soft * resistanceCoefficient); + const weight = weightTons; + const turretTraverseSpeed = turret.traverseSpeed * turretTraverseCoefficient; + const hullTraverseHardTerrain = + (resolvedEnginePower / stockEngine.power) * + track.traverseSpeed * + hullTraverseCoefficient * + (track.resistance.hard / (track.resistance.hard * resistanceCoefficient)) * + (stockWeight / weightKg); + const hullTraverseMediumTerrain = + (resolvedEnginePower / stockEngine.power) * + track.traverseSpeed * + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.medium * resistanceCoefficient)) * + (stockWeight / weightKg); + const hullTraverseSoftTerrain = + (resolvedEnginePower / stockEngine.power) * + track.traverseSpeed * + hullTraverseCoefficient * + (track.resistance.hard / (track.resistance.soft * resistanceCoefficient)) * + (stockWeight / weightKg); + const health = (tank.health + turret.health) * healthCoefficient; + const fireChance = engine.fireChance * fireChanceCoefficient; + const viewRange = turret.viewRange * viewRangeCoefficient; + const camouflageStill = tank.camouflage.still + camouflageSumStill; + const camouflageMoving = tank.camouflage.moving + camouflageSumMoving; + const camouflageShootingStill = + tank.camouflage.still * gun.camouflageLoss + camouflageSumStill; + const camouflageShootingMoving = + tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving; + const camouflageCaughtOnFire = + tank.camouflage.onFire * tank.camouflage.still + camouflageSumStill; + const tankSize = [size[0], size[2], size[1]]; + + return { + dpm, + dpmMaximum, + dpmEffective, + shells, + mostOptimalShellIndex, + shellReloads, + shellReload, + intraClip, + caliber, + penetration, + damage, + moduleDamage, + explosionRadius, + shellVelocity, + aimTime, + dispersion, + dispersionMoving, + dispersionHullTraversing, + dispersionTurretTraversing, + dispersionShooting, + dispersionGunDamaged, + gunDepression, + gunElevation, + gunFrontalDepression, + gunFrontalElevation, + gunRearDepression, + gunRearElevation, + azimuthLeft, + azimuthRight, + speedForwards, + speedBackwards, + enginePower, + powerToWeightRatioHardTerrain, + powerToWeightRatioMediumTerrain, + powerToWeightRatioSoftTerrain, + weight, + turretTraverseSpeed, + hullTraverseHardTerrain, + hullTraverseMediumTerrain, + hullTraverseSoftTerrain, + health, + fireChance, + viewRange, + camouflageStill, + camouflageMoving, + camouflageShootingStill, + camouflageShootingMoving, + camouflageCaughtOnFire, + tankSize, + }; +}