Compare commits

...
14 Commits
Author SHA1 Message Date
tresabhi 960279bf00 huh i forgor 2026-09-23 17:37:52 -05:00
tresabhi 4247004b13 canvas 2026-09-23 17:25:51 -05:00
tresabhi 960ca4b0f4 Merge branch 'main' into data-driven 2026-09-23 17:14:22 -05:00
tresabhi 424d9944ad smarter trigger 2026-09-23 17:12:53 -05:00
tresabhi a75c47865b checkpoint 2026-09-23 17:09:43 -05:00
tresabhi c2714c4d6b tank search filters 2026-09-23 17:01:34 -05:00
tresabhi 1c505818a4 to core 2026-09-23 16:58:36 -05:00
tresabhi 444705347f imports 2026-09-23 16:51:09 -05:00
tresabhi 19556b5fc9 game strings 2026-09-23 16:45:07 -05:00
tresabhi e52f003c1b build protos indexing on first shot 2026-09-23 16:42:30 -05:00
tresabhi 4fc57d7fc1 Merge branch 'main' into data-driven 2026-09-23 16:37:39 -05:00
tresabhi 8af41b91b5 logs 2026-09-23 16:32:19 -05:00
tresabhi 347efa75fe add compatibility for string game mode ids 2026-09-23 16:31:35 -05:00
tresabhi 1619ee1e00 checkpoint 2026-09-23 15:36:22 -05:00
65 changed files with 662 additions and 779 deletions

No files matched your search

