feat: generalized tank stats

This commit is contained in:
TrèsAbhi committed 2024-04-04 13:16:12 -05:00
1 parent 913411275b
commit bd412cdaa2
11 files changed
+633 -331

No files matched your search

@@ -14,25 +14,17 @@ import { lerp } from 'three/src/math/MathUtils';
import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton'; import { ShellButton } from '../../../../../../components/ModuleButtons/ShellButton';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { isExplosive } from '../../../../../../core/blitz/isExplosive';
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
import { coefficient } from '../../../../../../core/blitzkrieg/coefficient'; import { coefficient } from '../../../../../../core/blitzkrieg/coefficient';
import { degressiveStat } from '../../../../../../core/blitzkrieg/degressiveStat';
import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions';
import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox'; import { provisionDefinitions } from '../../../../../../core/blitzkrieg/provisionDefinitions';
import { progressiveStat } from '../../../../../../core/blitzkrieg/progressiveStat';
import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm';
import { sum } from '../../../../../../core/blitzkrieg/sum';
import { import {
CREW_MEMBER_NAMES, CREW_MEMBER_NAMES,
GUN_TYPE_NAMES, GUN_TYPE_NAMES,
SHELL_NAMES, SHELL_NAMES,
} from '../../../../../../core/blitzkrieg/tankDefinitions/constants'; } from '../../../../../../core/blitzkrieg/tankDefinitions/constants';
import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox';
import { useConsumable } from '../../../../../../hooks/useConsumable';
import { useEquipment } from '../../../../../../hooks/useEquipment'; import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../../hooks/useFullScreen'; import { useFullScreen } from '../../../../../../hooks/useFullScreen';
import { useProvision } from '../../../../../../hooks/useProvision'; import { useTankCharacteristics } from '../../../../../../hooks/useTankCharacteristics';
import { useProvisions } from '../../../../../../hooks/useProvisions';
import { mutateDuel, useDuel } from '../../../../../../stores/duel'; import { mutateDuel, useDuel } from '../../../../../../stores/duel';
import { useTankopediaTemporary } from '../../../../../../stores/tankopedia'; import { useTankopediaTemporary } from '../../../../../../stores/tankopedia';
import { Info } from './components/Info'; import { Info } from './components/Info';
@@ -40,22 +32,31 @@ import { InfoWithDelta } from './components/InfoWithDelta';
export function Characteristics() { export function Characteristics() {
const awaitedModelDefinitions = use(modelDefinitions); const awaitedModelDefinitions = use(modelDefinitions);
const awaitedProvisionDefinitions = use(provisionDefinitions);
const crewSkills = useTankopediaTemporary((state) => state.skills); const crewSkills = useTankopediaTemporary((state) => state.skills);
const { tank, turret, gun, engine, track, shell } = useDuel( const penetrationDistanceInput = useRef<HTMLInputElement>(null);
(state) => state.protagonist!,
);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const hasImprovedVentilation = useEquipment(102); const hasImprovedVentilation = useEquipment(102);
const provisions = useProvisions();
const crewMastery = useDuel((state) => state.protagonist!.crewMastery); const crewMastery = useDuel((state) => state.protagonist!.crewMastery);
const [penetrationDistance, setPenetrationDistance] = useState(250);
const provisions = useDuel((state) => state.protagonist!.provisions);
const provisionCrewBonus = const provisionCrewBonus =
provisions.reduce( provisions.reduce(
(total, provision) => (total, provision) =>
provision.crew ? total + provision.crew / 100 : total, awaitedProvisionDefinitions[provision].crew
? total + awaitedProvisionDefinitions[provision].crew! / 100
: total,
0, 0,
) + (hasImprovedVentilation ? 0.05 : 0); ) + (hasImprovedVentilation ? 0.05 : 0);
const commanderMastery = crewMastery + provisionCrewBonus; const commanderMastery = crewMastery + provisionCrewBonus;
const crewMemberMastery = commanderMastery * 1.1; 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 stockEngine = tank.engines[0];
const stockTrack = tank.tracks[0]; const stockTrack = tank.tracks[0];
const stockTurret = tank.turrets[0]; const stockTurret = tank.turrets[0];
@@ -63,187 +64,36 @@ export function Characteristics() {
const tankModelDefinition = awaitedModelDefinitions[tank.id]; const tankModelDefinition = awaitedModelDefinitions[tank.id];
const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const turretModelDefinition = tankModelDefinition.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id]; const gunModelDefinition = turretModelDefinition.guns[gun.id];
const size = normalizeBoundingBox( const stats = useTankCharacteristics({
unionBoundingBox( tank,
tankModelDefinition.boundingBox, camouflage,
turretModelDefinition.boundingBox, consumables,
), crewMastery,
); crewSkills,
const gunDefaultPitch = tankModelDefinition.turretRotation?.pitch ?? 0; engine,
const weight = equipmentMatrix,
tank.weight + engine.weight + track.weight + turret.weight + gun.weight; gun,
const weightTons = weight / 1000; provisions,
const stockWeight = shell,
tank.weight + turret,
stockEngine.weight + track,
stockTrack.weight + stockEngine,
stockTurret.weight + stockGun,
stockGun.weight; stockTrack,
const [penetrationDistance, setPenetrationDistance] = useState(250); stockTurret,
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<HTMLInputElement>(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 healthCoefficient = coefficient( const hasSupercharger = useEquipment(107);
[hasSandbagArmor, 0.03], const hasCalibratedShells = useEquipment(103);
[hasEnhancedSandbagArmor, 0.06], const hasImprovedVerticalStabilizer = useEquipment(122);
[hasImprovedAssembly, 0.06],
);
const reloadCoefficient =
(coefficient([hasRammer, -0.07]) *
coefficient([true, degressiveStat(crewMemberMastery)])) /
coefficient([hasAdrenaline && gun.type === 'regular', 0.2]);
const penetrationCoefficient = coefficient([ const penetrationCoefficient = coefficient([
hasCalibratedShells, hasCalibratedShells,
isExplosive(shell.type) ? 0.1 : 0.05, 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([ const penetrationLossOverDistanceCoefficient = coefficient([
hasSupercharger, hasSupercharger,
-0.5, -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(() => { useEffect(() => {
if (penetrationDistanceInput.current) { if (penetrationDistanceInput.current) {
@@ -294,19 +144,12 @@ export function Characteristics() {
<Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info> <Info name="Gun type">{GUN_TYPE_NAMES[gun.type]}</Info>
<InfoWithDelta name="DPM" decimals={0} unit="hp / min"> <InfoWithDelta name="DPM" decimals={0} unit="hp / min">
{dpm} {stats.dpm}
</InfoWithDelta> </InfoWithDelta>
{gun.type === 'autoReloader' && ( {gun.type === 'autoReloader' && (
<> <>
<InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min"> <InfoWithDelta decimals={0} indent name="Maximum" unit="hp / min">
{gun.reload.at(-1)! < gun.reload.at(-2)! {stats.dpmMaximum!}
? ((damageCoefficient * shell.damage.armor) /
(gun.reload.at(-1)! * reloadCoefficient +
gun.intraClip * intraClipCoefficient)) *
60
: ((damageCoefficient * shell.damage.armor) /
(gun.reload[0] * reloadCoefficient)) *
60}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={0} decimals={0}
@@ -314,27 +157,7 @@ export function Characteristics() {
name="Effective at 60s" name="Effective at 60s"
unit="hp / min" unit="hp / min"
> >
{gun.reload.at(-1)! < gun.reload.at(-2)! {stats.dpmEffective!}
? ((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}
</InfoWithDelta> </InfoWithDelta>
<Info <Info
indent indent
@@ -349,14 +172,17 @@ export function Characteristics() {
/> />
</> </>
)} )}
{gun.type !== 'regular' && (
<InfoWithDelta name="Shells">{stats.shells}</InfoWithDelta>
)}
{gun.type === 'autoReloader' ? ( {gun.type === 'autoReloader' ? (
<> <>
<InfoWithDelta name="Shells">{gun.reload.length}</InfoWithDelta>
<Info indent name="Most optimal"> <Info indent name="Most optimal">
{gun.reload.at(-1)! < gun.reload.at(-2)! ? gun.reload.length : 1} {stats.mostOptimalShellIndex}
</Info> </Info>
<Info name="Shell reloads" unit="s" /> <Info name="Shell reloads" unit="s" />
{gun.reload.map((reload, index) => ( {stats.shellReloads!.map((reload, index) => (
<InfoWithDelta <InfoWithDelta
key={index} key={index}
indent indent
@@ -364,7 +190,7 @@ export function Characteristics() {
decimals={2} decimals={2}
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{reload * reloadCoefficient} {reload}
</InfoWithDelta> </InfoWithDelta>
))} ))}
</> </>
@@ -375,7 +201,7 @@ export function Characteristics() {
unit="s" unit="s"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.reload * reloadCoefficient} {stats.shellReload!}
</InfoWithDelta> </InfoWithDelta>
)} )}
{gun.type === 'autoLoader' || {gun.type === 'autoLoader' ||
@@ -387,14 +213,14 @@ export function Characteristics() {
unit="s" unit="s"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.intraClip * intraClipCoefficient} {stats.intraClip!}
</InfoWithDelta> </InfoWithDelta>
))} ))}
<InfoWithDelta name="Caliber" decimals={0} unit="mm"> <InfoWithDelta name="Caliber" decimals={0} unit="mm">
{shell.caliber} {stats.caliber}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={0} name="Penetration" unit="mm"> <InfoWithDelta decimals={0} name="Penetration" unit="mm">
{resolveNearPenetration(shell.penetration) * penetrationCoefficient} {stats.penetration}
</InfoWithDelta> </InfoWithDelta>
{typeof shell.penetration !== 'number' && ( {typeof shell.penetration !== 'number' && (
<> <>
@@ -446,18 +272,18 @@ export function Characteristics() {
</> </>
)} )}
<InfoWithDelta name="Damage" unit="hp" decimals={0}> <InfoWithDelta name="Damage" unit="hp" decimals={0}>
{shell.damage.armor * damageCoefficient} {stats.damage}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta name="Module damage" unit="hp" decimals={0}> <InfoWithDelta name="Module damage" unit="hp" decimals={0}>
{shell.damage.module * damageCoefficient} {stats.moduleDamage}
</InfoWithDelta> </InfoWithDelta>
{isExplosive(shell.type) && ( {isExplosive(shell.type) && (
<InfoWithDelta name="Splash radius" unit="m" decimals={0}> <InfoWithDelta name="Splash radius" unit="m" decimals={0}>
{shell.explosionRadius!} {stats.explosionRadius!}
</InfoWithDelta> </InfoWithDelta>
)} )}
<InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}> <InfoWithDelta name="Shell velocity" unit="m/s" decimals={0}>
{shell.speed * shellVelocityCoefficient} {stats.shellVelocity}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={2} decimals={2}
@@ -465,7 +291,7 @@ export function Characteristics() {
unit="s" unit="s"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.aimTime * aimTimeCoefficient} {stats.aimTime}
</InfoWithDelta> </InfoWithDelta>
<Info name="Dispersion at 100m" /> <Info name="Dispersion at 100m" />
<InfoWithDelta <InfoWithDelta
@@ -475,7 +301,7 @@ export function Characteristics() {
unit="m" unit="m"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.dispersion.base * dispersionStillCoefficient} {stats.dispersion}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
prefix="+ " prefix="+ "
@@ -484,7 +310,7 @@ export function Characteristics() {
name="Moving" name="Moving"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{track.dispersion.move * dispersionMovingCoefficient} {stats.dispersionMoving}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={3} decimals={3}
@@ -493,7 +319,7 @@ export function Characteristics() {
name="Hull traversing" name="Hull traversing"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{track.dispersion.traverse * dispersionHullTraverseCoefficient} {stats.dispersionHullTraversing}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={3} decimals={3}
@@ -502,7 +328,7 @@ export function Characteristics() {
name="Turret traversing" name="Turret traversing"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.dispersion.traverse * dispersionTurretTraverseCoefficient} {stats.dispersionTurretTraversing}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={3} decimals={3}
@@ -511,7 +337,7 @@ export function Characteristics() {
name="After shooting" name="After shooting"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.dispersion.shot * dispersionAfterShotCoefficient} {stats.dispersionShooting}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={1} decimals={1}
@@ -521,46 +347,42 @@ export function Characteristics() {
unit="scalar" unit="scalar"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{gun.dispersion.damaged * dispersionDamagedCoefficient} {stats.dispersionGunDamaged}
</InfoWithDelta> </InfoWithDelta>
<Info name="Gun flexibility" unit="°" /> <Info name="Gun flexibility" unit="°" />
<InfoWithDelta decimals={1} indent name="Depression"> <InfoWithDelta decimals={1} indent name="Depression">
{gunModelDefinition.pitch.max + {stats.gunDepression}
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Elevation"> <InfoWithDelta decimals={1} indent name="Elevation">
{-gunModelDefinition.pitch.min - {stats.gunElevation}
gunDefaultPitch +
(hasImprovedVerticalStabilizer ? 3 : 0)}
</InfoWithDelta> </InfoWithDelta>
{gunModelDefinition.pitch.front && ( {gunModelDefinition.pitch.front && (
<> <>
<InfoWithDelta decimals={1} indent name="Frontal depression"> <InfoWithDelta decimals={1} indent name="Frontal depression">
{gunModelDefinition.pitch.front.max} {stats.gunFrontalDepression!}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Frontal elevation"> <InfoWithDelta decimals={1} indent name="Frontal elevation">
{-gunModelDefinition.pitch.front.min} {stats.gunFrontalElevation!}
</InfoWithDelta> </InfoWithDelta>
</> </>
)} )}
{gunModelDefinition.pitch.back && ( {gunModelDefinition.pitch.back && (
<> <>
<InfoWithDelta decimals={1} indent name="Rear depression"> <InfoWithDelta decimals={1} indent name="Rear depression">
{gunModelDefinition.pitch.back.max} {stats.gunRearDepression!}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Rear elevation"> <InfoWithDelta decimals={1} indent name="Rear elevation">
{-gunModelDefinition.pitch.back.min} {stats.gunRearElevation!}
</InfoWithDelta> </InfoWithDelta>
</> </>
)} )}
{turretModelDefinition.yaw && ( {turretModelDefinition.yaw && (
<> <>
<InfoWithDelta decimals={1} indent name="Azimuth left"> <InfoWithDelta decimals={1} indent name="Azimuth left">
{-turretModelDefinition.yaw.min} {stats.azimuthLeft!}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="Azimuth right"> <InfoWithDelta decimals={1} indent name="Azimuth right">
{turretModelDefinition.yaw.max} {stats.azimuthRight!}
</InfoWithDelta> </InfoWithDelta>
</> </>
)} )}
@@ -570,29 +392,23 @@ export function Characteristics() {
<Heading size="5">Maneuverability</Heading> <Heading size="5">Maneuverability</Heading>
<Info name="Speed" unit="km/hr" /> <Info name="Speed" unit="km/hr" />
<InfoWithDelta decimals={0} indent name="Forwards"> <InfoWithDelta decimals={0} indent name="Forwards">
{tank.speed.forwards + speedForwardsSum} {stats.speedForwards}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={0} indent name="Backwards"> <InfoWithDelta decimals={0} indent name="Backwards">
{tank.speed.backwards + speedBackwardsSum} {stats.speedBackwards}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={0} name="Power" unit="hp"> <InfoWithDelta decimals={0} name="Power" unit="hp">
{resolvedEnginePower} {stats.enginePower}
</InfoWithDelta> </InfoWithDelta>
<Info name="Power to weight ratio" unit="hp/tn" /> <Info name="Power to weight ratio" unit="hp/tn" />
<InfoWithDelta decimals={1} indent name="On hard terrain"> <InfoWithDelta decimals={1} indent name="On hard terrain">
{resolvedEnginePower / {stats.powerToWeightRatioHardTerrain}
weightTons /
(track.resistance.hard * resistanceCoefficient)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain"> <InfoWithDelta decimals={1} indent name="On medium terrain">
{resolvedEnginePower / {stats.powerToWeightRatioMediumTerrain}
weightTons /
(track.resistance.medium * resistanceCoefficient)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain"> <InfoWithDelta decimals={1} indent name="On soft terrain">
{resolvedEnginePower / {stats.powerToWeightRatioSoftTerrain}
weightTons /
(track.resistance.soft * resistanceCoefficient)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta <InfoWithDelta
decimals={1} decimals={1}
@@ -600,71 +416,57 @@ export function Characteristics() {
unit="tn" unit="tn"
deltaType="lowerIsBetter" deltaType="lowerIsBetter"
> >
{weightTons} {stats.weight}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={0}> <InfoWithDelta name="Turret traverse speed" unit="°/s" decimals={0}>
{turret.traverseSpeed * turretTraverseCoefficient} {stats.turretTraverseSpeed}
</InfoWithDelta> </InfoWithDelta>
<Info name="Effective traverse speed" unit="°/s" /> <Info name="Effective traverse speed" unit="°/s" />
<InfoWithDelta decimals={1} indent name="On hard terrain"> <InfoWithDelta decimals={1} indent name="On hard terrain">
{(resolvedEnginePower / stockEngine.power) * {stats.hullTraverseHardTerrain}
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.hard * resistanceCoefficient)) *
(stockWeight / weight)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On medium terrain"> <InfoWithDelta decimals={1} indent name="On medium terrain">
{(resolvedEnginePower / stockEngine.power) * {stats.hullTraverseMediumTerrain}
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.medium * resistanceCoefficient)) *
(stockWeight / weight)}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta decimals={1} indent name="On soft terrain"> <InfoWithDelta decimals={1} indent name="On soft terrain">
{(resolvedEnginePower / stockEngine.power) * {stats.hullTraverseSoftTerrain}
track.traverseSpeed *
hullTraverseCoefficient *
(track.resistance.hard /
(track.resistance.soft * resistanceCoefficient)) *
(stockWeight / weight)}
</InfoWithDelta> </InfoWithDelta>
</Flex> </Flex>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Heading size="5">Survivability</Heading> <Heading size="5">Survivability</Heading>
<InfoWithDelta name="Health" unit="hp" decimals={0}> <InfoWithDelta name="Health" unit="hp" decimals={0}>
{(tank.health + turret.health) * healthCoefficient} {stats.health}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta name="Fire chance" unit="%" deltaType="lowerIsBetter"> <InfoWithDelta
{Math.round(engine.fireChance * fireChanceCoefficient * 100)} name="Fire chance"
unit="%"
deltaType="lowerIsBetter"
decimals={0}
>
{stats.fireChance * 100}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta name="View range" unit="m" decimals={0}> <InfoWithDelta name="View range" unit="m" decimals={0}>
{turret.viewRange * viewRangeCoefficient} {stats.viewRange}
</InfoWithDelta> </InfoWithDelta>
<Info name="Camouflage" unit="%" /> <Info name="Camouflage" unit="%" />
<InfoWithDelta indent name="Still" decimals={2}> <InfoWithDelta indent name="Still" decimals={2}>
{(tank.camouflage.still + camouflageSumStill) * 100} {stats.camouflageStill * 100}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta indent name="Moving" decimals={2}> <InfoWithDelta indent name="Moving" decimals={2}>
{(tank.camouflage.moving + camouflageSumMoving) * 100} {stats.camouflageMoving * 100}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta indent name="Shooting still" decimals={2}> <InfoWithDelta indent name="Shooting still" decimals={2}>
{(tank.camouflage.still * gun.camouflageLoss + camouflageSumStill) * {stats.camouflageShootingStill * 100}
100}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta indent name="Shooting on move" decimals={2}> <InfoWithDelta indent name="Shooting on move" decimals={2}>
{(tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving) * {stats.camouflageShootingMoving * 100}
100}
</InfoWithDelta> </InfoWithDelta>
<InfoWithDelta indent name="Caught on fire" decimals={2}> <InfoWithDelta indent name="Caught on fire" decimals={2}>
{(tank.camouflage.onFire * tank.camouflage.still + {stats.camouflageCaughtOnFire * 100}
camouflageSumStill) *
100}
</InfoWithDelta> </InfoWithDelta>
<Info name="Size" unit="m"> <Info name="Size" unit="m">
{size[0].toFixed(2)} x {size[2].toFixed(2)} x {size[1].toFixed(2)} {stats.tankSize.map((component) => component.toFixed(2)).join(' x ')}
</Info> </Info>
</Flex> </Flex>
@@ -69,7 +69,12 @@ export function Controls() {
); );
async function handlePoseEvent(event: Pose) { 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) { switch (event) {
case Pose.HullDown: { case Pose.HullDown: {
@@ -162,7 +162,12 @@ export const TankModel = memo(() => {
} }
} }
async function handlePointerMove(event: PointerEvent) { 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] = applyPitchYawLimits(
pitch, pitch,
yaw + event.movementX * (Math.PI / canvas.width), yaw + event.movementX * (Math.PI / canvas.width),
@@ -223,7 +228,12 @@ export const TankModel = memo(() => {
window.addEventListener('pointerup', handlePointerUp); window.addEventListener('pointerup', handlePointerUp);
} }
async function handlePointerMove(event: PointerEvent) { 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] = applyPitchYawLimits(
pitch - event.movementY * (Math.PI / canvas.height), pitch - event.movementY * (Math.PI / canvas.height),
yaw + event.movementX * (Math.PI / canvas.width), yaw + event.movementX * (Math.PI / canvas.width),
@@ -84,18 +84,29 @@ export function CoreArmorSceneComponent({
async function handleProtagonistEquipmentChange( async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix, 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 material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.04 ? thickness * 1.04
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const shell = useDuel.getState().antagonist!.shell; const duel = useDuel.getState();
const penetration = resolveNearPenetration(shell.penetration); const penetration = resolveNearPenetration(
const hasCalibratedShells = await hasEquipment(103, true, equipment); duel.antagonist!.shell.penetration,
);
const hasCalibratedShells = await hasEquipment(
103,
duel.antagonist!.tank.equipment,
equipment,
);
material.uniforms.penetration.value = hasCalibratedShells material.uniforms.penetration.value = hasCalibratedShells
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) ? penetration * (isExplosive(duel.antagonist!.shell.type) ? 1.1 : 1.05)
: penetration; : penetration;
} }
@@ -50,17 +50,28 @@ export function SpacedArmorSubExternal({
async function handleProtagonistEquipmentChange( async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix, 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 material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.04 ? thickness * 1.04
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const shell = useDuel.getState().antagonist!.shell; const duel = useDuel.getState();
const penetration = resolveNearPenetration(shell.penetration); const penetration = resolveNearPenetration(
const hasCalibratedShells = await hasEquipment(103, true, equipment); duel.antagonist!.shell.penetration,
);
const hasCalibratedShells = await hasEquipment(
103,
duel.antagonist!.tank.equipment,
equipment,
);
material.uniforms.penetration.value = hasCalibratedShells material.uniforms.penetration.value = hasCalibratedShells
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) ? penetration * (isExplosive(duel.antagonist!.shell.type) ? 1.1 : 1.05)
: penetration; : penetration;
} }
@@ -62,15 +62,25 @@ export function SpacedArmorSubSpaced({
async function handleProtagonistEquipmentChange( async function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix, 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 material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.04 ? thickness * 1.04
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = useDuel.getState();
const shell = useDuel.getState().antagonist!.shell; const shell = useDuel.getState().antagonist!.shell;
const penetration = resolveNearPenetration(shell.penetration); 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 material.uniforms.penetration.value = hasCalibratedShells
? penetration * (isExplosive(shell.type) ? 1.1 : 1.05) ? penetration * (isExplosive(shell.type) ? 1.1 : 1.05)
: penetration; : penetration;
@@ -89,8 +89,17 @@ export function SpacedArmorSceneComponent({
async onClick(event) { async onClick(event) {
event.stopPropagation(); event.stopPropagation();
const hasCalibratedShells = await hasEquipment(103, true); const duel = useDuel.getState();
const hasEnhancedArmor = await hasEquipment(110); 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 thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
const shell = useDuel.getState().antagonist!.shell; const shell = useDuel.getState().antagonist!.shell;
const explosive = isExplosive(shell.type); const explosive = isExplosive(shell.type);
+4 -7
View File
@@ -1,16 +1,13 @@
import { useDuel } from '../../stores/duel'; import { EquipmentMatrix } from '../../stores/duel';
import { equipmentDefinitions } from './equipmentDefinitions'; import { equipmentDefinitions } from './equipmentDefinitions';
export async function hasEquipment( export async function hasEquipment(
id: number, id: number,
antagonist = false, equipmentPreset: string,
equipmentMatrix = useDuel.getState()[ equipmentMatrix: EquipmentMatrix,
antagonist ? 'antagonist' : 'protagonist'
]!.equipment,
) { ) {
const awaitedEquipmentDefinitions = await equipmentDefinitions; const awaitedEquipmentDefinitions = await equipmentDefinitions;
const member = useDuel.getState()[antagonist ? 'antagonist' : 'protagonist']!; const presetRows = awaitedEquipmentDefinitions.presets[equipmentPreset];
const presetRows = awaitedEquipmentDefinitions.presets[member.tank.equipment];
return presetRows.some((equipmentRow, rowIndex) => { return presetRows.some((equipmentRow, rowIndex) => {
return equipmentRow.some((equipment, columnIndex) => { return equipmentRow.some((equipment, columnIndex) => {
const choice = equipmentMatrix[rowIndex][columnIndex]; const choice = equipmentMatrix[rowIndex][columnIndex];
+1 -4
View File
@@ -1,6 +1,3 @@
import { useDuel } from '../stores/duel'; export function useConsumable(id: number, consumables: number[]) {
export function useConsumable(id: number) {
const consumables = useDuel((state) => state.protagonist!.consumables);
return consumables.includes(id); return consumables.includes(id);
} }
-9
View File
@@ -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]);
}
+459
View File
@@ -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<string, number>;
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,
};
}