diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 4c6ab6e3b..0b87de030 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -975,25 +975,10 @@ export async function definitions(production: boolean) { } } - Object.entries({ - ...provision.script, - ...provision.script.bonusValues, - }).forEach(([effectName, effect]) => { - if ( - !consumableProvisionEffectSuffixes.some((suffix) => - effectName.endsWith(suffix), - ) - ) { - return; - } - - if (!provisionDefinitions[provision.id].effects) { - provisionDefinitions[provision.id].effects = {}; - } - - provisionDefinitions[provision.id].effects![effectName] = - effect as number; - }); + if (provision.script.bonusValues?.crewLevelIncrease !== undefined) { + provisionDefinitions[provision.id].crew = + provision.script.bonusValues?.crewLevelIncrease; + } }); await commitAssets( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index fcc77e566..9edaaa7e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -37,9 +37,6 @@ export default function RootLayout({ children }: RootLayoutProps) { style={{ margin: 0, paddingTop: isEmbed ? 0 : '3.25rem', - background: isEmbed - ? 'transparent' - : 'url(https://i.imgur.com/PhS06NJ.png)', }} > Miscellaneous + { + mutateTankopediaTemporary((draft) => { + draft.camouflage = !camouflage; + }); + }} + /> + 0} + first + last + banner={ + cooldownBooster === 0 + ? undefined + : cooldownBooster === 1 + ? '#afb0abc0' + : cooldownBooster === 2 + ? '#5f72cbc0' + : '#9b3cc0c0' + } + iconStyles={{ + transform: 'translate(-50%, -50%) scale(0.8)', + }} + onClick={() => { + mutateTankopediaTemporary((draft) => { + draft.cooldownBooster++; + if (draft.cooldownBooster === 4) draft.cooldownBooster = 0; + }); + }} + /> + - - { - mutateTankopediaTemporary((draft) => { - draft.camouflage = !camouflage; - }); - }} - /> - - 0} - first - last - banner={ - cooldownBooster === 0 - ? undefined - : cooldownBooster === 1 - ? '#afb0abc0' - : cooldownBooster === 2 - ? '#5f72cbc0' - : '#9b3cc0c0' - } - iconStyles={{ - transform: 'translate(-50%, -50%) scale(0.8)', - }} - onClick={() => { - mutateTankopediaTemporary((draft) => { - draft.cooldownBooster++; - if (draft.cooldownBooster === 4) draft.cooldownBooster = 0; - }); - }} - /> ); diff --git a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx index 1d10ce98f..b423c689c 100644 --- a/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx +++ b/src/app/tools/tankopedia/[id]/components/Characteristics/index.tsx @@ -1,20 +1,24 @@ +import { AccessibilityIcon } from '@radix-ui/react-icons'; import { Flex, Heading, Slider, Text, TextField } from '@radix-ui/themes'; import { debounce } from 'lodash'; import { use, useEffect, useRef, useState } from 'react'; import { lerp } from 'three/src/math/MathUtils'; import { isExplosive } from '../../../../../../core/blitz/isExplosive'; import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration'; +import { coefficient } from '../../../../../../core/blitzkrieg/coefficient'; import { modelDefinitions } from '../../../../../../core/blitzkrieg/modelDefinitions'; import { normalizeBoundingBox } from '../../../../../../core/blitzkrieg/normalizeBoundingBox'; import { resolveDpm } from '../../../../../../core/blitzkrieg/resolveDpm'; +import { sum } from '../../../../../../core/blitzkrieg/sum'; import { CREW_MEMBER_NAMES, - CREW_MEMBER_NAMES_PLURAL, GUN_TYPE_NAMES, } from '../../../../../../core/blitzkrieg/tankDefinitions'; import { unionBoundingBox } from '../../../../../../core/blitzkrieg/unionBoundingBox'; import { useConsumable } from '../../../../../../core/blitzkrieg/useConsumable'; import { useEquipment } from '../../../../../../core/blitzkrieg/useEquipment'; +import { useProvision } from '../../../../../../core/blitzkrieg/useProvision'; +import { useProvisions } from '../../../../../../core/blitzkrieg/useProvisions'; import { useDuel } from '../../../../../../stores/duel'; import { useTankopediaTemporary } from '../../../../../../stores/tankopedia'; import { Info } from './components/Info'; @@ -49,6 +53,8 @@ export function Characteristics() { stockTurret.weight + stockGun.weight; const [penetrationDistance, setPenetrationDistance] = useState(250); + const provisions = useProvisions(); + const camouflage = useTankopediaTemporary((state) => state.camouflage); const hasRammer = useEquipment(100); const hasCalibratedShells = useEquipment(103); const hasEnhancedGunLayingDrive = useEquipment(104); @@ -69,41 +75,131 @@ export function Characteristics() { const hasTungsten = useConsumable(45); const hasReticleCalibration = useConsumable(28); const hasShellReloadBoost = useConsumable(29); - const camouflage = useTankopediaTemporary((state) => state.camouflage); - const camouflageBonus = camouflage - ? tank.type === 'tankDestroyer' - ? 0.04 - : tank.type === 'heavy' + 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 healthCoefficient = coefficient( + [hasSandbagArmor, 0.03], + [hasEnhancedSandbagArmor, 0.06], + [hasImprovedAssembly, 0.06], + ); + const reloadCoefficient = coefficient( + [hasRammer, -0.07], + [hasAdrenaline && gun.type === 'regular', -0.2], + ); + const penetrationCoefficient = coefficient([ + hasCalibratedShells, + isExplosive(shell.type) ? 0.1 : 0.05, + ]); + const damageCoefficient = coefficient( + // TODO: add max rolls + [hasTungsten, 0.15], + ); + const interClipCoefficient = 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], + [hasVerticalStabilizer, -0.15], + ); + const dispersionCoefficient = coefficient( + [hasRefinedGun, -0.1], + [hasReticleCalibration, -0.4], + ); + const enginePowerCoefficient = coefficient( + [ + hasEngineAccelerator && (tank.type === 'light' || tank.type === 'medium'), + 0.05, + ], + [ + hasEngineAccelerator && + (tank.type === 'heavy' || tank.type === 'tankDestroyer'), + 0.07, + ], + [hasEnginePowerBoost, 0.2], + [hasImprovedEnginePowerBoost, 0.4], + [hasStandardFuel, 0.03], + [hasImprovedFuel, 0.1], + ); + const turretTraverseCoefficient = coefficient( + [hasStandardFuel, 0.03], + [hasImprovedFuel, 0.1], + ); + const hullTraverseCoefficient = coefficient( + [hasImprovedControl, 0.1], + [hasImprovedEnginePowerBoost, 0.05], + ); + const resistanceCoefficient = coefficient([hasImprovedSuspension, -0.25]); + const viewRangeCoefficient = coefficient([ + hasImprovedOptics, + tank.type === 'tankDestroyer' ? 0.05 : tank.type === 'heavy' ? 0.07 : 0.1, + ]); + const fireChanceCoefficient = coefficient([hasProtectiveKit, -0.2]); + + const speedForwardsSum = sum([hasImprovedEnginePowerBoost, 5]); + const speedBackwardsSum = sum([hasImprovedEnginePowerBoost, 10]); + const camouflageSumMoving = sum( + [ + hasCamouflageNet, + tank.type === 'heavy' ? 0.03 - : 0.02 - : 0; - const shellReloadBoostBonus = hasShellReloadBoost ? 0.7 : 1; - const adrenalineBonus = hasAdrenaline && gun.type === 'regular' ? 0.8 : 1; - const enginePowerBoostBonus = - (hasEnginePowerBoost ? 1.2 : 1) * (hasImprovedEnginePowerBoost ? 1.4 : 1); - const camouflageNetBonus = hasCamouflageNet - ? tank.type === 'heavy' - ? 0.03 - : tank.type === 'tankDestroyer' - ? 0.07 - : 0.05 - : 0; - const calibratedShellsBonus = hasCalibratedShells - ? isExplosive(shell.type) - ? 1.1 - : 1.05 - : 1; - const engineAcceleratorBonus = hasEngineAccelerator - ? tank.type === 'light' || tank.type === 'medium' - ? 1.05 - : 1.07 - : 1; - const resolvedEnginePower = - engine.power * engineAcceleratorBonus * enginePowerBoostBonus; - const reticleCalibrationBonus = hasReticleCalibration ? 0.6 : 1; - const improvedEnginePowerBoostTraverseBonus = hasImprovedEnginePowerBoost - ? 1.05 - : 1; + : tank.type === 'tankDestroyer' + ? 0.07 + : 0.05, + ], + [ + camouflage, + tank.type === 'tankDestroyer' + ? 0.04 + : tank.type === 'heavy' + ? 0.03 + : 0.02, + ], + ); + const camouflageSumStill = sum( + [ + hasCamouflageNet, + 2 * + (tank.type === 'heavy' + ? 0.03 + : tank.type === 'tankDestroyer' + ? 0.07 + : 0.05), + ], + [ + camouflage, + tank.type === 'tankDestroyer' + ? 0.04 + : tank.type === 'heavy' + ? 0.03 + : 0.02, + ], + ); + + const provisionCrewBonus = provisions.reduce( + (total, provision) => + provision.crew ? total + provision.crew / 100 : total, + 0, + ); + const resolvedEnginePower = engine.power * enginePowerCoefficient; + const dpm = resolveDpm( + gun, + shell, + damageCoefficient, + reloadCoefficient, + interClipCoefficient, + ); useEffect(() => { if (penetrationDistanceInput.current) { @@ -117,14 +213,7 @@ export function Characteristics() { Fire {GUN_TYPE_NAMES[gun.type]} - {resolveDpm( - gun, - shell, - hasRammer, - hasShellReloadBoost, - hasAdrenaline, - hasTungsten, - )} + {dpm} {gun.type === 'autoReloader' && ( <> @@ -187,7 +276,7 @@ export function Characteristics() { unit="s" deltaType="lowerIsBetter" > - {gun.reload * (hasRammer ? 0.93 : 1) * adrenalineBonus} + {gun.reload * reloadCoefficient} )} {gun.type === 'autoLoader' && ( @@ -198,27 +287,18 @@ export function Characteristics() { unit="s" deltaType="lowerIsBetter" > - {gun.interClip * shellReloadBoostBonus} + {gun.interClip * interClipCoefficient} )} {shell.caliber} - {resolveNearPenetration(shell.penetration) * calibratedShellsBonus} + {resolveNearPenetration(shell.penetration) * penetrationCoefficient} {typeof shell.penetration !== 'number' && ( <> - + (penetrationDistance * penetrationLossOverDistanceCoefficient) / + 500, + ) * penetrationCoefficient} + Distance )} - {shell.damage.armor * (hasTungsten ? 1.15 : 1)} + {shell.damage.armor * damageCoefficient} - - {shell.damage.module} + + {shell.damage.module * damageCoefficient} - {shell.speed * (hasSupercharger ? 1.3 : 1)} + {shell.speed * shellVelocityCoefficient} - {gun.aimTime * - (hasEnhancedGunLayingDrive ? 0.9 : 1) * - reticleCalibrationBonus} + {gun.aimTime * aimTimeCoefficient} - {gun.dispersion.base * - (hasRefinedGun ? 0.9 : 1) * - reticleCalibrationBonus} + {gun.dispersion.base * dispersionCoefficient} - {track.dispersion.move * - (hasVerticalStabilizer ? 0.85 : 1) * - reticleCalibrationBonus} + {track.dispersion.move * dispersionCoefficient} - {track.dispersion.traverse * - (hasVerticalStabilizer ? 0.85 : 1) * - reticleCalibrationBonus} + {track.dispersion.traverse * dispersionCoefficient} - {gun.dispersion.traverse * - (hasVerticalStabilizer ? 0.85 : 1) * - reticleCalibrationBonus} + {gun.dispersion.traverse * dispersionCoefficient} - {gun.dispersion.shot * reticleCalibrationBonus} + {gun.dispersion.shot * dispersionCoefficient} - {gun.dispersion.damaged * reticleCalibrationBonus} + {gun.dispersion.damaged * dispersionCoefficient} @@ -396,10 +467,10 @@ export function Characteristics() { Maneuverability - {tank.speed.forwards + (hasImprovedEnginePowerBoost ? 5 : 0)} + {tank.speed.forwards + speedForwardsSum} - {tank.speed.backwards + (hasImprovedEnginePowerBoost ? 10 : 0)} + {tank.speed.backwards + speedBackwardsSum} {resolvedEnginePower} @@ -408,19 +479,17 @@ export function Characteristics() { {resolvedEnginePower / weightTons / - (track.resistance.hard * (hasImprovedSuspension ? 0.75 : 1))} + (track.resistance.hard * resistanceCoefficient)} - {(resolvedEnginePower / + {resolvedEnginePower / weightTons / - (track.resistance.medium * (hasImprovedSuspension ? 0.75 : 1))) * - engineAcceleratorBonus} + (track.resistance.medium * resistanceCoefficient)} - {(resolvedEnginePower / + {resolvedEnginePower / weightTons / - (track.resistance.soft * (hasImprovedSuspension ? 0.75 : 1))) * - engineAcceleratorBonus} + (track.resistance.soft * resistanceCoefficient)} {weightTons} + + {turret.traverseSpeed * turretTraverseCoefficient} + {(resolvedEnginePower / stockEngine.power) * track.traverseSpeed * - (track.resistance.hard / track.resistance.hard) * - (stockWeight / weight) * - (hasImprovedControl ? 1.1 : 1) * - improvedEnginePowerBoostTraverseBonus} + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.hard * resistanceCoefficient)) * + (stockWeight / weight)} {(resolvedEnginePower / stockEngine.power) * track.traverseSpeed * - (track.resistance.hard / track.resistance.medium) * - (stockWeight / weight) * - (hasImprovedControl ? 1.1 : 1) * - improvedEnginePowerBoostTraverseBonus} + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.medium * resistanceCoefficient)) * + (stockWeight / weight)} {(resolvedEnginePower / stockEngine.power) * track.traverseSpeed * - (track.resistance.hard / track.resistance.soft) * - (stockWeight / weight) * - (hasImprovedControl ? 1.1 : 1) * - improvedEnginePowerBoostTraverseBonus} + hullTraverseCoefficient * + (track.resistance.hard / + (track.resistance.soft * resistanceCoefficient)) * + (stockWeight / weight)} Survivability - - {(tank.health + turret.health) * (hasImprovedAssembly ? 1.06 : 1)} + + {(tank.health + turret.health) * healthCoefficient} - {Math.round(engine.fireChance * 100)} + {Math.round(engine.fireChance * fireChanceCoefficient * 100)} - {turret.viewRange * - (hasImprovedOptics - ? tank.type === 'tankDestroyer' - ? 1.05 - : tank.type === 'heavy' - ? 1.07 - : 1.1 - : 1)} + {turret.viewRange * viewRangeCoefficient} - {(tank.camouflage.still + camouflageNetBonus + camouflageBonus) * 100} + {(tank.camouflage.still + camouflageSumStill) * 100} - {(tank.camouflage.moving + camouflageNetBonus + camouflageBonus) * - 100} + {(tank.camouflage.moving + camouflageSumMoving) * 100} - {(tank.camouflage.still + camouflageNetBonus + camouflageBonus) * - gun.camouflageLoss * + {(tank.camouflage.still * gun.camouflageLoss + camouflageSumStill) * 100} - {(tank.camouflage.moving + camouflageNetBonus + camouflageBonus) * - gun.camouflageLoss * + {(tank.camouflage.moving * gun.camouflageLoss + camouflageSumMoving) * 100} - {(tank.camouflage.onFire + camouflageNetBonus + camouflageBonus) * - tank.camouflage.still * + {(tank.camouflage.onFire * tank.camouflage.still + + camouflageSumStill) * 100} @@ -512,20 +574,31 @@ export function Characteristics() { return ( <> 1 ? `${member.count} ` : ''}${ - (count > 1 ? CREW_MEMBER_NAMES_PLURAL : CREW_MEMBER_NAMES)[ - member.type - ] - }`} + name={`${CREW_MEMBER_NAMES[member.type]}${count > 1 ? ` x ${count}` : ''}`} unit="%" decimals={0} > - {crew * (member.type === 'commander' ? 1 : 1.1) * 100} + {(crew + provisionCrewBonus) * + (member.type === 'commander' ? 1 : 1.1) * + 100} - {member.substitute && - member.substitute.map((substitute) => ( - - ))} + {member.substitute && ( + + + + {member.substitute + .map((sub, index) => + index === 0 ? CREW_MEMBER_NAMES[sub] : sub, + ) + .join(', ')} + + + } + /> + )} ); })} diff --git a/src/core/blitzkrieg/coefficient.ts b/src/core/blitzkrieg/coefficient.ts new file mode 100644 index 000000000..b6ce2df34 --- /dev/null +++ b/src/core/blitzkrieg/coefficient.ts @@ -0,0 +1,3 @@ +export function coefficient(...effects: [boolean, number][]) { + return effects.reduce((accumulator, b) => accumulator + (b[0] ? b[1] : 0), 1); +} diff --git a/src/core/blitzkrieg/provisionDefinitions.ts b/src/core/blitzkrieg/provisionDefinitions.ts index d0476aa9e..46393d483 100644 --- a/src/core/blitzkrieg/provisionDefinitions.ts +++ b/src/core/blitzkrieg/provisionDefinitions.ts @@ -5,9 +5,9 @@ import { fetchCdonLz4 } from './fetchCdonLz4'; export interface ProvisionEntry { id: number; name: string; - effects?: Record; include: TankFilterDefinition[]; exclude?: TankFilterDefinition[]; + crew?: number; } export interface ProvisionDefinitions { @@ -17,3 +17,5 @@ export interface ProvisionDefinitions { export const provisionDefinitions = fetchCdonLz4( asset('definitions/provisions.cdon.lz4'), ); + +provisionDefinitions.then(console.log); diff --git a/src/core/blitzkrieg/resolveDpm.ts b/src/core/blitzkrieg/resolveDpm.ts index e7faaf51c..440f610ba 100644 --- a/src/core/blitzkrieg/resolveDpm.ts +++ b/src/core/blitzkrieg/resolveDpm.ts @@ -3,24 +3,18 @@ import { GunDefinition, ShellDefinition } from './tankDefinitions'; export function resolveDpm( gun: GunDefinition, shell: ShellDefinition, - hasRammer = false, - hasShellReloadBoost = false, - hasAdrenaline = false, - hasTungsten = false, + damageCoefficient = 1, + reloadCoefficient = 1, + interClipCoefficient = 1, ) { - const alpha = shell.damage.armor * (hasTungsten ? 1.15 : 1); + const alpha = shell.damage.armor * damageCoefficient; if (gun.type === 'regular') { - return ( - (alpha / - ((hasRammer ? 0.93 : 1) * (hasAdrenaline ? 0.8 : 1) * gun.reload)) * - 60 - ); + return (alpha / (reloadCoefficient * gun.reload)) * 60; } else if (gun.type === 'autoLoader') { return ( ((alpha * gun.count) / - (gun.reload + - (gun.count - 1) * gun.interClip * (hasShellReloadBoost ? 0.7 : 1))) * + (gun.reload + (gun.count - 1) * gun.interClip * interClipCoefficient)) * 60 ); } else { diff --git a/src/core/blitzkrieg/sum.ts b/src/core/blitzkrieg/sum.ts new file mode 100644 index 000000000..30dd6a572 --- /dev/null +++ b/src/core/blitzkrieg/sum.ts @@ -0,0 +1,3 @@ +export function sum(...effects: [boolean, number][]) { + return effects.reduce((accumulator, b) => accumulator + (b[0] ? b[1] : 0), 0); +} diff --git a/src/core/blitzkrieg/tankDefinitions.ts b/src/core/blitzkrieg/tankDefinitions.ts index 1b63cbbd8..6df4c7093 100644 --- a/src/core/blitzkrieg/tankDefinitions.ts +++ b/src/core/blitzkrieg/tankDefinitions.ts @@ -27,13 +27,6 @@ export const CREW_MEMBER_NAMES: Record = { driver: 'Driver', loader: 'Loader', }; -export const CREW_MEMBER_NAMES_PLURAL: Record = { - commander: 'Commanders', - radioman: 'Radiomen', - gunner: 'Gunners', - driver: 'Drivers', - loader: 'Loaders', -}; export interface Crew { type: CrewMember; count?: number; diff --git a/src/core/blitzkrieg/useProvision.ts b/src/core/blitzkrieg/useProvision.ts new file mode 100644 index 000000000..5ae13c156 --- /dev/null +++ b/src/core/blitzkrieg/useProvision.ts @@ -0,0 +1,6 @@ +import { useTankopediaTemporary } from '../../stores/tankopedia'; + +export function useProvision(id: number) { + const provisions = useTankopediaTemporary((state) => state.provisions); + return provisions.includes(id); +} diff --git a/src/core/blitzkrieg/useProvisions.ts b/src/core/blitzkrieg/useProvisions.ts new file mode 100644 index 000000000..b49e659d3 --- /dev/null +++ b/src/core/blitzkrieg/useProvisions.ts @@ -0,0 +1,9 @@ +import { use } from 'react'; +import { useTankopediaTemporary } from '../../stores/tankopedia'; +import { provisionDefinitions } from './provisionDefinitions'; + +export function useProvisions() { + const provisions = useTankopediaTemporary((state) => state.provisions); + const awaitedProvisionDefinitions = use(provisionDefinitions); + return provisions.map((id) => awaitedProvisionDefinitions[id]); +}