+8 -1
View File
@@ -139,7 +139,14 @@ jobs:
- name: Trigger deployment
if: ${{ github.ref == 'refs/heads/production' }}
# Deploy if:
# 1. Running cron from main using content from production to target production
# 2. Running manually and targeting non-production
if: >-
${{
github.ref == 'refs/heads/production' ||
(inputs.environment != '' && inputs.environment != 'production')
}}
env:
TOKEN: ${{ secrets.GH_NODE_DEPLOY_TOKEN }}
+3
View File
@@ -0,0 +1,3 @@
# BlitzKit 1.31.3
This update updates BlitzKit to work with the game's new ID system for game modes.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "blitzkit",
"version": "1.31.2",
"version": "1.31.3",
"license": "BlitzKit License",
"private": true,
"scripts": {
-2
View File
@@ -1,5 +1,3 @@
import { ShellDefinition, ShellType } from "../../../protos/src/blitzkit";
export function canSplash(
type: ShellDefinition["type"],
): type is ShellType.SHELL_TYPE_HE {
+2 -2
View File
@@ -1,3 +1,4 @@
import { Strings } from "@blitzkit/i18n";
import {
CamouflageDefinitions,
ConsumableDefinitions,
@@ -9,8 +10,7 @@ import {
ProvisionDefinitions,
SkillDefinitions,
TankDefinitions,
} from "@blitzkit/core";
import { Strings } from "@blitzkit/i18n";
} from "@blitzkit/protos";
import { BlitzScripts } from "../../types/blitzScripts";
export abstract class AbstractBlitzKitAPI {
@@ -8,24 +8,26 @@ import {
CamouflagesYaml,
CombatRolesYaml,
ConsumablesCommon,
ConsumableTankCategoryFilterCategory,
CrewType,
I18nString,
MapsYaml,
ModuleType,
OptionalDevices,
OptionalDeviceSlots,
ProvisionsCommon,
ShellKind,
ShellType,
SquadBattleTypeStylesYaml,
TankClass,
TankmenAvatar,
toUniqueId,
VehicleDefinitionList,
} from "@blitzkit/core";
import { AbstractVFS } from "@blitzkit/core/src/blitzkit/vfs/abstract";
import locales from "@blitzkit/i18n/locales.json";
import {
ConsumableTankCategoryFilterCategory,
CrewType,
I18nString,
ModuleType,
ShellType,
TankClass,
} from "@blitzkit/protos";
import type { extname, parse } from "node:path";
import { parse as parseYaml } from "yaml";
import { AbstractBlitzKitAPI } from "../abstract";
@@ -1,5 +1,6 @@
import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core";
import { fetchGlossary } from "@blitzkit/core";
import locales from "@blitzkit/i18n/locales.json";
import { Avatar, Gallery } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI9 } from "./9_game";
@@ -11,7 +12,7 @@ export abstract class ServerBlitzKitAPI10 extends ServerBlitzKitAPI9 {
await Promise.all(
locales.supported.map(async (supported) => {
const glossary = await fetchGlossary(
supported.blitz ?? supported.locale,
supported.variant_blitz_local ?? supported.locale,
);
console.log(
@@ -1,30 +1,32 @@
import {
AssaultRanges,
BlitzCrewType,
BlitzModuleType,
BlitzTankClass,
ChassisDefinitionsList,
Crew,
EngineDefinitionsList,
Equalizer,
GunDefinition,
GunDefinitionsList,
ModuleType,
ResearchCost,
ShellDefinitionsList,
ShellKind,
sluggify,
TankDefinitions,
TankPrice,
TankPriceType,
TankType,
toUniqueId,
TurretDefinitionsList,
Unlock,
UnlocksListing,
VehicleDefinitionList,
VehicleDefinitions,
} from "@blitzkit/core";
import {
AssaultRanges,
Crew,
Equalizer,
GunDefinition,
ModuleType,
ResearchCost,
TankDefinitions,
TankPrice,
TankPriceType,
TankType,
Unlock,
} from "@blitzkit/protos";
import { Cache, ServerBlitzKitAPI0 } from "./0_base";
export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
@@ -50,10 +52,11 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
async tanks() {
const tankDefinitions = TankDefinitions.create();
const gameModeNativeNames: Record<string, number> = {};
const gameModeNativeNames: Record<string, string> = {};
const squadBattleTypeGameModeNativeNameMatches =
this.squadBattleTypeStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
/eGameMode\.(\w+) -> "(battleType\/([a-zA-Z]+))"/g,
);
const gameTypeGameModeNativeNameMatches =
this.gameTypeSelectorStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
@@ -61,8 +64,7 @@ export abstract class ServerBlitzKitAPI1 extends ServerBlitzKitAPI0 {
);
for (const match of squadBattleTypeGameModeNativeNameMatches) {
const id = Number(match[1]);
gameModeNativeNames[match[3]] = id;
gameModeNativeNames[match[3]] = match[1];
}
for (const match of gameTypeGameModeNativeNameMatches) {
@@ -1,4 +1,4 @@
import { CamouflageDefinitions } from "@blitzkit/core";
import { CamouflageDefinitions } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI1 } from "./1_tanks";
@@ -1,19 +1,16 @@
import {
Armor,
BlitzCrewType,
ChassisDefinitionsList,
Crew,
EngineDefinitionsList,
GunDefinitionsList,
ModelDefinitions,
TankParameters,
toUniqueId,
TurretDefinitionsList,
Vector3,
VehicleDefinitionArmor,
VehicleDefinitionList,
VehicleDefinitions,
} from "@blitzkit/core";
import { Armor, Crew, ModelDefinitions, Vector3 } from "@blitzkit/protos";
import { Vector3Tuple } from "three";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI2 } from "./2_camouflages";
@@ -1,4 +1,4 @@
import { EquipmentDefinitions, EquipmentSlot } from "@blitzkit/core";
import { EquipmentDefinitions, EquipmentSlot } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI4 } from "./4_maps";
@@ -1,8 +1,5 @@
import {
BlitzTankFilterDefinitionCategory,
Consumable,
ConsumableDefinitions,
} from "@blitzkit/core";
import { BlitzTankFilterDefinitionCategory } from "@blitzkit/core";
import { Consumable, ConsumableDefinitions } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI5 } from "./5_equipments";
@@ -1,8 +1,5 @@
import {
BlitzTankFilterDefinitionCategory,
Provision,
ProvisionDefinitions,
} from "@blitzkit/core";
import { BlitzTankFilterDefinitionCategory } from "@blitzkit/core";
import { Provision, ProvisionDefinitions } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI6 } from "./6_consumables";
@@ -1,5 +1,6 @@
import { BlitzTankClass, SkillDefinitions } from "@blitzkit/core";
import { BlitzTankClass } from "@blitzkit/core";
import { SkillDefinitions } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI7 } from "./7_provisions";
@@ -1,5 +1,6 @@
import { AvailableNationsYaml, GameDefinitions } from "@blitzkit/core";
import { AvailableNationsYaml } from "@blitzkit/core";
import { GameDefinitions } from "@blitzkit/protos";
import { Cache } from "./0_base";
import { ServerBlitzKitAPI8 } from "./8_skills";
@@ -27,9 +28,9 @@ export abstract class ServerBlitzKitAPI9 extends ServerBlitzKitAPI8 {
).available_nations;
for (const match of this.squadBattleTypeStyles!.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "(battleType\/([a-zA-Z]+))"/g,
/eGameMode\.(\w+) -> "(battleType\/([a-zA-Z]+))"/g,
)) {
const id = Number(match[1]);
const id = match[1];
const name = this.getString(match[2]);
gameDefinitions.gameModes[id] = {
@@ -3,6 +3,7 @@ import {
ProvisionDefinitions,
TankDefinition,
} from "../../../protos/src/blitzkit";
import { isTankCompatible } from "../tankopedia";
export function availableProvisions(
tank: TankDefinition,
@@ -10,6 +11,6 @@ export function availableProvisions(
provisionDefinitions: ProvisionDefinitions,
) {
return Object.values(provisionDefinitions.provisions).filter((provision) =>
checkConsumableProvisionInclusivity(provision, tank, gun),
isTankCompatible(tank, gun, provision.include, provision.exclude),
);
}
+8
View File
@@ -0,0 +1,8 @@
import { GunDefinition } from "@blitzkit/protos";
import { CaseType } from "../types";
export const gunTypeOrder: CaseType<GunDefinition>[] = [
"regular",
"auto_loader",
"auto_reloader",
];
+3
View File
@@ -0,0 +1,3 @@
export * from "./gunTypeOrder";
export * from "./tankClassOrder";
export * from "./treeTypeOrder";
+2 -1
View File
@@ -1,10 +1,11 @@
export * from "../../protos/src/blitzkit";
export * from "./api";
export * from "./blitz";
export * from "./blitzkit";
export * from "./config";
export * from "./imgur";
export * from "./math";
export * from "./patreon";
export * from "./statistics";
export * from "./streams";
export * from "./tankopedia";
export * from "./types";
+1
View File
@@ -0,0 +1 @@
export * from "./isTankCompatible";
@@ -1,20 +1,11 @@
import {
ConsumableTankCategoryFilterCategory,
type GunDefinition,
type TankDefinition,
type TankInclusivityFilter,
} from "@blitzkit/core";
GunDefinition,
TankDefinition,
TankInclusivityFilter,
} from "@blitzkit/protos";
export function useCompatibility(tank: TankDefinition, gun: GunDefinition) {
return function (
include?: TankInclusivityFilter[],
exclude?: TankInclusivityFilter[],
) {
return tankCompatibility(tank, gun, include, exclude);
};
}
function tankCompatibility(
export function isTankCompatible(
tank: TankDefinition,
gun: GunDefinition,
include?: TankInclusivityFilter[],
+5
View File
@@ -0,0 +1,5 @@
export type CaseType<T> = T extends {
gun_type?: { $case: infer U; value: any };
}
? U
: never;
+1
View File
@@ -4,6 +4,7 @@ export * from "./blitzStrings";
export * from "./blitzTankFilterDefinitionCategory";
export * from "./camouflagesXml";
export * from "./camouflagesYaml";
export * from "./caseType";
export * from "./chassisDefinitionsList";
export * from "./combatRolesYaml";
export * from "./consumablesCommon";
+3 -3
View File
@@ -1,7 +1,7 @@
import { I18nString } from "../../core/src";
import locales from "../locales.json";
import locales from "@blitzkit/i18n/locales.json";
import { I18nString } from "@blitzkit/protos";
export function unwrapper(locale = locales.default) {
export function unwrapper(locale: string) {
return function (i18nString: I18nString) {
if (locale in i18nString.locales) {
return i18nString.locales[locale];
+68 -319
View File
@@ -82,6 +82,74 @@
}
},
"tools": {
"tanks": {
"name": "Tankopedia",
"slogan": "Blitz tank encyclopedia",
"action": "Find tanks"
},
"avatars": {
"name": "Avatars",
"slogan": "Blitz player avatars",
"action": "Browse all"
},
"backgrounds": {
"name": "Backgrounds",
"slogan": "Blitz player backgrounds",
"action": "View all"
},
"api": {
"name": "API",
"action": "To docs",
"slogan": "BlitzKit hosted API"
},
"players": {
"name": "Players",
"slogan": "Player statistics",
"action": "Stalk player"
},
"compare": {
"name": "Compare",
"slogan": "Compare tanks characteristics",
"action": "Compare now"
},
"performance": {
"name": "Performance",
"slogan": "Tank performance statistics",
"action": "View tanks"
},
"playlist": {
"name": "Playlist",
"slogan": "Tank playlist generator",
"action": "Generate"
},
"mixer": {
"name": "Mixer",
"slogan": "Tank module mixer",
"action": "Mix and match"
},
"session": {
"name": "Session",
"slogan": "Live player statistics",
"action": "Track"
},
"guesser": {
"name": "Guesser",
"slogan": "Silhouette mini-game",
"action": "Play"
},
"changelogs": {
"name": "Changelogs",
"slogan": "BlitzKit patch notes",
"action": "Read up"
},
"settings": {
"name": "Settings",
"slogan": "",
"action": ""
}
},
"home": {
"tanks": "Tanks",
"players": "Players",
@@ -427,325 +495,6 @@
}
},
"tools": {
"backgrounds": {
"name": "Backgrounds",
"description": "Profile backgrounds",
"seo_description": "World of Tanks Blitz player profile backgrounds"
},
"players": {
"name": "Players",
"description": "Player statistics",
"seo_description": "Player statistics in World of Tanks Blitz"
},
"playlist": {
"reset": "Reset",
"shuffle": "Shuffle",
"remove": "Remove played",
"tracking": "BlitzKit is tracking your battles",
"sort": {
"button": "Sort by",
"last_played": "Last played",
"least_played": "Least played"
},
"table": {
"index": "#",
"never": "Never",
"tank": "Tank",
"last_played": "Last played",
"battles": "Career battles",
"unrecorded": "--",
"wins": "Wins"
},
"large_warning": {
"title": "That's a lot of tanks!",
"description": "Are you sure you want to create a playlist of {count} tanks? That's {time} hours of gameplay at 50% winrate!",
"cancel": "Cancel",
"proceed": "Proceed"
},
"generate": "Generate playlist",
"name": "Playlist",
"description": "Tank playlist generator",
"seo_description": "Don't know what to play in World of Tanks Blitz? Use BlitzKit Playlist to get suggestions!",
"pool_size": "{count} possible tanks"
},
"mixer": {
"name": "Mixer",
"description": "Mix tank modules",
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz",
"screenshot": "Screenshot",
"randomize": "Randomize"
},
"guesser": {
"name": "Guesser",
"description": "Silhouette mini-game",
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
"search": {
"placeholder": "Type to guess...",
"skip": "Skip",
"guess": "Guess",
"next": "Next",
"no_results": "No results"
},
"stats": "{correct} / {total} correct, {streak} streak",
"help": {
"title": "Partially reveal tank?",
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
"cancel": "Cancel",
"reveal": "Reveal"
},
"clear": "Clear",
"select_all": "Select all",
"tier": "Tier {tier}"
},
"compare": {
"name": "Compare",
"description": "Compare tank statistics",
"seo_description": "Compare tanks statistics and loadouts in World of Tanks Blitz",
"actions": {
"add": {
"button": "Add",
"title": "Add tanks",
"description": "Select tanks to add to comparison"
},
"clear": "Clear",
"deltas": {
"none": "No deltas",
"percentage": "Percentage",
"absolute": "Absolute"
}
},
"no_tanks": {
"title": "No tanks selected",
"body": "Press the Add button to get started"
},
"table": {
"equipment": {
"clear": "Clear",
"apply_to_all": "Apply to all"
},
"skills": {
"clear": "Clear",
"maximize": "Maximize",
"equalize": "Equalize"
},
"modules": {
"stock": "Stock",
"upgrade": "Upgrade"
},
"provisions": {
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
},
"consumables": {
"clear": "Clear this tank",
"clear_all": "Clear all tanks"
}
}
},
"performance": {
"name": "Performance",
"description": "Tank performance statistics",
"seo_description": "Best tanks and their performances in World of Tanks Blitz",
"estimation": {
"body": "Estimated stats based on {count} players. Updated {age} ago.",
"hours": "{count} hours",
"minutes": "{count} minutes"
},
"player_count": {
"label": "Player count"
},
"table": {
"tanks": {
"header": "Tank",
"total": "Total"
},
"stats": {
"winrate": "Winrate",
"players": "Players",
"damage": "Damage",
"survival": "Survival",
"xp": "XP",
"kills": "Kills",
"spots": "Spots",
"accuracy": "Accuracy",
"shots": "Shots",
"hits": "Hits",
"damage_ratio": "Dmg. ratio",
"damage_taken": "Dmg. taken",
"capture_points": "Cap points"
}
}
},
"avatars": {
"name": "Avatars",
"description": "Blitz player avatars",
"seo_description": "All World of Tanks Blitz player avatars",
"last_updated": "Last updated on {time}",
"search": {
"hint": "Search avatars...",
"results": "{count} avatars"
}
},
"session": {
"name": "Session",
"description": "Live session stats",
"seo_description": "Track your stats in real time as you play World of Tanks Blitz",
"search": {
"lookup": "Look up a player to get started",
"hint": "Search players...",
"searching": "Searching...",
"no_results": "No players found"
},
"controls": {
"title": "Tracking {name}",
"subtitle": "Since {time}",
"reset": "Reset",
"change": "Change"
},
"no_cc": {
"title": "You're attempting to track a CC account",
"body": "CC (community contributor) accounts do not have public tank statistics. We are in the process of adding embeds for CC accounts, stay tuned.",
"alright": "Alright"
},
"no_battles": {
"title": "No battles",
"hint": "Go ahead and play a game"
},
"table": {
"tank": "Tank",
"total": "Total",
"remove": "Remove"
}
},
"tier_list": {
"name": "Tier list",
"description": "Rank Blitz tanks",
"seo_description": "Explore tier lists ranking tanks from best to worst, based on player ratings and statistical performance in World of Tanks Blitz",
"buttons": {
"reset": "Reset",
"share": "Share"
}
},
"embed": {
"name": "Embed",
"description": "Embeds for streaming",
"seo_description": "Create embeds for streaming World of Tanks Blitz with extreme customization",
"coming_soon": "More embeds coming soon.",
"suggest": "Suggest ideas.",
"preview": {
"title": "Example preview",
"upload": "Upload test background"
},
"configuration": {
"import": {
"title": "Import",
"description": "Paste your current URL if you have one already to import your settings",
"hint": "Paste URL...",
"apply": "Apply",
"idiot_title": "Invalid URL",
"idiot_description": "Try pasting the URL that BlitzKit previously generated, you silly goose!",
"idiot_button": "Okay, my bad"
},
"export": {
"title": "Export",
"description": "Copy all properties for you streaming software",
"generate": {
"button": "Generate URL",
"title": "Generate URL",
"subtitle": "Search an account to track",
"cancel": "Cancel",
"success": {
"title": "Copy URL",
"info": "Generated the URL for {name}. The embed will start tracking as soon as the URL is pasted. Reset at any time by right-clicking on PC or long-pressing on mobile and then selecting \"Reset\".",
"back": "Back",
"copy": "Copy URL"
}
},
"css": "Copy CSS",
"width": "Copy Width",
"height": "Copy Height",
"framerate": "Copy Framerate"
},
"customize": {
"title": "Customize",
"reset": "Reset",
"description": "Bells and whistles of your embed"
}
},
"types": {
"breakdown": {
"name": "Breakdown",
"options": {
"width": "Width",
"height": "Height",
"showTotal": "Show total",
"listGap": "List gap",
"listMaxTanks": "Max tanks",
"cardRadius": "Card radius",
"cardHeaderBackgroundColor": "Card header background color",
"cardHeaderPadding": "Card header padding",
"cardTitle": "Card title",
"cardTitleTypeColor": "Card title type color",
"cardTitleClassIcon": "Card title class icon",
"cardBodyBackgroundColor": "Card body background color",
"cardBodyPadding": "Card body padding",
"columnGap": "Column gap",
"columnValue": "Column value",
"columnLabel": "Column label",
"column1": "Column 1",
"column2": "Column 2",
"column3": "Column 3",
"column4": "Column 4",
"column1CustomLabel": "Column 1 label",
"column2CustomLabel": "Column 2 label",
"column3CustomLabel": "Column 3 label"
}
}
}
},
"more": {
"name": "More coming soon",
"description": "What we're making",
"seo_description": "What we're making with BlitzKit"
},
"api": {
"name": "API",
"description": "BlitzKit hosted API",
"seo_description": "BlitzKit hosted static API for World of Tanks Blitz"
},
"changelogs": {
"name": "Changelogs",
"description": "BlitzKit changelogs",
"seo_description": "Changelogs and patch notes for BlitzKit"
}
},
"footer": {
"motto": "Everything World of Tanks Blitz",
"copyright": "Unaffiliated with Wargaming or any service",
@@ -7,7 +7,7 @@ import "i18n.proto";
message GameDefinitions {
required string version = 1;
repeated string nations = 2;
map<uint32, GameMode> gameModes = 3;
map<string, GameMode> gameModes = 3;
map<uint32, GameModeRole> roles = 4;
}
@@ -11,7 +11,7 @@ message TankDefinitions {
message TankDefinition {
required uint32 id = 1;
required string slug = 2;
map<uint32, uint32> roles = 3;
map<string, uint32> roles = 3;
required bool fixed_camouflage = 5;
repeated int32 camouflages = 6;
repeated int32 ancestors = 7;
+5 -5
View File
@@ -29,15 +29,15 @@ for (const root of ROOTS) {
for (const file of filesRaw) {
if (file.endsWith(".proto")) {
files.push(file);
const name = file.slice(0, -6);
index += `export * from "./${name}";\n`;
args += `${root}/${file}\n`;
files.push(file);
}
if (file.endsWith(".ts")) {
const name = file.slice(0, -3);
index += `export * from "./${name}";\n`;
await rm(`${root}/${file}`);
}
}
@@ -4,9 +4,7 @@ import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
import { useArmor } from "../../../hooks/useArmor";
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../hooks/useTankTransform";
import { Duel } from "../../../stores/duel";
import { DuelModule, useDuel } from "../../../hooks/useDuel";
import { ModelTankWrapper } from "./ModelTankWrapper";
import { PrimaryArmorSceneComponent } from "./PrimaryArmorSceneComponent";
@@ -14,10 +12,10 @@ export const PrimaryArmorScene = memo(() => {
const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null!);
const gunContainer = useRef<Group>(null!);
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const tank = useDuel("protagonist", DuelModule.Tank);
const track = useDuel("protagonist", DuelModule.Track);
const turret = useDuel("protagonist", DuelModule.Turret);
const gun = useDuel("protagonist", DuelModule.Gun);
const armorGltf = useArmor(tank.id).gltf;
const armorNodes = Object.values(armorGltf.nodes);
const tankModelDefinition = useTankModelDefinition();
@@ -22,7 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { transitionEvent } from "../../../Lighting";
import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from "./target";
@@ -27,7 +27,7 @@ import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { useEquipment } from "../../../hooks/useEquipment";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { transitionEvent } from "../../Lighting";
import { ArmorType } from "./SpacedArmorScene";
import type {
ArmorUserData,
@@ -1,6 +1,6 @@
import { useFrame } from "@react-three/fiber";
export function AutoClear() {
export function CanvasAutoClear() {
useFrame(({ gl }) => {
gl.clear();
}, 0);
@@ -5,13 +5,13 @@ import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import { PerspectiveCamera, Vector3 } from "three";
import { OrbitControls as OrbitControlsClass } from "three-stdlib";
import { api } from "../../../../../../api/dynamic";
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { Pose, poseEvent } from "../../../../../../core/blitzkit/pose";
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { api } from "../api/dynamic";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { hasEquipment } from "../core/blitzkit/hasEquipment";
import { Pose, poseEvent } from "../core/blitzkit/pose";
import { controlsEnabledEvent } from "../core/controlsEnabled";
import { Tankopedia } from "../stores/tankopedia";
const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15,
@@ -29,7 +29,7 @@ interface ControlsProps {
enableRotate?: boolean;
}
export function Controls({
export function CanvasControl({
autoRotate = true,
zoomable = true,
enableRotate = true,
@@ -1,11 +1,15 @@
import { TankClass } from "@blitzkit/core";
import { TankClass } from "@blitzkit/protos";
import type { ComponentProps, ReactNode } from "react";
import { ClassHeavy } from "./components/ClassHeavy";
import { ClassLight } from "./components/ClassLight";
import { ClassMedium } from "./components/ClassMedium";
import { ClassTankDestroyer } from "./components/ClassTankDestroyer";
export const classIcons: Record<
interface ClassIconProps extends ComponentProps<"svg"> {
class: TankClass;
}
const classIcons: Record<
TankClass,
(props: ComponentProps<"svg">) => ReactNode
> = {
@@ -14,3 +18,8 @@ export const classIcons: Record<
[TankClass.TANK_CLASS_LIGHT]: ClassLight,
[TankClass.TANK_CLASS_MEDIUM]: ClassMedium,
};
export function ClassIcon({ class: tankClass, ...props }: ClassIconProps) {
const Icon = classIcons[tankClass];
return <Icon {...props} />;
}
@@ -1,9 +1,9 @@
import { useModel } from "../hooks/useModel";
import { Guess, GuessState } from "../stores/guess";
import { Controls } from "./CanvasControl";
import { Lighting } from "./Lighting";
import { SmartCanvas } from "./SmartCanvas";
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { TankModel } from "./Tankopedia/HeroSection/components/TankSandbox/components/TankModel";
import { TankModel } from "./TankModel";
const BRIGHTNESS = 2 ** -5.5;
const CONTRAST = 2 ** 2;
@@ -0,0 +1,24 @@
import type { CaseType } from "@blitzkit/core";
import type { GunDefinition } from "@blitzkit/protos";
import type { ComponentProps, ReactNode } from "react";
import { GunAutoloaderIcon } from "../GunAutoloaderIcon";
import { GunAutoreloaderIcon } from "../GunAutoreloaderIcon";
import { GunRegularIcon } from "../GunRegularIcon";
const typeIcons: Record<
CaseType<GunDefinition>,
(props: ComponentProps<"svg">) => ReactNode
> = {
regular: GunRegularIcon,
auto_loader: GunAutoloaderIcon,
auto_reloader: GunAutoreloaderIcon,
};
interface Props extends ComponentProps<"svg"> {
type: CaseType<GunDefinition>;
}
export function GunTypeIcon({ type, ...props }: Props) {
const Icon = typeIcons[type];
return <Icon {...props} />;
}
@@ -1,13 +1,13 @@
import { useEffect } from "react";
import { degToRad } from "three/src/math/MathUtils.js";
import { api } from "../../../../../../api/dynamic";
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
import { Duel } from "../../../../../../stores/duel";
import { api } from "../api/dynamic";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
const modelDefinitions = await api.models();
export function InitialAligner() {
export function InitialCameraAligner() {
const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
@@ -12,9 +12,9 @@ import {
} from "react";
import { HemisphereLight, SpotLight, type Group } from "three";
import { degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import { Tankopedia } from "../stores/tankopedia";
import { TankopediaPersistent } from "../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
const ANGLE = degToRad(15);
const REVEAL_ANIMATION_TIME = 3;
@@ -4,8 +4,8 @@ import type { QuicklimeEvent } from "quicklime";
import { forwardRef, Suspense, useEffect, useRef } from "react";
import type { OrbitControls as OrbitControlsClass } from "three-stdlib";
import { controlsEnabledEvent } from "../../core/controlsEnabled";
import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { SceneProps } from "../Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
import { Lighting } from "../Lighting";
import { SceneProps } from "../SceneProps";
import { MixerFallback } from "./components/MixerFallback";
import { Model } from "./components/Model";
@@ -9,7 +9,7 @@ import {
import { Jimp } from "jimp";
import { useCallback, type RefObject } from "react";
import { useLocale } from "../hooks/useLocale";
import { screenshotReadyEvent } from "./Tankopedia/HeroSection/components/TankSandbox/components/SceneProps";
import { screenshotReadyEvent } from "./SceneProps";
interface Props extends ButtonProps {
canvas: RefObject<HTMLCanvasElement>;
@@ -1,18 +1,18 @@
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
import { useRef } from "react";
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
import { useModel } from "../../../../../../hooks/useModel";
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { hasEquipment } from "../core/blitzkit/hasEquipment";
import { jsxTree } from "../core/blitzkit/jsxTree";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
import { controlsEnabledEvent } from "../core/controlsEnabled";
import { useModel } from "../hooks/useModel";
import { useTankTransform } from "../hooks/useTankTransform";
import { Tankopedia } from "../stores/tankopedia";
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
import { ModelTankWrapper } from "./Armor/components/ModelTankWrapper";
export function TankModel() {
const protagonist = Duel.use((draft) => draft.protagonist);
@@ -4,9 +4,9 @@ import {
resolveDpm,
SEARCH_KEYS,
unionBoundingBox,
type TankDefinition,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import type { TankDefinition } from "@blitzkit/protos";
import fuzzysort from "fuzzysort";
import { uniq } from "lodash-es";
import { memo, useMemo } from "react";
@@ -16,6 +16,7 @@ import { awaitableTankNames } from "../../core/awaitables/tankNames";
import { filterTanks } from "../../core/blitzkit/filterTanks";
import { resolveReload } from "../../core/blitzkit/resolveReload";
import { useLocale } from "../../hooks/useLocale";
import { useStrings } from "../../hooks/useStrings";
import { App } from "../../stores/app";
import { TankFilters } from "../../stores/tankFilters";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
@@ -53,7 +54,8 @@ const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
export const TankSearch = memo<TankSearchProps>(
({ compact, onSelect, onSelectAll, skeleton }) => {
const { strings, locale } = useLocale();
const strings = useStrings();
const locale = useLocale();
const wargaming = App.use((state) => state.logins.wargaming);
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
const tankopediaSort = TankSort.use();
@@ -1,14 +1,13 @@
import {
alias,
GunDefinition,
ShellType,
TANK_CLASSES,
TANK_TYPES,
TankType,
gunTypeOrder,
TIER_ROMAN_NUMERALS,
treeTypeOrder,
} from "@blitzkit/core";
import { tankClassOrder } from "@blitzkit/core/src/config/tankClassOrder";
import { literals } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import { ShellType, TankType } from "@blitzkit/protos";
import {
LockClosedIcon,
LockOpen2Icon,
@@ -16,18 +15,17 @@ import {
TrashIcon,
} from "@radix-ui/react-icons";
import { times } from "lodash-es";
import { Fragment, type ComponentProps, type ReactNode } from "react";
import { Fragment } from "react";
import { api } from "../../api/dynamic";
import { useLocale } from "../../hooks/useLocale";
import { useStrings } from "../../hooks/useStrings";
import { useUnwrapper } from "../../hooks/useUnwrapper";
import { App } from "../../stores/app";
import { TankFilters, type CaseType } from "../../stores/tankFilters";
import { TankFilters } from "../../stores/tankFilters";
import { Button } from "../Button";
import { classIcons } from "../ClassIcon";
import { ClassIcon } from "../ClassIcon";
import { DropdownMenu } from "../DropdownMenu";
import { Flex } from "../Flex";
import { GunAutoloaderIcon } from "../GunAutoloaderIcon";
import { GunAutoreloaderIcon } from "../GunAutoreloaderIcon";
import { GunRegularIcon } from "../GunRegularIcon";
import { GunTypeIcon } from "../GunTypeIcon";
import { IconButton } from "../IconButton";
import { MissingShellIcon } from "../MissingShellIcon";
import { ResearchedIcon } from "../ResearchedIcon";
@@ -42,14 +40,13 @@ const consumableDefinitions = await api.consumables();
const provisionDefinitions = await api.provisions();
const tankDefinitions = await api.tanks();
const gameModeRoleSets: Record<number, Set<number>> = {};
const gameModeRoleSets: Record<string, Set<number>> = {};
for (const tankIdString in tankDefinitions.tanks) {
const gameMode = tankDefinitions.tanks[tankIdString];
for (const gameModeIdString in gameMode.roles) {
const role = gameMode.roles[gameModeIdString];
const gameModeId = Number(gameModeIdString);
for (const gameModeId in gameMode.roles) {
const role = gameMode.roles[gameModeId];
if (gameModeId in gameModeRoleSets) {
gameModeRoleSets[gameModeId].add(role);
@@ -60,7 +57,7 @@ for (const tankIdString in tankDefinitions.tanks) {
}
const gameModeRoles: {
gameModeId: number;
gameModeId: string;
consumables: number[];
provisions: number[];
}[] = [];
@@ -69,8 +66,7 @@ const allGameModeConsumables = new Set<number>();
const allGameModeProvisions = new Set<number>();
for (const gameModeIdString in gameModeRoleSets) {
const gameModeId = Number(gameModeIdString);
const roles = gameModeRoleSets[gameModeId];
const roles = gameModeRoleSets[gameModeIdString];
const consumables = new Set<number>();
const provisions = new Set<number>();
@@ -113,7 +109,7 @@ for (const gameModeIdString in gameModeRoleSets) {
}
gameModeRoles.push({
gameModeId: gameModeId,
gameModeId: gameModeIdString,
consumables: Array.from(consumables.values()),
provisions: Array.from(provisions.values()),
});
@@ -149,25 +145,12 @@ const shellTypeIcons: Record<ShellType, string> = {
[ShellType.SHELL_TYPE_HEAT]: "hc",
};
const GUN_TYPE_ICONS: Record<
CaseType<GunDefinition>,
(props: ComponentProps<"svg">) => ReactNode
> = {
regular: GunRegularIcon,
auto_loader: GunAutoloaderIcon,
auto_reloader: GunAutoreloaderIcon,
};
const TANK_TYPE_COLORS: Record<TankType, string> = {
[TankType.TANK_TYPE_RESEARCHABLE]: "gray",
[TankType.TANK_TYPE_PREMIUM]: "amber",
[TankType.TANK_TYPE_COLLECTOR]: "blue",
};
const GUN_TYPES = Object.keys(
GUN_TYPE_ICONS,
) as (keyof typeof GUN_TYPE_ICONS)[];
const MAX_ICONS = 4;
const TIERS = times(10, (i) => 10 - i);
@@ -209,7 +192,7 @@ function ResetButton() {
}
function TiersFilter() {
const { strings } = useLocale();
const strings = useStrings();
const tiersRaw = TankFilters.use((state) => state.tiers);
const tiers = tiersRaw.length === 0 ? TIERS : tiersRaw;
@@ -286,7 +269,7 @@ function NationsFilter() {
const rawNations = TankFilters.use((state) => state.nations);
const nations =
rawNations.length === 0 ? gameDefinitions.nations : rawNations;
const { strings } = useLocale();
const strings = useStrings();
return (
<DropdownMenu.Root modal={false}>
@@ -368,9 +351,9 @@ function NationsFilter() {
}
function ClassFilter() {
const { strings } = useLocale();
const strings = useStrings();
const rawClasses = TankFilters.use((state) => state.classes);
const classes = rawClasses.length === 0 ? TANK_CLASSES : rawClasses;
const classes = rawClasses.length === 0 ? tankClassOrder : rawClasses;
return (
<DropdownMenu.Root modal={false}>
@@ -378,10 +361,10 @@ function ClassFilter() {
<Button size="minor" color="gray" variant="surface">
<Flex>
{classes.map((tankClass, index) => {
const Icon = classIcons[tankClass];
return (
<Icon
<ClassIcon
key={tankClass}
class={tankClass}
style={{
color: "var(--gray-12)",
opacity: 1,
@@ -398,9 +381,8 @@ function ClassFilter() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{TANK_CLASSES.map((tankClass) => {
{tankClassOrder.map((tankClass) => {
const selected = rawClasses.includes(tankClass);
const Icon = classIcons[tankClass];
return (
<DropdownMenu.CheckboxItem
@@ -420,7 +402,10 @@ function ClassFilter() {
}}
checked={selected}
>
<Icon style={{ width: "1.25em", height: "1.25em" }} />
<ClassIcon
class={tankClass}
style={{ width: "1.25em", height: "1.25em" }}
/>
{strings.common.tank_class_medium[tankClass]}
</DropdownMenu.CheckboxItem>
@@ -449,8 +434,8 @@ function ClassFilter() {
function GunTypeFilter() {
const rawGunTypes = TankFilters.use((state) => state.gunType);
const gunTypes = rawGunTypes.length === 0 ? GUN_TYPES : rawGunTypes;
const { strings } = useLocale();
const gunTypes = rawGunTypes.length === 0 ? gunTypeOrder : rawGunTypes;
const strings = useStrings();
return (
<DropdownMenu.Root modal={false}>
@@ -458,11 +443,10 @@ function GunTypeFilter() {
<Button size="minor" color="gray" variant="surface">
<Flex>
{gunTypes.map((gunType) => {
const Icon = GUN_TYPE_ICONS[gunType];
return (
<Icon
<GunTypeIcon
key={gunType}
type={gunType}
style={{
margin: gunType === "regular" ? "0 -0.125em" : undefined,
opacity: 1,
@@ -478,9 +462,8 @@ function GunTypeFilter() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{GUN_TYPES.map((gunType) => {
{gunTypeOrder.map((gunType) => {
const selected = rawGunTypes.includes(gunType);
const Icon = GUN_TYPE_ICONS[gunType];
return (
<DropdownMenu.CheckboxItem
@@ -498,7 +481,10 @@ function GunTypeFilter() {
checked={selected}
key={gunType}
>
<Icon style={{ width: "1.25em", height: "1.25em" }} />
<GunTypeIcon
type={gunType}
style={{ width: "1.25em", height: "1.25em" }}
/>
{strings.common.gun_types[gunType]}
</DropdownMenu.CheckboxItem>
@@ -526,9 +512,9 @@ function GunTypeFilter() {
}
function TypeFilter() {
const { strings } = useLocale();
const strings = useStrings();
const rawTypes = TankFilters.use((state) => state.types);
const types = rawTypes.length === 0 ? TANK_TYPES : rawTypes;
const types = rawTypes.length === 0 ? treeTypeOrder : rawTypes;
return (
<DropdownMenu.Root modal={false}>
@@ -556,7 +542,7 @@ function TypeFilter() {
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{TANK_TYPES.map((tankType) => {
{treeTypeOrder.map((tankType) => {
const selected = rawTypes.includes(tankType);
return (
@@ -628,7 +614,7 @@ function IndividualShellFilter({
index: number;
premium?: boolean;
}) {
const { strings } = useLocale();
const strings = useStrings();
const shells = TankFilters.use((state) => state.shells);
return (
@@ -730,7 +716,7 @@ function IndividualShellFilter({
function OwnershipFilter() {
const wargaming = App.use((state) => state.logins.wargaming);
const { strings } = useLocale();
const strings = useStrings();
return wargaming ? (
<OwnershipFilterInternal />
@@ -837,7 +823,8 @@ function TestFilter() {
}
function ConsumablesFilter() {
const { unwrap, strings } = useLocale();
const strings = useStrings();
const unwrap = useUnwrapper();
const rawConsumables = TankFilters.use((state) => state.consumables);
const consumables =
rawConsumables.length === 0 ? consumablesArray : rawConsumables;
@@ -931,7 +918,8 @@ function ConsumablesFilter() {
}
function ProvisionsFilter() {
const { unwrap, strings } = useLocale();
const strings = useStrings();
const unwrap = useUnwrapper();
const rawProvisions = TankFilters.use((state) => state.provisions);
const provisions =
rawProvisions.length === 0 ? provisionsArray : rawProvisions;
@@ -1025,7 +1013,8 @@ function ProvisionsFilter() {
}
function GameModeAbilitiesFilter() {
const { unwrap, strings } = useLocale();
const strings = useStrings();
const unwrap = useUnwrapper();
const rawAbilities = TankFilters.use((state) => state.abilities);
const rawPowers = TankFilters.use((state) => state.powers);
@@ -21,8 +21,7 @@ export function GameModeSection() {
</Heading>
<Flex justify="center" gap="4" wrap="wrap" px="4">
{roles.map(([key, value]) => {
const id = Number(key);
{roles.map(([id, value]) => {
const gameMode = gameDefinitions.gameModes[id];
return (
@@ -1,200 +0,0 @@
import {
forwardRef,
Suspense,
useEffect,
useImperativeHandle,
useRef,
} from "react";
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
import { useEquipment } from "../../../../../hooks/useEquipment";
import { useModel } from "../../../../../hooks/useModel";
import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
import { Duel } from "../../../../../stores/duel";
import { Tankopedia } from "../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
import { Armor } from "../../../../Armor";
import { ArmorPlateDisplay } from "../../../../Armor/components/ArmorPlateDisplay";
import { ShotDisplay } from "../../../../Armor/components/ShotDisplay";
import {
StaticArmor,
type ThicknessRange,
} from "../../../../Armor/components/StaticArmor";
import { SmartCanvas } from "../../../../SmartCanvas";
import { AutoClear } from "./components/AutoClear";
import { Controls } from "./components/Control";
import { InitialAligner } from "./components/InitialAligner";
import { Lighting } from "./components/Lighting";
import { SceneProps } from "./components/SceneProps";
import { TankModel } from "./components/TankModel";
interface TankSandboxProps {
thicknessRange: ThicknessRange;
}
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange }, ref) => {
const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124);
const protagonistGun = Duel.use((state) => state.protagonist.gun);
const protagonistTurret = Duel.use((state) => state.protagonist.turret);
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition =
tankModelDefinition.turrets[protagonistTurret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
const display = Tankopedia.use((state) => state.display);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor,
);
const id = Duel.use((state) => state.protagonist.tank.id);
const { hasPbr } = useModel(id);
useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() {
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
event.preventDefault();
}
function handlePointerUp(event: PointerEvent) {
event.preventDefault();
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
useEffect(() => {
function handlePoseEvent(pose: Pose) {
switch (pose) {
case Pose.HullDown: {
const [pitch, yaw] = applyPitchYawLimits(
-Infinity,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
case Pose.FaceHug: {
const [pitch, yaw] = applyPitchYawLimits(
Infinity,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
case Pose.Default:
const [pitch, yaw] = applyPitchYawLimits(
0,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
}
poseEvent.on(handlePoseEvent);
return () => {
poseEvent.off(handlePoseEvent);
};
});
useEffect(() => {
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}, [protagonistGun, protagonistTurret]);
useEffect(() => {
if (display !== TankopediaDisplay.DynamicArmor) {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
if (display !== TankopediaDisplay.StaticArmor) {
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
}, [display]);
return (
<SmartCanvas
resize={{ debounce: 0 }}
ref={canvas}
gl={{
clippingPlanes: [],
localClippingEnabled: true,
preserveDrawingBuffer: true,
}}
shadows="soft"
onPointerDown={handlePointerDown}
onPointerMissed={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
}}
style={{
userSelect: "none",
width: "100%",
height: "100%",
outline: undefined,
}}
>
<Lighting hasPbr={hasPbr} />
<SceneProps />
{(display === TankopediaDisplay.Model ||
(display === TankopediaDisplay.DynamicArmor &&
!hideTankModelUnderArmor)) && <TankModel />}
<ShotDisplay />
<ArmorPlateDisplay />
<AutoClear />
<Suspense>
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
<Controls />
{display === TankopediaDisplay.DynamicArmor && <Armor />}
{display === TankopediaDisplay.StaticArmor && (
<StaticArmor thicknessRange={thicknessRange} />
)}
</Suspense>
<InitialAligner />
</SmartCanvas>
);
},
);
@@ -1,10 +1,10 @@
import type { Consumable } from "@blitzkit/core";
import { TrashIcon } from "@radix-ui/react-icons";
import { useCompatibility } from "../../hooks/useCompatibility";
import { useConsumables } from "../../hooks/useConsumables";
import { useProtagonist } from "../../hooks/useProtagonist";
import { useProtagonistGun } from "../../hooks/useProtagonistGun";
import { useProtagonistTank } from "../../hooks/useProtagonistTank";
import { useStrings } from "../../hooks/useStrings";
import { useTankCompatibility } from "../../hooks/useTankCompatibility";
import { Tankopedia } from "../../stores/tankopedia";
import { hasUpgrades } from "../../tankopedia/hasUpgrades";
import { vehicleStatusKeys } from "../../tankopedia/tankState";
@@ -19,7 +19,7 @@ interface TankopediaLoadoutProps {
}
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
const tank = useProtagonist();
const tank = useProtagonistTank();
const showModules = hasUpgrades(tank);
return (
@@ -120,9 +120,9 @@ interface ConsumablesProps {
function Consumables({ characteristics }: ConsumablesProps) {
const strings = useStrings();
const consumables = useConsumables();
const tank = useProtagonist();
const tank = useProtagonistTank();
const gun = useProtagonistGun();
const isCompatible = useCompatibility(tank, gun);
const isCompatible = useTankCompatibility(tank, gun);
return (
<div className={styles.section}>
@@ -165,7 +165,7 @@ function Consumable({ id, consumable }: ConsumableProps) {
const isSelected = Tankopedia.use((state) =>
state.protagonist.consumables.includes(id),
);
const tank = useProtagonist();
const tank = useProtagonistTank();
const unwrap = useUnwrap();
return (
@@ -214,7 +214,7 @@ function Consumable({ id, consumable }: ConsumableProps) {
function Equipment() {
const strings = useStrings();
const tank = useProtagonist();
const tank = useProtagonistTank();
const equipment = useEquipment(tank.tank!);
return (
@@ -273,7 +273,7 @@ interface EquipmentSlotProps {
function EquipmentSlot({ slot, rowIndex, columnIndex }: EquipmentSlotProps) {
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
const tank = useProtagonist();
const tank = useProtagonistTank();
const equipment = useEquipment(tank.tank!);
const { price } = equipment.price.unlock_slot_prices[equipmentIndex];
@@ -307,7 +307,7 @@ function EquipmentOption({
columnIndex,
}: EquipmentOptionProps) {
const equipmentIndex = rowIndex * equipmentColumns + columnIndex;
const tank = useProtagonist();
const tank = useProtagonistTank();
const equipment = useEquipment(tank.tank!);
const slot = equipment.preset.slots[equipmentIndex];
const id = slot.options_catalog_i_ds[optionIndex];
@@ -346,7 +346,7 @@ function EquipmentOption({
function Modules() {
const strings = useStrings();
const tank = useProtagonist();
const tank = useProtagonistTank();
const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
const alternates = Tankopedia.use((state) => state.protagonist.alternates);
const isStock = useMemo(() => {
@@ -496,7 +496,7 @@ interface LineElementProps {
function LineElement({ index, lineName, stage }: LineElementProps) {
const upgrades = Tankopedia.use((state) => state.protagonist.upgrades);
const alternates = Tankopedia.use((state) => state.protagonist.alternates);
const tank = useProtagonist();
const tank = useProtagonistTank();
const strings = useStrings();
const gameStrings = useGameStrings("TankEntity");
const upgradePreset = useUpgradePreset(tank.tank!.tank_upgrade_preset);
@@ -0,0 +1,16 @@
.wrapper {
height: calc(100dvh - 16rem);
position: relative;
.sandbox {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.canvas {
background-color: black;
}
}
}
@@ -0,0 +1,202 @@
import {
forwardRef,
Suspense,
useEffect,
useImperativeHandle,
useRef,
} from "react";
import { applyPitchYawLimits } from "../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../../core/blitzkit/modelTransform";
import { Pose, poseEvent } from "../../core/blitzkit/pose";
import { useEquipment } from "../../hooks/useEquipment";
import { useModel } from "../../hooks/useModel";
import { useProtagonistGun } from "../../hooks/useProtagonistGun";
import { useProtagonistTank } from "../../hooks/useProtagonistTank";
import { useProtagonistTurret } from "../../hooks/useProtagonistTurret";
import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import { Armor } from "../Armor";
import { ArmorPlateDisplay } from "../Armor/components/ArmorPlateDisplay";
import { ShotDisplay } from "../Armor/components/ShotDisplay";
import {
StaticArmor,
type ThicknessRange,
} from "../Armor/components/StaticArmor";
import { CanvasAutoClear } from "../CanvasAutoClear";
import { CanvasControl } from "../CanvasControl";
import { InitialCameraAligner } from "../InitialCameraAligner";
import { Lighting } from "../Lighting";
import { SceneProps } from "../SceneProps";
import { SmartCanvas } from "../SmartCanvas";
import { TankModel } from "../TankModel";
import styles from "./index.module.css";
interface TankSandboxProps {
thicknessRange: ThicknessRange;
}
export const TankopediaSandbox = forwardRef<
HTMLCanvasElement,
TankSandboxProps
>(({ thicknessRange }, ref) => {
const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124);
const tank = useProtagonistTank();
const gun = useProtagonistGun();
const turret = useProtagonistTurret();
const model = Tankopedia.use((state) => state.protagonist.model);
const turretModelDefinition = model.turrets[turret.id];
const gunModelDefinition = turretModelDefinition.guns[gun.id];
const display = Tankopedia.use((state) => state.display);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor,
);
const { hasPbr } = useModel(tank.id);
useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() {
window.addEventListener("pointermove", handlePointerMove);
window.addEventListener("pointerup", handlePointerUp);
}
function handlePointerMove(event: PointerEvent) {
event.preventDefault();
}
function handlePointerUp(event: PointerEvent) {
event.preventDefault();
window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener("pointerup", handlePointerUp);
}
useEffect(() => {
function handlePoseEvent(pose: Pose) {
switch (pose) {
case Pose.HullDown: {
const [pitch, yaw] = applyPitchYawLimits(
-Infinity,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
case Pose.FaceHug: {
const [pitch, yaw] = applyPitchYawLimits(
Infinity,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
case Pose.Default:
const [pitch, yaw] = applyPitchYawLimits(
0,
0,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
break;
}
}
poseEvent.on(handlePoseEvent);
return () => {
poseEvent.off(handlePoseEvent);
};
});
useEffect(() => {
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
}, [gun, turret]);
useEffect(() => {
if (display !== TankopediaDisplay.DynamicArmor) {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
if (display !== TankopediaDisplay.StaticArmor) {
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
}, [display]);
return (
<div className={styles.wrapper}>
<SmartCanvas
className={styles.canvas}
resize={{ debounce: 0 }}
ref={canvas}
gl={{
clippingPlanes: [],
localClippingEnabled: true,
preserveDrawingBuffer: true,
}}
shadows="soft"
onPointerDown={handlePointerDown}
onPointerMissed={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
}}
>
<Lighting hasPbr={hasPbr} />
<SceneProps />
{(display === TankopediaDisplay.Model ||
(display === TankopediaDisplay.DynamicArmor &&
!hideTankModelUnderArmor)) && <TankModel />}
<ShotDisplay />
<ArmorPlateDisplay />
<CanvasAutoClear />
<Suspense>
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
<CanvasControl />
{display === TankopediaDisplay.DynamicArmor && <Armor />}
{display === TankopediaDisplay.StaticArmor && (
<StaticArmor thicknessRange={thicknessRange} />
)}
</Suspense>
<InitialCameraAligner />
</SmartCanvas>
</div>
);
});
@@ -1,7 +1,7 @@
---
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { api } from "../api/dynamic";
import type { Tool } from "../constants/tools";
import { getStrings } from "../core/i18n/getStrings";
import { Logo } from "../icons/Logo";
import { Back } from "./Back";
import { Flex } from "./Flex";
@@ -14,9 +14,8 @@ interface Props {
}
const { tool, locale } = Astro.props;
const strings = getStrings(locale);
const toolStrings =
strings.website.tools[tool.id as keyof typeof strings.website.tools];
const strings = await api.strings(locale);
const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
---
<style>
@@ -12,10 +12,7 @@ interface Props {
const { locale, tool, minor } = Astro.props;
const strings = await api.strings(locale);
const toolStrings =
strings.website.tools[
(tool.strings ?? tool.id) as keyof typeof strings.website.tools
];
const toolStrings = strings.tools[tool.id as keyof typeof strings.tools];
---
<style>
+14
View File
@@ -0,0 +1,14 @@
import { degToRad } from "three/src/math/MathUtils.js";
export const fov = 20;
export const initialCameraR = 15;
export const initialCameraPhi = degToRad(90 - 10);
export const initialCameraTheta = degToRad(90 + 15);
export const cameraSwingVerticalAmplitude = degToRad(5);
export const cameraSwingVerticalFrequency = Math.E ** -6;
export const cameraSwingHorizontalAmplitude = degToRad(30);
export const cameraSwingHorizontalFrequency = Math.E ** -5;
export const cameraCenterCorrectionAmplitude = 0.8;
@@ -1,7 +1,7 @@
import type { TankDefinition } from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import type { TankDefinition } from "@blitzkit/protos";
import { times } from "lodash-es";
import { api } from "../../api/dynamic";
import { isTankCompatible } from "../../hooks/useTankCompatibility";
import type { TankFilters } from "../../stores/tankFilters";
const SHELLS = times(3, (index) => index);
@@ -51,10 +51,11 @@ export async function filterTank(
filters.consumables.every((consumable) =>
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
checkConsumableProvisionInclusivity(
consumableDefinitions.consumables[consumable],
isTankCompatible(
tank,
gun,
consumableDefinitions.consumables[consumable].include,
consumableDefinitions.consumables[consumable].exclude,
),
),
),
@@ -63,10 +64,11 @@ export async function filterTank(
filters.provisions.every((provision) =>
tank.turrets.some((turret) =>
turret.guns.some((gun) =>
checkConsumableProvisionInclusivity(
provisionDefinitions.provisions[provision],
isTankCompatible(
tank,
gun,
provisionDefinitions.provisions[provision].include,
provisionDefinitions.provisions[provision].exclude,
),
),
),
+74
View File
@@ -0,0 +1,74 @@
import type {
EngineDefinition,
GunDefinition,
ShellDefinition,
TankDefinition,
TrackDefinition,
TurretDefinition,
} from "@blitzkit/protos";
import { api } from "../api/dynamic";
import { Tankopedia } from "../stores/tankopedia";
import type { DuelSide } from "./useEquipment";
export enum DuelModule {
Tank,
Track,
Engine,
Turret,
Gun,
Shell,
}
const tanks = await api.tanks();
export function useDuel(
side: DuelSide,
module: DuelModule.Tank,
): TankDefinition;
export function useDuel(
side: DuelSide,
module: DuelModule.Track,
): TrackDefinition;
export function useDuel(
side: DuelSide,
module: DuelModule.Engine,
): EngineDefinition;
export function useDuel(
side: DuelSide,
module: DuelModule.Turret,
): TurretDefinition;
export function useDuel(side: DuelSide, module: DuelModule.Gun): GunDefinition;
export function useDuel(
side: DuelSide,
module: DuelModule.Shell,
): ShellDefinition;
export function useDuel(side: DuelSide, module: DuelModule) {
const tankId = Tankopedia.use((state) => state[side].tank);
const tank = tanks.tanks[tankId];
if (module === DuelModule.Tank) return tank;
if (module === DuelModule.Track) {
const trackId = Tankopedia.use((state) => state[side].track);
return tank.tracks.find((track) => track.id === trackId)!;
}
if (module === DuelModule.Engine) {
const engineId = Tankopedia.use((state) => state[side].engine);
return tank.engines.find((engine) => engine.id === engineId)!;
}
const turretId = Tankopedia.use((state) => state[side].turret);
const turret = tank.turrets.find((turret) => turret.id === turretId)!;
if (module === DuelModule.Turret) return turret;
const gunId = Tankopedia.use((state) => state[side].gun);
const gun = turret.guns.find((gun) => gun.id === gunId)!;
if (module === DuelModule.Gun) return gun;
const shellId = Tankopedia.use((state) => state[side].shell);
return gun.shells.find((shell) => shell.id === shellId)!;
}
+12 -10
View File
@@ -1,18 +1,20 @@
import { useMemo } from "react";
import { api } from "../api/dynamic";
import { Duel } from "../stores/duel";
import { Tankopedia } from "../stores/tankopedia";
const equipmentDefinitions = await api.equipment();
const equipment = await api.equipment();
const tanks = await api.tanks();
export function useEquipment(id: number, antagonist = false) {
const member = Duel.use(
(state) => state[antagonist ? "antagonist" : "protagonist"]!,
);
const preset = equipmentDefinitions.presets[member.tank.equipment_preset];
const equipmentMatrix = Duel.use(
(state) =>
state[antagonist ? "antagonist" : "protagonist"]!.equipmentMatrix,
export type DuelSide = "protagonist" | "antagonist";
export function useEquipment(id: number, side: DuelSide) {
const member = Tankopedia.use((state) => state[side]);
const tank = tanks.tanks[member.tank];
const preset = equipment.presets[tank.equipment_preset];
const equipmentMatrix = Tankopedia.use(
(state) => state[side].equipment_matrix,
);
const value = useMemo(() => {
return preset.slots.some((slot, index) => {
const row = Math.floor(index / 3);
@@ -1,9 +0,0 @@
import { api } from "../api/dynamic";
import { Tankopedia } from "../stores/tankopedia";
import { useAwait } from "./useAwait";
export function useProtagonist() {
const id = Tankopedia.use((state) => state.protagonist.tank);
const tank = useAwait(() => api.tank(id), `tank-${id}`);
return tank;
}
@@ -1,8 +0,0 @@
import { Tankopedia } from "../stores/tankopedia";
import { useProtagonistTurret } from "./useProtagonistTurret";
export function useProtagonistGun() {
const id = Tankopedia.use((state) => state.protagonist.gun);
const turret = useProtagonistTurret();
return turret.guns[id];
}
@@ -1,8 +0,0 @@
import { Tankopedia } from "../stores/tankopedia";
import { useProtagonist } from "./useProtagonist";
export function useProtagonistTurret() {
const id = Tankopedia.use((state) => state.protagonist.gun);
const tank = useProtagonist();
return tank.turrets[id];
}
@@ -0,0 +1,15 @@
import { isTankCompatible } from "@blitzkit/core";
import {
GunDefinition,
TankDefinition,
TankInclusivityFilter,
} from "@blitzkit/protos";
export function useTankCompatibility(tank: TankDefinition, gun: GunDefinition) {
return function (
include?: TankInclusivityFilter[],
exclude?: TankInclusivityFilter[],
) {
return isTankCompatible(tank, gun, include, exclude);
};
}
@@ -1,5 +0,0 @@
import { Tankopedia } from "../stores/tankopedia";
export function useTankModelDefinition() {
return Tankopedia.use((state) => state.model);
}
@@ -0,0 +1,9 @@
import { unwrapper } from "@blitzkit/i18n";
import { useLocale } from "./useLocale";
export function useUnwrapper() {
const locale = useLocale();
const unwrap = unwrapper(locale);
return unwrap;
}
@@ -17,15 +17,15 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
const bannerMatches: { name: string; path: string }[] = [];
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g,
/eGameMode\.(\w+) -> "~res:([^"]+)"/g,
)) {
bannerMatches.push({ name: match[1], path: match[2] });
}
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
/"(\d+)" -> "battleType\/([a-zA-Z]+)"/g,
/eGameMode\.(\w+) -> "battleType\/([a-zA-Z]+)"/g,
)) {
const id = Number(match[1]);
const id = match[1];
const name = match[2];
let path = bannerMatches.find(
(bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase(),
@@ -1,10 +1,12 @@
import { useMemo } from "react";
import { api } from "../../../../../api/dynamic";
import { computeCharacteristics } from "../../../../../tankopedia/computeCharacteristics";
import { TankopediaLoadout } from "../../../../../components/TankopediaLoadout";
import { TankopediaSandbox } from "../../../../../components/TankopediaSandbox";
import { withErrorWrapper } from "../../../../../hocs/withErrorWrapper";
import { withLocale } from "../../../../../hocs/withLocale";
import { useAwait } from "../../../../../hooks/useAwait";
import { Tankopedia } from "../../../../../stores/tankopedia";
import { computeCharacteristics } from "../../../../../tankopedia/computeCharacteristics";
import styles from "./_index.module.css";
interface PageProps {
+2 -7
View File
@@ -1,17 +1,12 @@
import type { CaseType } from "@blitzkit/core";
import type {
GunDefinition,
ShellType,
TankClass,
TankType,
} from "@blitzkit/core";
} from "@blitzkit/protos";
import { Varuna } from "varuna";
export type CaseType<T> = T extends {
gun_type?: { $case: infer U; value: any };
}
? U
: never;
export interface TankFilters {
tiers: number[];
nations: string[];
+4 -5
View File
@@ -1,8 +1,5 @@
import {
createDefaultSkills,
ShellDefinition,
TankDefinition,
} from "@blitzkit/core";
import { createDefaultSkills } from "@blitzkit/core";
import type { ShellDefinition, TankDefinition } from "@blitzkit/protos";
import type { Vector3 } from "three";
import { Varuna } from "varuna";
import { api } from "../api/dynamic";
@@ -83,6 +80,7 @@ interface Tankopedia {
revealed: boolean;
protagonist: TankState;
antagonist: TankState;
shot?: Shot;
skills: Record<string, number>;
@@ -117,6 +115,7 @@ export const Tankopedia = new Varuna<Tankopedia, TankDefinition>((tank) => ({
revealed: false,
protagonist: createTankState(tank),
antagonist: createTankState(tank),
model: models.models[tank.id],
relativeAgainst: TankopediaRelativeAgainst.Class,