Merge branch 'main' into data-driven

This commit is contained in:
tresabhi committed 2026-10-04 16:33:53 -05:00
commit 6ebb2810ad
37 files changed
+1404 -350

No files matched your search

@@ -352,6 +352,15 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
const tankClass =
this.blitzTankClassToBlitzkit[blitzTankClass as BlitzTankClass];
const canonicalFullName = this.getString(tank.userString);
const name =
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? canonicalFullName;
// only supply full name if it's different from the displayed short name to save some bytes
const name_full =
name === canonicalFullName ? undefined : canonicalFullName;
tankDefinitions.tanks[tankId] = {
ancestors: [],
successors: [],
@@ -369,10 +378,8 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
typeof equipment === "string" ? equipment : equipment.at(-1)!,
max_consumables: tankDefinition.root.consumableSlots,
max_provisions: tankDefinition.root.provisionSlots,
name:
(tank.shortUserString
? this.getString(tank.shortUserString)
: undefined) ?? this.getString(tank.userString),
name,
name_full,
slug,
nation,
type: tankTags.includes("collectible")
+11 -2
View File
@@ -1,9 +1,9 @@
import { BlitzKitAPI } from "@blitzkit/core/src/blitzkit/api/base";
import locales from "@blitzkit/i18n/locales.json";
import { I18nString } from "@blitzkit/protos";
import { deburr, times } from "lodash-es";
import { AbstractBlitzKitAPI } from "./api";
export async function fetchTankNames(api: BlitzKitAPI) {
export async function fetchTankNames(api: AbstractBlitzKitAPI) {
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
api.tanks(),
api.camouflages(),
@@ -30,6 +30,15 @@ export async function fetchTankNames(api: BlitzKitAPI) {
});
});
const searchableNameFull: I18nString = {
locales: { ...(tank.name_full?.locales ?? tank.name!.locales) },
};
const searchableNameFullDeburr: I18nString = { locales: {} };
Object.entries(searchableNameFull.locales).forEach(([key, value]) => {
searchableNameFullDeburr.locales[key] = deburr(value);
});
return {
id: tank.id,
slug: tank.slug,
+5 -4
View File
@@ -515,8 +515,7 @@
"title": "",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "Beta"
"equalize": "Equalize"
},
"modules": {
"title": "Módulos",
@@ -572,6 +571,8 @@
"shellRicochet": "Rebote",
"dpm": "DPM",
"dpmEffective": "Effective at 60s",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Proyectiles",
"mostOptimalShellIndex": "Proyectil más óptimo",
"shellReloads": "Reloads",
@@ -623,8 +624,8 @@
"viewRange": "Rango de visión",
"camouflageStill": "Quieto",
"camouflageMoving": "En movimiento",
"camouflageShootingStill": "Quieto",
"camouflageShootingMoving": "En movimiento",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Atrapado en fuego",
"width": "Ancho",
"height": "Altura",
+5 -4
View File
@@ -515,8 +515,7 @@
"title": "Configuration",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "Beta"
"equalize": "Equalize"
},
"modules": {
"title": "Modules",
@@ -572,6 +571,8 @@
"shellRicochet": "Ricochet",
"dpm": "DPM",
"dpmEffective": "Effective at 60s",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
@@ -623,8 +624,8 @@
"viewRange": "View range",
"camouflageStill": "Still",
"camouflageMoving": "Moving",
"camouflageShootingStill": "Still",
"camouflageShootingMoving": "Moving",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Caught on fire",
"width": "Width",
"height": "Height",
+5 -4
View File
@@ -515,8 +515,7 @@
"title": "構成",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "Beta"
"equalize": "Equalize"
},
"modules": {
"title": "モジュール",
@@ -572,6 +571,8 @@
"shellRicochet": "跳弾角度",
"dpm": "DPM",
"dpmEffective": "60秒での有効値",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "砲弾",
"mostOptimalShellIndex": "最適な弾数",
"shellReloads": "Reloads",
@@ -623,8 +624,8 @@
"viewRange": "視認範囲",
"camouflageStill": "静止時",
"camouflageMoving": "移動時",
"camouflageShootingStill": "静止時",
"camouflageShootingMoving": "移動時",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Caught on fire",
"width": "全幅",
"height": "全高",
+5 -4
View File
@@ -515,8 +515,7 @@
"title": "Configuration",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "Beta"
"equalize": "Equalize"
},
"modules": {
"title": "Modules",
@@ -572,6 +571,8 @@
"shellRicochet": "Ricochet",
"dpm": "DPM",
"dpmEffective": "Effective at 60s",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Shells",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
@@ -623,8 +624,8 @@
"viewRange": "View range",
"camouflageStill": "Still",
"camouflageMoving": "Moving",
"camouflageShootingStill": "Still",
"camouflageShootingMoving": "Moving",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Caught on fire",
"width": "Width",
"height": "Height",
+5 -4
View File
@@ -515,8 +515,7 @@
"title": "Configuração",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "Beta"
"equalize": "Equalize"
},
"modules": {
"title": "Módulos",
@@ -572,6 +571,8 @@
"shellRicochet": "Ricochete",
"dpm": "DPM",
"dpmEffective": "Efetivo aos 60s",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Projéteis",
"mostOptimalShellIndex": "Projétil mais ideal",
"shellReloads": "Reloads",
@@ -623,8 +624,8 @@
"viewRange": "Alcance de visão",
"camouflageStill": "Parado",
"camouflageMoving": "Em movimento",
"camouflageShootingStill": "Parado",
"camouflageShootingMoving": "Em movimento",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "Preso em chamas",
"width": "Largura",
"height": "Altura",
+5 -4
View File
@@ -515,8 +515,7 @@
"title": "Конфигурация",
"equalizer": {
"title": "Уравнение",
"equalize": "Уравнять",
"beta": "Бета"
"equalize": "Уравнять"
},
"modules": {
"title": "Модули",
@@ -572,6 +571,8 @@
"shellRicochet": "Рикошет",
"dpm": "ДПМ",
"dpmEffective": "Эффективность за 60с",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Снарядов в барабане",
"mostOptimalShellIndex": "Самый эффективный снаряд",
"shellReloads": "Перезарядка",
@@ -623,8 +624,8 @@
"viewRange": "Радиус обзора",
"camouflageStill": "Когда неподвижен",
"camouflageMoving": "В движении",
"camouflageShootingStill": "После выстрела, если неподвижен",
"camouflageShootingMoving": "После выстрела в движении",
"camouflageShootingStill": "Shooting while still",
"camouflageShootingMoving": "Shooting while moving",
"camouflageCaughtOnFire": "При пожаре",
"width": "Ширина",
"height": "Высота",
+3 -2
View File
@@ -515,8 +515,7 @@
"title": "Configuration",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "Beta"
"equalize": "Equalize"
},
"modules": {
"title": "Модулі",
@@ -572,6 +571,8 @@
"shellRicochet": "Рикошет",
"dpm": "DPM",
"dpmEffective": "Effective at 60s",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "Снаряди",
"mostOptimalShellIndex": "Most optimal shell",
"shellReloads": "Reloads",
+150 -149
View File
@@ -7,13 +7,13 @@
"common": {
"name": "BlitzKit",
"units": {
"m": "{value} 公尺",
"mm": "{value} 毫米",
"deg": "{value} 度°",
"hp": "{value} 生命值",
"m": "{value}m",
"mm": "{value}mm",
"deg": "{value} °",
"hp": "{value}血量",
"x": "x{value}",
"percentage": "{value}%",
"deg_s": "{value}度每秒",
"deg_s": "{value}°/s",
"plus": "+{value}"
},
"gun_types": {
@@ -23,7 +23,7 @@
},
"shells": {
"ap": "穿甲弹",
"ap_cr": "硬式复合穿甲弹",
"ap_cr": "硬芯穿甲弹",
"hc": "破甲弹",
"he": "高爆弹"
},
@@ -38,10 +38,10 @@
},
"branches": {
"beta": "测试版",
"development": "Development",
"opentest": "开放测试",
"preview": "Preview",
"production": "Production"
"development": "开发分支",
"opentest": "公开测试",
"preview": "预览分支",
"production": "生产环境"
},
"locales": {
"en": "英语",
@@ -61,23 +61,23 @@
"3": "平均",
"4": "高于平均",
"5": "好",
"6": "好",
"6": "非常好",
"7": "很好",
"8": "非常好",
"9": "极品"
"8": "强势",
"9": "超强势"
},
"bkni_percentile": {
"0": "垃圾",
"1": "非常差",
"2": "很差",
"3": "差",
"4": "低于平均",
"4": "较差",
"5": "平均",
"6": "较好",
"7": "好",
"8": "很好",
"9": "非常好",
"10": "极品"
"9": "强势",
"10": "超强势"
},
"nations": {
"ussr": "苏联",
@@ -87,8 +87,8 @@
"uk": "英国",
"france": "法国",
"japan": "日本",
"european": "欧盟 (E系)",
"other": "X系"
"european": "泛欧",
"other": "混合系"
},
"nations_adjectives": {
"ussr": "苏联",
@@ -98,14 +98,14 @@
"uk": "英国",
"france": "法国",
"japan": "日本",
"european": "欧盟 (E系)",
"other": "X系"
"european": "泛欧",
"other": "混合系"
},
"tank_class_short": {
"-1": "未知",
"0": "轻型坦克",
"1": "中型坦克",
"2": "重型战车",
"2": "重型坦克",
"3": "坦克歼击车"
},
"tank_class_short_plural_lowercase": {
@@ -129,14 +129,14 @@
},
"regions": {
"normal": {
"com": "北美",
"eu": "欧洲",
"asia": "亚太地区"
"com": "北美服务器",
"eu": "欧洲服务器",
"asia": "亚太服务器"
},
"short": {
"com": "北美",
"com": "美服",
"eu": "欧洲",
"asia": "亚太地区"
"asia": "亚服"
}
},
"leagues": {
@@ -155,7 +155,7 @@
"normalized_losses": "战败率",
"normalized_misses": "未命中数",
"normalized_shots": "开火次数",
"normalized_spotted": "侦查次数",
"normalized_spotted": "侦查数",
"normalized_survived_battles": "存活率",
"normalized_win_and_survived": "胜利并幸存的概率",
"normalized_wins": "胜率",
@@ -178,7 +178,7 @@
"cumulative_shots": "总射击数",
"cumulative_spotted": "总侦查敌人数",
"cumulative_survived_battles": "存活场次",
"cumulative_win_and_survived": "胜利与存活场次",
"cumulative_win_and_survived": "胜利并存活场次",
"cumulative_wins": "胜利",
"cumulative_xp": "总经验",
"cumulative_wn8": "WN8",
@@ -203,27 +203,27 @@
"searching": "搜索中..."
},
"copy_button": {
"copied": "已拷贝!"
"copied": "已复制!"
},
"tank_search": {
"select_dialog_title": "搜索坦克",
"select_dialog_title": "查找坦克",
"swap_dialog_title": "交换坦克",
"clear_filters": "清除筛选",
"login": "登录以使用它",
"search_bar_hint": "搜寻战车……",
"recent": "最近查看的",
"clear_filters": "清空筛选器",
"login": "请先登录",
"search_bar_hint": "查找坦克……",
"recent": "最近查过",
"select_all": "全选",
"count_singular": "1 辆坦克",
"count_plural": "{count} 辆坦克",
"sorting_by": "按 {name} 排序",
"clear": "清除",
"tier": "{tier} 阶",
"clear": "清空",
"tier": "{tier} 级",
"passive": "{name} (被动)",
"active": "{name} (主动)",
"no_tanks_found": {
"body": "未找到坦克。",
"search_again": "请尝试重新搜索。",
"clear_filters": "请尝试清除筛选。"
"clear_filters": "请尝试清空筛选器。"
},
"sort": {
"meta.none": "无",
@@ -231,30 +231,30 @@
"survivability.viewRange": "侦察范围",
"survivability.camouflageStill": "静止中隐蔽值",
"survivability.camouflageMoving": "移动中隐蔽值",
"survivability.camouflageShooting": "射击隐蔽值",
"survivability.camouflageShooting": "射击隐蔽",
"survivability.volume": "体积",
"survivability.length": "长度",
"fire.dpm": "标准炮弹 每分钟伤害血量",
"fire.dpmPremium": "金币炮弹 每分钟伤害值",
"fire.dpm": "银币弹 每分钟伤害",
"fire.dpmPremium": "金币弹 每分钟伤害",
"fire.reload": "装填时间",
"fire.caliber": "口径",
"fire.standardPenetration": "标准炮弹穿透力",
"fire.premiumPenetration": "金币炮弹穿透力",
"fire.tertiaryPenetration": "第三种炮弹穿透力",
"fire.damage": "炮弹伤害值",
"fire.standardPenetration": "银币弹穿深",
"fire.premiumPenetration": "金币弹穿深",
"fire.tertiaryPenetration": "高爆弹穿深",
"fire.damage": "均伤",
"fire.moduleDamage": "模组伤害值",
"fire.shellVelocity": "炮弹飞行速度",
"fire.shellVelocity": "弹速",
"fire.shellRange": "最大射程",
"fire.shellCapacity": "携弹量",
"fire.shellCapacity": "载弹量",
"fire.aimTime": "瞄准时间",
"fire.dispersionStill": "一百米处扩散度",
"fire.dispersionMoving": "移动时扩散度",
"fire.dispersionStill": "百米精度",
"fire.dispersionMoving": "移动扩圈",
"fire.gunDepression": "俯角",
"fire.gunElevation": "仰角",
"maneuverability.forwardsSpeed": "最高前进速度",
"maneuverability.backwardsSpeed": "最高后退速度",
"maneuverability.forwardsSpeed": "前进极速",
"maneuverability.backwardsSpeed": "后退极速",
"maneuverability.power": "引擎功率",
"maneuverability.powerToWeight": "功重比",
"maneuverability.powerToWeight": "推重比",
"maneuverability.weight": "重量",
"maneuverability.traverseSpeed": "转速"
},
@@ -266,10 +266,10 @@
"label": "排序依据",
"fire": "开火",
"maneuverability": "机动性",
"survivability": "生存能力"
"survivability": "生存性"
},
"direction": {
"label": "排序算法",
"label": "排序方式",
"ascending": "升序",
"descending": "倒序"
}
@@ -301,8 +301,8 @@
},
"docs": {
"patches": {
"name": "BlitzKit 变更日志",
"seo_description": "BlitzKit Discord 机器人和网站的变更日志"
"name": "BlitzKit 更新日志",
"seo_description": "BlitzKit Discord 机器人和网站的更新日志"
}
},
"tools": {
@@ -339,44 +339,44 @@
"pool_size": "{count} 辆可生成的坦克"
},
"mixer": {
"name": "组装",
"name": "拼装器",
"button": "混合搭配",
"description": "混搭不同坦克间的模块",
"seo_description": "《坦克世界:闪击战》的车体, 炮塔, 主炮混合搭配",
"seo_description": "混搭WoTB的坦克车体、炮塔与主炮",
"screenshot": "屏幕截图",
"randomize": "随机搭配"
},
"guess": {
"name": "猜测",
"button": "这会是什么?",
"button": "这可能是什么?",
"description": "坦克剪影小游戏",
"seo_description": "看剪影猜坦克",
"search": {
"placeholder": "输入你的猜想……",
"placeholder": "输入你的猜测……",
"skip": "跳过",
"guess": "猜测",
"next": "下一个",
"no_results": "沒有結果"
"no_results": "无结果"
},
"stats": "猜对了 {correct} / {total}",
"stats": "猜对了 {correct} / {total},连对 {streak} 个",
"help": {
"title": "显示坦克?",
"description": "显示坦克而不显示名字。这轮会被视为不正确,并打破你的连续记录!",
"title": "部分揭示坦克?",
"description": "揭示坦克但不显示答案。本轮会被视为答错,并中断连对!",
"cancel": "取消",
"reveal": "显示"
"reveal": "揭示"
},
"clear": "清除",
"select_all": "全选",
"tier": "{tier} 阶"
"tier": "{tier} 级"
},
"tankopedia": {
"search": "搜索属性……",
"name": "战车百科",
"button": "搜索战车",
"description": "闪击战坦克百科全书",
"seo_description": "《坦克世界:闪击战》的所有坦克参数, 装甲, 等等...",
"tank_seo_title": "{name} - {tier} 阶 {nationality} {class} | {website} 坦克百科 | 坦克世界闪击战 (WoTB)",
"tank_seo_description": "{name} 装甲,统计资料,装备,规定配给等。 {dpm} 每分钟伤害, {alpha} 伤害值, {penetration}毫米 穿透, {speed}公里/小時 最高速度, {health} 生命值。",
"name": "坦克百科",
"button": "搜索坦克",
"description": "WoTB坦克百科",
"seo_description": "WoTB所有坦克的参数、 装甲等",
"tank_seo_title": "{name} - {tier} 级 {nationality} {class} | {website} 坦克百科 | 坦克世界闪击战(坦闪/WoTB)",
"tank_seo_description": "{name} 装甲、参数、配件、补给等。 {dpm} 每分钟伤害, {alpha} 均伤, {penetration}mm 穿深, {speed}km/h 极速, {health} 血量。",
"visualizers": {
"reload": {
"skip": "跳过"
@@ -387,15 +387,15 @@
"rotator": {
"hull": "车体",
"turret": "炮塔",
"hard": "硬地质",
"medium": "中性地質",
"soft": "軟地質",
"effective": "效率"
"hard": "公路",
"medium": "平地",
"soft": "水面",
"effective": "等效"
},
"ricochet": {
"angle": "角度",
"normalization": "转正",
"effective_angle": "有效角度",
"effective_angle": "有效倾角",
"armor": "装甲",
"no_ricochet": "高爆弹不会跳弹"
},
@@ -407,20 +407,20 @@
"flexibility": {
"yaw": "水平角度",
"pitch": "俯仰角度",
"min": "最少俯角",
"max": "最大仰角"
"min": "极限俯角",
"max": "极限仰角"
}
},
"sandbox": {
"poses": {
"hull_down": "俯角视角",
"face_hug": "抱脸视角",
"face_hug": "顶牛视角",
"default": "默认"
},
"settings": {
"armor": "装甲",
"green_penetration": "可击穿区域",
"hide_model_under_armor": "隐藏装甲下的模型",
"green_penetration": "绿色可击穿",
"hide_model_under_armor": "隐藏坦克模型",
"opaque": "不透明",
"dev_wireframe": "开发选项:网格"
},
@@ -454,14 +454,14 @@
"track": "履带"
},
"status": {
"penetration": "{damage}生命值 击穿",
"penetration": "击穿:{damage}血量",
"blocked": "抵挡伤害",
"ricochet": "跳弹",
"splash": "{damage}生命值 溅射"
"splash": "溅射:{damage}血量"
},
"stats": {
"thickness_and_angle": "{thickness}毫米 {angle}度°",
"nominal": "{thickness}毫米 装甲厚度",
"thickness_and_angle": "{thickness}mm/倾角{angle}°",
"nominal": "{thickness}mm 垂直厚度",
"ricochet_loss": "{loss}% 击穿"
}
}
@@ -490,9 +490,9 @@
"nation": "国别",
"class": "坦克车型",
"tier": "阶级",
"type": "战车类型",
"type": "坦克类型",
"dev_id": "开发参数:ID",
"dev_name": "DEV: NAME",
"dev_name": "开发者名称",
"purchase": "购买价格",
"restoration": "恢复坦克价格",
"sale": "出售价格",
@@ -500,9 +500,9 @@
},
"tech_tree": {
"title": "科技树",
"battle": "一场比赛",
"battles": "{count} 战斗场次",
"route": "路线 {index} / {count}"
"battle": "1场比赛",
"battles": "{count} 场次",
"route": "第 {index} / {count} 种科技线"
},
"game_modes": {
"title": "游戏模式技能"
@@ -514,33 +514,32 @@
"configuration": {
"title": "配置设定",
"equalizer": {
"title": "Equalizer",
"equalize": "Equalize",
"beta": "测试版"
"title": "平衡器",
"equalize": "平衡"
},
"modules": {
"title": "坦克模块",
"stock": "白板",
"upgrade": "顶级配置"
"upgrade": "满配"
},
"equipment": {
"title": "装备",
"clear": "清除"
},
"provisions": {
"title": "规定配给",
"title": "补给",
"clear": "清除"
},
"consumables": {
"title": "消耗品",
"clear": "清除",
"info": "黄色高亮消耗品不影响坦克参数。"
"info": "以黄色标识的消耗品不影响坦克参数。"
},
"skills": {
"title": "乘员技能",
"clear": "清除",
"maximize": "最大化",
"info": "黄色高亮技能只适用于特殊情况。 按空格键可在0到7级之间快速切换。"
"info": "以黄色标识的技能只在特定情形起效。 按空格键可在0到7级之间快速切换。"
},
"miscellaneous": {
"title": "杂项"
@@ -550,48 +549,50 @@
"title": "属性",
"compare": {
"label": "比较",
"class": "{tier} 阶的 {class}",
"tier": "所有{tier} 阶的",
"class": "{tier}级{class}",
"tier": "所有{tier}级坦克",
"all": "所有坦克"
},
"values": {
"shell_index": "炮弹 {index}",
"at_distance": "在 {distance} 公尺",
"at_distance": "距离{distance}m",
"at_distance_slider_label": "距离",
"dispersion_at_distance": "在 {distance} 公尺的扩散度",
"gun_flexibility": "炮管转动界限",
"dispersion_at_distance": "在 {distance}m 的精度",
"gun_flexibility": "主炮俯仰角",
"speed": "速度",
"terrain_coefficients": "地形系数",
"terrain_coefficients": "地形适应性",
"raw_terrain_coefficients": "原始地形系数",
"power_to_weight_ratio": "功重比",
"power_to_weight_ratio": "推重比",
"hull_traverse_speed": "车体转向速率",
"camouflage": "迷彩",
"camouflage": "涂装",
"reload": "装填时间",
"crewCount": "乘员人数",
"shellNormalization": "转正",
"shellRicochet": "跳弹",
"dpm": "每分钟伤害",
"dpmEffective": "60秒内生效",
"dpmEffective": "60秒内实际输出",
"peekabooDpm": "Peekaboo DPM",
"peekabooDpmInfo": "Damage per minute assuming you only fire after resetting camouflage (or the reload finishes, if it's slower) — at least 10 seconds between shots.",
"shells": "炮弹 ",
"mostOptimalShellIndex": "最优炮弹",
"shellReloads": "装填时间",
"shellReload": "装填时间",
"intraClip": "炮弹装填",
"intraClip": "短装",
"caliber": "口径",
"penetration": "穿深",
"damage": "伤害",
"damageWithoutAssault": "无突击弹的伤害",
"clipDamage": "弹匣伤害期望值",
"moduleDamage": "模组伤害",
"clipDamage": "总爆发",
"moduleDamage": "模块伤害",
"explosionRadius": "溅射半径",
"shellVelocity": "炮弹飞行速度",
"aimTime": "瞄准时间",
"dispersion": "静止時",
"dispersionMoving": "移动时",
"dispersionHullTraversing": "车体转向时",
"dispersionTurretTraversing": "炮塔转向时",
"dispersionTurretTraversing": "炮转扩圈",
"dispersionShooting": "开火时",
"dispersionGunDamaged": "主炮损坏时",
"dispersionGunDamaged": "主炮损坏扩圈",
"gunDepression": "俯角",
"gunElevation": "仰角",
"gunFrontalDepression": "车体前部俯角",
@@ -600,42 +601,42 @@
"gunRearElevation": "车体后部仰角",
"azimuthLeft": "左射界",
"azimuthRight": "右射界",
"speedForwards": "最高前进速度",
"speedBackwards": "最高后退速度",
"hardTerrainCoefficient": "硬地质",
"hardTerrainCoefficientRaw": "硬地质",
"mediumTerrainCoefficient": "中性地质",
"mediumTerrainCoefficientRaw": "中性地质",
"softTerrainCoefficient": "软地质",
"softTerrainCoefficientRaw": "软地质",
"speedForwards": "前进极速",
"speedBackwards": "后退极速",
"hardTerrainCoefficient": "公路",
"hardTerrainCoefficientRaw": "公路-原始",
"mediumTerrainCoefficient": "平地",
"mediumTerrainCoefficientRaw": "平地-原始",
"softTerrainCoefficient": "水面",
"softTerrainCoefficientRaw": "水面-原始",
"enginePower": "引擎功率",
"powerToWeightRatioHardTerrain": "硬地质",
"powerToWeightRatioMediumTerrain": "中性地质",
"powerToWeightRatioSoftTerrain": "软地质",
"powerToWeightRatioHardTerrain": "公路实际推重",
"powerToWeightRatioMediumTerrain": "平地实际推重",
"powerToWeightRatioSoftTerrain": "水面实际推重",
"weight": "重量",
"turretTraverseSpeed": "炮塔转向速率",
"gunTraverseSpeed": "炮管转向速率",
"hullTraverseHardTerrain": "硬地质",
"hullTraverseMediumTerrain": "中性地质",
"hullTraverseSoftTerrain": "软地质",
"health": "生命值",
"fireChance": "着火概率",
"hullTraverseHardTerrain": "公路上车转速率",
"hullTraverseMediumTerrain": "平地上车转速率",
"hullTraverseSoftTerrain": "水面上车转速率",
"health": "血量",
"fireChance": "发动机受击时着火概率",
"viewRange": "视野范围",
"camouflageStill": "静止时",
"camouflageStill": "静止隐蔽",
"camouflageMoving": "移动时",
"camouflageShootingStill": "静止时",
"camouflageShootingMoving": "移动时",
"camouflageCaughtOnFire": "着火时",
"camouflageShootingMoving": "移动开火隐蔽",
"camouflageCaughtOnFire": "着火时隐蔽",
"width": "宽度",
"height": "高度",
"length": "长度",
"volume": "体积",
"shellRange": "射程",
"shellCapacity": "携弹量",
"gunType": "炮管类型",
"penetrationAt250m": "两百五十米处的穿透力",
"burstShells": "爆破计数",
"burstInterShell": "爆破弹"
"shellCapacity": "载弹量",
"gunType": "主炮类型",
"penetrationAt250m": "250m处穿深",
"burstShells": "小弹夹发数",
"burstInterShell": "小弹夹短装"
}
},
"firepower": {
@@ -662,7 +663,7 @@
"name": "比较",
"button": "坦克比较",
"description": "比较坦克参数",
"seo_description": "比较《坦克世界:闪击战》中的坦克参数和装备配置",
"seo_description": "比较坦克世界闪击战中的坦克参数和装备配置",
"actions": {
"add": {
"button": "添加",
@@ -692,15 +693,15 @@
},
"modules": {
"stock": "白板",
"upgrade": "顶级配置"
"upgrade": "满配"
},
"provisions": {
"clear": "清除",
"clear_all": "清除所有坦克"
"clear": "清空此坦克",
"clear_all": "清空所有坦克"
},
"consumables": {
"clear": "清除",
"clear_all": "清除所有坦克"
"clear": "清空此坦克",
"clear_all": "清空所有坦克"
}
}
},
@@ -708,7 +709,7 @@
"name": "性能表现",
"button": "查看坦克",
"description": "坦克性能统计",
"seo_description": "最好的坦克及其在《坦克世界:闪击战》中的表现",
"seo_description": "最好的坦克及其在坦克世界闪击战中的数据",
"estimation": {
"body": "基于 {count} 个玩家的估计数据。更新于 {age} 之前。",
"hours": "{count} 小时",
@@ -726,15 +727,15 @@
"winrate": "胜率",
"players": "玩家",
"damage": "伤害",
"survival": "生存性",
"survival": "生存率",
"xp": "经验值",
"kills": "击杀数",
"spots": "侦查次数",
"spots": "侦查数",
"accuracy": "命中率",
"shots": "开火次数",
"hits": "命中数",
"damage_ratio": "伤害效率",
"damage_taken": "承受伤害值",
"damage_ratio": "伤害比率",
"damage_taken": "承受伤害",
"capture_points": "占领值"
}
}
@@ -743,11 +744,11 @@
"name": "图库",
"button": "浏览全部",
"description": "Blitz玩家头像",
"seo_description": "所有《坦克世界:闪击战》玩家头像",
"seo_description": "所有坦克世界闪击战玩家头像",
"last_updated": "最后更新于 {time}",
"search": {
"hint": "搜索頭像……",
"results": "{count} 头像"
"hint": "搜索头像……",
"results": "{count}个头像"
}
},
"session": {
@@ -27,8 +27,6 @@ export const DynamicArmor = memo(() => {
const newRenderSize = new Vector2();
useFrame(({ gl, camera, scene }) => {
gl.autoClear = true;
gl.getSize(newRenderSize).multiplyScalar(gl.getPixelRatio());
if (!newRenderSize.equals(renderSize)) {
@@ -50,6 +48,8 @@ export const DynamicArmor = memo(() => {
gl.clearDepth();
gl.render(primaryArmorScene, camera);
gl.autoClear = true;
}, 1);
return (
+7 -7
View File
@@ -2,8 +2,7 @@
import { assertSecret } from "@blitzkit/core";
import { Box, Code, Flex, Heading, Link, Text } from "@radix-ui/themes";
import packageJson from "../../../../package.json";
import { api } from "../core/blitzkit/api";
import { getStrings } from "../core/i18n/getStrings";
import { api } from "../api/dynamic";
import { LinkI18n } from "./LinkI18n";
import { LocaleSwitcherThemeWrapper } from "./LocaleSwitcher";
@@ -12,9 +11,10 @@ export interface Props {
}
const gameDefinitions = await api.game();
const gameVersion = gameDefinitions.version.split("_")[0];
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
const { locale } = Astro.props;
const strings = getStrings(locale);
const strings = await api.strings(locale);
---
<style>
@@ -141,7 +141,7 @@ const strings = getStrings(locale);
</Text>
)
}
<Text size="2" color="gray">CDN</Text>
<Text size="2" color="gray">Branch</Text>
</Flex>
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
@@ -151,7 +151,7 @@ const strings = getStrings(locale);
{
environment !== "preview" && (
<Code size="2" color="gray">
{gameDefinitions.version}
{gameVersion}
</Code>
)
}
@@ -174,13 +174,13 @@ const strings = getStrings(locale);
WoTB
</Text>
<Code size="2" color="gray">
{gameDefinitions.version}
{gameVersion}
</Code>
</>
)
}
<Text size="2" color="gray">CDN</Text>
<Text size="2" color="gray">Branch</Text>
<Code size="2" color="gray">
{environment}
</Code>
@@ -5,7 +5,7 @@ import {
} from "@blitzkit/core";
import { invalidate, useFrame } from "@react-three/fiber";
import type { QuicklimeEvent } from "quicklime";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import {
MeshBasicMaterial,
NormalBlending,
@@ -43,59 +43,61 @@ export function PrimaryArmorSceneComponent({
node,
thickness,
}: PrimaryArmorSceneComponentProps) {
const material = new ShaderMaterial({
fragmentShader,
vertexShader,
const material = useRef(
new ShaderMaterial({
fragmentShader,
vertexShader,
fog: true,
transparent: true,
fog: true,
transparent: true,
blending: isHalloween ? SubtractiveBlending : NormalBlending,
blending: isHalloween ? SubtractiveBlending : NormalBlending,
uniforms: UniformsUtils.merge([
UniformsLib.common,
UniformsLib.fog,
uniforms: UniformsUtils.merge([
UniformsLib.common,
UniformsLib.fog,
{
thickness: { value: null },
penetration: { value: null },
caliber: { value: null },
ricochet: { value: null },
normalization: { value: null },
isExplosive: { value: null },
canSplash: { value: null },
damage: { value: null },
explosionRadius: { value: null },
greenPenetration: { value: null },
advancedHighlighting: { value: null },
opaque: { value: null },
{
thickness: { value: null },
penetration: { value: null },
caliber: { value: null },
ricochet: { value: null },
normalization: { value: null },
isExplosive: { value: null },
canSplash: { value: null },
damage: { value: null },
explosionRadius: { value: null },
greenPenetration: { value: null },
advancedHighlighting: { value: null },
opaque: { value: null },
inverseProjectionMatrix: { value: null },
resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null },
inverseProjectionMatrix: { value: null },
resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null },
opacity: { value: 0 },
},
]),
});
opacity: { value: 0 },
},
]),
}),
);
useEffect(() => {
function handleShellChange() {
const shell =
Tankopedia.state.customShell ?? getDuel("antagonist", DuelModule.Shell);
material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad(
material.current.uniforms.caliber.value = shell.caliber;
material.current.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!,
);
material.uniforms.normalization.value = degToRad(
material.current.uniforms.normalization.value = degToRad(
shell.normalization ?? 0,
);
material.uniforms.isExplosive.value = isExplosive(shell.type);
material.uniforms.canSplash.value = canSplash(shell.type);
material.uniforms.damage.value = shell.armor_damage;
material.uniforms.explosionRadius.value = shell.explosion_radius;
material.current.uniforms.isExplosive.value = isExplosive(shell.type);
material.current.uniforms.canSplash.value = canSplash(shell.type);
material.current.uniforms.damage.value = shell.armor_damage;
material.current.uniforms.explosionRadius.value = shell.explosion_radius;
handleProtagonistEquipmentChange(true);
handleAntagonistEquipmentChange(true);
@@ -103,17 +105,18 @@ export function PrimaryArmorSceneComponent({
invalidate();
}
function handleGreenPenetrationChange(greenPenetration: boolean) {
material.uniforms.greenPenetration.value = greenPenetration;
material.current.uniforms.greenPenetration.value = greenPenetration;
}
function handleAdvancedHighlightingChange(advancedHighlighting: boolean) {
material.uniforms.advancedHighlighting.value = advancedHighlighting;
material.current.uniforms.advancedHighlighting.value =
advancedHighlighting;
invalidate();
}
function handleOpaqueChange(opaque: boolean) {
material.uniforms.opaque.value = opaque;
material.current.uniforms.opaque.value = opaque;
}
function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe;
material.current.wireframe = wireframe;
}
function handleProtagonistEquipmentChange(noInvalidate = false) {
const equipment = Tankopedia.state.protagonist.equipment;
@@ -128,7 +131,7 @@ export function PrimaryArmorSceneComponent({
? protagonist.equalizer?.armor
: undefined) ?? 1;
material.uniforms.thickness.value =
material.current.uniforms.thickness.value =
thickness * (hasEnhancedArmor ? 1.04 : 1) * equalizer;
if (!noInvalidate) invalidate();
@@ -148,7 +151,7 @@ export function PrimaryArmorSceneComponent({
equipment,
);
material.uniforms.penetration.value =
material.current.uniforms.penetration.value =
penetration *
resolvePenetrationCoefficient(
hasCalibratedShells,
@@ -161,7 +164,7 @@ export function PrimaryArmorSceneComponent({
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
material.uniforms.opacity.value = event.data;
material.current.uniforms.opacity.value = event.data;
}
handleShellChange();
@@ -212,13 +215,14 @@ export function PrimaryArmorSceneComponent({
}, []);
useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getSize(material.current.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio(),
);
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value =
material.current.uniforms.spacedArmorBuffer.value =
spacedArmorRenderTarget.texture;
material.current.uniforms.spacedArmorDepth.value =
spacedArmorRenderTarget.depthTexture;
material.uniforms.inverseProjectionMatrix.value =
material.current.uniforms.inverseProjectionMatrix.value =
camera.projectionMatrixInverse;
});
@@ -243,7 +247,7 @@ export function PrimaryArmorSceneComponent({
{...props}
key={`${key}-include`}
renderOrder={1}
material={material}
material={material.current}
/>
);
},
@@ -88,7 +88,7 @@ export function SpacedArmorSceneComponent({
const { customShell } = Tankopedia.state;
const shell = customShell ?? getDuel("protagonist", DuelModule.Shell);
const { equalize } = Tankopedia.state;
const { equalize } = Tankopedia.state.environment;
const protagonistEqualizer =
(equalize ? protagonist.equalizer : undefined) ?? defaultEqualizer;
const antagonistEqualizer =
@@ -382,6 +382,13 @@ export function SpacedArmorSceneComponent({
shot.damage *= antagonistEqualizer.damage;
const hasSpallLiner = Duel.state.protagonist.provisions.includes(
SPALL_LINER_PROVISION_ID,
);
if (hasSpallLiner && shell.type === ShellType.SHELL_TYPE_HE) {
shot.damage *= 1 + SPALL_LINER_HE_DAMAGE_DELTA;
}
return shot;
},
[camera],
@@ -746,6 +746,7 @@ function OwnershipFilterInternal() {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showOwned = !draft.showOwned;
if (draft.showOwned) draft.showUnowned = false;
});
}}
>
@@ -765,6 +766,7 @@ function OwnershipFilterInternal() {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showUnowned = !draft.showUnowned;
if (draft.showUnowned) draft.showOwned = false;
});
}}
>
@@ -791,6 +793,7 @@ function TestFilter() {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showNonTesting = !draft.showNonTesting;
if (draft.showNonTesting) draft.showTesting = false;
});
}}
>
@@ -811,6 +814,7 @@ function TestFilter() {
onClick={() => {
TankFilters.mutate((draft) => {
draft.showTesting = !draft.showTesting;
if (draft.showTesting) draft.showNonTesting = false;
});
}}
>
@@ -1,6 +1,14 @@
import { alias, isExplosive } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
import { InfoCircledIcon } from "@radix-ui/react-icons";
import {
Flex,
Heading,
IconButton,
Popover,
Text,
Tooltip,
} from "@radix-ui/themes";
import { useLocale } from "../../../../../../hooks/useLocale";
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
import { Duel } from "../../../../../../stores/duel";
@@ -9,7 +17,7 @@ import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
import { Info } from "./Info";
import { InfoWithDelta } from "./InfoWithDelta";
import { ReloadVisualizer } from "./ReloadVisualizer";
import { RELOAD_PRECISION, ReloadVisualizer } from "./ReloadVisualizer";
import { RicochetVisualizer } from "./RicochetVisualizer";
import { StatsTableWrapper } from "./StatsTableWrapper";
import type { StatsAcceptorProps } from "./TraverseVisualizer";
@@ -77,6 +85,40 @@ export function Firepower({
{strings.common.gun_types[gun.gun_type!.$case]}
</Info>
<InfoWithDelta stats={stats} decimals={0} value="dpm" />
{gun.gun_type!.$case === "regular" && (
<InfoWithDelta
stats={stats}
indent
decimals={0}
noRanking
name={
<Flex align="center" gap="1" display="inline-flex">
{
strings.website.tools.tankopedia.characteristics.values
.peekabooDpm
}
<Popover.Root>
<Popover.Trigger>
<IconButton variant="ghost" size="1">
<InfoCircledIcon />
</IconButton>
</Popover.Trigger>
<Popover.Content style={{ maxWidth: 280 }}>
<Text size="2">
{
strings.website.tools.tankopedia.characteristics.values
.peekabooDpmInfo
}
</Text>
</Popover.Content>
</Popover.Root>
</Flex>
}
value={(tank) => tank.peekabooDpm}
/>
)}
<InfoWithDelta stats={stats} decimals={0} value="damage" />
{gun.gun_type!.$case !== "regular" && (
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
@@ -121,7 +163,7 @@ export function Firepower({
.shell_index,
{ index: index + 1 },
)}
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
noRanking
value={() => reload}
@@ -131,7 +173,7 @@ export function Firepower({
) : (
<InfoWithDelta
stats={stats}
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
value="shellReload"
/>
@@ -141,7 +183,7 @@ export function Firepower({
<InfoWithDelta
stats={stats}
indent
decimals={2}
decimals={RELOAD_PRECISION}
deltaType="lowerIsBetter"
value="intraClip"
/>
@@ -6,7 +6,6 @@ import {
modelTransformEvent,
type ModelTransformEventData,
} from "../../../../../../../../core/blitzkit/modelTransform";
import { Duel } from "../../../../../../../../stores/duel";
import { ModelChunk } from "./ModelChunk";
const modelDefinitions = await api.models();
@@ -21,6 +20,13 @@ export function CanvasContent() {
const tankModel = modelDefinitions.models[tank.id];
const turretModel = tankModel.turrets[turret.id];
// baked-in turret mount tilt (e.g. Minotauro sits pitched 3deg forward);
// applied as a static base rotation so the live gun pitch composes on top
// of it, mirroring useTankTransform.ts's turretContainer/gunContainer nesting
const initialTurretPitch = -degToRad(
tankModel.initial_turret_rotation?.pitch ?? 0,
);
useEffect(() => {
function handleModelTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>,
@@ -45,10 +51,19 @@ export function CanvasContent() {
</group>
</group>
<ModelChunk only="turret" />
<group
position={[0, turretModel.gun_origin!.y, turretModel.gun_origin!.z]}
rotation={[initialTurretPitch, 0, 0]}
>
<group
position={[0, -turretModel.gun_origin!.y, -turretModel.gun_origin!.z]}
>
<ModelChunk only="turret" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
<group ref={gunWrapper}>
<ModelChunk only="gun" />
</group>
</group>
</group>
</>
);
@@ -48,13 +48,27 @@ export function GunFlexibilityVisualizer({
const turretModel = tankModel.turrets[turret.id];
const gunModel = turretModel.guns[gun.id];
// turret's own baked-in mount tilt (e.g. Minotauro's turret sits pitched
// 3deg forward on the hull); must be folded into displayed pitch values
// the same way tankCharacteristics.ts and QuickInputs do
const initialTurretPitch = degToRad(
tankModel.initial_turret_rotation?.pitch ?? 0,
);
// radial map is in the tilt-compensated frame so the neutrality circle sits
// at the displayed 0deg
const tiltDeg = radToDeg(initialTurretPitch);
const magTilted = (deg: number) => mag(deg + tiltDeg);
const [yaw, setYaw] = useState(0);
const [pitch, setPitch] = useState(0);
const [minPitch, setMinPitch] = useState(
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max),
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max) -
initialTurretPitch,
);
const [maxPitch, setMaxPitch] = useState(
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min) -
initialTurretPitch,
);
const container = useRef<HTMLDivElement>(null);
@@ -70,7 +84,7 @@ export function GunFlexibilityVisualizer({
let d = "";
// right
const mo = mag(p.max);
const mo = magTilted(p.max);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mo)}`;
@@ -87,7 +101,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.max);
const m = magTilted(f.max);
d += `L ${c(90 - f.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
}
@@ -108,7 +122,7 @@ export function GunFlexibilityVisualizer({
// back
if (!y && b) {
const m = mag(b.max);
const m = magTilted(b.max);
d += `L ${c(-90 - b.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
}
@@ -116,7 +130,7 @@ export function GunFlexibilityVisualizer({
d += `Z`;
// right
const mi = mag(p.min);
const mi = magTilted(p.min);
if (y) {
d += `M 0 0`;
d += `L ${c(90 - y.max, mi)}`;
@@ -133,7 +147,7 @@ export function GunFlexibilityVisualizer({
// front
if (f) {
const m = mag(f.min);
const m = magTilted(f.min);
d += `L ${c(90 - f.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
}
@@ -154,14 +168,14 @@ export function GunFlexibilityVisualizer({
// back
if (!y && b) {
const m = mag(b.min);
const m = magTilted(b.min);
d += `L ${c(-90 - b.range / 2, m)}`;
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
}
d += `Z`;
const r = mag(radToDeg(-pitch));
const r = mag(radToDeg(-(pitch - initialTurretPitch)));
const dotX = r * Math.sin(yaw);
const dotY = r * Math.cos(yaw);
@@ -251,13 +265,13 @@ export function GunFlexibilityVisualizer({
yaw = min[1];
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
t = clamp(t, 0, 1);
let pitch = degToRad(magInverse(-t));
let pitch = degToRad(magInverse(-t)) + initialTurretPitch;
pitch = clamp(pitch, min[0], max[0]);
setPitch(pitch);
setYaw(yaw);
setMinPitch(min[0]);
setMaxPitch(max[0]);
setMinPitch(min[0] - initialTurretPitch);
setMaxPitch(max[0] - initialTurretPitch);
modelTransformEvent.dispatch({ pitch, yaw });
}}
@@ -376,7 +390,7 @@ export function GunFlexibilityVisualizer({
<VisualizerCornerStat
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
value={literals(strings.common.units.deg, {
value: radToDeg(pitch).toFixed(0),
value: radToDeg(pitch - initialTurretPitch).toFixed(0),
})}
side="top-left"
/>
@@ -11,7 +11,7 @@ import { Shell } from './components/Shell';
import { Target } from './components/Target';
import './index.css';
const PRECISION = 2;
export const RELOAD_PRECISION = 1;
export interface ReloadUpdateData {
reload: number;
@@ -82,9 +82,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
);
}
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
totalTime.current.innerHTML = shellReload.toFixed(RELOAD_PRECISION);
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
PRECISION,
RELOAD_PRECISION,
);
if (state.current >= reloadThreshold) shoot();
@@ -117,9 +117,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
shells = times(stats.shells, (index) => index > state.current - 1);
}
totalTime.current.innerHTML = interval.toFixed(PRECISION);
totalTime.current.innerHTML = interval.toFixed(RELOAD_PRECISION);
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
PRECISION,
RELOAD_PRECISION,
);
if (lastState % 1 > state.current % 1) shoot();
@@ -262,7 +262,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
transform: 'translate(50%, -50%)',
}}
>
0.00
{(0).toFixed(RELOAD_PRECISION)}
</Code>
<Code
ref={totalTime}
@@ -277,7 +277,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
transform: 'translate(-50%, -50%)',
}}
>
0.00
{(0).toFixed(RELOAD_PRECISION)}
</Code>
{stats.shells > 1 &&
@@ -1,5 +1,5 @@
import { alias } from "@blitzkit/core";
import { Badge, Box, Flex, Heading, Switch, Text } from "@radix-ui/themes";
import { Box, Flex, Heading, Switch, Text } from "@radix-ui/themes";
import { useLocale } from "../../../../hooks/useLocale";
import { Duel } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia";
@@ -12,12 +12,7 @@ export function Equalizer() {
return (
<ConfigurationChildWrapper>
<Heading size="4">
<Flex gap="2" align="center">
{strings.website.tools.tankopedia.configuration.equalizer.title}{" "}
<Badge variant="outline">
{strings.website.tools.tankopedia.configuration.equalizer.beta}
</Badge>
</Flex>
{strings.website.tools.tankopedia.configuration.equalizer.title}
</Heading>
<Box
@@ -34,9 +29,9 @@ export function Equalizer() {
style={{
cursor: "pointer",
borderRadius: "var(--radius-3)",
backgroundImage: `url(${alias("api", `/gamemodes/45/banner.webp`)})`,
backgroundImage: `url(${alias("api", `/gamemodes/Insanity/banner.webp`)})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundPosition: "50% 25%",
overflow: "hidden",
}}
>
@@ -0,0 +1,128 @@
import { alias, availableProvisions } from "@blitzkit/core";
import { ShuffleIcon } from "@radix-ui/react-icons";
import { Flex, IconButton, Popover } from "@radix-ui/themes";
import { api } from "../../../../../../core/blitzkit/api";
import { SPALL_LINER_PROVISION_ID } from "../../../../../../core/blitzkit/spallLiner";
import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
const provisionDefinitions = await api.provisionDefinitions();
export function SpallLinerSwitcher() {
const { tank, gun, provisions } = Duel.use((state) => state.protagonist);
const { unwrap } = useLocale();
const isSpallLinerActive = provisions.includes(SPALL_LINER_PROVISION_ID);
const hasSlotAvailable = provisions.length < tank.max_provisions;
const canEquipSpallLiner = availableProvisions(
tank,
gun,
provisionDefinitions,
).some((provision) => provision.id === SPALL_LINER_PROVISION_ID);
if (!canEquipSpallLiner) return null;
function equip(replacing?: number) {
Duel.mutate((draft) => {
if (replacing !== undefined) {
const provisions = draft.protagonist.provisions;
provisions[provisions.indexOf(replacing)] = SPALL_LINER_PROVISION_ID;
} else {
draft.protagonist.provisions.push(SPALL_LINER_PROVISION_ID);
}
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
function unequip() {
Duel.mutate((draft) => {
draft.protagonist.provisions = draft.protagonist.provisions.filter(
(id) => id !== SPALL_LINER_PROVISION_ID,
);
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
const button = (
<IconButton
color={isSpallLinerActive ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="medium"
onClick={() => {
if (isSpallLinerActive) {
unequip();
} else if (hasSlotAvailable) {
equip();
}
}}
>
<img
alt={unwrap(
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
)}
src={alias("api", `/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
);
if (isSpallLinerActive || hasSlotAvailable) return button;
return (
<Popover.Root>
<Popover.Trigger>{button}</Popover.Trigger>
<Popover.Content>
<Flex gap="2" align="center">
<img
alt={unwrap(
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
)}
src={alias(
"api",
`/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`,
)}
style={{
width: "1.25em",
height: "1.25em",
}}
/>
<ShuffleIcon color="var(--gray-9)" />
<Flex gap="2">
{provisions.map((id) => (
<Popover.Close key={id}>
<IconButton
color="gray"
variant="soft"
size="3"
radius="medium"
onClick={() => equip(id)}
>
<img
alt={unwrap(provisionDefinitions.provisions[id].name!)}
src={alias("api", `/icons/provisions/${id}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
</Popover.Close>
))}
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
);
}
@@ -46,6 +46,7 @@ import { TankSearch } from "../../../../TankSearch";
import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from "./components/QuickInputs";
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
import { Thicknesses } from "./components/Thicknesses";
type OptionsProps = MaybeSkeletonComponentProps & {
@@ -278,6 +279,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
}}
/>
</IconButton>
<SpallLinerSwitcher />
</Flex>
{!skeleton && (
@@ -15,7 +15,7 @@ export function Tracker({ fontSize }: Props) {
top="50%"
width="min(100%, 16rem)"
style={{
transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4)))`,
}}
>
<Progress color="gray" value={progress} />
@@ -16,13 +16,15 @@ export async function filterTank(
owned: number[] = [],
) {
return (
((filters.showOwned && owned.includes(tank.id)) ||
((!filters.showOwned && !filters.showUnowned) ||
(filters.showOwned && owned.includes(tank.id)) ||
(filters.showUnowned && !owned.includes(tank.id))) &&
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
((filters.showTesting && tank.testing) ||
((!filters.showTesting && !filters.showNonTesting) ||
(filters.showTesting && tank.testing) ||
(filters.showNonTesting && !tank.testing)) &&
(filters.gunType.length === 0 ||
(filters.gunType.includes("regular") &&
@@ -0,0 +1,8 @@
export const SPALL_LINER_PROVISION_ID = 101;
/**
* Matches the in-game reduction Spall Liner grants against HE splash
* damage. Shared between the characteristics panel and the Dynamic
* Armor 3D viewer's damage readout so the figure isn't duplicated.
*/
export const SPALL_LINER_HE_DAMAGE_DELTA = -0.2;
@@ -24,6 +24,7 @@ import {
type TurretDefinition,
} from "@blitzkit/protos";
import type { EquipmentMatrix } from "../../stores/duel";
import { SPALL_LINER_HE_DAMAGE_DELTA } from "./spallLiner";
import { defaultEqualizer } from "./tankToDuelMember";
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
@@ -172,7 +173,10 @@ export function tankCharacteristics(
coefficient(
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
[applyDynamicArmor, -0.1],
[applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE, -0.2],
[
applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE,
SPALL_LINER_HE_DAMAGE_DELTA,
],
);
const assaultDamageCoefficient =
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
@@ -420,6 +424,10 @@ export function tankCharacteristics(
: (gun.gun_type!.$case === "regular"
? gun.gun_type!.value.reload
: gun.gun_type!.value.clip_reload) * reloadCoefficient;
const peekabooDpm =
gun.gun_type!.$case === "regular"
? Math.round(damage * (60 / Math.max(shellReload!, 10)))
: undefined;
const caliber = shell.caliber;
const penetration = shell.penetration!.near * penetrationCoefficient;
const clipDamage =
@@ -547,6 +555,7 @@ export function tankCharacteristics(
shellRicochet,
dpm,
dpmEffective,
peekabooDpm,
shells,
mostOptimalShellIndex,
shellReloads,
@@ -11,13 +11,6 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
.dir(`Data/Gfx/Shared/tank-supply/ammunition/big`)
.then((files) => files.filter((file) => file.endsWith("_l.txt")));
console.log(
"Shell files raw",
await vfs.dir(`Data/Gfx/Shared/tank-supply/ammunition/big`),
);
console.log("Shell file paths", files);
for (const file of files) {
const id = file.match(/(.+)_l\.txt/)![1];
const sizes = (
@@ -33,8 +26,6 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
});
}
console.log("Shell paths", paths);
return paths;
});
@@ -17,7 +17,10 @@ export function Page({
}: Props & MaybeSkeletonComponentProps) {
const generated = Playlist.use((state) => state.list !== undefined);
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
if (App.state.logins.wargaming) {
TankFilters.state.showOwned = true;
TankFilters.state.showUnowned = false;
}
return (
<LocaleProvider locale={locale}>
+4 -4
View File
@@ -39,8 +39,8 @@ export const TankFilters = new Soapstone<TankFilters>({
abilities: [],
powers: [],
showTesting: true,
showNonTesting: true,
showOwned: true,
showUnowned: true,
showTesting: false,
showNonTesting: false,
showOwned: false,
showUnowned: false,
});
+35
View File
@@ -0,0 +1,35 @@
# Point this to your local installation of the game (make sure you see the Data
# folder within the directory you're putting here); don't forget to use
# double quotation marks if your path has spaces
CLIENT_DIR=
# Should be set to * for local development to build all chunks at once
CHUNK=*
# Should always be development when using CHUNKS=*
PUBLIC_ENVIRONMENT=development
# GuidesBlitz is the official textual partner of BlitzKit, but, really any
# website that supports BlitzKit tank slugs like example.com/tank-slug works
# too
PUBLIC_GUIDE_WEBSITE=https://guidesblitz.com
# The tag manager doesn't do anything in localhost, but here's what BlitzKit
# uses for completeness sake
PUBLIC_GOOGLE_TAG_MANAGER_ID=G-GL2JVHCGPQ
# A very rare, but required variable, enabling the advertising of BlitzKit
# OpenTest; TODO: make this a string field to allow for cross-advertising for
# any branch of BlitzKit
PUBLIC_PROMOTE_OPENTEST=false
# Legacy variable; this will be removed soon
PUBLIC_STATIC_ASSET_BASE=https://assets.blitzkit.app
# Proprietary Wargaming fields; you shouldn't need these for 99% of the tasks
WOTB_GLOSSARY=
WOTB_GLOSSARY_DOMAIN=
WOTB_GLOSSARY_PATH=
# Generate your own application id: https://developers.wargaming.net
PUBLIC_WARGAMING_APPLICATION_ID=