Compare commits

...
15 Commits
Author SHA1 Message Date
TrèsAbhi 1558b69931 Merge branch 'main' of https://github.com/blitzkit/blitzkit 2025-08-27 17:19:45 -05:00
TrèsAbhi a560a3d3ae feat: logs 2025-08-27 17:19:40 -05:00
TrèsAbhi b620110d0f New Crowdin updates (#167)
* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)
2025-08-27 11:36:57 -05:00
TrèsAbhi 23ea72bf40 Merge branch 'dev' 2025-08-26 11:29:09 -05:00
TrèsAbhi aaee0b7965 feat: servicing 2025-08-26 11:28:57 -05:00
TrèsAbhi 347523f9e7 feat: burst dpm 2025-08-22 13:38:02 -05:00
TrèsAbhi 3205630a0f feat: recoil 2025-08-22 12:44:34 -05:00
TrèsAbhi 8446e3fbbd feat: flashes 2025-08-22 12:40:26 -05:00
TrèsAbhi 6d25a97fc8 fix: dynamic sizing 2025-08-22 12:23:57 -05:00
TrèsAbhi bdbfce0e18 feat: auto reloader compatibility 2025-08-22 12:21:04 -05:00
TrèsAbhi 8033e8f7cc feat: burst compatiblity 2025-08-21 20:22:12 -05:00
TrèsAbhi f98c801d6c New translations en.json (Russian) (#163) 2025-08-21 13:45:37 -05:00
TrèsAbhi 339663adaf Merge branch 'main' into dev 2025-08-20 11:07:50 -05:00
TrèsAbhi 2b15357dee New Crowdin updates (#162)
* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Japanese)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Russian)
2025-08-19 19:02:36 -05:00
TrèsAbhi f22f4eeccd Update website.yml 2025-08-18 11:56:41 -05:00
23 changed files with 310 additions and 204 deletions

No files matched your search

