diff --git a/packages/core/src/blitzkit/degressiveStat.ts b/packages/core/src/blitzkit/degressiveStat.ts index 7fdcac47a..b2424e01a 100644 --- a/packages/core/src/blitzkit/degressiveStat.ts +++ b/packages/core/src/blitzkit/degressiveStat.ts @@ -1,3 +1,3 @@ export function degressiveStat(crew: number) { - return 0.875 / (0.00375 * 100 * crew + 0.5) - 1; + return 0.875 / (0.00375 * 100 * crew + 0.5); } diff --git a/packages/core/src/blitzkit/index.ts b/packages/core/src/blitzkit/index.ts index a5f4bf29b..4c0660d83 100644 --- a/packages/core/src/blitzkit/index.ts +++ b/packages/core/src/blitzkit/index.ts @@ -17,7 +17,6 @@ export * from "./normalizeBoundingBox"; export * from "./patientFetchJSON"; export * from "./popularTanks"; export * from "./progressiveStat"; -export * from "./resolveDpm"; export * from "./resolvePenetrationCoefficient"; export * from "./reviews"; export * from "./romanize"; diff --git a/packages/core/src/blitzkit/progressiveStat.ts b/packages/core/src/blitzkit/progressiveStat.ts index 59fb0b420..36dff660f 100644 --- a/packages/core/src/blitzkit/progressiveStat.ts +++ b/packages/core/src/blitzkit/progressiveStat.ts @@ -1,3 +1,3 @@ export function progressiveStat(crew: number) { - return (0.00375 * 100 * crew + 0.5) / 0.875 - 1; + return (0.00375 * 100 * crew + 0.5) / 0.875; } diff --git a/packages/core/src/blitzkit/resolveDpm.ts b/packages/core/src/blitzkit/resolveDpm.ts deleted file mode 100644 index 1d35df571..000000000 --- a/packages/core/src/blitzkit/resolveDpm.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - GunDefinition, - GunDefinitionAutoReloader, - ShellDefinition, -} from '../protos'; - -export function resolveDpm( - gun: GunDefinition, - shell: ShellDefinition, - damageCoefficient = 1, - reloadCoefficient = 1, - intraClipCoefficient = 1, -) { - const alpha = shell.armor_damage * damageCoefficient; - let dps: number; - - if (gun.gun_type!.$case === 'regular') { - dps = alpha / (reloadCoefficient * gun.gun_type!.value.reload); - } else if (gun.gun_type!.$case === 'auto_loader') { - const damage = alpha * gun.gun_type!.value.shell_count; - let time = gun.gun_type!.value.clip_reload * reloadCoefficient; - - if (gun.burst) { - time += - (gun.gun_type!.value.shell_count / gun.burst.count - 1) * - gun.gun_type!.value.intra_clip * - intraClipCoefficient; - - time += - (gun.gun_type!.value.shell_count / gun.burst.count) * - (gun.burst.count - 1) * - gun.burst.interval; - } else { - time += - (gun.gun_type!.value.shell_count - 1) * - gun.gun_type!.value.intra_clip * - intraClipCoefficient; - } - - dps = damage / time; - } else { - const mostOptimalShell = gun.gun_type!.value.shell_reloads.reduce((current, reloadRaw, index) => { - const reload = - reloadRaw * reloadCoefficient + - (index > 0 - ? (gun.gun_type!.value as GunDefinitionAutoReloader).intra_clip * - intraClipCoefficient - : 0); - - if (current === null || reload < current.reload) { - return { index, reload }; - } - return current; - }, null)!; - - dps = alpha / mostOptimalShell?.reload; - } - - return dps * 60; -} diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index d7dbf64be..327c5d704 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -8,6 +8,13 @@ "s": "{value}s", "percentage": "{value}%", "deg_s": "{value}°/s", + "m_s": "{value}m/s", + "km_h": "{value}km/h", + "t": "{value}t", + "bhp": "{value}bhp", + "bhp_t": "{value}bhp/t", + "kN": "{value}kN", + "hp_s": "{value}hp/s", "plus": "+{value}" }, @@ -38,7 +45,7 @@ "burst_size": "Burst size", "intra_burst": "Intra-burst", "penetration": "Penetration", - "penetration_loss_by_distance": "Penetration loss by distance", + "penetration_loss_by_distance": "Penetration loss by 500m", "penetration_loss_after_ricochet": "Penetration loss after ricochet", "shell_velocity": "Shell velocity", "shell_range": "Shell range", diff --git a/packages/website/src/config/characteristics.ts b/packages/website/src/config/characteristics.ts index 2f5f27b13..5ae672351 100644 --- a/packages/website/src/config/characteristics.ts +++ b/packages/website/src/config/characteristics.ts @@ -1,18 +1,243 @@ -import { blitzkitToBlitzShellKind } from "@blitzkit/core"; -import { ShellType } from "@blitzkit/protos"; +import { + blitzkitToBlitzShellKind, + isExplosive, + normalizeBoundingBox, + unionBoundingBox, +} from "@blitzkit/core"; +import { ShellType, TankClass } from "@blitzkit/protos"; import { CharacteristicType, type Characteristic, + type CharacteristicContext, } from "../types/characteristics"; +function intraClipCoefficient({ + script, +}: Pick) { + let coefficient = 1; + + script("protagonist", "NewAutoloader", (effect) => { + coefficient *= effect.clipReloadTimeFactor; + }); + + return coefficient; +} + +function aimTimeCoefficient({ script }: Pick) { + let coefficient = 1; + + script("protagonist", "miscAttrs/gunAimingTimeFactor", (effect) => { + coefficient += effect.bonusValues!.percent / 100; + }); + + script("protagonist", "FireControlSystem", (effect) => { + coefficient *= effect.aimingTimeFactor; + }); + + return coefficient; +} + +function dispersionCoefficient({ + script, +}: Pick) { + let coefficient = 1; + + script("protagonist", "miscAttrs/shotDispersionAngleFactor", (effect) => { + coefficient += effect.bonusValues!.percent / 100; + }); + + script("protagonist", "FireControlSystem", (effect) => { + coefficient *= effect.shotDispersionAngleFactor; + }); + + return coefficient; +} + export const characteristics = { gun_type: { type: CharacteristicType.Enum, + compute({ gun }) { return gun.gun_type!.$case; }, }, + damage: { + type: CharacteristicType.Number, + + compute({ shell, environment, equalizer, script, gun }) { + let coefficient = 1; + + if (environment.equalize) coefficient *= equalizer.damage; + + script("protagonist", "TungstenTip", (effect) => { + coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2; + }); + + script("antagonist", "ShieldKit", (effect) => { + if (shell.type === ShellType.SHELL_TYPE_HE) return; + coefficient *= 1 - effect.shieldCoef; + }); + + script("antagonist", "ArmorMover", (effect) => { + coefficient *= 1 - effect.bonusValues!.damageReductionPercent / 100; + }); + + script("antagonist", "AntiHighExplosive", (effect) => { + if (shell.type !== ShellType.SHELL_TYPE_HE) return; + coefficient *= effect.bonusValues!.factor; + }); + + if (gun.assault_ranges?.types.includes(shell.type)) { + const range = gun.assault_ranges.ranges.find( + ({ distance }) => distance >= environment.distance, + ); + + coefficient *= range?.factor ?? 0; + } + + return coefficient * shell.armor_damage; + }, + }, + + _reload_coefficient: { + type: CharacteristicType.Number, + + compute({ script, degressive }) { + let c0 = 1; + + script("protagonist", "miscAttrs/gunReloadTimeFactor", (effect) => { + c0 += effect.bonusValues!.percent / 100; + }); + + const c1 = degressive("loader"); + + let c2 = 1; + + script("protagonist", "Berserk", (effect) => { + c2 += effect.bonusValues!.gunReloadSpeedIncrease / 100; + }); + + return (c0 * c1) / c2; + }, + }, + + reload: { + type: CharacteristicType.Number, + + should_render({ gun }) { + return gun.gun_type!.$case !== "auto_reloader"; + }, + + compute({ gun, characteristic }) { + if (gun.gun_type!.$case === "auto_reloader") return -Infinity; + + const c = characteristic("_reload_coefficient") as number; + + if (gun.gun_type!.$case === "regular") { + return c * gun.gun_type!.value.reload; + } + + return c * gun.gun_type!.value.clip_reload; + }, + }, + + intra_clip: { + type: CharacteristicType.Number, + + should_render({ gun }) { + return gun.gun_type!.$case !== "regular"; + }, + + compute({ gun, script }) { + if (gun.gun_type!.$case === "regular") return 0; + + let c = 1; + + script("protagonist", "NewAutoloader", (effect) => { + c = effect.clipReloadTimeFactor; + }); + + return c * gun.gun_type!.value.intra_clip; + }, + }, + + dpm: { + type: CharacteristicType.Number, + + compute({ gun, characteristic }) { + const alpha = characteristic("damage") as number; + + let dps: number; + + if (gun.gun_type!.$case === "regular") { + const reload = characteristic("reload") as number; + dps = alpha / reload; + } else if (gun.gun_type!.$case === "auto_loader") { + const damage = alpha * gun.gun_type!.value.shell_count; + + const intraClip = characteristic("intra_clip") as number; + let time = characteristic("reload") as number; + + if (gun.burst) { + time += + (gun.gun_type!.value.shell_count / gun.burst.count - 1) * intraClip; + + time += + (gun.gun_type!.value.shell_count / gun.burst.count) * + (gun.burst.count - 1) * + gun.burst.interval; + } else { + time += (gun.gun_type!.value.shell_count - 1) * intraClip; + } + + dps = damage / time; + } else { + const intraClip = characteristic("intra_clip") as number; + const reloadCoefficient = characteristic( + "_reload_coefficient", + ) as number; + + const mostOptimalShell = + gun.gun_type!.value.shell_reloads.reduce((current, reloadRaw, index) => { + const reload = + reloadRaw * reloadCoefficient + (index > 0 ? intraClip : 0); + + if (current === null || reload < current.reload) { + return { index, reload }; + } + return current; + }, null)!; + + dps = alpha / mostOptimalShell?.reload; + } + + return dps * 60; + }, + }, + + // damage_range: unavailable, + + module_damage: { + type: CharacteristicType.Number, + compute({ shell, script }) { + let c = 1; + + script("protagonist", "TungstenTip", (effect) => { + c *= (effect.minDamageFactor + effect.maxDamageFactor) / 2; + }); + + return c * shell.module_damage; + }, + }, + + // module_damage_range: unavailable, + + // reloads: unavailable, + clip_size: { type: CharacteristicType.Number, @@ -26,99 +251,455 @@ export const characteristics = { }, }, - damage: { + clipping_potential: { type: CharacteristicType.Number, - compute({ shell, environment, equalizer, script, gun }) { - let coefficient = 1; - if (environment.equalize) coefficient *= equalizer.damage; + should_render({ gun }) { + return gun.gun_type!.$case !== "regular"; + }, - // tungsten shells - script("protagonist", "TungstenTip", (effect) => { - coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2; - }); + compute({ characteristic }) { + const clipSize = characteristic("clip_size") as number; + const damage = characteristic("damage") as number; - // reactive armor - script("antagonist", "ShieldKit", (effect) => { - if (shell.type === ShellType.SHELL_TYPE_HE) return; - coefficient *= 1 - effect.shieldCoef; - }); - - // dynamic armour system - script("antagonist", "ArmorMover", (effect) => { - coefficient *= 1 - effect.bonusValues!.damageReductionPercent / 100; - }); - - // spall liner - script("antagonist", "AntiHighExplosive", (effect) => { - if (shell.type !== ShellType.SHELL_TYPE_HE) return; - coefficient *= effect.bonusValues!.factor; - }); - - if (gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)) { - const range = gun.assault_ranges.ranges.find( - ({ distance }) => distance >= environment.distance, - ); - - if (range) coefficient *= range.factor; - } - - return coefficient * shell.armor_damage; + return clipSize * damage; }, }, - module_damage: { + // TODO: custom type of shell index + // optimal_shell: { + // type: CharacteristicType.Number, + + // should_render({ gun }) { + // return gun.gun_type!.$case === "auto_reloader"; + // }, + + // compute({ gun, script, degressive }) { + // if (gun.gun_type!.$case !== "auto_reloader") return -1; + + // const autoReloader = gun.gun_type!.value; + // const coefficient = reloadCoefficient({ script, degressive, gun }); + // const intraClip = intraClipCoefficient({ script }); + + // return autoReloader.shell_reloads.reduce( + // (bestIndex, reload, index, reloads) => { + // const effectiveReload = + // reload * coefficient + + // (index > 0 ? autoReloader.intra_clip * intraClip : 0); + // const bestReload = + // reloads[bestIndex] * coefficient + + // (bestIndex > 0 ? autoReloader.intra_clip * intraClip : 0); + + // return effectiveReload < bestReload ? index : bestIndex; + // }, + // 0, + // ); + // }, + // }, + + burst_size: { type: CharacteristicType.Number, - compute({ shell, script }) { - let coefficient = 1; - // tungsten shells - script("protagonist", "TungstenTip", (effect) => { - coefficient *= (effect.minDamageFactor + effect.maxDamageFactor) / 2; - }); + should_render({ gun }) { + return gun.burst !== undefined; + }, - return coefficient * shell.module_damage; + compute({ gun }) { + return gun.burst!.count; + }, + }, + + intra_burst: { + type: CharacteristicType.Number, + + should_render({ gun }) { + return gun.burst !== undefined; + }, + + compute({ gun }) { + return gun.burst!.interval; }, }, penetration: { type: CharacteristicType.Number, - compute({ shell, script }) { - let coefficient = 1; + + compute({ shell, environment, equalizer, script }) { + let c = 1; script("protagonist", "CalibratedShells", (effect) => { - coefficient += + c += effect.piercingFactors![blitzkitToBlitzShellKind[shell.type]] / 100; }); - return coefficient * shell.penetration!.near; + if (environment.equalize) c *= equalizer.penetration; + + return c * shell.penetration!.near; }, }, - reload: { + penetration_loss_by_distance: { type: CharacteristicType.Number, - should_render({ gun }) { - return gun.gun_type!.$case !== "auto_reloader"; - }, + compute({ shell, script }) { + let c = 1; - compute({ gun, script, degressive }) { - // should never happen! just for typescript - if (gun.gun_type!.$case === "auto_reloader") return -Infinity; - - let c0 = 1; - - script("protagonist", "miscAttrs/gunReloadTimeFactor", (effect) => { - c0 += effect.bonusValues!.percent / 100; + script("protagonist", "StaticMiscAttrsModifier", (effect) => { + c += effect.bonusValues!.piercingPenaltyFactor500m / 100; }); - const c = c0 * degressive; + return ( + c * + 100 * + ((shell.penetration!.near - shell.penetration!.far) / + shell.penetration!.near) + ); + }, + }, - if (gun.gun_type!.$case === "regular") { - return c * gun.gun_type!.value.reload; + // penetration_loss_after_ricochet: unavailable, + + shell_velocity: { + type: CharacteristicType.Number, + + compute({ shell, script }) { + let c = 1; + + script("protagonist", "StaticMiscAttrsModifier", (effect) => { + c += effect.bonusValues!.projectileSpeedFactor / 100; + }); + + script("protagonist", "GunPowder", (effect) => { + c += effect.bonusValues!.projectileSpeedFactor / 100; + }); + + return c * shell.velocity; + }, + }, + + shell_range: { + type: CharacteristicType.Number, + + compute({ shell }) { + return shell.range; + }, + }, + + shell_capacity: { + type: CharacteristicType.Number, + + compute({ gun }) { + return gun.shell_capacity; + }, + }, + + caliber: { + type: CharacteristicType.Number, + + compute({ shell }) { + return shell.caliber; + }, + }, + + ricochet: { + type: CharacteristicType.Number, + + should_render({ shell }) { + return !isExplosive(shell.type); + }, + + compute({ shell }) { + return shell.ricochet ?? 0; + }, + }, + + normalization: { + type: CharacteristicType.Number, + + should_render({ shell }) { + return !isExplosive(shell.type); + }, + + compute({ shell }) { + return shell.normalization ?? 0; + }, + }, + + speed_forward: { + type: CharacteristicType.Number, + compute({ tank }) { + return tank.speed_forwards; + }, + }, + + speed_backward: { + type: CharacteristicType.Number, + compute({ tank }) { + return tank.speed_backwards; + }, + }, + + aim_time: { + type: CharacteristicType.Number, + compute({ gun, turret, track, degressive, script, characteristic }) { + /* + T = (A / C) × ln(√(1 + k² × S)) / (1 − p/100) + + S = (turret rotation speed × turret dispersion factor)² + + (hull rotation speed × hull dispersion factor)² + + (min(top speed, 30) × movement dispersion factor)² + A: the gun’s raw aimingTime—the time for the reticle to shrink by a factor of e ≈ 2.718. + C: gunner crew factor, accounting for training, provisions, and shared crew roles. + k: stabilization multiplier; normally 1, reduced by the stabilizer. + p: signed aiming-drive modifier. For −12%, the displayed time is divided by 1.12, reducing it by approximately 10.7%. + */ + + const topSpeed = characteristic("speed_forward") as number; + + const S = + (turret.traverse_speed * gun.dispersion_traverse) ** 2 + + (track.traverse_speed * track.dispersion_traverse) ** 2 + + (Math.min(topSpeed, 30) * track.dispersion_move) ** 2; + + const A = gun.aim_time; + const C = degressive("gunner"); + + let p = 0; + + script("protagonist", "miscAttrs/gunAimingTimeFactor", (effect) => { + p = effect.bonusValues!.percent / 100; + }); + + let k = 1; + + script( + "protagonist", + "miscAttrs/additiveShotDispersionFactor", + (effect) => { + k += effect.bonusValues!.percent / 100; + }, + ); + + const T = ((A / C) * Math.log(Math.sqrt(1 + k ** 2 * S))) / (1 - p / 100); + + return T; + }, + }, + + dispersion: { + type: CharacteristicType.Number, + compute({ gun, degressive, script }) { + return ( + gun.dispersion_base * + degressive("gunner") * + dispersionCoefficient({ script }) + ); + }, + }, + + // dispersion_angle: unavailable, + + gun_depression: { + type: CharacteristicType.Number, + compute({ state, turret, gun }) { + const turretModel = state.model.turrets[turret.id]; + const gunModel = turretModel.guns[gun.id]; + + return ( + gunModel.pitch!.max + (state.model.initial_turret_rotation?.pitch ?? 0) + ); + }, + }, + + gun_elevation: { + type: CharacteristicType.Number, + compute({ state, turret, gun }) { + const turretModel = state.model.turrets[turret.id]; + const gunModel = turretModel.guns[gun.id]; + + return ( + -gunModel.pitch!.min - (state.model.initial_turret_rotation?.pitch ?? 0) + ); + }, + }, + + azimuth_left: { + type: CharacteristicType.Number, + should_render({ state, turret }) { + return state.model.turrets[turret.id].yaw !== undefined; + }, + compute({ state, turret }) { + return -state.model.turrets[turret.id].yaw!.min; + }, + }, + + azimuth_right: { + type: CharacteristicType.Number, + should_render({ state, turret }) { + return state.model.turrets[turret.id].yaw !== undefined; + }, + compute({ state, turret }) { + return state.model.turrets[turret.id].yaw!.max; + }, + }, + + weight: { + type: CharacteristicType.Number, + compute({ tank, engine, track, turret, gun }) { + return ( + (tank.weight + + engine.weight + + track.weight + + turret.weight + + gun.weight) / + 1000 + ); + }, + }, + + engine_power: { + type: CharacteristicType.Number, + compute({ engine }) { + return engine.power; + }, + }, + + power_to_weight: { + type: CharacteristicType.Number, + compute({ engine, characteristic }) { + const weight = characteristic("weight"); + if (typeof weight !== "number") { + throw new Error("Weight characteristic must be numeric"); } - return c * gun.gun_type!.value.clip_reload; + return engine.power / weight; + }, + }, + + // brake_force: unavailable, + // terrain_coefficient: unavailable, + + hull_traverse_speed: { + type: CharacteristicType.Number, + compute({ track, progressive }) { + return track.traverse_speed * progressive("driver"); + }, + }, + + turret_traverse_speed: { + type: CharacteristicType.Number, + compute({ turret, progressive }) { + return turret.traverse_speed * progressive("gunner"); + }, + }, + + gun_traverse_speed: { + type: CharacteristicType.Number, + compute({ gun, progressive }) { + return gun.rotation_speed * progressive("gunner"); + }, + }, + + health: { + type: CharacteristicType.Number, + compute({ tank, turret, environment, equalizer, script }) { + let c0 = 1; + + script("protagonist", "SmallHPStock", (effect) => { + c0 += effect.hpStockPercent / 100; + }); + + script("protagonist", "HighQualityAssembly", (effect) => { + c0 += effect.maxHpBonusPercent / 100; + }); + + let c1 = 1; + + if (environment.equalize) { + c1 *= equalizer.health; + } + + let health = tank.health + turret.health; + + health *= c0 * c1; + + if (environment.equalize) { + health = 50 * Math.round(health / 50); + } + + return health; + }, + }, + + fire_chance: { + type: CharacteristicType.Number, + compute({ engine }) { + return engine.fire_chance; + }, + }, + + // fire_rate: unavailable, + // ramming_resistance: unavailable, + + view_range: { + type: CharacteristicType.Number, + compute({ turret, progressive }) { + return turret.view_range * progressive("commander"); + }, + }, + + camouflage: { + type: CharacteristicType.Number, + compute({ tank, state }) { + const camouflageBonus = + tank.class === TankClass.TANK_CLASS_TANK_DESTROYER + ? 0.04 + : tank.class === TankClass.TANK_CLASS_HEAVY + ? 0.03 + : 0.02; + + return ( + tank.camouflage_still * (1 + (state.camouflage ? camouflageBonus : 0)) + ); + }, + }, + + width: { + type: CharacteristicType.Number, + compute({ state, turret }) { + const size = normalizeBoundingBox( + unionBoundingBox( + state.model.bounding_box!, + state.model.turrets[turret.id].bounding_box!, + ), + ); + return size.z; + }, + }, + + height: { + type: CharacteristicType.Number, + compute({ state, turret }) { + const size = normalizeBoundingBox( + unionBoundingBox( + state.model.bounding_box!, + state.model.turrets[turret.id].bounding_box!, + ), + ); + return size.x; + }, + }, + + length: { + type: CharacteristicType.Number, + compute({ state, turret }) { + const size = normalizeBoundingBox( + unionBoundingBox( + state.model.bounding_box!, + state.model.turrets[turret.id].bounding_box!, + ), + ); + return size.y; }, }, } satisfies Record; diff --git a/packages/website/src/config/characteristicsGroups.ts b/packages/website/src/config/characteristicsGroups.ts index e84cf8a2b..aa52f49f0 100644 --- a/packages/website/src/config/characteristicsGroups.ts +++ b/packages/website/src/config/characteristicsGroups.ts @@ -110,7 +110,7 @@ export const characteristicsGroups: CharacteristicsGroup[] = [ // render: CharacteristicsGroupRender.Statistical, order: [ { name: "penetration", units: "mm" }, - { name: "penetration_loss_by_distance" }, + { name: "penetration_loss_by_distance", units: "percentage" }, { name: "penetration_loss_after_ricochet" }, { name: "shell_velocity", units: "m_s" }, { name: "shell_range", units: "m" }, @@ -127,7 +127,7 @@ export const characteristicsGroups: CharacteristicsGroup[] = [ name: "accuracy", // render: CharacteristicsGroupRender.Statistical, order: [ - { name: "aim_time", decimals: 1, units: "s" }, + { name: "aim_time", decimals: 2, units: "s" }, { name: "dispersion", decimals: 3, units: "m" }, { name: "dispersion_angle", decimals: 3, units: "deg" }, { toy: "aim_time" }, @@ -178,9 +178,9 @@ export const characteristicsGroups: CharacteristicsGroup[] = [ // render: CharacteristicsGroupRender.Statistical, order: [ { name: "health", units: "hp" }, - { name: "fire_chance", units: "%" }, + { name: "fire_chance", units: "percentage" }, { name: "fire_rate", units: "hp_s" }, - { name: "ramming_resistance", units: "%" }, + { name: "ramming_resistance", units: "percentage" }, { toy: "fire_health_burn" }, @@ -193,7 +193,7 @@ export const characteristicsGroups: CharacteristicsGroup[] = [ // render: CharacteristicsGroupRender.Statistical, order: [ { name: "view_range", units: "m" }, - { name: "camouflage", decimals: 0, units: "%" }, + { name: "camouflage", decimals: 0, units: "percentage" }, { toy: "view_range" }, diff --git a/packages/website/src/hooks/useCharacteristics.ts b/packages/website/src/hooks/useCharacteristics.ts index 1e3dedd21..35dad845f 100644 --- a/packages/website/src/hooks/useCharacteristics.ts +++ b/packages/website/src/hooks/useCharacteristics.ts @@ -1,4 +1,6 @@ +import { degressiveStat, progressiveStat } from "@blitzkit/core"; import type { BlitzEffectScript } from "@blitzkit/core/src/types/blitzEffectScript"; +import { CrewType } from "@blitzkit/protos"; import { api } from "../api/dynamic"; import { characteristics } from "../config/characteristics"; import { defaultEqualizer } from "../config/equalizer"; @@ -44,39 +46,91 @@ export function computeCharacteristics( (shell) => shell.id === states.protagonist.shell, )!; - const equipmentPreset = equipment.presets[tank.equipment_preset]; - - const scriptsMap: Record> = { + const scriptsMap: Record> = { protagonist: new Map(), antagonist: new Map(), }; + function addScript(side: DuelSide, name: string, script: BlitzEffectScript) { + if (!scriptsMap[side].has(name)) { + scriptsMap[side].set(name, []); + } + + scriptsMap[side].get(name)!.push(script); + } + for (const side of duelSides) { + const sideTank = tanks.tanks[states[side].tank]; + const sideEquipmentPreset = equipment.presets[sideTank.equipment_preset]; + for (const id of states[side].consumables) { if (!(id in scripts.consumables)) continue; const script = scripts.consumables[id]; - scriptsMap.protagonist.set(script["#text"], script); + + addScript(side, script["#text"], script); } for (const id of states[side].provisions) { if (!(id in scripts.provisions)) continue; const script = scripts.provisions[id]; - scriptsMap.protagonist.set(script["#text"], script); + + addScript(side, script["#text"], script); } for (const index in states[side].equipment) { const choice = states[side].equipment[index]; - const id = equipmentPreset.slots[index].options[choice]; + const id = sideEquipmentPreset.slots[index].options[choice]; if (!(id in scripts.equipment)) continue; const script = scripts.equipment[id]; - scriptsMap.protagonist.set(script["#text"], script); + + addScript(side, script["#text"], script); } } + const protagonist = states.protagonist; + const protagonistEquipmentPreset = equipment.presets[tank.equipment_preset]; + const hasImprovedVentilation = Object.entries(protagonist.equipment).some( + ([index, choice]) => + protagonistEquipmentPreset.slots[Number(index)]?.options[choice] === 102, + ); + const provisionCrewBonus = + protagonist.provisions.reduce( + (total, id) => + total + + (scripts.provisions[id]?.bonusValues?.crewLevelIncrease ?? 0) / 100, + 0, + ) + (hasImprovedVentilation ? 0.08 : 0); + const commanderMastery = 1 + provisionCrewBonus; + const crewMastery = { + commander: commanderMastery, + loader: + commanderMastery * + (tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_LOADER) + ? 1.1 + : 1.05), + gunner: + commanderMastery * + (tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_GUNNER) + ? 1.1 + : 1.05), + driver: + commanderMastery * + (tank.crew.some(({ type }) => type === CrewType.CREW_TYPE_DRIVER) + ? 1.1 + : 1.05), + }; + + function progressive(role: keyof typeof crewMastery) { + return progressiveStat(crewMastery[role]); + } + function degressive(role: keyof typeof crewMastery) { + return degressiveStat(crewMastery[role]); + } + function characteristic(name: CharacteristicName) { if (!computed.has(name)) { throw new Error(`Characteristic ${name} not computed yet`); @@ -93,7 +147,12 @@ export function computeCharacteristics( callback: (effect: BlitzEffectScript) => void, ) { if (!scriptsMap[side].has(name)) return; - callback(scriptsMap[side].get(name)!); + + const scripts = scriptsMap[side].get(name)!; + + for (const script of scripts) { + callback(script); + } } const context = { @@ -125,6 +184,7 @@ export function computeCharacteristics( } if (!shouldRender) continue; + if (!("compute" in characteristic)) continue; const value = characteristic.compute(context); diff --git a/packages/website/src/tankopedia/createTankEquipment.ts b/packages/website/src/tankopedia/createTankEquipment.ts new file mode 100644 index 000000000..c71b8ec8f --- /dev/null +++ b/packages/website/src/tankopedia/createTankEquipment.ts @@ -0,0 +1,3 @@ +export function createTankEquipment() { + return { 0: 0, 1: 1, 2: 0 }; +} diff --git a/packages/website/src/tankopedia/createTankState.ts b/packages/website/src/tankopedia/createTankState.ts index 967d2fbdf..856c057ac 100644 --- a/packages/website/src/tankopedia/createTankState.ts +++ b/packages/website/src/tankopedia/createTankState.ts @@ -1,6 +1,7 @@ import { createDefaultProvisions } from "@blitzkit/core"; import type { TankDefinition } from "@blitzkit/protos"; import { api } from "../api/dynamic"; +import { createTankEquipment } from "./createTankEquipment"; import { createTankStatus } from "./createTankStatus"; import type { TankState } from "./tankState"; @@ -27,13 +28,9 @@ export function createTankState(tank: TankDefinition) { shell: gun.shells[0].id, track: tank.tracks.at(-1)!.id, - environment: { - assault_distance: (gun.assault_ranges?.ranges[0].distance ?? 0) / 2, - }, - speed: 0, - equipment: {}, + equipment: createTankEquipment(), status: createTankStatus(), model: models.models[tank.id], diff --git a/packages/website/src/types/characteristics.ts b/packages/website/src/types/characteristics.ts index e660074c4..9fc096aaa 100644 --- a/packages/website/src/types/characteristics.ts +++ b/packages/website/src/types/characteristics.ts @@ -34,8 +34,8 @@ export interface CharacteristicContext { equalizer: Equalizer; - progressive: number; - degressive: number; + progressive(role: CrewStatRole): number; + degressive(role: CrewStatRole): number; tank: TankDefinition; engine: EngineDefinition; @@ -55,6 +55,8 @@ export interface CharacteristicContext { ): void; } +export type CrewStatRole = "commander" | "loader" | "gunner" | "driver"; + export enum CharacteristicType { Enum, Number,