mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
Merge branch 'main' into data-driven
This commit is contained in:
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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "全高",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Высота",
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+47
-5
@@ -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"
|
||||
/>
|
||||
|
||||
+19
-4
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
+27
-13
@@ -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"
|
||||
/>
|
||||
|
||||
+7
-7
@@ -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 &&
|
||||
|
||||
+4
-9
@@ -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",
|
||||
}}
|
||||
>
|
||||
|
||||
+128
@@ -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 && (
|
||||
|
||||
+1
-1
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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=
|
||||
Reference in new issue
Block a user