+2
View File
@@ -29,6 +29,8 @@ env:
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
SERVICING: ${{ secrets.SERVICING }}
jobs:
build:
+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.27.17
This update was released early for the website. Today, it'll be officially announced and released for the bot
## Features
- Added the shell reload visualizer to aid people in getting a feel for how long shell reloads really are
- Added lines of stats for the burst shells
## Changes
- Removed the redundant word "Shell" from "Shell reload" and "Shell reloads"
- Removed the penetration over distance slider
## Fixes
- Fixed the DPM of tanks with burst shells like the ATAC
- The DPM will still be different from the game due to a bug in World of Tanks Blitz; I am trying to get Wargaming to fix it; trust BlitzKit!
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.16",
"version": "1.27.17",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
+20 -5
View File
@@ -17,12 +17,27 @@ export function resolveDpm(
if (gun.gun_type!.$case === 'regular') {
dps = alpha / (reloadCoefficient * gun.gun_type!.value.reload);
} else if (gun.gun_type!.$case === 'auto_loader') {
dps =
(alpha * gun.gun_type!.value.shell_count) /
(gun.gun_type!.value.clip_reload * reloadCoefficient +
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);
gun.gun_type!.value.intra_clip *
intraClipCoefficient;
}
dps = damage / time;
} else {
const mostOptimalShell = gun.gun_type!.value.shell_reloads.reduce<null | {
index: number;
@@ -136,7 +136,6 @@ message GunDefinitionAutoLoader {
required float clip_reload = 1;
required float intra_clip = 2;
required float shell_count = 3;
optional Burst burst = 4;
}
message Burst {
+1 -21
View File
@@ -408,7 +408,6 @@ export interface GunDefinitionAutoLoader {
clip_reload: number;
intra_clip: number;
shell_count: number;
burst?: Burst;
}
export interface Burst {
@@ -3023,7 +3022,7 @@ export const GunDefinitionRegular: MessageFns<GunDefinitionRegular> = {
};
function createBaseGunDefinitionAutoLoader(): GunDefinitionAutoLoader {
return { clip_reload: 0, intra_clip: 0, shell_count: 0, burst: undefined };
return { clip_reload: 0, intra_clip: 0, shell_count: 0 };
}
export const GunDefinitionAutoLoader: MessageFns<GunDefinitionAutoLoader> = {
@@ -3040,9 +3039,6 @@ export const GunDefinitionAutoLoader: MessageFns<GunDefinitionAutoLoader> = {
if (message.shell_count !== 0) {
writer.uint32(29).float(message.shell_count);
}
if (message.burst !== undefined) {
Burst.encode(message.burst, writer.uint32(34).fork()).join();
}
return writer;
},
@@ -3081,14 +3077,6 @@ export const GunDefinitionAutoLoader: MessageFns<GunDefinitionAutoLoader> = {
message.shell_count = reader.float();
continue;
}
case 4: {
if (tag !== 34) {
break;
}
message.burst = Burst.decode(reader, reader.uint32());
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@@ -3109,7 +3097,6 @@ export const GunDefinitionAutoLoader: MessageFns<GunDefinitionAutoLoader> = {
shell_count: isSet(object.shell_count)
? globalThis.Number(object.shell_count)
: 0,
burst: isSet(object.burst) ? Burst.fromJSON(object.burst) : undefined,
};
},
@@ -3124,9 +3111,6 @@ export const GunDefinitionAutoLoader: MessageFns<GunDefinitionAutoLoader> = {
if (message.shell_count !== 0) {
obj.shell_count = message.shell_count;
}
if (message.burst !== undefined) {
obj.burst = Burst.toJSON(message.burst);
}
return obj;
},
@@ -3142,10 +3126,6 @@ export const GunDefinitionAutoLoader: MessageFns<GunDefinitionAutoLoader> = {
message.clip_reload = object.clip_reload ?? 0;
message.intra_clip = object.intra_clip ?? 0;
message.shell_count = object.shell_count ?? 0;
message.burst =
object.burst !== undefined && object.burst !== null
? Burst.fromPartial(object.burst)
: undefined;
return message;
},
};
+5 -3
View File
@@ -1096,8 +1096,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"shellReloads": "Reloads",
"shellReload": "Reload",
"intraClip": "Intra-clip",
"caliber": "Caliber",
"penetration": "Penetration",
@@ -1155,7 +1155,9 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Gun type",
"penetrationAt250m": "Penetration at 250m"
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
+11 -4
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Vídeos de reseña",
"disclaimer": "BlitzKit no respalda ningún vídeo de reseñas.",
"join": "Aplica para el programa de YouTube de %s1"
},
"configuration": {
@@ -973,8 +978,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Proyectiles",
"mostOptimalShellIndex": "Proyectil más óptimo",
"shellReloads": "Tiempos de recarga",
"shellReload": "Tiempo de recarga",
"shellReloads": "Reloads",
"shellReload": "Recarga",
"intraClip": "Intra-clip",
"caliber": "Calibre",
"penetration": "Penetración",
@@ -1032,7 +1037,9 @@
"shellRange": "Rango del proyectil",
"shellCapacity": "Capacidad de proyectiles",
"gunType": "Tipo de cañón",
"penetrationAt250m": "Penetración a 250 m"
"penetrationAt250m": "Penetración a 250 m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+11 -4
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Review videos",
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
@@ -973,8 +978,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"shellReloads": "Reloads",
"shellReload": "Reload",
"intraClip": "Intra-clip",
"caliber": "Caliber",
"penetration": "Penetration",
@@ -1032,7 +1037,9 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Gun type",
"penetrationAt250m": "Penetration at 250m"
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+10 -3
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Review videos",
"disclaimer": "BlitzKitはいかなるレビュー動画も後援していません。",
"join": "%s1 YouTuberプログラムに応募する"
},
"configuration": {
@@ -973,7 +978,7 @@
"dpmEffective": "60秒での有効値",
"shells": "砲弾",
"mostOptimalShellIndex": "最適な弾数",
"shellReloads": "自動装填時間",
"shellReloads": "Reloads",
"shellReload": "装填時間",
"intraClip": "砲弾再装填時間",
"caliber": "口径",
@@ -1032,7 +1037,9 @@
"shellRange": "射程",
"shellCapacity": "総弾数",
"gunType": "主砲タイプ",
"penetrationAt250m": "250m での貫通力"
"penetrationAt250m": "250m での貫通力",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+11 -4
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Review videos",
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
@@ -973,8 +978,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"shellReloads": "Reloads",
"shellReload": "Reload",
"intraClip": "Intra-clip",
"caliber": "Caliber",
"penetration": "Penetration",
@@ -1032,7 +1037,9 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Gun type",
"penetrationAt250m": "Penetration at 250m"
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+11 -4
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Vídeos de análise",
"disclaimer": "BlitzKit não aprova nenhum vídeo de análise.",
"join": "Candidate-se para %s1 um programa YouTuber"
},
"configuration": {
@@ -973,8 +978,8 @@
"dpmEffective": "Efetivo aos 60s",
"shells": "Projéteis",
"mostOptimalShellIndex": "Projétil mais ideal",
"shellReloads": "Recarga de projétil",
"shellReload": "Recarregamento de projétil",
"shellReloads": "Reloads",
"shellReload": "Recarga",
"intraClip": "Intra-clipe",
"caliber": "Calibre",
"penetration": "Penetração",
@@ -1032,7 +1037,9 @@
"shellRange": "Alcance de projétil",
"shellCapacity": "Capacidade de projéteis",
"gunType": "Tipo de arma",
"penetrationAt250m": "Penetração à 250m"
"penetrationAt250m": "Penetração à 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+12 -5
View File
@@ -788,7 +788,7 @@
"tank_seo_description": "%s1 броня, статистика, оборудование, снабжение и многое другое. %s2 УВМ, %s3hp альфа, %s4mm пробитие, %s5км/ч лучшая скорость, %s6hp здоровье.",
"visualizers": {
"assault": {
"title": "Assault shells"
"title": "Штурмовые снаряды"
},
"rotator": {
"hull": "Корпус",
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Промзона",
"dead_rail": "Эшелон"
},
"flexibility": {
"yaw": "Отклонение",
"pitch": "Наклон",
"min": "Вниз",
"max": "Вверх"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Видеообзоры",
"disclaimer": "BlitzKit не несёт ответственности за видеообзоры.",
"join": "Предложить ютубера для программы %s1"
},
"configuration": {
@@ -973,13 +978,13 @@
"dpmEffective": "Эффективность за 60с",
"shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Перезарядка снарядов",
"shellReloads": "Reloads",
"shellReload": "Перезарядка",
"intraClip": "Между снарядами",
"caliber": "Калибр",
"penetration": "Пробитие",
"damage": "Урон",
"damageWithoutAssault": "Damage without assault shells",
"damageWithoutAssault": "Урон без штурмовых снарядов",
"clipDamage": "Урон с барабана",
"moduleDamage": "Урон по модулям",
"explosionRadius": "Разлет осколков",
@@ -1032,7 +1037,9 @@
"shellRange": "Дальность снаряда",
"shellCapacity": "Боезапас",
"gunType": "Тип орудия",
"penetrationAt250m": "Пробитие на 250м"
"penetrationAt250m": "Пробитие на 250м",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+11 -4
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "Ghost Factory",
"dead_rail": "Dead Rail"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "Review videos",
"disclaimer": "BlitzKit does not endorse any review videos.",
"join": "Apply to %s1 YouTuber program"
},
"configuration": {
@@ -973,8 +978,8 @@
"dpmEffective": "Effective at 60s",
"shells": "Снаряди",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Shell reloads",
"shellReload": "Shell reload",
"shellReloads": "Reloads",
"shellReload": "Час перезаряджання",
"intraClip": "Intra-clip",
"caliber": "Калібр",
"penetration": "Бронепробиття",
@@ -1032,7 +1037,9 @@
"shellRange": "Shell range",
"shellCapacity": "Shell capacity",
"gunType": "Тип гармати",
"penetrationAt250m": "Penetration at 250m"
"penetrationAt250m": "Penetration at 250m",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
+11 -4
View File
@@ -810,6 +810,12 @@
"percentage": "%s1%",
"ghost_factory": "幽灵工厂",
"dead_rail": "死亡铁道"
},
"flexibility": {
"yaw": "Yaw",
"pitch": "Pitch",
"min": "Min",
"max": "Max"
}
},
"sandbox": {
@@ -912,7 +918,6 @@
},
"review": {
"title": "回顾影片",
"disclaimer": "BlitzKit 不支援任何影片回顾。",
"join": "应用到 %s1 YouTuber程序"
},
"configuration": {
@@ -973,8 +978,8 @@
"dpmEffective": "60秒内生效",
"shells": "炮弹 ",
"mostOptimalShellIndex": "最优炮弹",
"shellReloads": "弹匣装填",
"shellReload": "炮弹装填",
"shellReloads": "Reloads",
"shellReload": "装填时间",
"intraClip": "炮弹装填",
"caliber": "口径",
"penetration": "穿深",
@@ -1032,7 +1037,9 @@
"shellRange": "射程",
"shellCapacity": "携弹量",
"gunType": "炮管类型",
"penetrationAt250m": "两百五十米处的穿透力"
"penetrationAt250m": "两百五十米处的穿透力",
"burstShells": "Burst shell count",
"burstInterShell": "Burst inter-shell"
}
},
"firepower": {
@@ -1324,7 +1324,13 @@ export async function definitions() {
unlocks: resolveUnlocks(gun.unlocks),
shell_capacity: gun.maxAmmo ?? gunListEntry.maxAmmo,
assault_ranges,
burst:
gun.burst && gun.burst.count > 1
? {
count: gun.burst.count,
interval: 60 / gun.burst.rate,
}
: undefined,
gun_type:
gunType === 'regular'
? {
@@ -1350,13 +1356,6 @@ export async function definitions() {
intra_clip: 60 / gun.clip!.rate,
clip_reload: gun.reloadTime,
shell_count: gunClipCount,
burst:
gun.burst && gun.burst.count > 1
? {
count: gun.burst.count,
interval: 60 / gun.burst.rate,
}
: undefined,
},
},
} satisfies GunDefinition);
@@ -0,0 +1,14 @@
---
import { Heading, Link, Text } from '@radix-ui/themes';
import { PageWrapper } from './PageWrapper';
---
<PageWrapper align="center" justify="center" gap="0">
<Heading>BlitzKit down for servicing</Heading>
<Text color="gray">
<Link color="blue" href="https://discord.gg/nDt7AjGJQH">
Join the official Discord server
</Link>
for updates.
</Text>
</PageWrapper>
@@ -5,18 +5,9 @@ import {
resolvePenetrationCoefficient,
} from '@blitzkit/core';
import { literals } from '@blitzkit/i18n';
import {
Flex,
Heading,
IconButton,
Slider,
Text,
TextField,
Tooltip,
} from '@radix-ui/themes';
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
import { debounce } from 'lodash-es';
import { useEffect, useRef, useState } from 'react';
import { lerp } from 'three/src/math/MathUtils.js';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useLocale } from '../../../../../../hooks/useLocale';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
@@ -41,7 +32,9 @@ export function Firepower({
const setPenetrationDistanceDebounced = debounce((value: number) => {
setPenetrationDistance(value);
}, 500);
const { gun, shell, turret } = Duel.use((state) => state.protagonist);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const shell = Duel.use((state) => state.protagonist.shell);
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
const hasSupercharger = useEquipment(107);
const hasCalibratedShells = useEquipment(103);
@@ -185,73 +178,6 @@ export function Firepower({
)}
<ReloadVisualizer stats={stats} />
{typeof shell.penetration !== 'number' && (
<>
<InfoWithDelta
stats={stats}
indent
decimals={0}
name={literals(
strings.website.tools.tankopedia.characteristics.values
.at_distance,
[`${penetrationDistance}`],
)}
noRanking
value={() =>
lerp(
shell.penetration.near,
shell.penetration.far,
(penetrationDistance * penetrationLossOverDistanceCoefficient) /
500,
) * penetrationCoefficient
}
/>
<Flex align="center" gap="2" style={{ paddingLeft: 24 }}>
<Text color="gray">
{
strings.website.tools.tankopedia.characteristics.values
.at_distance_slider_label
}
</Text>
<Slider
variant="classic"
key={penetrationDistance}
min={0}
max={500}
style={{ flex: 1 }}
defaultValue={[penetrationDistance]}
onValueChange={([value]) => {
setPenetrationDistanceDebounced(value);
if (!penetrationDistanceInput.current) return;
penetrationDistanceInput.current.value = value.toString();
}}
/>
<TextField.Root
variant="classic"
style={{ width: 64 }}
ref={penetrationDistanceInput}
defaultValue={penetrationDistance}
onBlur={() => {
setPenetrationDistance(
Math.min(
parseInt(penetrationDistanceInput.current!.value),
500,
),
);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.currentTarget.blur();
}
}}
>
<TextField.Slot side="right">m</TextField.Slot>
</TextField.Root>
</Flex>
</>
)}
{isExplosive(shell.type) && (
<InfoWithDelta
stats={stats}
@@ -40,7 +40,7 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
<Box
left="50%"
position="absolute"
ml="calc(25% + 2.5rem)"
ml="40%"
width="1.25rem"
height="1.25rem"
style={{
@@ -70,11 +70,9 @@ export function Shell({ index, stats }: ShellProps & StatsAcceptorProps) {
overflow: 'hidden',
maskImage:
'url(/assets/images/tankopedia/visualizers/reload/shell.png)',
// maskComposite: 'exclude',
maskSize: 'contain',
maskRepeat: 'no-repeat',
maskPosition: 'center',
// background: 'red',
}}
>
<Box
@@ -0,0 +1,21 @@
#reload-visualizer-flash {
background: transparent;
transition-duration: 200ms;
}
#reload-visualizer-flash.go {
background: var(--white-a3);
transition: none;
}
.reload {
width: 65%;
height: 65%;
transition-duration: 200ms;
}
.reload.go {
width: 70%;
height: 70%;
transition: none;
}
@@ -1,13 +1,15 @@
import { type Datum, type Serie } from '@nivo/line';
import { Box } from '@radix-ui/themes';
import { Box, Code } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { Quicklime, type QuicklimeCallback } from 'quicklime';
import { useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Var } from '../../../../../../../core/radix/var';
import type { StatsAcceptorProps } from '../TraverseVisualizer';
import { VisualizerCard } from '../VisualizerCard';
import { Shell } from './components/Shell';
import { Target } from './components/Target';
import './index.css';
const PRECISION = 2;
export interface ReloadUpdateData {
reload: number;
@@ -16,29 +18,29 @@ export interface ReloadUpdateData {
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
const T = 60;
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
const reloadCircle = useRef<HTMLDivElement>(null);
const reloadCore = useRef<HTMLDivElement>(null);
const reloadGlow = useRef<HTMLDivElement>(null);
const totalTime = useRef<HTMLSpanElement>(null);
const progressTime = useRef<HTMLSpanElement>(null);
const flash = useRef<HTMLDivElement>(null);
/**
* 3-shell gun example:
*
* 0-1: clip reload
* ~~~: shell shot
* 1-2: intra-clip
* ~~~: shell shot
* 2-3: intra-clip
* ~~~: shell shot & reset to 0
*
* intra-clip time = intraClip * (n - 1)
*/
const state = useRef(0);
const dpm = useRef(0);
const shotPool = useRef(new Set<number>());
const data = useRef<Serie[]>([{ id: 'value', data: [] }]);
const shoot = useCallback(() => {
flash.current?.classList.add('go');
reloadCircle.current?.classList.add('go');
reloadGlow.current?.classList.add('go');
void flash.current?.offsetWidth;
void reloadCircle.current?.offsetWidth;
void reloadGlow.current?.offsetWidth;
flash.current?.classList.remove('go');
reloadCircle.current?.classList.remove('go');
reloadGlow.current?.classList.remove('go');
}, []);
useEffect(() => {
let cancel = false;
@@ -46,37 +48,76 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
let lastState = state.current;
const shellArray = new Array(stats.shells);
function shoot() {
shotPool.current.add(Date.now() / 1000);
dpm.current += stats.damage;
}
function frame() {
if (!totalTime.current || !progressTime.current) return;
const reloadThreshold = stats.shellReloads
? stats.shellReloads.length
: 1;
const now = Date.now() / 1000;
const dt = now - lastT;
let reload;
let shells;
shotPool.current.forEach((t) => {
if (now - t > 60) shotPool.current.delete(t);
});
if (state.current < reloadThreshold) {
const shellReload =
reloadThreshold === 1
? stats.shellReload!
: stats.shellReloads![Math.floor(state.current)];
dpm.current = shotPool.current.size * stats.damage;
if (state.current < 1) {
state.current = Math.min(1, state.current + dt / stats.shellReload!);
reload = state.current;
state.current = Math.min(
reloadThreshold,
state.current + dt / shellReload,
);
reload = state.current % 1;
shells = shellArray.fill(reload);
if (state.current >= 1) shoot();
} else if (state.current < stats.shells) {
if (reloadThreshold > 1) {
shells = shells.map((_, index) =>
Math.max(0, state.current - shellArray.length + index + 1),
);
}
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
PRECISION,
);
if (state.current >= reloadThreshold) shoot();
} else if (state.current < stats.shells + reloadThreshold - 1) {
let interval;
if (
stats.burstShells === 1 ||
Math.floor(state.current) % stats.burstShells === 0
) {
interval = stats.intraClip!;
} else {
interval = stats.burstInterShell!;
}
state.current = Math.min(
stats.shells,
state.current + dt / stats.intraClip!,
stats.shells + reloadThreshold,
state.current + dt / interval,
);
shells = times(stats.shells, (index) => index > state.current - 1);
reload = state.current % 1;
if (reloadThreshold > 1) {
shells = shellArray.map((_, index) =>
Math.max(
0,
Math.floor(index + 1 + reloadThreshold - state.current),
),
);
} else {
shells = times(stats.shells, (index) => index > state.current - 1);
}
totalTime.current.innerHTML = interval.toFixed(PRECISION);
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
PRECISION,
);
if (lastState % 1 > state.current % 1) shoot();
} else {
state.current = 0;
@@ -90,15 +131,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
lastT = now;
lastState = state.current;
(data.current[0].data as Datum[]).push({
x: now,
y: dpm.current / (stats.dpm + stats.damage),
});
(data.current[0].data as Datum[]).splice(
0,
data.current[0].data.findIndex((d) => now - (d.x as number) < T),
);
}
frame();
@@ -151,8 +183,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
position="absolute"
top="50%"
left="50%"
width="12rem"
height="12rem"
className="reload"
ref={reloadGlow}
style={{
@@ -167,8 +197,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
position="absolute"
top="50%"
left="50%"
width="12rem"
height="12rem"
className="reload"
ref={reloadCircle}
style={{
@@ -216,10 +244,52 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
}}
/>
<Code
ref={progressTime}
size="1"
variant="ghost"
weight="bold"
color="gray"
highContrast
style={{
position: 'absolute',
top: '50%',
right: 'calc(50% + var(--space-8))',
transform: 'translateX(50%)',
}}
>
0.00
</Code>
<Code
ref={totalTime}
size="1"
variant="ghost"
weight="bold"
color="gray"
style={{
position: 'absolute',
top: '50%',
left: 'calc(50% + var(--space-8))',
transform: 'translateX(-50%)',
}}
>
0.00
</Code>
{stats.shells > 1 &&
times(stats.shells, (index) => (
<Shell stats={stats} index={index} key={index} />
))}
<Box
id="reload-visualizer-flash"
ref={flash}
position="absolute"
top="0"
left="0"
width="100%"
height="100%"
/>
</VisualizerCard>
);
}
@@ -151,6 +151,8 @@ export function tankCharacteristics(
commanderMastery *
(tank.crew.some(({ type }) => type === CrewType.DRIVER) ? 1.1 : 1.05);
const intraClipCoefficient = coefficient([hasShellReloadBoost, -0.3]);
const burstShells = gun.burst?.count ?? 1;
const burstInterShell = gun.burst?.interval;
const armorDamageCoefficient =
coefficient([hasTungsten, 0.15]) *
coefficient(
@@ -581,6 +583,8 @@ export function tankCharacteristics(
shellCapacity,
gunType,
penetrationAt250m,
burstShells,
burstInterShell,
};
}
@@ -8,6 +8,7 @@ import Navbar from '../components/Navbar.astro';
import Party3 from '../components/Party3.astro';
import './base.css';
import { DEFAULT_LOCALE } from '@blitzkit/i18n';
import Servicing from '../components/Servicing.astro';
export interface Props {
noFooter?: boolean;
@@ -15,6 +16,7 @@ export interface Props {
integrated?: boolean;
}
const servicing = import.meta.env.SERVICING;
const { noFooter, noNavbar, integrated } = Astro.props;
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
---
@@ -65,7 +67,7 @@ const locale = Astro.params.locale ?? DEFAULT_LOCALE;
<Checks {locale} client:only="react" />
<slot />
{servicing ? <Servicing /> : <slot />}
{!noFooter && <Footer />}
</Flex>