mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-12 20:26:10 +01:00
Compare commits
47
Commits
v1.27.17
...
e26bb4efe0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e26bb4efe0 | ||
|
|
66cf1fd223 | ||
|
|
335cd4d699 | ||
|
|
95fa5bc1a5 | ||
|
|
1598ae973f | ||
|
|
69f3a95379 | ||
|
|
3846cbab4a | ||
|
|
30e87743fc | ||
|
|
fe4afdcbf0 | ||
|
|
ef2a47e756 | ||
|
|
c2d625e1ed | ||
|
|
d33990bdb4 | ||
|
|
96482d4268 | ||
|
|
8b527f2333 | ||
|
|
f56c31fb48 | ||
|
|
7d164e415a | ||
|
|
b3a9f0889e | ||
|
|
6d24ed92d0 | ||
|
|
d287e66265 | ||
|
|
ac29d1ead0 | ||
|
|
b6c3fdc4e9 | ||
|
|
4efdaaf7c9 | ||
|
|
87b6a36520 | ||
|
|
cf38cb3a6e | ||
|
|
9cb5113f6e | ||
|
|
7dd1c0c32a | ||
|
|
03233f3fdc | ||
|
|
377159c298 | ||
|
|
d8431fb620 | ||
|
|
0546ddefbd | ||
|
|
26f6889012 | ||
|
|
3ad6088565 | ||
|
|
b6fd568cc6 | ||
|
|
3861b07aef | ||
|
|
2bf57e2c72 | ||
|
|
744c6654cf | ||
|
|
b82e905757 | ||
|
|
c42e5b7dc3 | ||
|
|
b716218ecd | ||
|
|
511346e4bb | ||
|
|
5c2e49453e | ||
|
|
5d397de766 | ||
|
|
aa763f65d1 | ||
|
|
94cbd95509 | ||
|
|
f3762e356f | ||
|
|
5747d7549f | ||
|
|
aea4905d20 |
No files matched your search
@@ -22,6 +22,19 @@ env:
|
|||||||
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
PUBLIC_WARGAMING_APPLICATION_ID: ${{ secrets.PUBLIC_WARGAMING_APPLICATION_ID }}
|
||||||
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
PUBLIC_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
||||||
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
PUBLIC_ASSET_FAST_REPO: ${{ secrets.PUBLIC_ASSET_FAST_REPO }}
|
||||||
|
PUBLIC_PROMOTE_OPENTEST: ${{ secrets.PUBLIC_PROMOTE_OPENTEST }}
|
||||||
|
ALLOW_ROBOTS: ${{ secrets.ALLOW_ROBOTS }}
|
||||||
|
WOTB_GLOSSARY: ${{ secrets.WOTB_GLOSSARY }}
|
||||||
|
PUBLIC_GOOGLE_TAG_MANAGER_ID: ${{ secrets.PUBLIC_GOOGLE_TAG_MANAGER_ID }}
|
||||||
|
PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ${{ secrets.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID }}
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||||
|
WOTB_GLOSSARY_DOMAIN: ${{ secrets.WOTB_GLOSSARY_DOMAIN }}
|
||||||
|
WOTB_GLOSSARY_PATH: ${{ secrets.WOTB_GLOSSARY_PATH }}
|
||||||
|
PUBLIC_DISCORD_SERVER_ID: ${{ secrets.PUBLIC_DISCORD_SERVER_ID }}
|
||||||
|
PUBLIC_ASSET_BASE: ${{ secrets.PUBLIC_ASSET_BASE }}
|
||||||
|
PUBLIC_BRANCH: ${{ secrets.PUBLIC_BRANCH }}
|
||||||
|
PUBLIC_GUIDE_WEBSITE: ${{ secrets.PUBLIC_GUIDE_WEBSITE }}
|
||||||
|
SERVICING: ${{ secrets.SERVICING }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
host:
|
host:
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "all",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80
|
|
||||||
}
|
|
||||||
Vendored
+3
-7
@@ -167,6 +167,7 @@
|
|||||||
"searchtanks",
|
"searchtanks",
|
||||||
"SHAREDEXP",
|
"SHAREDEXP",
|
||||||
"SKLLD",
|
"SKLLD",
|
||||||
|
"smoothstep",
|
||||||
"Smri",
|
"Smri",
|
||||||
"Specularity",
|
"Specularity",
|
||||||
"SSAO",
|
"SSAO",
|
||||||
@@ -222,12 +223,7 @@
|
|||||||
"yolo",
|
"yolo",
|
||||||
"youtuber",
|
"youtuber",
|
||||||
"youtubers",
|
"youtubers",
|
||||||
"zoomable",
|
"zoomable"
|
||||||
"zustand"
|
|
||||||
],
|
],
|
||||||
"deno.enable": false,
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
"deno.unstable": false,
|
|
||||||
"cSpell.enableFiletypes": ["glsl", "proto3"],
|
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
|
||||||
"liveServer.settings.multiRootWorkspaceName": "blitzkit"
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# BlitzKit 1.27.18
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added a mini-game to BlitzKit similar to "Guess Who's That Pokemon" but with tanks: http://blitzkit.vercel.app/guess
|
||||||
|
- Switched to a new renderer for better lighting for the Tankopedia
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- The loading bar for the Tankopedia sandbox now shows up again
|
||||||
|
- Removed a lot of dead code, improving performance
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.27.19
|
||||||
|
|
||||||
|
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The camera is now lower town in the Tankopedia, giving the tank a more heroic feel
|
||||||
|
- Tanks are better framed across all devices, minimizing whitespace
|
||||||
|
- Added a skip button for the reload visualizer to skip to the next event
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Removed the useless "show grid" setting
|
||||||
|
- Taking screenshots now no longer includes the scene props like the floor
|
||||||
|
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
|
||||||
|
- The shot display card is now properly aligned as a proper table again
|
||||||
|
- Tank modules are once again considered in the penetration tester
|
||||||
|
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.17",
|
"version": "1.27.19",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ReadStream } from '@blitzkit/core';
|
import { ReadStream } from "@blitzkit/core";
|
||||||
import AdmZip from 'adm-zip';
|
import AdmZip from "adm-zip";
|
||||||
import { readFile, writeFile } from 'fs/promises';
|
import { readFile } from "fs/promises";
|
||||||
import { Parser } from 'pickleparser';
|
import { Parser } from "pickleparser";
|
||||||
import protobuf from 'protobufjs';
|
import protobuf from "protobufjs";
|
||||||
|
|
||||||
interface WotbReplayMetaJson {
|
interface WotbReplayMetaJson {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -24,26 +24,24 @@ export class WotbReplayReadStream extends ReadStream {
|
|||||||
async wotbReplay() {
|
async wotbReplay() {
|
||||||
const pickleParser = new Parser();
|
const pickleParser = new Parser();
|
||||||
const zip = new AdmZip(Buffer.from(this.buffer));
|
const zip = new AdmZip(Buffer.from(this.buffer));
|
||||||
const metaJsonBuffer = zip.getEntry('meta.json')?.getData();
|
const metaJsonBuffer = zip.getEntry("meta.json")?.getData();
|
||||||
const battleResultsDat = zip.getEntry('battle_results.dat')?.getData();
|
const battleResultsDat = zip.getEntry("battle_results.dat")?.getData();
|
||||||
|
|
||||||
if (!metaJsonBuffer || !battleResultsDat)
|
if (!metaJsonBuffer || !battleResultsDat)
|
||||||
throw new SyntaxError('meta.json not found');
|
throw new SyntaxError("meta.json not found");
|
||||||
|
|
||||||
const metaJson = JSON.parse(
|
const metaJson = JSON.parse(
|
||||||
metaJsonBuffer.toString(),
|
metaJsonBuffer.toString()
|
||||||
) as WotbReplayMetaJson;
|
) as WotbReplayMetaJson;
|
||||||
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
|
const unpickledBattleResults = pickleParser.parse(battleResultsDat);
|
||||||
const [, battleResultsString] = unpickledBattleResults as [number, string];
|
const [, battleResultsString] = unpickledBattleResults as [number, string];
|
||||||
const root = await protobuf.load(
|
const root = await protobuf.load(
|
||||||
`${__dirname}/../../protos/BattleResultsClient.proto`,
|
`${__dirname}/../../protos/BattleResultsClient.proto`
|
||||||
);
|
);
|
||||||
const BattleResults = root.lookupType(
|
const BattleResults = root.lookupType(
|
||||||
'BattleResultsGenerated.BattleResultsClient',
|
"BattleResultsGenerated.BattleResultsClient"
|
||||||
);
|
);
|
||||||
|
|
||||||
writeFile('test.dat', Buffer.from(battleResultsString));
|
|
||||||
|
|
||||||
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
|
const reader = protobuf.Reader.create(Buffer.from(battleResultsString));
|
||||||
|
|
||||||
// const decodedBattleResults = BattleResults.decodeDelimited(
|
// const decodedBattleResults = BattleResults.decodeDelimited(
|
||||||
@@ -53,7 +51,7 @@ export class WotbReplayReadStream extends ReadStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replay = await readFile('temp/test3.wotbreplay');
|
const replay = await readFile("temp/test3.wotbreplay");
|
||||||
const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
|
const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer);
|
||||||
|
|
||||||
stream.wotbReplay();
|
stream.wotbReplay();
|
||||||
@@ -861,6 +861,13 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"mixer": {
|
||||||
|
"name": "Mixer",
|
||||||
|
"button": "Mix and match",
|
||||||
|
"description": "Mix tank modules",
|
||||||
|
"seo_description": "Mix and match hulls, turrets, and guns of tanks in World of Tanks Blitz"
|
||||||
|
},
|
||||||
|
|
||||||
"guess": {
|
"guess": {
|
||||||
"name": "Guess the tank",
|
"name": "Guess the tank",
|
||||||
"button": "Guess away",
|
"button": "Guess away",
|
||||||
@@ -887,6 +894,10 @@
|
|||||||
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
|
||||||
|
|
||||||
"visualizers": {
|
"visualizers": {
|
||||||
|
"reload": {
|
||||||
|
"skip": "Skip"
|
||||||
|
},
|
||||||
|
|
||||||
"assault": {
|
"assault": {
|
||||||
"title": "Assault shells"
|
"title": "Assault shells"
|
||||||
},
|
},
|
||||||
@@ -935,9 +946,7 @@
|
|||||||
"green_penetration": "Green penetration",
|
"green_penetration": "Green penetration",
|
||||||
"hide_model_under_armor": "Hide model under armor",
|
"hide_model_under_armor": "Hide model under armor",
|
||||||
"opaque": "Opaque",
|
"opaque": "Opaque",
|
||||||
"dev_wireframe": "DEV: wireframe",
|
"dev_wireframe": "DEV: wireframe"
|
||||||
"environment": "Environment",
|
|
||||||
"show_grid": "Show grid"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"screenshot": {
|
"screenshot": {
|
||||||
|
|||||||
@@ -1,50 +1,52 @@
|
|||||||
import { toUniqueId } from '@blitzkit/core';
|
import { toUniqueId } from "@blitzkit/core";
|
||||||
import { NodeIO } from '@gltf-transform/core';
|
import { NodeIO } from "@gltf-transform/core";
|
||||||
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||||
import { readdir } from 'fs/promises';
|
import { readdir } from "fs/promises";
|
||||||
import { extractModel } from '../core/blitz/extractModel';
|
import { extractModel } from "../core/blitz/extractModel";
|
||||||
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
import { readXMLDVPL } from "../core/blitz/readXMLDVPL";
|
||||||
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
import { readYAMLDVPL } from "../core/blitz/readYAMLDVPL";
|
||||||
import { commitAssets } from '../core/github/commitAssets';
|
import { commitAssets } from "../core/github/commitAssets";
|
||||||
import { FileChange } from '../core/github/commitMultipleFiles';
|
import { FileChange } from "../core/github/commitMultipleFiles";
|
||||||
import { DATA } from './constants';
|
import { DATA } from "./constants";
|
||||||
import { VehicleDefinitionList } from './definitions';
|
import { VehicleDefinitionList } from "./definitions";
|
||||||
import { TankParameters } from './tankIcons';
|
import { TankParameters } from "./tankIcons";
|
||||||
|
|
||||||
export async function tankModels() {
|
export async function tankModels() {
|
||||||
console.log('Building tank models...');
|
console.log("Building tank models...");
|
||||||
|
|
||||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||||
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
const nations = await readdir(`${DATA}/XML/item_defs/vehicles`).then(
|
||||||
(nations) => nations.filter((nation) => nation !== 'common'),
|
(nations) => nations.filter((nation) => nation !== "common")
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const nationIndex in nations) {
|
for (const nationIndex in nations) {
|
||||||
const nation = nations[nationIndex];
|
const nation = nations[nationIndex];
|
||||||
const changes: FileChange[] = [];
|
const changes: FileChange[] = [];
|
||||||
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
const tanks = await readXMLDVPL<{ root: VehicleDefinitionList }>(
|
||||||
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`,
|
`${DATA}/XML/item_defs/vehicles/${nation}/list.xml`
|
||||||
);
|
);
|
||||||
|
const entries = Object.entries(tanks.root);
|
||||||
|
|
||||||
console.log(`Building models for ${nation}`);
|
console.log(`Building models for ${nation}`);
|
||||||
|
|
||||||
for (const [tankKey, tank] of Object.entries(tanks.root)) {
|
let index = 0;
|
||||||
if (tankKey.includes('tutorial_bot')) continue;
|
for (const [tankKey, tank] of entries) {
|
||||||
|
console.log(` ${++index} / ${entries.length} : ${tankKey}`);
|
||||||
|
|
||||||
|
if (tankKey.includes("tutorial_bot")) continue;
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
const id = toUniqueId(nation, tank.id);
|
||||||
|
|
||||||
const parameters = await readYAMLDVPL<TankParameters>(
|
const parameters = await readYAMLDVPL<TankParameters>(
|
||||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`
|
||||||
);
|
);
|
||||||
const model = await extractModel(
|
const model = await extractModel(
|
||||||
DATA,
|
DATA,
|
||||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ''),
|
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, "")
|
||||||
);
|
);
|
||||||
|
const content = Buffer.from(await nodeIO.writeBinary(model));
|
||||||
|
|
||||||
changes.push({
|
changes.push({ path: `3d/tanks/models/${id}.glb`, content });
|
||||||
path: `3d/tanks/models/${id}.glb`,
|
|
||||||
content: Buffer.from(await nodeIO.writeBinary(model)),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await commitAssets(`tank models ${nation}`, changes);
|
await commitAssets(`tank models ${nation}`, changes);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { VertexAttribute, vertexAttributeVectorSizes } from '@blitzkit/core';
|
import { VertexAttribute, vertexAttributeVectorSizes } from "@blitzkit/core";
|
||||||
|
|
||||||
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
export const vertexAttributeGLTFName: Partial<Record<VertexAttribute, string>> =
|
||||||
{
|
{
|
||||||
[VertexAttribute.VERTEX]: 'POSITION',
|
[VertexAttribute.VERTEX]: "POSITION",
|
||||||
[VertexAttribute.NORMAL]: 'NORMAL',
|
[VertexAttribute.NORMAL]: "NORMAL",
|
||||||
[VertexAttribute.TEXCOORD0]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD0]: "TEXCOORD_0",
|
||||||
[VertexAttribute.TEXCOORD1]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD1]: "TEXCOORD_1",
|
||||||
[VertexAttribute.TEXCOORD2]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD2]: "TEXCOORD_2",
|
||||||
[VertexAttribute.TEXCOORD3]: 'TEXCOORD_0',
|
[VertexAttribute.TEXCOORD3]: "TEXCOORD_3",
|
||||||
};
|
};
|
||||||
export const vertexAttributeGltfVectorSizes = {
|
export const vertexAttributeGltfVectorSizes = {
|
||||||
...vertexAttributeVectorSizes,
|
...vertexAttributeVectorSizes,
|
||||||
|
|||||||
@@ -4,35 +4,35 @@ import {
|
|||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
Textures,
|
Textures,
|
||||||
VertexAttribute,
|
VertexAttribute,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { Document, Material, Node, Scene } from '@gltf-transform/core';
|
import { Document, Material, Node, Scene } from "@gltf-transform/core";
|
||||||
import { dedup, prune } from '@gltf-transform/functions';
|
import { dedup, prune } from "@gltf-transform/functions";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { dirname } from 'path';
|
import { dirname } from "path";
|
||||||
import { Matrix4, Quaternion, Vector3, Vector4Tuple } from 'three';
|
import { readBaseColor } from "../readBaseColor";
|
||||||
import { readDVPLFile } from '../readDVPLFile';
|
import { readDVPLFile } from "../readDVPLFile";
|
||||||
import { readTexture } from '../readTexture';
|
import { readNormal } from "../readNormal";
|
||||||
import { TextureMutation } from '../readTexture/constants';
|
import { readRoughnessMetallic } from "../readRoughnessMetallic";
|
||||||
import {
|
import {
|
||||||
vertexAttributeGLTFName,
|
vertexAttributeGLTFName,
|
||||||
vertexAttributeGltfVectorSizes,
|
vertexAttributeGltfVectorSizes,
|
||||||
} from './constants';
|
} from "./constants";
|
||||||
|
|
||||||
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
const ERROR_ON_UNKNOWN_COMPONENT = false;
|
||||||
|
|
||||||
const omitMeshNames = {
|
const omitMeshNames = {
|
||||||
start: ['chassis_chassis_', 'chassis_track_crash_', 'HP_'],
|
start: ["chassis_chassis_", "chassis_track_crash_", "HP_"],
|
||||||
end: ['_POINT'],
|
end: ["_POINT"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function extractModel(data: string, path: string) {
|
export async function extractModel(data: string, path: string) {
|
||||||
const sc2Path = `${data}/3d/${path}.sc2`;
|
const sc2Path = `${data}/3d/${path}.sc2`;
|
||||||
const scgPath = `${data}/3d/${path}.scg`;
|
const scgPath = `${data}/3d/${path}.scg`;
|
||||||
const sc2 = new Sc2ReadStream(
|
const sc2 = new Sc2ReadStream(
|
||||||
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer,
|
(await readDVPLFile(sc2Path)).buffer as ArrayBuffer
|
||||||
).sc2();
|
).sc2();
|
||||||
const scg = new ScgReadStream(
|
const scg = new ScgReadStream(
|
||||||
(await readDVPLFile(scgPath)).buffer as ArrayBuffer,
|
(await readDVPLFile(scgPath)).buffer as ArrayBuffer
|
||||||
).scg();
|
).scg();
|
||||||
const document = new Document();
|
const document = new Document();
|
||||||
const scene = document.createScene();
|
const scene = document.createScene();
|
||||||
@@ -41,8 +41,8 @@ export async function extractModel(data: string, path: string) {
|
|||||||
|
|
||||||
// create materials
|
// create materials
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
sc2['#dataNodes'].map(async (node) => {
|
sc2["#dataNodes"].map(async (node) => {
|
||||||
const id = new DataView(node['#id']).getBigUint64(0, true);
|
const id = new DataView(node["#id"]).getBigUint64(0, true);
|
||||||
|
|
||||||
if (node.parentMaterialKey !== undefined) {
|
if (node.parentMaterialKey !== undefined) {
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +58,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
|
|
||||||
if (node.textures) {
|
if (node.textures) {
|
||||||
textures = node.textures;
|
textures = node.textures;
|
||||||
} else if (typeof node.configCount === 'number') {
|
} else if (typeof node.configCount === "number") {
|
||||||
textures = node.configArchive_0.textures;
|
textures = node.configArchive_0.textures;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,26 +66,27 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setBaseColorTexture(
|
material.setBaseColorTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/jpeg')
|
.setMimeType("image/jpeg")
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readBaseColor(
|
||||||
`${data}/3d/${dirname(path)}/${textures.albedo}`,
|
`${data}/3d/${dirname(path)}/${textures.baseColorMap ?? textures.albedo}`,
|
||||||
TextureMutation.Albedo,
|
textures.miscMap
|
||||||
),
|
? `${data}/3d/${dirname(path)}/${textures.miscMap}`
|
||||||
),
|
: undefined
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (textures.baseRMMap) {
|
if (textures.baseRMMap) {
|
||||||
material.setMetallicRoughnessTexture(
|
material.setMetallicRoughnessTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/jpeg')
|
.setMimeType("image/jpeg")
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readRoughnessMetallic(
|
||||||
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`,
|
`${data}/3d/${dirname(path)}/${textures.baseRMMap}`
|
||||||
TextureMutation.RoughnessMetallicness,
|
)
|
||||||
),
|
)
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,47 +96,33 @@ export async function extractModel(data: string, path: string) {
|
|||||||
material.setNormalTexture(
|
material.setNormalTexture(
|
||||||
document
|
document
|
||||||
.createTexture(node.materialName)
|
.createTexture(node.materialName)
|
||||||
.setMimeType('image/jpeg')
|
.setMimeType("image/jpeg")
|
||||||
.setImage(
|
.setImage(
|
||||||
await readTexture(
|
await readNormal(
|
||||||
`${data}/3d/${dirname(path)}/${
|
`${data}/3d/${dirname(path)}/${
|
||||||
textures.baseNormalMap ?? textures.normalmap
|
textures.baseNormalMap ?? textures.normalmap
|
||||||
}`,
|
}`,
|
||||||
isBase ? TextureMutation.Normal : undefined,
|
isBase
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textures.miscMap) {
|
|
||||||
material.setOcclusionTexture(
|
|
||||||
document
|
|
||||||
.createTexture(node.materialName)
|
|
||||||
.setMimeType('image/jpeg')
|
|
||||||
.setImage(
|
|
||||||
await readTexture(
|
|
||||||
`${data}/3d/${dirname(path)}/${textures.miscMap}`,
|
|
||||||
TextureMutation.Miscellaneous,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
materials.set(
|
materials.set(
|
||||||
new DataView(node['#id']).getBigUint64(0, true),
|
new DataView(node["#id"]).getBigUint64(0, true),
|
||||||
material,
|
material
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// replace children materials with parents
|
// replace children materials with parents
|
||||||
materials.forEach((material, id) => {
|
materials.forEach((material, id) => {
|
||||||
if (typeof material !== 'bigint') return;
|
if (typeof material !== "bigint") return;
|
||||||
|
|
||||||
let resolvedMaterial: Material | bigint | undefined = material;
|
let resolvedMaterial: Material | bigint | undefined = material;
|
||||||
|
|
||||||
while (typeof resolvedMaterial === 'bigint') {
|
while (typeof resolvedMaterial === "bigint") {
|
||||||
const linkedParentMaterial = materials.get(resolvedMaterial);
|
const linkedParentMaterial = materials.get(resolvedMaterial);
|
||||||
|
|
||||||
resolvedMaterial = linkedParentMaterial;
|
resolvedMaterial = linkedParentMaterial;
|
||||||
@@ -155,36 +142,36 @@ export async function extractModel(data: string, path: string) {
|
|||||||
const node = document.createNode(hierarchy.name);
|
const node = document.createNode(hierarchy.name);
|
||||||
const components = times(
|
const components = times(
|
||||||
hierarchy.components.count,
|
hierarchy.components.count,
|
||||||
(index) => hierarchy.components[index.toString().padStart(4, '0')],
|
(index) => hierarchy.components[index.toString().padStart(4, "0")]
|
||||||
);
|
);
|
||||||
|
|
||||||
components.forEach((component) => {
|
components.forEach((component) => {
|
||||||
switch (component['comp.typename']) {
|
switch (component["comp.typename"]) {
|
||||||
case 'LodComponent':
|
case "LodComponent":
|
||||||
// found and used later by transform component
|
// found and used later by transform component
|
||||||
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
// TODO: lod component always shows up before transform component; cache it before parsing transform
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'TransformComponent': {
|
case "TransformComponent": {
|
||||||
node.setTranslation(component['tc.localTranslation']);
|
node.setTranslation(component["tc.localTranslation"]);
|
||||||
node.setRotation(component['tc.localRotation']);
|
node.setRotation(component["tc.localRotation"]);
|
||||||
node.setScale(component['tc.localScale']);
|
node.setScale(component["tc.localScale"]);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'RenderComponent': {
|
case "RenderComponent": {
|
||||||
const renderObject = component['rc.renderObj'];
|
const renderObject = component["rc.renderObj"];
|
||||||
|
|
||||||
times(renderObject['ro.batchCount'], (batchIndex): void => {
|
times(renderObject["ro.batchCount"], (batchIndex): void => {
|
||||||
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
const lodIndex = renderObject[`rb${batchIndex}.lodIndex`];
|
||||||
|
|
||||||
if (lodIndex !== 0) return;
|
if (lodIndex !== 0) return;
|
||||||
|
|
||||||
const batchKey = batchIndex.toString().padStart(4, '0');
|
const batchKey = batchIndex.toString().padStart(4, "0");
|
||||||
const batch = renderObject['ro.batches'][batchKey];
|
const batch = renderObject["ro.batches"][batchKey];
|
||||||
const material = materials.get(batch['rb.nmatname']);
|
const material = materials.get(batch["rb.nmatname"]);
|
||||||
const polygonGroup = scg.get(batch['rb.datasource']);
|
const polygonGroup = scg.get(batch["rb.datasource"]);
|
||||||
|
|
||||||
if (!(material instanceof Material)) {
|
if (!(material instanceof Material)) {
|
||||||
// probably shadow material
|
// probably shadow material
|
||||||
@@ -192,14 +179,14 @@ export async function extractModel(data: string, path: string) {
|
|||||||
}
|
}
|
||||||
if (polygonGroup === undefined) {
|
if (polygonGroup === undefined) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing polygon group ${batch['rb.datasource']}`,
|
`Missing polygon group ${batch["rb.datasource"]}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lodNode = document.createNode(batchKey);
|
const lodNode = document.createNode(batchKey);
|
||||||
const indicesAccessor = document
|
const indicesAccessor = document
|
||||||
.createAccessor()
|
.createAccessor()
|
||||||
.setType('SCALAR')
|
.setType("SCALAR")
|
||||||
.setArray(new Uint16Array(polygonGroup.indices))
|
.setArray(new Uint16Array(polygonGroup.indices))
|
||||||
.setBuffer(buffer);
|
.setBuffer(buffer);
|
||||||
const primitive = document
|
const primitive = document
|
||||||
@@ -233,7 +220,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
const vertexSize = vertexAttributeGltfVectorSizes[attribute];
|
||||||
const attributeAccessor = document
|
const attributeAccessor = document
|
||||||
.createAccessor(name)
|
.createAccessor(name)
|
||||||
.setType(vertexSize === 1 ? 'SCALAR' : `VEC${vertexSize}`)
|
.setType(vertexSize === 1 ? "SCALAR" : `VEC${vertexSize}`)
|
||||||
.setArray(new Float32Array(value.flat()))
|
.setArray(new Float32Array(value.flat()))
|
||||||
.setBuffer(buffer);
|
.setBuffer(buffer);
|
||||||
|
|
||||||
@@ -241,7 +228,7 @@ export async function extractModel(data: string, path: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mesh = document
|
const mesh = document
|
||||||
.createMesh(batch['##name'])
|
.createMesh(batch["##name"])
|
||||||
.addPrimitive(primitive);
|
.addPrimitive(primitive);
|
||||||
lodNode.setMesh(mesh);
|
lodNode.setMesh(mesh);
|
||||||
node.addChild(lodNode);
|
node.addChild(lodNode);
|
||||||
@@ -253,24 +240,24 @@ export async function extractModel(data: string, path: string) {
|
|||||||
default: {
|
default: {
|
||||||
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
if (ERROR_ON_UNKNOWN_COMPONENT) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unhandled component type: ${component['comp.typename']}`,
|
`Unhandled component type: ${component["comp.typename"]}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hierarchy['#hierarchy']) {
|
if (hierarchy["#hierarchy"]) {
|
||||||
parseHierarchies(hierarchy['#hierarchy'], node);
|
parseHierarchies(hierarchy["#hierarchy"], node);
|
||||||
}
|
}
|
||||||
|
|
||||||
parent.addChild(node);
|
parent.addChild(node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
parseHierarchies(sc2['#hierarchy'], scene);
|
parseHierarchies(sc2["#hierarchy"], scene);
|
||||||
|
|
||||||
scene.addChild(document.createNode('test'));
|
scene.addChild(document.createNode("test"));
|
||||||
|
|
||||||
await document.transform(prune({ keepAttributes: true }), dedup());
|
await document.transform(prune({ keepAttributes: true }), dedup());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import sharp from "sharp";
|
||||||
|
import { readTexture } from "./readTexture";
|
||||||
|
|
||||||
|
export async function readBaseColor(path: string, occlusionPath?: string) {
|
||||||
|
const baseRaw = await readTexture(path);
|
||||||
|
const occlusionRaw = occlusionPath
|
||||||
|
? await readTexture(occlusionPath)
|
||||||
|
: undefined;
|
||||||
|
const base = await sharp(baseRaw.data, { raw: baseRaw })
|
||||||
|
.removeAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer();
|
||||||
|
const occlusion = occlusionRaw
|
||||||
|
? await sharp(occlusionRaw.data, { raw: occlusionRaw })
|
||||||
|
.extractChannel(3)
|
||||||
|
.raw()
|
||||||
|
.toBuffer()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const combined = Buffer.alloc(baseRaw.width * baseRaw.height * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < baseRaw.width * baseRaw.height; i++) {
|
||||||
|
let c = 1;
|
||||||
|
|
||||||
|
if (occlusionRaw) {
|
||||||
|
const x = i % baseRaw.width;
|
||||||
|
const y = Math.floor(i / baseRaw.width);
|
||||||
|
const u = Math.floor(x * (occlusionRaw.width / baseRaw.width));
|
||||||
|
const v = Math.floor(y * (occlusionRaw.height / baseRaw.height));
|
||||||
|
|
||||||
|
const occlusionI = u + v * occlusionRaw.width;
|
||||||
|
|
||||||
|
c = occlusion![occlusionI] / 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
combined[i * 3 + 0] = Math.round(base[i * 3 + 0] * c);
|
||||||
|
combined[i * 3 + 1] = Math.round(base[i * 3 + 1] * c);
|
||||||
|
combined[i * 3 + 2] = Math.round(base[i * 3 + 2] * c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await sharp(combined, {
|
||||||
|
raw: { width: baseRaw.width, height: baseRaw.height, channels: 3 },
|
||||||
|
})
|
||||||
|
.jpeg()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import sharp from "sharp";
|
||||||
|
import { readTexture } from "./readTexture";
|
||||||
|
|
||||||
|
export async function readNormal(path: string, isBase: boolean) {
|
||||||
|
const raw = await readTexture(path);
|
||||||
|
|
||||||
|
if (!isBase) {
|
||||||
|
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = 4 * raw.width * raw.height;
|
||||||
|
|
||||||
|
for (let index = 0; index < bytes; index += 4) {
|
||||||
|
/**
|
||||||
|
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
||||||
|
* sort of information.
|
||||||
|
*/
|
||||||
|
let x = raw.data[index + 3] * (2 / 255) - 1;
|
||||||
|
let y = raw.data[index + 1] * (2 / 255) - 1;
|
||||||
|
let z = Math.sqrt(Math.max(0, 1 - x ** 2 - y ** 2));
|
||||||
|
|
||||||
|
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
||||||
|
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
||||||
|
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
||||||
|
raw.data[index + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import sharp from "sharp";
|
||||||
|
import { readTexture } from "./readTexture";
|
||||||
|
|
||||||
|
export async function readRoughnessMetallic(path: string) {
|
||||||
|
const raw = await readTexture(path);
|
||||||
|
const blitz = sharp(raw.data, { raw: raw });
|
||||||
|
const metallicness = await blitz?.extractChannel(1).raw().toBuffer();
|
||||||
|
const roughness = await blitz?.extractChannel(3).raw().toBuffer();
|
||||||
|
|
||||||
|
const combined = Buffer.alloc(raw.width * raw.height * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < raw.width * raw.height; i++) {
|
||||||
|
combined[i * 3 + 1] = roughness[i];
|
||||||
|
combined[i * 3 + 2] = metallicness[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = await sharp(combined, {
|
||||||
|
raw: { width: raw.width, height: raw.height, channels: 3 },
|
||||||
|
})
|
||||||
|
.jpeg()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
@@ -1,81 +1,83 @@
|
|||||||
import { DdsReadStream, PvrReadStream } from '@blitzkit/core';
|
import { DdsReadStream, PvrReadStream } from "@blitzkit/core";
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from "fs";
|
||||||
import sharp from 'sharp';
|
import sharp from "sharp";
|
||||||
import { DATA_COMPRESSED } from '../../../buildAssets/constants';
|
import { DATA_COMPRESSED } from "../../../buildAssets/constants";
|
||||||
import { readDVPLFile } from '../readDVPLFile';
|
import { readDVPLFile } from "../readDVPLFile";
|
||||||
import { TextureMutation } from './constants';
|
import { TextureMutation } from "./constants";
|
||||||
|
|
||||||
export async function readTexture(path: string, mutation?: TextureMutation) {
|
export async function readTexture(path: string) {
|
||||||
const ddsTexturePath = path.replace('.tex', '.dx11.dds');
|
const ddsTexturePath = path.replace(".tex", ".dx11.dds");
|
||||||
const isDds = existsSync(
|
const isDds = existsSync(
|
||||||
`${ddsTexturePath}${DATA_COMPRESSED ? '.dvpl' : ''}`,
|
`${ddsTexturePath}${DATA_COMPRESSED ? ".dvpl" : ""}`
|
||||||
);
|
);
|
||||||
const resolvedTexturePath = isDds
|
const resolvedTexturePath = isDds
|
||||||
? ddsTexturePath
|
? ddsTexturePath
|
||||||
: ddsTexturePath.replace('.dds', '.pvr');
|
: ddsTexturePath.replace(".dds", ".pvr");
|
||||||
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
|
const decompressedDvpl = await readDVPLFile(`${resolvedTexturePath}.dvpl`);
|
||||||
|
|
||||||
const raw = isDds
|
const raw = isDds
|
||||||
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
|
? await new DdsReadStream(decompressedDvpl.buffer as ArrayBuffer).dds()
|
||||||
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
|
: new PvrReadStream(decompressedDvpl.buffer as ArrayBuffer).pvr();
|
||||||
|
|
||||||
switch (mutation) {
|
return raw;
|
||||||
case TextureMutation.Normal: {
|
|
||||||
const bytes = 4 * raw.width * raw.height;
|
|
||||||
for (let index = 0; index < bytes; index += 4) {
|
|
||||||
/**
|
|
||||||
* Red is always 255 and blue is always 0. Only alpha and green contain any
|
|
||||||
* sort of information.
|
|
||||||
*/
|
|
||||||
let x = raw.data[index + 3] * (2 / 255) - 1;
|
|
||||||
let y = raw.data[index + 1] * (2 / 255) - 1;
|
|
||||||
const underSqrt = 1 - x ** 2 - y ** 2;
|
|
||||||
let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
|
|
||||||
|
|
||||||
raw.data[index] = Math.round((x + 1) * (255 / 2));
|
// switch (mutation) {
|
||||||
raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
// case TextureMutation.Normal: {
|
||||||
raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
// const bytes = 4 * raw.width * raw.height;
|
||||||
raw.data[index + 3] = 255;
|
// for (let index = 0; index < bytes; index += 4) {
|
||||||
}
|
// /**
|
||||||
|
// * Red is always 255 and blue is always 0. Only alpha and green contain any
|
||||||
|
// * sort of information.
|
||||||
|
// */
|
||||||
|
// let x = raw.data[index + 3] * (2 / 255) - 1;
|
||||||
|
// let y = raw.data[index + 1] * (2 / 255) - 1;
|
||||||
|
// const underSqrt = 1 - x ** 2 - y ** 2;
|
||||||
|
// let z = underSqrt > 0 ? Math.sqrt(underSqrt) : -Math.sqrt(-underSqrt);
|
||||||
|
|
||||||
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
// raw.data[index] = Math.round((x + 1) * (255 / 2));
|
||||||
}
|
// raw.data[index + 1] = Math.round((y + 1) * (255 / 2));
|
||||||
|
// raw.data[index + 2] = Math.round((z + 1) * (255 / 2));
|
||||||
|
// raw.data[index + 3] = 255;
|
||||||
|
// }
|
||||||
|
|
||||||
case TextureMutation.RoughnessMetallicness: {
|
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
|
// }
|
||||||
raw: { ...raw, channels: 1 },
|
|
||||||
});
|
|
||||||
const metallicness = sharp(raw.data, { raw })
|
|
||||||
.extractChannel('green')
|
|
||||||
.jpeg()
|
|
||||||
.toBuffer();
|
|
||||||
const roughness = sharp(raw.data, { raw })
|
|
||||||
.extractChannel('alpha')
|
|
||||||
.jpeg()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
return await newImage
|
// case TextureMutation.RoughnessMetallicness: {
|
||||||
.joinChannel(await Promise.all([roughness, metallicness]))
|
// const newImage = sharp(Buffer.alloc(raw.width * raw.height), {
|
||||||
.jpeg()
|
// raw: { ...raw, channels: 1 },
|
||||||
.toBuffer();
|
// });
|
||||||
}
|
// const metallicness = sharp(raw.data, { raw })
|
||||||
|
// .extractChannel("green")
|
||||||
|
// .jpeg()
|
||||||
|
// .toBuffer();
|
||||||
|
// const roughness = sharp(raw.data, { raw })
|
||||||
|
// .extractChannel("alpha")
|
||||||
|
// .jpeg()
|
||||||
|
// .toBuffer();
|
||||||
|
|
||||||
case TextureMutation.Miscellaneous: {
|
// return await newImage
|
||||||
/**
|
// .joinChannel(await Promise.all([roughness, metallicness]))
|
||||||
* Alpha is ambient occlusion and green is emissive. But very few tanks
|
// .jpeg()
|
||||||
* use emissive so I am ignoring it for now.
|
// .toBuffer();
|
||||||
*/
|
// }
|
||||||
return await sharp(raw.data, { raw })
|
|
||||||
.extractChannel('alpha')
|
|
||||||
.jpeg()
|
|
||||||
.toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
case TextureMutation.Albedo: {
|
// case TextureMutation.Miscellaneous: {
|
||||||
return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
// /**
|
||||||
}
|
// * Alpha is ambient occlusion and green is emissive. But very few tanks
|
||||||
|
// * use emissive so I am ignoring it for now.
|
||||||
|
// */
|
||||||
|
// return await sharp(raw.data, { raw })
|
||||||
|
// .extractChannel("alpha")
|
||||||
|
// .jpeg()
|
||||||
|
// .toBuffer();
|
||||||
|
// }
|
||||||
|
|
||||||
default:
|
// case TextureMutation.Albedo: {
|
||||||
return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
// return await sharp(raw.data, { raw }).removeAlpha().jpeg().toBuffer();
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// return await sharp(raw.data, { raw }).jpeg().toBuffer();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "varuna",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { produce } from "immer";
|
||||||
|
import { merge } from "lodash-es";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export class Varuna<Type, Arguments = void> {
|
||||||
|
private listeners = new Set<(state: Type) => void>();
|
||||||
|
|
||||||
|
private initialized = false;
|
||||||
|
private _initial?: Type;
|
||||||
|
private _state?: Type;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private creator: Type | ((...args: Arguments[]) => Type),
|
||||||
|
private persistence?: string
|
||||||
|
) {
|
||||||
|
if (typeof creator !== "function") this.initialize(creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(initial: Type) {
|
||||||
|
let data = initial;
|
||||||
|
|
||||||
|
if (this.persistence && typeof localStorage !== "undefined") {
|
||||||
|
const instance = this;
|
||||||
|
const dehydrated = localStorage.getItem(this.persistence);
|
||||||
|
|
||||||
|
if (dehydrated) {
|
||||||
|
const rehydrated = JSON.parse(dehydrated) as unknown;
|
||||||
|
data = merge(data, rehydrated);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", () => {
|
||||||
|
localStorage.setItem(this.persistence!, JSON.stringify(instance.state));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
this._initial = data;
|
||||||
|
this._state = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
useInitialization(...args: Arguments[]) {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
if (typeof this.creator !== "function") {
|
||||||
|
throw new Error("Provider must be used with a function creator");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialize((this.creator as (...args: Arguments[]) => Type)(...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatch() {
|
||||||
|
const state = this.state;
|
||||||
|
this.listeners.forEach((callback) => callback(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
on<Slice>(slicer: (state: Type) => Slice, callback: (state: Slice) => void) {
|
||||||
|
let last = slicer(this.state);
|
||||||
|
|
||||||
|
function listener(state: Type) {
|
||||||
|
const next = slicer(state);
|
||||||
|
|
||||||
|
if (next !== last) {
|
||||||
|
callback(next);
|
||||||
|
last = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate(mutator: (state: Type) => void) {
|
||||||
|
this.set(produce(this.state, mutator));
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertInitialized() {
|
||||||
|
if (!this.initialized) {
|
||||||
|
throw new Error("Varuna store used before initialization");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
this.assertInitialized();
|
||||||
|
return this._state as Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(state: Type) {
|
||||||
|
this.assertInitialized();
|
||||||
|
this._state = state;
|
||||||
|
this.dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
get initial() {
|
||||||
|
this.assertInitialized();
|
||||||
|
return this._initial as Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
use<Slice = Type>(
|
||||||
|
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
|
||||||
|
) {
|
||||||
|
return this.useDeferred(slicer, slicer(this.state));
|
||||||
|
}
|
||||||
|
|
||||||
|
useDeferred<Slice = Type>(slicer: (state: Type) => Slice, initial: Slice) {
|
||||||
|
const instance = this;
|
||||||
|
const [slice, setSlice] = useState(initial);
|
||||||
|
|
||||||
|
useEffect(() => instance.on(slicer, setSlice), []);
|
||||||
|
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json"
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"@astrolib/seo": "^1.0.0-beta.8",
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
|
"varuna": "workspace:*",
|
||||||
"@fontsource/inter": "^5.2.5",
|
"@fontsource/inter": "^5.2.5",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
@@ -35,8 +36,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-promise-suspense": "^0.3.4",
|
"react-promise-suspense": "^0.3.4",
|
||||||
"three": "^0.169.0",
|
"three": "^0.169.0",
|
||||||
"urlon": "^3.1.0",
|
"urlon": "^3.1.0"
|
||||||
"zustand": "^4.5.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 490 KiB After Width: | Height: | Size: 669 KiB |
@@ -1,4 +1,4 @@
|
|||||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Code,
|
Code,
|
||||||
@@ -6,26 +6,21 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from "@react-three/drei";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
|
import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
|
||||||
import { App } from '../../../stores/app';
|
import { App } from "../../../stores/app";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { layerTypeNames } from './ShotDisplayCard';
|
import { layerTypeNames } from "./ShotDisplayCard";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export function ArmorPlateDisplay() {
|
export function ArmorPlateDisplay() {
|
||||||
const highlightArmor = TankopediaEphemeral.use(
|
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
|
||||||
(state) => state.highlightArmor,
|
|
||||||
);
|
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
|
|
||||||
if (highlightArmor === undefined) return null;
|
if (highlightArmor === undefined) return null;
|
||||||
|
|
||||||
@@ -33,20 +28,20 @@ export function ArmorPlateDisplay() {
|
|||||||
<group position={highlightArmor.point}>
|
<group position={highlightArmor.point}>
|
||||||
<Html
|
<Html
|
||||||
center
|
center
|
||||||
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
variant="classic"
|
variant="classic"
|
||||||
mb="4"
|
mb="4"
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
color: highlightArmor.color,
|
color: highlightArmor.color,
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
<Text weight="bold">
|
<Text weight="bold">
|
||||||
{layerTypeNames[highlightArmor.type]}{' '}
|
{layerTypeNames[highlightArmor.type]}{" "}
|
||||||
{highlightArmor.thickness.toFixed(0)}
|
{highlightArmor.thickness.toFixed(0)}
|
||||||
<Text size="1" weight="regular">
|
<Text size="1" weight="regular">
|
||||||
mm
|
mm
|
||||||
@@ -92,34 +87,34 @@ export function ArmorPlateDisplay() {
|
|||||||
|
|
||||||
if (isNaN(thickness) || thickness < 0) return;
|
if (isNaN(thickness) || thickness < 0) return;
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const { protagonist } = duelStore.getState();
|
const { protagonist } = Duel.state;
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
const gun = turret.guns[protagonist.gun.id];
|
const gun = turret.guns[protagonist.gun.id];
|
||||||
const track = tank.tracks[protagonist.track.id];
|
const track = tank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor.thickness[index] = thickness;
|
tank.armor.thickness[index] = thickness;
|
||||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||||
turret.armor.thickness[index] = thickness;
|
turret.armor.thickness[index] = thickness;
|
||||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||||
if (highlightArmor.name.includes('_armor_')) {
|
if (highlightArmor.name.includes("_armor_")) {
|
||||||
gun.armor.thickness[index] = thickness;
|
gun.armor.thickness[index] = thickness;
|
||||||
} else {
|
} else {
|
||||||
gun.thickness = thickness;
|
gun.thickness = thickness;
|
||||||
}
|
}
|
||||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||||
track.thickness = thickness;
|
track.thickness = thickness;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === "Escape") {
|
||||||
input.current?.blur();
|
input.current?.blur();
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,7 +132,7 @@ export function ArmorPlateDisplay() {
|
|||||||
mt="2"
|
mt="2"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
if (!highlightArmor.editingPlate) return;
|
if (!highlightArmor.editingPlate) return;
|
||||||
|
|
||||||
const index = resolveArmorIndex(highlightArmor.name);
|
const index = resolveArmorIndex(highlightArmor.name);
|
||||||
@@ -145,9 +140,8 @@ export function ArmorPlateDisplay() {
|
|||||||
if (index === undefined) return;
|
if (index === undefined) return;
|
||||||
|
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
const initialState =
|
const initialState = Tankopedia.initial;
|
||||||
tankopediaEphemeralStore.getInitialState();
|
const { protagonist } = Duel.state;
|
||||||
const { protagonist } = duelStore.getState();
|
|
||||||
const tank = draft.model;
|
const tank = draft.model;
|
||||||
const initialTank = initialState.model;
|
const initialTank = initialState.model;
|
||||||
const turret = tank.turrets[protagonist.turret.id];
|
const turret = tank.turrets[protagonist.turret.id];
|
||||||
@@ -159,20 +153,20 @@ export function ArmorPlateDisplay() {
|
|||||||
const initialTrack =
|
const initialTrack =
|
||||||
initialTank.tracks[protagonist.track.id];
|
initialTank.tracks[protagonist.track.id];
|
||||||
|
|
||||||
if (highlightArmor.name.startsWith('hull_')) {
|
if (highlightArmor.name.startsWith("hull_")) {
|
||||||
tank.armor.thickness[index] =
|
tank.armor.thickness[index] =
|
||||||
initialTank.armor.thickness[index];
|
initialTank.armor.thickness[index];
|
||||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||||
turret.armor.thickness[index] =
|
turret.armor.thickness[index] =
|
||||||
initialTurret.armor.thickness[index];
|
initialTurret.armor.thickness[index];
|
||||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||||
if (highlightArmor.name.includes('_armor_')) {
|
if (highlightArmor.name.includes("_armor_")) {
|
||||||
gun.armor.thickness[index] =
|
gun.armor.thickness[index] =
|
||||||
initialGun.armor.thickness[index];
|
initialGun.armor.thickness[index];
|
||||||
} else {
|
} else {
|
||||||
gun.thickness = initialGun.thickness;
|
gun.thickness = initialGun.thickness;
|
||||||
}
|
}
|
||||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||||
track.thickness = initialTrack.thickness;
|
track.thickness = initialTrack.thickness;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const PrimaryArmorScene = memo(() => {
|
|||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
// const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
|
|||||||
+57
-71
@@ -2,9 +2,9 @@ import {
|
|||||||
canSplash,
|
canSplash,
|
||||||
isExplosive,
|
isExplosive,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
NormalBlending,
|
NormalBlending,
|
||||||
@@ -14,16 +14,16 @@ import {
|
|||||||
UniformsLib,
|
UniformsLib,
|
||||||
UniformsUtils,
|
UniformsUtils,
|
||||||
Vector2,
|
Vector2,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
import { spacedArmorRenderTarget } from './target';
|
import { spacedArmorRenderTarget } from "./target";
|
||||||
|
|
||||||
interface PrimaryArmorSceneComponentProps {
|
interface PrimaryArmorSceneComponentProps {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
@@ -41,9 +41,6 @@ export function PrimaryArmorSceneComponent({
|
|||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
}: PrimaryArmorSceneComponentProps) {
|
}: PrimaryArmorSceneComponentProps) {
|
||||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -80,30 +77,30 @@ export function PrimaryArmorSceneComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
shell.normalization ?? 0
|
||||||
);
|
);
|
||||||
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||||
material.uniforms.canSplash.value = canSplash(shell.type);
|
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||||
material.uniforms.damage.value = shell.armor_damage;
|
material.uniforms.damage.value = shell.armor_damage;
|
||||||
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||||
|
|
||||||
await handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
await handleAntagonistEquipmentChange(
|
handleAntagonistEquipmentChange(
|
||||||
duel.antagonist.equipmentMatrix,
|
Duel.state.antagonist.equipmentMatrix,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidate();
|
invalidate();
|
||||||
@@ -121,15 +118,14 @@ export function PrimaryArmorSceneComponent({
|
|||||||
function handleWireframeChange(wireframe: boolean) {
|
function handleWireframeChange(wireframe: boolean) {
|
||||||
material.wireframe = wireframe;
|
material.wireframe = wireframe;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
equipment: EquipmentMatrix,
|
||||||
noInvalidate = false,
|
noInvalidate = false
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const hasEnhancedArmor = hasEquipment(
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
@@ -137,18 +133,18 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
if (!noInvalidate) invalidate();
|
if (!noInvalidate) invalidate();
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(
|
function handleAntagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
equipment: EquipmentMatrix,
|
||||||
noInvalidate = false,
|
noInvalidate = false
|
||||||
) {
|
) {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -158,45 +154,35 @@ export function PrimaryArmorSceneComponent({
|
|||||||
if (!noInvalidate) invalidate();
|
if (!noInvalidate) invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState = tankopediaPersistentStore.getState();
|
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleGreenPenetrationChange(initialState.greenPenetration);
|
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
|
||||||
handleAdvancedHighlightingChange(initialState.advancedHighlighting);
|
handleAdvancedHighlightingChange(
|
||||||
handleOpaqueChange(initialState.opaque);
|
TankopediaPersistent.state.advancedHighlighting
|
||||||
handleWireframeChange(initialState.wireframe);
|
|
||||||
handleProtagonistEquipmentChange(
|
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
handleAntagonistEquipmentChange(
|
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
|
||||||
);
|
);
|
||||||
|
handleOpaqueChange(TankopediaPersistent.state.opaque);
|
||||||
|
handleWireframeChange(TankopediaPersistent.state.wireframe);
|
||||||
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
TankopediaPersistent.on(
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
tankopediaPersistentStore.subscribe(
|
|
||||||
(state) => state.greenPenetration,
|
(state) => state.greenPenetration,
|
||||||
handleGreenPenetrationChange,
|
handleGreenPenetrationChange
|
||||||
),
|
),
|
||||||
tankopediaPersistentStore.subscribe(
|
TankopediaPersistent.on(
|
||||||
(state) => state.advancedHighlighting,
|
(state) => state.advancedHighlighting,
|
||||||
handleAdvancedHighlightingChange,
|
handleAdvancedHighlightingChange
|
||||||
),
|
),
|
||||||
tankopediaPersistentStore.subscribe(
|
TankopediaPersistent.on((state) => state.opaque, handleOpaqueChange),
|
||||||
(state) => state.opaque,
|
Duel.on(
|
||||||
handleOpaqueChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
(equipment) => handleProtagonistEquipmentChange(equipment),
|
(equipment) => handleProtagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
Duel.on(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
(equipment) => handleAntagonistEquipmentChange(equipment),
|
(equipment) => handleAntagonistEquipmentChange(equipment)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -207,7 +193,7 @@ export function PrimaryArmorSceneComponent({
|
|||||||
|
|
||||||
useFrame(({ gl, camera }) => {
|
useFrame(({ gl, camera }) => {
|
||||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||||
gl.getPixelRatio(),
|
gl.getPixelRatio()
|
||||||
);
|
);
|
||||||
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||||
material.uniforms.spacedArmorDepth.value =
|
material.uniforms.spacedArmorDepth.value =
|
||||||
|
|||||||
+2
-2
@@ -37,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
|
|||||||
|
|
||||||
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
||||||
if (advancedHighlighting && couldHaveRicochet) {
|
if (advancedHighlighting && couldHaveRicochet) {
|
||||||
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0);
|
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vec3(0.0, 1.0, 0.0);
|
return vec3(0.0, 1.0, 0.0);
|
||||||
@@ -107,7 +107,7 @@ void main() {
|
|||||||
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
||||||
|
|
||||||
if (advancedHighlighting && didRicochet) {
|
if (advancedHighlighting && didRicochet) {
|
||||||
baseColor = vec3(1.0, baseColor.g, 0.5);
|
baseColor = vec3(1.0, baseColor.g, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (greenPenetration || advancedHighlighting) {
|
if (greenPenetration || advancedHighlighting) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { J_HAT, K_HAT } from '@blitzkit/core';
|
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
import { Html } from '@react-three/drei';
|
import { Html } from "@react-three/drei";
|
||||||
import { invalidate, useFrame } from '@react-three/fiber';
|
import { invalidate, useFrame } from "@react-three/fiber";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import { useEffect, useRef, type ComponentProps } from 'react';
|
import { useEffect, useRef, type ComponentProps } from "react";
|
||||||
import {
|
import {
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
DoubleSide,
|
DoubleSide,
|
||||||
@@ -15,23 +15,24 @@ import {
|
|||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
Path,
|
Path,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
import { isHalloween } from "../../../core/blitzkit/isHalloween";
|
||||||
|
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
type ShotStatus,
|
type ShotStatus,
|
||||||
} from '../../../stores/tankopediaEphemeral';
|
} from "../../../stores/tankopedia";
|
||||||
import { ShotDisplayCard } from './ShotDisplayCard';
|
import { ShotDisplayCard } from "./ShotDisplayCard";
|
||||||
|
|
||||||
export const shotStatusColors: Record<
|
export const shotStatusColors: Record<
|
||||||
ShotStatus,
|
ShotStatus,
|
||||||
ComponentProps<typeof Text>['color']
|
ComponentProps<typeof Text>["color"]
|
||||||
> = {
|
> = {
|
||||||
blocked: 'red',
|
blocked: "red",
|
||||||
penetration: 'green',
|
penetration: "green",
|
||||||
ricochet: 'yellow',
|
ricochet: "yellow",
|
||||||
splash: 'orange',
|
splash: "orange",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SURFACE_POINT_SIZE = 12;
|
const SURFACE_POINT_SIZE = 12;
|
||||||
@@ -40,7 +41,7 @@ const TRACER_THICK = 1 / 32;
|
|||||||
const TRACER_THIN = TRACER_THICK / 2;
|
const TRACER_THIN = TRACER_THICK / 2;
|
||||||
|
|
||||||
export function ShotDisplay() {
|
export function ShotDisplay() {
|
||||||
const shot = TankopediaEphemeral.use((state) => state.shot);
|
const shot = Tankopedia.use((state) => state.shot);
|
||||||
const inTracer = useRef<Mesh>(null);
|
const inTracer = useRef<Mesh>(null);
|
||||||
const outTracer = useRef<Mesh>(null);
|
const outTracer = useRef<Mesh>(null);
|
||||||
const splashRadiusWrapper = useRef<Group>(null);
|
const splashRadiusWrapper = useRef<Group>(null);
|
||||||
@@ -51,16 +52,16 @@ export function ShotDisplay() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shot?.splashRadius !== undefined) {
|
if (shot?.splashRadius !== undefined) {
|
||||||
const outlineGeometry = new BufferGeometry().setFromPoints(
|
const outlineGeometry = new BufferGeometry().setFromPoints(
|
||||||
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50),
|
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50)
|
||||||
);
|
);
|
||||||
const outlineMaterial = new LineDashedMaterial({
|
const outlineMaterial = new LineDashedMaterial({
|
||||||
color: 'orange',
|
color: "orange",
|
||||||
dashSize: 0.05,
|
dashSize: 0.05,
|
||||||
gapSize: 0.025,
|
gapSize: 0.025,
|
||||||
});
|
});
|
||||||
const outline = new Line(
|
const outline = new Line(
|
||||||
outlineGeometry,
|
outlineGeometry,
|
||||||
outlineMaterial,
|
outlineMaterial
|
||||||
).computeLineDistances();
|
).computeLineDistances();
|
||||||
|
|
||||||
splashRadiusWrapper.current?.add(outline);
|
splashRadiusWrapper.current?.add(outline);
|
||||||
@@ -72,15 +73,12 @@ export function ShotDisplay() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data = new Date();
|
|
||||||
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isHalloween &&
|
isHalloween() &&
|
||||||
shot?.splashRadius !== undefined &&
|
shot?.splashRadius !== undefined &&
|
||||||
shot.in.status === 'penetration'
|
shot.in.status === "penetration"
|
||||||
) {
|
) {
|
||||||
const audio = new Audio('/assets/audio/lotta-damage.mp3');
|
const audio = new Audio("/assets/audio/lotta-damage.mp3");
|
||||||
|
|
||||||
audio.currentTime = 0.7;
|
audio.currentTime = 0.7;
|
||||||
audio.volume = 0.25;
|
audio.volume = 0.25;
|
||||||
@@ -136,11 +134,11 @@ export function ShotDisplay() {
|
|||||||
const outLength =
|
const outLength =
|
||||||
shot.out && shot.out.layers.length > 0
|
shot.out && shot.out.layers.length > 0
|
||||||
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
|
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
|
||||||
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
|
(shot.out.layers.at(-1) as ShotLayerNonExternal).point
|
||||||
)
|
)
|
||||||
: SHOT_DISPLAY_LENGTH_INFINITY;
|
: SHOT_DISPLAY_LENGTH_INFINITY;
|
||||||
const inLast = shot.in.layers.findLast(
|
const inLast = shot.in.layers.findLast(
|
||||||
(layer) => layer.type !== null,
|
(layer) => layer.type !== null
|
||||||
) as ShotLayerNonExternal;
|
) as ShotLayerNonExternal;
|
||||||
const outTitleColor = shot.out
|
const outTitleColor = shot.out
|
||||||
? shotStatusColors[shot.out.status]
|
? shotStatusColors[shot.out.status]
|
||||||
@@ -156,7 +154,7 @@ export function ShotDisplay() {
|
|||||||
position={inLast.point}
|
position={inLast.point}
|
||||||
ref={splashRadiusWrapper}
|
ref={splashRadiusWrapper}
|
||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
|
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh>
|
<mesh>
|
||||||
@@ -174,13 +172,13 @@ export function ShotDisplay() {
|
|||||||
<Html position={inLast.point} center>
|
<Html position={inLast.point} center>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
||||||
}}
|
}}
|
||||||
mb={tracerGoingUp ? '0' : '9'}
|
mb={tracerGoingUp ? "0" : "9"}
|
||||||
pb={tracerGoingUp ? '0' : '4'}
|
pb={tracerGoingUp ? "0" : "4"}
|
||||||
mt={tracerGoingUp ? '9' : '0'}
|
mt={tracerGoingUp ? "9" : "0"}
|
||||||
pt={tracerGoingUp ? '4' : '0'}
|
pt={tracerGoingUp ? "4" : "0"}
|
||||||
>
|
>
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<ShotDisplayCard shot={shot} />
|
<ShotDisplayCard shot={shot} />
|
||||||
@@ -192,7 +190,7 @@ export function ShotDisplay() {
|
|||||||
if (layer.type === null) return null;
|
if (layer.type === null) return null;
|
||||||
|
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.in.status === 'splash'
|
shot.in.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
@@ -201,8 +199,8 @@ export function ShotDisplay() {
|
|||||||
position={layer.point}
|
position={layer.point}
|
||||||
key={index}
|
key={index}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -227,7 +225,7 @@ export function ShotDisplay() {
|
|||||||
<group
|
<group
|
||||||
position={inLast.point}
|
position={inLast.point}
|
||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal),
|
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
|
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
|
||||||
@@ -255,8 +253,8 @@ export function ShotDisplay() {
|
|||||||
rotation={new Euler().setFromQuaternion(
|
rotation={new Euler().setFromQuaternion(
|
||||||
new Quaternion().setFromUnitVectors(
|
new Quaternion().setFromUnitVectors(
|
||||||
J_HAT,
|
J_HAT,
|
||||||
shot.out.surfaceNormal.clone(),
|
shot.out.surfaceNormal.clone()
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
|
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -6,19 +6,19 @@ import {
|
|||||||
Separator,
|
Separator,
|
||||||
Text,
|
Text,
|
||||||
type CardProps,
|
type CardProps,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import type { ComponentProps } from 'react';
|
import type { ComponentProps } from "react";
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral';
|
import type { Shot, ShotLayer } from "../../../stores/tankopedia";
|
||||||
import { shotStatusColors } from './ShotDisplay';
|
import { shotStatusColors } from "./ShotDisplay";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
|
|
||||||
export const layerTypeNames = {
|
export const layerTypeNames = {
|
||||||
[ArmorType.Primary]: 'primary',
|
[ArmorType.Primary]: "primary",
|
||||||
[ArmorType.Spaced]: 'spaced',
|
[ArmorType.Spaced]: "spaced",
|
||||||
[ArmorType.External]: 'external',
|
[ArmorType.External]: "external",
|
||||||
null: 'gap',
|
null: "gap",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
function LayerEntry({
|
function LayerEntry({
|
||||||
@@ -29,7 +29,7 @@ function LayerEntry({
|
|||||||
angle,
|
angle,
|
||||||
}: {
|
}: {
|
||||||
layerIndex?: string;
|
layerIndex?: string;
|
||||||
shotStatusColor: ComponentProps<typeof Text>['color'];
|
shotStatusColor: ComponentProps<typeof Text>["color"];
|
||||||
layerName: string;
|
layerName: string;
|
||||||
layer: ShotLayer;
|
layer: ShotLayer;
|
||||||
angle?: number;
|
angle?: number;
|
||||||
@@ -37,44 +37,38 @@ function LayerEntry({
|
|||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex align="center" gap="2">
|
<tr>
|
||||||
<Text size="1" color="gray" style={{ width: 12 }}>
|
<td>
|
||||||
{layerIndex}
|
<Text size="1" color="gray">
|
||||||
</Text>
|
{layerIndex}
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 48 }}>
|
</Text>
|
||||||
{layerName}
|
</td>
|
||||||
</Text>
|
|
||||||
|
|
||||||
{layer.type === null && (
|
<td>
|
||||||
<>
|
<Text size="2" color={shotStatusColor}>
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
{layerName}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2" color={shotStatusColor}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
Math.round(layer.distance * 1000).toLocaleString(locale),
|
Math.round(layer.distance * 1000).toLocaleString(locale),
|
||||||
])}
|
])}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text size="2" color="gray">
|
{layer.type === ArmorType.External && (
|
||||||
{literals(
|
<Text size="2" color={shotStatusColor}>
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
{literals(strings.common.units.mm, [
|
||||||
.ricochet_loss,
|
Math.round(layer.thickness).toLocaleString(locale),
|
||||||
[Math.max(-100, -50 * layer.distance).toFixed(0)],
|
])}
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
)}
|
||||||
)}
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
{layer.type === ArmorType.External && (
|
<Text size="2" color={shotStatusColor}>
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
|
||||||
{literals(strings.common.units.mm, [
|
|
||||||
Math.round(layer.thickness).toLocaleString(locale),
|
|
||||||
])}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(layer.type === ArmorType.Primary ||
|
|
||||||
layer.type === ArmorType.Spaced) && (
|
|
||||||
<>
|
|
||||||
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
|
|
||||||
{literals(
|
{literals(
|
||||||
angle === undefined
|
angle === undefined
|
||||||
? strings.common.units.mm
|
? strings.common.units.mm
|
||||||
@@ -83,20 +77,35 @@ function LayerEntry({
|
|||||||
[
|
[
|
||||||
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
||||||
radToDeg(angle ?? 0).toFixed(0),
|
radToDeg(angle ?? 0).toFixed(0),
|
||||||
],
|
]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{layer.type === null && (
|
||||||
|
<Text size="2" color="gray">
|
||||||
|
{literals(
|
||||||
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||||
|
.ricochet_loss,
|
||||||
|
[Math.max(-100, -50 * layer.distance).toFixed(0)]
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(layer.type === ArmorType.Primary ||
|
||||||
|
layer.type === ArmorType.Spaced) && (
|
||||||
<Text size="2" color="gray">
|
<Text size="2" color="gray">
|
||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||||
.nominal,
|
.nominal,
|
||||||
[Math.round(layer.thickness).toLocaleString(locale)],
|
[Math.round(layer.thickness).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
)}
|
||||||
)}
|
</td>
|
||||||
</Flex>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
const inTitleColor = shotStatusColors[shot.in.status];
|
const inTitleColor = shotStatusColors[shot.in.status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="classic" style={{ width: 300 }} {...props}>
|
<Card variant="classic" {...props}>
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Text color={inTitleColor} weight="bold">
|
<Text color={inTitleColor} weight="bold">
|
||||||
@@ -123,11 +132,16 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
||||||
shot.in.status
|
shot.in.status
|
||||||
],
|
],
|
||||||
[Math.round(shot.damage).toLocaleString(locale)],
|
[Math.round(shot.damage).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Flex direction="column">
|
<table
|
||||||
|
style={{
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
borderSpacing: "var(--space-1) 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{shot.in.layers.map((layer, index) => {
|
{shot.in.layers.map((layer, index) => {
|
||||||
const layerName =
|
const layerName =
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
@@ -141,7 +155,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
? undefined
|
? undefined
|
||||||
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.in.status === 'splash'
|
shot.in.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flex>
|
</table>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{shot.out && shot.out.layers.length > 0 && (
|
{shot.out && shot.out.layers.length > 0 && (
|
||||||
@@ -188,7 +202,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||||
.status[shot.out.status],
|
.status[shot.out.status],
|
||||||
[Math.round(shot.damage).toLocaleString(locale)],
|
[Math.round(shot.damage).toLocaleString(locale)]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -210,7 +224,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
|||||||
1
|
1
|
||||||
}.`;
|
}.`;
|
||||||
const shotStatusColor =
|
const shotStatusColor =
|
||||||
shot.out!.status === 'splash'
|
shot.out!.status === "splash"
|
||||||
? shotStatusColors.splash
|
? shotStatusColors.splash
|
||||||
: shotStatusColors[layer.status];
|
: shotStatusColors[layer.status];
|
||||||
|
|
||||||
|
|||||||
+34
-45
@@ -1,20 +1,20 @@
|
|||||||
import { resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AdditiveBlending,
|
AdditiveBlending,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import type { ArmorUserData, ExternalModuleVariant } from '../..';
|
import type { ArmorUserData, ExternalModuleVariant } from "../..";
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from "../../../SpacedArmorScene";
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
|
|
||||||
interface SpacedArmorSubExternalProps {
|
interface SpacedArmorSubExternalProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -29,8 +29,6 @@ export function SpacedArmorSubExternal({
|
|||||||
variant,
|
variant,
|
||||||
clip,
|
clip,
|
||||||
}: SpacedArmorSubExternalProps) {
|
}: SpacedArmorSubExternalProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -49,33 +47,30 @@ export function SpacedArmorSubExternal({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
equipment: EquipmentMatrix,
|
const hasEnhancedArmor = hasEquipment(
|
||||||
) {
|
|
||||||
const duel = duelStore.getState();
|
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
: thickness;
|
: thickness;
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -84,26 +79,19 @@ export function SpacedArmorSubExternal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
);
|
|
||||||
handleAntagonistEquipmentChange(
|
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
Duel.on(
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
Duel.on(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
handleAntagonistEquipmentChange
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -125,6 +113,7 @@ export function SpacedArmorSubExternal({
|
|||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
{...props}
|
{...props}
|
||||||
|
onClick={() => {}}
|
||||||
key={`${key}-spaced-sub-external-exclude`}
|
key={`${key}-spaced-sub-external-exclude`}
|
||||||
renderOrder={3}
|
renderOrder={3}
|
||||||
material={
|
material={
|
||||||
|
|||||||
+36
-48
@@ -1,20 +1,20 @@
|
|||||||
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { isExplosive, resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
AdditiveBlending,
|
AdditiveBlending,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
type Object3D,
|
type Object3D,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import type { ArmorUserData } from '../..';
|
import type { ArmorUserData } from "../..";
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from "../../../SpacedArmorScene";
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||||
|
|
||||||
interface SpacedArmorSubSpacedProps {
|
interface SpacedArmorSubSpacedProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -30,8 +30,6 @@ export function SpacedArmorSubSpaced({
|
|||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
}: SpacedArmorSubSpacedProps) {
|
}: SpacedArmorSubSpacedProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -51,41 +49,38 @@ export function SpacedArmorSubSpaced({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleShellChange() {
|
function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
material.uniforms.penetration.value = shell.penetration.near;
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
material.uniforms.normalization.value = degToRad(
|
material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
shell.normalization ?? 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async function handleProtagonistEquipmentChange(
|
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
equipment: EquipmentMatrix,
|
const hasEnhancedArmor = hasEquipment(
|
||||||
) {
|
|
||||||
const duel = duelStore.getState();
|
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
|
||||||
110,
|
110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
? thickness * 1.03
|
||||||
: thickness;
|
: thickness;
|
||||||
}
|
}
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
const tankopediaEphemeral = Tankopedia.state;
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
const shell =
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
tankopediaEphemeral.customShell ?? Duel.state.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
equipment
|
||||||
);
|
);
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
material.uniforms.penetration.value =
|
||||||
@@ -94,26 +89,19 @@ export function SpacedArmorSubSpaced({
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleShellChange();
|
handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
|
||||||
);
|
|
||||||
handleAntagonistEquipmentChange(
|
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
|
||||||
);
|
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
Duel.on((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||||
(state) => state.customShell,
|
Duel.on(
|
||||||
handleShellChange,
|
|
||||||
),
|
|
||||||
duelStore.subscribe(
|
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
handleProtagonistEquipmentChange
|
||||||
),
|
),
|
||||||
duelStore.subscribe(
|
Duel.on(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
(state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
handleAntagonistEquipmentChange
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+64
-67
@@ -2,9 +2,9 @@ import {
|
|||||||
ShellType,
|
ShellType,
|
||||||
isExplosive,
|
isExplosive,
|
||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from "@react-three/fiber";
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
type Intersection,
|
type Intersection,
|
||||||
MeshBasicMaterial,
|
MeshBasicMaterial,
|
||||||
@@ -14,21 +14,21 @@ import {
|
|||||||
Raycaster,
|
Raycaster,
|
||||||
Scene,
|
Scene,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
import { degToRad } from "three/src/math/MathUtils.js";
|
||||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||||
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
|
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
type Shot,
|
type Shot,
|
||||||
type ShotLayerBase,
|
type ShotLayerBase,
|
||||||
type ShotLayerNonExternal,
|
type ShotLayerNonExternal,
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
} from '../../../../stores/tankopediaEphemeral';
|
} from "../../../../stores/tankopedia";
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
import { ArmorType } from "../SpacedArmorScene";
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
import { SpacedArmorSubExternal } from "./components/SpacedArmorSubExternal";
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
import { SpacedArmorSubSpaced } from "./components/SpacedArmorSubSpaced";
|
||||||
|
|
||||||
type SpacedArmorSceneComponentProps = {
|
type SpacedArmorSceneComponentProps = {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
@@ -45,7 +45,7 @@ type SpacedArmorSceneComponentProps = {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ExternalModuleVariant = 'gun' | 'track';
|
export type ExternalModuleVariant = "gun" | "track";
|
||||||
export type ArmorUserData = {
|
export type ArmorUserData = {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
} & (
|
} & (
|
||||||
@@ -76,20 +76,17 @@ export function SpacedArmorSceneComponent({
|
|||||||
clip,
|
clip,
|
||||||
...props
|
...props
|
||||||
}: SpacedArmorSceneComponentProps) {
|
}: SpacedArmorSceneComponentProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
const shoot = useCallback(
|
const shoot = useCallback(
|
||||||
async (
|
(
|
||||||
point: Vector3,
|
point: Vector3,
|
||||||
intersections: Intersection[],
|
intersections: Intersection[],
|
||||||
allowRicochet: boolean,
|
allowRicochet: boolean,
|
||||||
remainingPenetrationInput?: number,
|
remainingPenetrationInput?: number
|
||||||
) => {
|
) => {
|
||||||
const { antagonist, protagonist } = duelStore.getState();
|
const { customShell } = Tankopedia.state;
|
||||||
const { customShell } = tankopediaEphemeralStore.getState();
|
const shell = customShell ?? Duel.state.antagonist.shell;
|
||||||
const shell = customShell ?? antagonist.shell;
|
|
||||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||||
const shot: Shot = {
|
const shot: Shot = {
|
||||||
splashRadius:
|
splashRadius:
|
||||||
@@ -99,19 +96,19 @@ export function SpacedArmorSceneComponent({
|
|||||||
in: {
|
in: {
|
||||||
surfaceNormal: cameraNormal,
|
surfaceNormal: cameraNormal,
|
||||||
layers: [],
|
layers: [],
|
||||||
status: 'blocked',
|
status: "blocked",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasCalibratedShells = await hasEquipment(
|
const hasCalibratedShells = hasEquipment(
|
||||||
103,
|
103,
|
||||||
antagonist.tank.equipment_preset,
|
Duel.state.antagonist.tank.equipment_preset,
|
||||||
antagonist.equipmentMatrix,
|
Duel.state.antagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
const hasEnhancedArmor = hasEquipment(
|
||||||
110,
|
110,
|
||||||
protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const penetration =
|
const penetration =
|
||||||
shell.penetration.near *
|
shell.penetration.near *
|
||||||
@@ -119,8 +116,8 @@ export function SpacedArmorSceneComponent({
|
|||||||
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
||||||
const filteredIntersections = intersections.filter(
|
const filteredIntersections = intersections.filter(
|
||||||
(intersection) =>
|
(intersection) =>
|
||||||
'type' in intersection.object.userData &&
|
"type" in intersection.object.userData &&
|
||||||
!discardClippingPlane(intersection.object, intersection.point),
|
!discardClippingPlane(intersection.object, intersection.point)
|
||||||
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
||||||
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
||||||
const noDuplicateIntersections: typeof filteredIntersections = [];
|
const noDuplicateIntersections: typeof filteredIntersections = [];
|
||||||
@@ -129,12 +126,12 @@ export function SpacedArmorSceneComponent({
|
|||||||
if (intersection.object.userData.type === ArmorType.External) {
|
if (intersection.object.userData.type === ArmorType.External) {
|
||||||
if (
|
if (
|
||||||
!encounteredExternalModuleVariants.includes(
|
!encounteredExternalModuleVariants.includes(
|
||||||
intersection.object.userData.variant,
|
intersection.object.userData.variant
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
noDuplicateIntersections.push(intersection);
|
noDuplicateIntersections.push(intersection);
|
||||||
encounteredExternalModuleVariants.push(
|
encounteredExternalModuleVariants.push(
|
||||||
intersection.object.userData.variant,
|
intersection.object.userData.variant
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -162,7 +159,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
const surfaceNormal = intersection
|
const surfaceNormal = intersection
|
||||||
.normal!.clone()
|
.normal!.clone()
|
||||||
.applyQuaternion(
|
.applyQuaternion(
|
||||||
intersection.object.getWorldQuaternion(new Quaternion()),
|
intersection.object.getWorldQuaternion(new Quaternion())
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shell.type === ShellType.HEAT && index > 0) {
|
if (shell.type === ShellType.HEAT && index > 0) {
|
||||||
@@ -174,7 +171,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
shot.in.layers.push({
|
shot.in.layers.push({
|
||||||
type: null,
|
type: null,
|
||||||
distance,
|
distance,
|
||||||
status: blocked ? 'blocked' : 'penetration',
|
status: blocked ? "blocked" : "penetration",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocked) break;
|
if (blocked) break;
|
||||||
@@ -194,16 +191,16 @@ export function SpacedArmorSceneComponent({
|
|||||||
surfaceNormal,
|
surfaceNormal,
|
||||||
status:
|
status:
|
||||||
shell.type === ShellType.HE
|
shell.type === ShellType.HE
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: 'penetration',
|
: "penetration",
|
||||||
variant: layer.variant,
|
variant: layer.variant,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const thickness = layer.thickness * thicknessCoefficient;
|
const thickness = layer.thickness * thicknessCoefficient;
|
||||||
const ricochet = degToRad(
|
const ricochet = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||||
);
|
);
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
|
|
||||||
@@ -227,7 +224,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
thicknessAngled: finalThickness,
|
thicknessAngled: finalThickness,
|
||||||
point: intersection.point,
|
point: intersection.point,
|
||||||
surfaceNormal,
|
surfaceNormal,
|
||||||
status: 'ricochet',
|
status: "ricochet",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,10 +242,10 @@ export function SpacedArmorSceneComponent({
|
|||||||
surfaceNormal: intersection.face!.normal,
|
surfaceNormal: intersection.face!.normal,
|
||||||
status:
|
status:
|
||||||
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: blocked
|
: blocked
|
||||||
? 'blocked'
|
? "blocked"
|
||||||
: 'penetration',
|
: "penetration",
|
||||||
angle,
|
angle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -282,10 +279,10 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
return acc + layer.thicknessAngled;
|
return acc + layer.thicknessAngled;
|
||||||
},
|
},
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
||||||
lastLayer.point,
|
lastLayer.point
|
||||||
);
|
);
|
||||||
const finalDamage = Math.max(
|
const finalDamage = Math.max(
|
||||||
0,
|
0,
|
||||||
@@ -294,7 +291,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
||||||
1.1 *
|
1.1 *
|
||||||
(lastLayer.thicknessAngled +
|
(lastLayer.thicknessAngled +
|
||||||
Math.min(penetration, totalSpacedArmorThickness)),
|
Math.min(penetration, totalSpacedArmorThickness))
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,19 +305,19 @@ export function SpacedArmorSceneComponent({
|
|||||||
* for parity's sake, I am capping the value here.
|
* for parity's sake, I am capping the value here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
|
if (shot.in.layers.length > 1 || lastLayer.status === "blocked") {
|
||||||
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
|
shot.in.status = finalDamage > 0 ? "splash" : "blocked";
|
||||||
shot.damage = finalDamage;
|
shot.damage = finalDamage;
|
||||||
} else {
|
} else {
|
||||||
shot.in.status = 'penetration';
|
shot.in.status = "penetration";
|
||||||
shot.damage = shell.armor_damage;
|
shot.damage = shell.armor_damage;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (lastLayer.status === 'blocked') {
|
if (lastLayer.status === "blocked") {
|
||||||
shot.in.status = 'blocked';
|
shot.in.status = "blocked";
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
} else if (lastLayer.status === 'penetration') {
|
} else if (lastLayer.status === "penetration") {
|
||||||
shot.in.status = 'penetration';
|
shot.in.status = "penetration";
|
||||||
shot.damage = shell.armor_damage;
|
shot.damage = shell.armor_damage;
|
||||||
} else {
|
} else {
|
||||||
const caster = new Raycaster();
|
const caster = new Raycaster();
|
||||||
@@ -331,29 +328,29 @@ export function SpacedArmorSceneComponent({
|
|||||||
.sub(
|
.sub(
|
||||||
lastLayer.surfaceNormal
|
lastLayer.surfaceNormal
|
||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
|
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
|
||||||
);
|
);
|
||||||
|
|
||||||
caster.set(lastLayer.point, ricochetNormal);
|
caster.set(lastLayer.point, ricochetNormal);
|
||||||
|
|
||||||
const ricochetIntersections = caster.intersectObjects(
|
const ricochetIntersections = caster.intersectObjects(
|
||||||
props.scene.children,
|
props.scene.children,
|
||||||
true,
|
true
|
||||||
);
|
);
|
||||||
const outShot = await shoot(
|
const outShot = shoot(
|
||||||
lastLayer.point,
|
lastLayer.point,
|
||||||
ricochetIntersections,
|
ricochetIntersections,
|
||||||
false,
|
false,
|
||||||
remainingPenetration,
|
remainingPenetration
|
||||||
);
|
);
|
||||||
|
|
||||||
shot.in.status = 'ricochet';
|
shot.in.status = "ricochet";
|
||||||
if (outShot === null) {
|
if (outShot === null) {
|
||||||
shot.damage = 0;
|
shot.damage = 0;
|
||||||
shot.out = {
|
shot.out = {
|
||||||
surfaceNormal: ricochetNormal,
|
surfaceNormal: ricochetNormal,
|
||||||
layers: [],
|
layers: [],
|
||||||
status: 'ricochet',
|
status: "ricochet",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
shot.damage = outShot.damage;
|
shot.damage = outShot.damage;
|
||||||
@@ -366,7 +363,7 @@ export function SpacedArmorSceneComponent({
|
|||||||
|
|
||||||
return shot;
|
return shot;
|
||||||
},
|
},
|
||||||
[camera],
|
[camera]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -386,14 +383,14 @@ export function SpacedArmorSceneComponent({
|
|||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData
|
} satisfies ArmorUserData
|
||||||
}
|
}
|
||||||
onClick={async (event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const shot = (await shoot(
|
|
||||||
event.point,
|
const shot = shoot(event.point, event.intersections, true)!;
|
||||||
event.intersections,
|
|
||||||
true,
|
Tankopedia.mutate((draft) => {
|
||||||
))!;
|
draft.shot = shot;
|
||||||
tankopediaEphemeralStore.setState({ shot });
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
import { useThree, type ThreeEvent } from "@react-three/fiber";
|
||||||
import { memo, useRef } from 'react';
|
import { memo, useRef } from "react";
|
||||||
import { Group, Plane, Vector2, Vector3 } from 'three';
|
import { Group, Plane, Vector2, Vector3 } from "three";
|
||||||
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from "../../../core/blitz/applyPitchYawLimits";
|
||||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
|
||||||
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../core/blitzkit/hasEquipment";
|
||||||
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
|
import { modelTransformEvent } from "../../../core/blitzkit/modelTransform";
|
||||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
|
||||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
|
||||||
import { useArmor } from '../../../hooks/useArmor';
|
import { useArmor } from "../../../hooks/useArmor";
|
||||||
import { useModel } from '../../../hooks/useModel';
|
import { useModel } from "../../../hooks/useModel";
|
||||||
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
|
||||||
import { useTankTransform } from '../../../hooks/useTankTransform';
|
import { useTankTransform } from "../../../hooks/useTankTransform";
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from "../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
import { ModelTankWrapper } from "./ModelTankWrapper";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
|
import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
|
||||||
|
|
||||||
interface ArmorSceneProps {
|
interface ArmorSceneProps {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
@@ -30,7 +30,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const wrapper = useRef<Group>(null);
|
const wrapper = useRef<Group>(null);
|
||||||
const turretContainer = useRef<Group>(null);
|
const turretContainer = useRef<Group>(null);
|
||||||
const gunContainer = useRef<Group>(null);
|
const gunContainer = useRef<Group>(null);
|
||||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
const track = Duel.use((state) => state.protagonist.track);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
@@ -46,25 +45,23 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const canvas = useThree((state) => state.gl.domElement);
|
const canvas = useThree((state) => state.gl.domElement);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const maskOrigin =
|
const maskOrigin =
|
||||||
gunModelDefinition.mask === undefined
|
gunModelDefinition.mask === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
||||||
const showPrimaryArmor = TankopediaPersistent.use(
|
const showPrimaryArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showPrimaryArmor,
|
(state) => state.showPrimaryArmor
|
||||||
);
|
);
|
||||||
const showSpacedArmor = TankopediaPersistent.use(
|
const showSpacedArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showSpacedArmor,
|
(state) => state.showSpacedArmor
|
||||||
);
|
);
|
||||||
const showExternalModules = TankopediaPersistent.use(
|
const showExternalModules = TankopediaPersistent.use(
|
||||||
(state) => state.showExternalModules,
|
(state) => state.showExternalModules
|
||||||
);
|
);
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73)
|
||||||
);
|
);
|
||||||
|
|
||||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||||
@@ -73,12 +70,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<ModelTankWrapper ref={wrapper}>
|
<ModelTankWrapper ref={wrapper}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isHull = node.name.startsWith("hull_");
|
||||||
const isVisible = isHull;
|
const isVisible = isHull;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
tankModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -86,8 +83,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -106,8 +103,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
</group>
|
</group>
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
const isTrack = node.name.startsWith("chassis_track_");
|
||||||
const isVisible = isWheel || isTrack;
|
const isVisible = isWheel || isTrack;
|
||||||
|
|
||||||
if (!isVisible || !showExternalModules) return null;
|
if (!isVisible || !showExternalModules) return null;
|
||||||
@@ -129,13 +126,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<group ref={turretContainer}>
|
<group ref={turretContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
const isCurrentTurret = node.name.startsWith(
|
||||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`,
|
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentTurret;
|
const isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -143,8 +140,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -157,27 +154,26 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
@@ -191,16 +187,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -209,7 +205,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<StaticArmorSceneComponent
|
<StaticArmorSceneComponent
|
||||||
name={node.name}
|
name={node.name}
|
||||||
thicknessRange={thicknessRange}
|
thicknessRange={thicknessRange}
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -223,13 +218,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
<group ref={gunContainer}>
|
<group ref={gunContainer}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentGun = node.name.startsWith(
|
const isCurrentGun = node.name.startsWith(
|
||||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`,
|
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentGun;
|
const isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
gunModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -237,8 +232,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
thickness === undefined ||
|
thickness === undefined ||
|
||||||
(!showPrimaryArmor && !spaced) ||
|
(!showPrimaryArmor && !spaced) ||
|
||||||
(!showSpacedArmor && spaced) ||
|
(!showSpacedArmor && spaced) ||
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||||
)
|
)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -248,30 +243,29 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
@@ -285,16 +279,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -316,7 +310,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
{modelNodes.map((node) => {
|
{modelNodes.map((node) => {
|
||||||
const gunString = `gun_${gunModelDefinition.model_id
|
const gunString = `gun_${gunModelDefinition.model_id
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}`;
|
.padStart(2, "0")}`;
|
||||||
const isCurrentGun = gunModelDefinition.mask
|
const isCurrentGun = gunModelDefinition.mask
|
||||||
? node.name.startsWith(gunString)
|
? node.name.startsWith(gunString)
|
||||||
: node.name === gunString;
|
: node.name === gunString;
|
||||||
@@ -330,29 +324,28 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = false;
|
draft.controlsEnabled = false;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
position.set(event.clientX, event.clientY);
|
position.set(event.clientX, event.clientY);
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
async function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
const duel = duelStore.getState();
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
|
||||||
122,
|
122,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
duel.protagonist.tank.equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
duel.protagonist.equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const boundingRect = canvas.getBoundingClientRect();
|
const boundingRect = canvas.getBoundingClientRect();
|
||||||
delta.set(event.clientX, event.clientY).sub(position);
|
delta.set(event.clientX, event.clientY).sub(position);
|
||||||
@@ -366,17 +359,17 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
}
|
}
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.controlsEnabled = true;
|
draft.controlsEnabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
import { I_HAT, J_HAT } from "@blitzkit/core";
|
||||||
import { type MeshProps, useThree } from '@react-three/fiber';
|
import { type MeshProps, useThree } from "@react-three/fiber";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import type { QuicklimeEvent } from 'quicklime';
|
import type { QuicklimeEvent } from "quicklime";
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Box3,
|
Box3,
|
||||||
Color,
|
Color,
|
||||||
@@ -10,33 +10,33 @@ import {
|
|||||||
FrontSide,
|
FrontSide,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
Mesh,
|
Mesh,
|
||||||
MeshStandardMaterial,
|
MeshBasicMaterial,
|
||||||
Object3D,
|
Object3D,
|
||||||
Plane,
|
Plane,
|
||||||
Quaternion,
|
Quaternion,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from "three";
|
||||||
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
|
import { unrotateDavaVector } from "../../../core/blitz/unrotateDavaVector";
|
||||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||||
import {
|
import {
|
||||||
type ModelTransformEventData,
|
type ModelTransformEventData,
|
||||||
modelTransformEvent,
|
modelTransformEvent,
|
||||||
} from '../../../core/blitzkit/modelTransform';
|
} from "../../../core/blitzkit/modelTransform";
|
||||||
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
|
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../stores/tankopedia";
|
||||||
import { ArmorType } from './SpacedArmorScene';
|
import { ArmorType } from "./SpacedArmorScene";
|
||||||
import type {
|
import type {
|
||||||
ArmorUserData,
|
ArmorUserData,
|
||||||
ExternalModuleVariant,
|
ExternalModuleVariant,
|
||||||
} from './SpacedArmorSceneComponent';
|
} from "./SpacedArmorSceneComponent";
|
||||||
import type { ThicknessRange } from './StaticArmor';
|
import type { ThicknessRange } from "./StaticArmor";
|
||||||
|
|
||||||
type StaticArmorSceneComponentProps = {
|
type StaticArmorSceneComponentProps = {
|
||||||
name: string;
|
name: string;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
onPointerDown?: MeshProps['onPointerDown'];
|
onPointerDown?: MeshProps["onPointerDown"];
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
@@ -69,8 +69,6 @@ export function StaticArmorSceneComponent({
|
|||||||
onPointerDown,
|
onPointerDown,
|
||||||
...props
|
...props
|
||||||
}: StaticArmorSceneComponentProps) {
|
}: StaticArmorSceneComponentProps) {
|
||||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const x = thickness / thicknessRange.value;
|
const x = thickness / thicknessRange.value;
|
||||||
const xClamped = clamp(x, 0, 1);
|
const xClamped = clamp(x, 0, 1);
|
||||||
@@ -92,7 +90,7 @@ export function StaticArmorSceneComponent({
|
|||||||
color = new Color(
|
color = new Color(
|
||||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||||
0,
|
0,
|
||||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
clamp(1 - (1 / 8) * xClamped, 0, 1)
|
||||||
);
|
);
|
||||||
opacity = clamp(x + 1 / 2, 0, 1);
|
opacity = clamp(x + 1 / 2, 0, 1);
|
||||||
break;
|
break;
|
||||||
@@ -108,14 +106,14 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
const surfaceMaterial = useMemo(
|
const surfaceMaterial = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new MeshStandardMaterial({
|
new MeshBasicMaterial({
|
||||||
color,
|
color,
|
||||||
opacity,
|
opacity,
|
||||||
transparent: opacity < 1,
|
transparent: opacity < 1,
|
||||||
depthWrite,
|
depthWrite,
|
||||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||||
}),
|
}),
|
||||||
[thickness],
|
[thickness]
|
||||||
);
|
);
|
||||||
const outlineMaterial = useMemo(
|
const outlineMaterial = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -124,7 +122,7 @@ export function StaticArmorSceneComponent({
|
|||||||
.clone()
|
.clone()
|
||||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||||
}),
|
}),
|
||||||
[thickness],
|
[thickness]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -139,11 +137,11 @@ export function StaticArmorSceneComponent({
|
|||||||
outlineMaterial.visible = true;
|
outlineMaterial.visible = true;
|
||||||
} else if (
|
} else if (
|
||||||
selectedName === name ||
|
selectedName === name ||
|
||||||
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) ||
|
(name.startsWith("chassis_") && selectedName.startsWith("chassis_")) ||
|
||||||
(name.startsWith('gun_') &&
|
(name.startsWith("gun_") &&
|
||||||
selectedName.startsWith('gun_') &&
|
selectedName.startsWith("gun_") &&
|
||||||
!name.includes('_armor_') &&
|
!name.includes("_armor_") &&
|
||||||
!selectedName.includes('_armor_'))
|
!selectedName.includes("_armor_"))
|
||||||
) {
|
) {
|
||||||
// this selected, stand out!
|
// this selected, stand out!
|
||||||
surfaceMaterial.opacity = 1;
|
surfaceMaterial.opacity = 1;
|
||||||
@@ -166,9 +164,9 @@ export function StaticArmorSceneComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
tankopediaEphemeralStore.subscribe(
|
Tankopedia.on(
|
||||||
(state) => state.highlightArmor?.name,
|
(state) => state.highlightArmor?.name,
|
||||||
handleHighlightArmor,
|
handleHighlightArmor
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -181,14 +179,14 @@ export function StaticArmorSceneComponent({
|
|||||||
const { clip } = props;
|
const { clip } = props;
|
||||||
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
||||||
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
||||||
unrotateDavaVector(props.turretOrigin.clone()),
|
unrotateDavaVector(props.turretOrigin.clone())
|
||||||
);
|
);
|
||||||
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
||||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||||
const point = new Vector3();
|
const point = new Vector3();
|
||||||
|
|
||||||
function handleModelTransform(
|
function handleModelTransform(
|
||||||
event: QuicklimeEvent<ModelTransformEventData>,
|
event: QuicklimeEvent<ModelTransformEventData>
|
||||||
) {
|
) {
|
||||||
clip.normal
|
clip.normal
|
||||||
.set(0, 0, -1)
|
.set(0, 0, -1)
|
||||||
@@ -229,7 +227,7 @@ export function StaticArmorSceneComponent({
|
|||||||
{
|
{
|
||||||
type: props.type,
|
type: props.type,
|
||||||
variant:
|
variant:
|
||||||
props.type === ArmorType.External ? props.variant : 'gun',
|
props.type === ArmorType.External ? props.variant : "gun",
|
||||||
thickness,
|
thickness,
|
||||||
} satisfies ArmorUserData
|
} satisfies ArmorUserData
|
||||||
}
|
}
|
||||||
@@ -238,7 +236,7 @@ export function StaticArmorSceneComponent({
|
|||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
const { editStatic } = Tankopedia.state;
|
||||||
|
|
||||||
const bounds = new Box3().setFromObject(event.object);
|
const bounds = new Box3().setFromObject(event.object);
|
||||||
const point = bounds.min
|
const point = bounds.min
|
||||||
@@ -253,13 +251,13 @@ export function StaticArmorSceneComponent({
|
|||||||
const surfaceNormal = event
|
const surfaceNormal = event
|
||||||
.normal!.clone()
|
.normal!.clone()
|
||||||
.applyQuaternion(
|
.applyQuaternion(
|
||||||
event.object.getWorldQuaternion(new Quaternion()),
|
event.object.getWorldQuaternion(new Quaternion())
|
||||||
);
|
);
|
||||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||||
const thicknessAngled =
|
const thicknessAngled =
|
||||||
thickness / Math.sin(Math.PI / 2 - angle);
|
thickness / Math.sin(Math.PI / 2 - angle);
|
||||||
|
|
||||||
mutateTankopediaEphemeralStore((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = {
|
draft.highlightArmor = {
|
||||||
type: props.type,
|
type: props.type,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { assertSecret, idToRegion } from '@blitzkit/core';
|
import { assertSecret, idToRegion } from "@blitzkit/core";
|
||||||
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
|
import { Button, Flex, Heading, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
useLocale,
|
useLocale,
|
||||||
type LocaleAcceptorProps,
|
type LocaleAcceptorProps,
|
||||||
} from '../../hooks/useLocale';
|
} from "../../hooks/useLocale";
|
||||||
import { App } from '../../stores/app';
|
import { App } from "../../stores/app";
|
||||||
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../../stores/app/constants';
|
import { CURRENT_POLICIES_AGREEMENT_INDEX } from "../../stores/app/constants";
|
||||||
import { LinkI18n } from '../LinkI18n';
|
import { LinkI18n } from "../LinkI18n";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
type Extension =
|
type Extension =
|
||||||
| {
|
| {
|
||||||
status: 'ok';
|
status: "ok";
|
||||||
data: {
|
data: {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
account_id: number;
|
account_id: number;
|
||||||
@@ -21,15 +21,13 @@ type Extension =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
status: 'error';
|
status: "error";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Checks({ locale }: LocaleAcceptorProps) {
|
export function Checks({ locale }: LocaleAcceptorProps) {
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<App.Provider>
|
<Content />
|
||||||
<Content />
|
|
||||||
</App.Provider>
|
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,19 +38,17 @@ export function Checks({ locale }: LocaleAcceptorProps) {
|
|||||||
function Content() {
|
function Content() {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const logins = App.use((state) => state.logins);
|
const logins = App.use((state) => state.logins);
|
||||||
const mutateApp = App.useMutation();
|
|
||||||
const appStore = App.useStore();
|
|
||||||
const policiesAgreementIndex = App.use(
|
const policiesAgreementIndex = App.use(
|
||||||
(state) => state.policiesAgreementIndex,
|
(state) => state.policiesAgreementIndex
|
||||||
);
|
);
|
||||||
const isLegal = window.location.pathname.startsWith('/legal');
|
const isLegal = window.location.pathname.startsWith("/legal");
|
||||||
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
|
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function unsubscribe() {
|
function unsubscribe() {
|
||||||
window.removeEventListener('scroll', show);
|
window.removeEventListener("scroll", show);
|
||||||
window.removeEventListener('pointermove', show);
|
window.removeEventListener("pointermove", show);
|
||||||
window.removeEventListener('pointerdown', show);
|
window.removeEventListener("pointerdown", show);
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
@@ -64,9 +60,9 @@ function Content() {
|
|||||||
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
||||||
!isLegal
|
!isLegal
|
||||||
) {
|
) {
|
||||||
window.addEventListener('scroll', show);
|
window.addEventListener("scroll", show);
|
||||||
window.addEventListener('pointermove', show);
|
window.addEventListener("pointermove", show);
|
||||||
window.addEventListener('pointerdown', show);
|
window.addEventListener("pointerdown", show);
|
||||||
}
|
}
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
@@ -78,25 +74,25 @@ function Content() {
|
|||||||
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||||
|
|
||||||
if (expiresInDays < 0) {
|
if (expiresInDays < 0) {
|
||||||
mutateApp((draft) => {
|
App.mutate((draft) => {
|
||||||
draft.logins.wargaming = undefined;
|
draft.logins.wargaming = undefined;
|
||||||
});
|
});
|
||||||
} else if (expiresInDays < 7) {
|
} else if (expiresInDays < 7) {
|
||||||
const { wargaming } = appStore.getState().logins;
|
const { wargaming } = App.state.logins;
|
||||||
|
|
||||||
if (!wargaming) return;
|
if (!wargaming) return;
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
`https://api.worldoftanks.${idToRegion(
|
`https://api.worldoftanks.${idToRegion(
|
||||||
wargaming.id,
|
wargaming.id
|
||||||
)}/wot/auth/prolongate/?application_id=${assertSecret(
|
)}/wot/auth/prolongate/?application_id=${assertSecret(
|
||||||
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID,
|
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
|
||||||
)}&access_token=${wargaming.token}`,
|
)}&access_token=${wargaming.token}`
|
||||||
)
|
)
|
||||||
.then((response) => response.json() as Promise<Extension>)
|
.then((response) => response.json() as Promise<Extension>)
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
mutateApp((draft) => {
|
App.mutate((draft) => {
|
||||||
if (json.status === 'error') {
|
if (json.status === "error") {
|
||||||
draft.logins.wargaming = undefined;
|
draft.logins.wargaming = undefined;
|
||||||
} else {
|
} else {
|
||||||
draft.logins.wargaming = {
|
draft.logins.wargaming = {
|
||||||
@@ -126,11 +122,11 @@ function Content() {
|
|||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
This website utilizes cookies to perform analytics and personalize
|
This website utilizes cookies to perform analytics and personalize
|
||||||
your experience. You can learn more through{' '}
|
your experience. You can learn more through{" "}
|
||||||
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
|
<LinkI18n locale={locale} href="/docs/legal/privacy-policy">
|
||||||
our privacy policy
|
our privacy policy
|
||||||
</LinkI18n>
|
</LinkI18n>
|
||||||
. By using BlitzKit, you also agree to our{' '}
|
. By using BlitzKit, you also agree to our{" "}
|
||||||
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
|
<LinkI18n locale={locale} href="/docs/legal/terms-of-service">
|
||||||
terms of service
|
terms of service
|
||||||
</LinkI18n>
|
</LinkI18n>
|
||||||
@@ -141,8 +137,8 @@ function Content() {
|
|||||||
<Button
|
<Button
|
||||||
mt="2"
|
mt="2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appStore.setState({
|
App.mutate((draft) => {
|
||||||
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
|
draft.policiesAgreementIndex = CURRENT_POLICIES_AGREEMENT_INDEX;
|
||||||
});
|
});
|
||||||
setShowPoliciesAgreement(false);
|
setShowPoliciesAgreement(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from "@blitzkit/core";
|
||||||
import { Flex, Heading, IconButton, Table } from '@radix-ui/themes';
|
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
||||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { CompareRow } from './CompareRow';
|
import { CompareRow } from "./CompareRow";
|
||||||
import { CompareSectionTitle } from './CompareSectionTitle';
|
import { CompareSectionTitle } from "./CompareSectionTitle";
|
||||||
|
|
||||||
interface BodyProps {
|
interface BodyProps {
|
||||||
stats: TankCharacteristics[];
|
stats: TankCharacteristics[];
|
||||||
@@ -14,10 +14,9 @@ interface BodyProps {
|
|||||||
export function Body({ stats }: BodyProps) {
|
export function Body({ stats }: BodyProps) {
|
||||||
const members = CompareEphemeral.use((state) => state.members);
|
const members = CompareEphemeral.use((state) => state.members);
|
||||||
const hasNonRegularGun = members.some(
|
const hasNonRegularGun = members.some(
|
||||||
({ gun }) => gun.gun_type!.$case !== 'regular',
|
({ gun }) => gun.gun_type!.$case !== "regular"
|
||||||
);
|
);
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -35,7 +34,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
<Flex>
|
<Flex>
|
||||||
{gun.shells.map((thisShell, shellIndex) => (
|
{gun.shells.map((thisShell, shellIndex) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
color={thisShell.id === shell.id ? undefined : "gray"}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
key={thisShell.id}
|
key={thisShell.id}
|
||||||
style={{
|
style={{
|
||||||
@@ -49,7 +48,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].shell = thisShell;
|
draft.members[index].shell = thisShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -88,7 +87,7 @@ export function Body({ stats }: BodyProps) {
|
|||||||
}
|
}
|
||||||
display={(stats) =>
|
display={(stats) =>
|
||||||
stats.shellReload?.toFixed(2) ??
|
stats.shellReload?.toFixed(2) ??
|
||||||
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(', ')
|
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(", ")
|
||||||
}
|
}
|
||||||
deltaNominalDisplay={(delta) => delta.toFixed(2)}
|
deltaNominalDisplay={(delta) => delta.toFixed(2)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ import {
|
|||||||
CaretLeftIcon,
|
CaretLeftIcon,
|
||||||
CaretRightIcon,
|
CaretRightIcon,
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import { Flex, IconButton, Table, Text } from '@radix-ui/themes';
|
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from "../../core/radix/var";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { ComparePersistent } from '../../stores/comparePersistent';
|
import { ComparePersistent } from "../../stores/comparePersistent";
|
||||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||||
|
|
||||||
type CompareRowProps = {
|
type CompareRowProps = {
|
||||||
display?: (
|
display?: (
|
||||||
member: Awaited<TankCharacteristics>,
|
member: Awaited<TankCharacteristics>
|
||||||
) => number | string | undefined;
|
) => number | string | undefined;
|
||||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
deltaNominalDisplay?: (delta: number) => string | number;
|
deltaNominalDisplay?: (delta: number) => string | number;
|
||||||
indent?: boolean;
|
indent?: boolean;
|
||||||
@@ -33,7 +33,7 @@ type CompareRowProps = {
|
|||||||
|
|
||||||
export function CompareRow({
|
export function CompareRow({
|
||||||
display,
|
display,
|
||||||
deltaType = 'higherIsBetter',
|
deltaType = "higherIsBetter",
|
||||||
decimals,
|
decimals,
|
||||||
deltaNominalDisplay,
|
deltaNominalDisplay,
|
||||||
indent,
|
indent,
|
||||||
@@ -41,17 +41,16 @@ export function CompareRow({
|
|||||||
...props
|
...props
|
||||||
}: CompareRowProps) {
|
}: CompareRowProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const sorting = CompareEphemeral.use((state) => state.sorting);
|
const sorting = CompareEphemeral.use((state) => state.sorting);
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
const values = stats.map((stat) =>
|
const values = stats.map((stat) =>
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(stat)!
|
? props.value(stat)!
|
||||||
: (stat[props.value] as number),
|
: (stat[props.value] as number)
|
||||||
);
|
);
|
||||||
const id = useRef(Math.random());
|
const id = useRef(Math.random());
|
||||||
const name =
|
const name =
|
||||||
typeof props.value === 'string'
|
typeof props.value === "string"
|
||||||
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
||||||
: props.name;
|
: props.name;
|
||||||
|
|
||||||
@@ -60,21 +59,21 @@ export function CompareRow({
|
|||||||
<StickyRowHeaderCell>
|
<StickyRowHeaderCell>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
style={{ whiteSpace: 'nowrap' }}
|
style={{ whiteSpace: "nowrap" }}
|
||||||
gap={indent ? { initial: '4', sm: '6' } : { initial: '1', sm: '2' }}
|
gap={indent ? { initial: "4", sm: "6" } : { initial: "1", sm: "2" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={sorting?.by === id.current ? undefined : 'gray'}
|
color={sorting?.by === id.current ? undefined : "gray"}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.sort((memberA, memberB) => {
|
draft.members.sort((memberA, memberB) => {
|
||||||
const indexA = draft.members.indexOf(memberA);
|
const indexA = draft.members.indexOf(memberA);
|
||||||
const indexB = draft.members.indexOf(memberB);
|
const indexB = draft.members.indexOf(memberB);
|
||||||
const valueA = values[indexA];
|
const valueA = values[indexA];
|
||||||
const valueB = values[indexB];
|
const valueB = values[indexB];
|
||||||
|
|
||||||
return draft.sorting?.direction === 'ascending' &&
|
return draft.sorting?.direction === "ascending" &&
|
||||||
draft.sorting.by === id.current
|
draft.sorting.by === id.current
|
||||||
? valueA - valueB
|
? valueA - valueB
|
||||||
: valueB - valueA;
|
: valueB - valueA;
|
||||||
@@ -83,26 +82,26 @@ export function CompareRow({
|
|||||||
draft.sorting = {
|
draft.sorting = {
|
||||||
by: id.current,
|
by: id.current,
|
||||||
direction:
|
direction:
|
||||||
draft.sorting?.direction === 'ascending' &&
|
draft.sorting?.direction === "ascending" &&
|
||||||
draft.sorting.by === id.current
|
draft.sorting.by === id.current
|
||||||
? 'descending'
|
? "descending"
|
||||||
: 'ascending',
|
: "ascending",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(sorting === undefined || sorting.by !== id.current) && (
|
{(sorting === undefined || sorting.by !== id.current) && (
|
||||||
<CaretSortIcon style={{ transform: 'rotate(90deg)' }} />
|
<CaretSortIcon style={{ transform: "rotate(90deg)" }} />
|
||||||
)}
|
)}
|
||||||
{sorting?.by === id.current && (
|
{sorting?.by === id.current && (
|
||||||
<>
|
<>
|
||||||
{sorting.direction === 'ascending' && <CaretRightIcon />}
|
{sorting.direction === "ascending" && <CaretRightIcon />}
|
||||||
{sorting.direction === 'descending' && <CaretLeftIcon />}
|
{sorting.direction === "descending" && <CaretLeftIcon />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Text size={{ initial: '1', sm: '2' }}>{name}</Text>
|
<Text size={{ initial: "1", sm: "2" }}>{name}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</StickyRowHeaderCell>
|
</StickyRowHeaderCell>
|
||||||
|
|
||||||
@@ -110,7 +109,7 @@ export function CompareRow({
|
|||||||
const delta = value - values[0];
|
const delta = value - values[0];
|
||||||
const deltaPercentage = value / values[0] - 1;
|
const deltaPercentage = value / values[0] - 1;
|
||||||
const normalizedDeltaPercentage = Math.round(
|
const normalizedDeltaPercentage = Math.round(
|
||||||
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25),
|
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
|
||||||
);
|
);
|
||||||
const resolvedDisplayValue = display
|
const resolvedDisplayValue = display
|
||||||
? display(stats[index])
|
? display(stats[index])
|
||||||
@@ -129,12 +128,12 @@ export function CompareRow({
|
|||||||
value === values[0]
|
value === values[0]
|
||||||
? undefined
|
? undefined
|
||||||
: (
|
: (
|
||||||
deltaType === 'higherIsBetter'
|
deltaType === "higherIsBetter"
|
||||||
? value > values[0]
|
? value > values[0]
|
||||||
: value < values[0]
|
: value < values[0]
|
||||||
)
|
)
|
||||||
? `color-mix(in srgb, ${Var('green-7')} ${normalizedDeltaPercentage}%, ${Var('green-3')})`
|
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
|
||||||
: `color-mix(in srgb, ${Var('red-7')} ${normalizedDeltaPercentage}%, ${Var('red-3')})`,
|
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -142,11 +141,11 @@ export function CompareRow({
|
|||||||
justify="center"
|
justify="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ textAlign: 'center' }} wrap="nowrap">
|
<Text style={{ textAlign: "center" }} wrap="nowrap">
|
||||||
{resolvedDisplayValue}
|
{resolvedDisplayValue}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@@ -154,10 +153,10 @@ export function CompareRow({
|
|||||||
resolvedDisplayValue !== undefined &&
|
resolvedDisplayValue !== undefined &&
|
||||||
values[0] !== undefined && (
|
values[0] !== undefined && (
|
||||||
<>
|
<>
|
||||||
{deltaMode === 'absolute' && (
|
{deltaMode === "absolute" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
(
|
(
|
||||||
{`${delta > 0 ? '+' : ''}${
|
{`${delta > 0 ? "+" : ""}${
|
||||||
deltaNominalDisplay
|
deltaNominalDisplay
|
||||||
? deltaNominalDisplay(delta)
|
? deltaNominalDisplay(delta)
|
||||||
: decimals === undefined
|
: decimals === undefined
|
||||||
@@ -168,11 +167,11 @@ export function CompareRow({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{deltaMode === 'percentage' && (
|
{deltaMode === "percentage" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
(
|
(
|
||||||
{`${deltaPercentage > 0 ? '+' : ''}${Math.round(
|
{`${deltaPercentage > 0 ? "+" : ""}${Math.round(
|
||||||
deltaPercentage * 100,
|
deltaPercentage * 100
|
||||||
)}%`}
|
)}%`}
|
||||||
)
|
)
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { asset, availableProvisions, permanentSkills } from '@blitzkit/core';
|
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
|
||||||
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
||||||
import { ComponentPlaceholderIcon } from '@radix-ui/react-icons';
|
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Flex, IconButton, Popover, Table } from '@radix-ui/themes';
|
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
|
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
|
||||||
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableSkillDefinitions } from '../../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from "../../core/awaitables/skillDefinitions";
|
||||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from "../../core/radix/var";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import type { EquipmentMatrix } from '../../stores/duel';
|
import type { EquipmentMatrix } from "../../stores/duel";
|
||||||
import { BlitzkitButtonGrayIcon } from '../BlitzkitButtonGrayIcon';
|
import { BlitzkitButtonGrayIcon } from "../BlitzkitButtonGrayIcon";
|
||||||
import { ConsumablesManager } from '../ConsumablesManager';
|
import { ConsumablesManager } from "../ConsumablesManager";
|
||||||
import { CrewSkillManager } from '../CrewSkillManager';
|
import { CrewSkillManager } from "../CrewSkillManager";
|
||||||
import { EquipmentManager } from '../EquipmentManager';
|
import { EquipmentManager } from "../EquipmentManager";
|
||||||
import { ModuleManager } from '../ModuleManager';
|
import { ModuleManager } from "../ModuleManager";
|
||||||
import { ProvisionsManager } from '../ProvisionsManager';
|
import { ProvisionsManager } from "../ProvisionsManager";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { StickyTableRoot } from '../StickyTableRoot';
|
import { StickyTableRoot } from "../StickyTableRoot";
|
||||||
import { Body } from './Body';
|
import { Body } from "./Body";
|
||||||
import { InsertionMarker } from './IntersectionMarker';
|
import { InsertionMarker } from "./IntersectionMarker";
|
||||||
import { TankCard } from './TankCard';
|
import { TankCard } from "./TankCard";
|
||||||
|
|
||||||
interface CompareTableProps {
|
interface CompareTableProps {
|
||||||
stats: TankCharacteristics[];
|
stats: TankCharacteristics[];
|
||||||
@@ -43,7 +43,6 @@ const [
|
|||||||
export function CompareTable({ stats }: CompareTableProps) {
|
export function CompareTable({ stats }: CompareTableProps) {
|
||||||
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
||||||
const members = CompareEphemeral.use((state) => state.members);
|
const members = CompareEphemeral.use((state) => state.members);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,20 +50,20 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
variant="surface"
|
variant="surface"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
maxHeight: '100%',
|
maxHeight: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
// height: 'calc(100vh - 128px)'
|
// height: 'calc(100vh - 128px)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<StickyColumnHeaderCell width="0" style={{ height: '100%' }}>
|
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -82,11 +81,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
|
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell style={{ height: '100%' }}>
|
<Table.Cell style={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
@@ -107,15 +106,15 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
crewSkills[skill] === 0
|
crewSkills[skill] === 0
|
||||||
? Var('gray-11')
|
? Var("gray-11")
|
||||||
: permanentSkills.includes(skill)
|
: permanentSkills.includes(skill)
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('amber-11'),
|
: Var("amber-11"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -125,7 +124,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<CrewSkillManager
|
<CrewSkillManager
|
||||||
skillLevels={crewSkills}
|
skillLevels={crewSkills}
|
||||||
onChange={(skills) => {
|
onChange={(skills) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.crewSkills = skills;
|
draft.crewSkills = skills;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +134,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 0;
|
draft.crewSkills[skill] = 0;
|
||||||
});
|
});
|
||||||
@@ -147,7 +146,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||||
draft.crewSkills[skill] = 7;
|
draft.crewSkills[skill] = 7;
|
||||||
});
|
});
|
||||||
@@ -177,19 +176,19 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
consumables,
|
consumables,
|
||||||
camouflage,
|
camouflage,
|
||||||
},
|
},
|
||||||
index,
|
index
|
||||||
) => {
|
) => {
|
||||||
const equipmentPreset =
|
const equipmentPreset =
|
||||||
equipmentDefinitions.presets[tank.equipment_preset];
|
equipmentDefinitions.presets[tank.equipment_preset];
|
||||||
const provisionsList = availableProvisions(
|
const provisionsList = availableProvisions(
|
||||||
tank,
|
tank,
|
||||||
gun,
|
gun,
|
||||||
provisionDefinitions,
|
provisionDefinitions
|
||||||
);
|
);
|
||||||
const consumablesList = Object.values(
|
const consumablesList = Object.values(
|
||||||
consumableDefinitions.consumables,
|
consumableDefinitions.consumables
|
||||||
).filter((consumable) =>
|
).filter((consumable) =>
|
||||||
checkConsumableProvisionInclusivity(consumable, tank, gun),
|
checkConsumableProvisionInclusivity(consumable, tank, gun)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -205,7 +204,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
{(
|
{(
|
||||||
['turret', 'gun', 'engine', 'chassis'] as const
|
["turret", "gun", "engine", "chassis"] as const
|
||||||
).map((module, index) => (
|
).map((module, index) => (
|
||||||
<img
|
<img
|
||||||
key={module}
|
key={module}
|
||||||
@@ -215,7 +214,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
src={asset(`icons/modules/${module}.webp`)}
|
src={asset(`icons/modules/${module}.webp`)}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: index > 0 ? -8 : 0,
|
marginLeft: index > 0 ? -8 : 0,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -232,7 +231,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
engine={engine}
|
engine={engine}
|
||||||
track={track}
|
track={track}
|
||||||
onChange={(modules) => {
|
onChange={(modules) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index] = {
|
draft.members[index] = {
|
||||||
...draft.members[index],
|
...draft.members[index],
|
||||||
...modules,
|
...modules,
|
||||||
@@ -245,7 +244,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const member = draft.members[index];
|
const member = draft.members[index];
|
||||||
|
|
||||||
member.turret = member.tank.turrets[0];
|
member.turret = member.tank.turrets[0];
|
||||||
@@ -261,7 +260,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const member = draft.members[index];
|
const member = draft.members[index];
|
||||||
|
|
||||||
member.turret = member.tank.turrets.at(-1)!;
|
member.turret = member.tank.turrets.at(-1)!;
|
||||||
@@ -288,9 +287,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
radius="large"
|
radius="large"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: 12,
|
width: 12,
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
@@ -298,14 +297,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<img
|
<img
|
||||||
alt={unwrap(
|
alt={unwrap(
|
||||||
provisionDefinitions.provisions[provision]
|
provisionDefinitions.provisions[provision]
|
||||||
.name,
|
.name
|
||||||
)}
|
)}
|
||||||
key={provision}
|
key={provision}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/provisions/${provision}.webp`,
|
`/icons/provisions/${provision}.webp`
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: "50%",
|
||||||
/**
|
/**
|
||||||
* max = 100% - 16px - 4px
|
* max = 100% - 16px - 4px
|
||||||
* min = 4px
|
* min = 4px
|
||||||
@@ -320,11 +319,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: index / (provisions.length - 1)
|
: index / (provisions.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -339,12 +338,12 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Popover.Content>
|
<Popover.Content>
|
||||||
<ProvisionsManager
|
<ProvisionsManager
|
||||||
provisions={provisionsList.map(
|
provisions={provisionsList.map(
|
||||||
(provision) => provision.id,
|
(provision) => provision.id
|
||||||
)}
|
)}
|
||||||
selected={provisions}
|
selected={provisions}
|
||||||
disabled={tank.max_provisions === provisions.length}
|
disabled={tank.max_provisions === provisions.length}
|
||||||
onChange={(provisions) => {
|
onChange={(provisions) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].provisions = provisions;
|
draft.members[index].provisions = provisions;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -353,7 +352,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].provisions = [];
|
draft.members[index].provisions = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -394,8 +393,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
equipment === -1
|
equipment === -1
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('gray-11'),
|
: Var("gray-11"),
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -405,8 +404,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
height: 8,
|
height: 8,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
equipment === 1
|
equipment === 1
|
||||||
? Var('crimson-11')
|
? Var("crimson-11")
|
||||||
: Var('gray-11'),
|
: Var("gray-11"),
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -424,7 +423,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
matrix={equipmentMatrix}
|
matrix={equipmentMatrix}
|
||||||
preset={equipmentPreset}
|
preset={equipmentPreset}
|
||||||
onChange={(matrix) => {
|
onChange={(matrix) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].equipmentMatrix = matrix;
|
draft.members[index].equipmentMatrix = matrix;
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -436,10 +435,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].equipmentMatrix = times(
|
draft.members[index].equipmentMatrix = times(
|
||||||
3,
|
3,
|
||||||
() => times(3, () => 0),
|
() => times(3, () => 0)
|
||||||
) as EquipmentMatrix;
|
) as EquipmentMatrix;
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -453,7 +452,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.forEach((member) => {
|
draft.members.forEach((member) => {
|
||||||
member.equipmentMatrix =
|
member.equipmentMatrix =
|
||||||
draft.members[index].equipmentMatrix;
|
draft.members[index].equipmentMatrix;
|
||||||
@@ -477,9 +476,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
radius="large"
|
radius="large"
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: 12,
|
width: 12,
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" justify="center">
|
<Flex direction="column" justify="center">
|
||||||
@@ -488,14 +487,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
alt={unwrap(
|
alt={unwrap(
|
||||||
consumableDefinitions.consumables[
|
consumableDefinitions.consumables[
|
||||||
consumable
|
consumable
|
||||||
].name,
|
].name
|
||||||
)}
|
)}
|
||||||
key={consumable}
|
key={consumable}
|
||||||
src={asset(
|
src={asset(
|
||||||
`/icons/consumables/${consumable}.webp`,
|
`/icons/consumables/${consumable}.webp`
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: '50%',
|
left: "50%",
|
||||||
/**
|
/**
|
||||||
* max = 100% - 16px - 4px
|
* max = 100% - 16px - 4px
|
||||||
* min = 4px
|
* min = 4px
|
||||||
@@ -510,11 +509,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
? 0.5
|
? 0.5
|
||||||
: index / (consumables.length - 1)
|
: index / (consumables.length - 1)
|
||||||
} * (100% - 24px))`,
|
} * (100% - 24px))`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -531,7 +530,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
consumables={consumablesList}
|
consumables={consumablesList}
|
||||||
selected={consumables}
|
selected={consumables}
|
||||||
onConsumablesChange={(consumables) => {
|
onConsumablesChange={(consumables) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].consumables = consumables;
|
draft.members[index].consumables = consumables;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -544,7 +543,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].consumables = [];
|
draft.members[index].consumables = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -562,18 +561,18 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
{!tank.fixed_camouflage && (
|
{!tank.fixed_camouflage && (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members[index].camouflage = !camouflage;
|
draft.members[index].camouflage = !camouflage;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
variant={camouflage ? 'solid' : 'outline'}
|
variant={camouflage ? "solid" : "outline"}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={asset('icons/camo.webp')}
|
src={asset("icons/camo.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '1em',
|
width: "1em",
|
||||||
height: '1em',
|
height: "1em",
|
||||||
transform: 'scale(2) translate(15%, 15%)',
|
transform: "scale(2) translate(15%, 15%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -581,7 +580,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
|
|||||||
@@ -1,33 +1,27 @@
|
|||||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
|
||||||
import {
|
import {
|
||||||
ComparePersistent,
|
ComparePersistent,
|
||||||
type DeltaMode,
|
type DeltaMode,
|
||||||
} from '../../stores/comparePersistent';
|
} from "../../stores/comparePersistent";
|
||||||
import { TankSearch } from '../TankSearch';
|
import { TankSearch } from "../TankSearch";
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
addTankDialogOpen: boolean;
|
addTankDialogOpen: boolean;
|
||||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
awaitableProvisionDefinitions,
|
|
||||||
awaitableModelDefinitions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function Controls({
|
export function Controls({
|
||||||
addTankDialogOpen,
|
addTankDialogOpen,
|
||||||
onAddTankDialogOpenChange,
|
onAddTankDialogOpenChange,
|
||||||
}: ControlsProps) {
|
}: ControlsProps) {
|
||||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const mutateComparePersistent = ComparePersistent.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,28 +54,20 @@ export function Controls({
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
CompareEphemeral.mutate((draft) => {
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
tankToCompareMember(tank, model, provisionDefinitions),
|
tankToCompareMember(tank, provisionDefinitions)
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
onAddTankDialogOpenChange(false);
|
onAddTankDialogOpenChange(false);
|
||||||
}}
|
}}
|
||||||
onSelectAll={(tanks) => {
|
onSelectAll={(tanks) => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.push(
|
draft.members.push(
|
||||||
...tanks.map((tank) => {
|
...tanks.map((tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
return tankToCompareMember(tank, provisionDefinitions);
|
||||||
|
})
|
||||||
return tankToCompareMember(
|
|
||||||
tank,
|
|
||||||
model,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -97,7 +83,7 @@ export function Controls({
|
|||||||
variant="soft"
|
variant="soft"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members = [];
|
draft.members = [];
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -110,18 +96,18 @@ export function Controls({
|
|||||||
variant="classic"
|
variant="classic"
|
||||||
value={deltaMode}
|
value={deltaMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateComparePersistent((draft) => {
|
ComparePersistent.mutate((draft) => {
|
||||||
draft.deltaMode = value as DeltaMode;
|
draft.deltaMode = value as DeltaMode;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"none" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.none}
|
{strings.website.tools.compare.actions.deltas.none}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"percentage" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.percentage}
|
{strings.website.tools.compare.actions.deltas.percentage}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={'absolute' satisfies DeltaMode}>
|
<SegmentedControl.Item value={"absolute" satisfies DeltaMode}>
|
||||||
{strings.website.tools.compare.actions.deltas.absolute}
|
{strings.website.tools.compare.actions.deltas.absolute}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { type TankDefinition, tankIcon } from '@blitzkit/core';
|
import { type TankDefinition, tankIcon } from "@blitzkit/core";
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
import { Flex, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import { Vector2 } from 'three';
|
import { Vector2 } from "three";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
import { InsertionMarker } from './IntersectionMarker';
|
import { InsertionMarker } from "./IntersectionMarker";
|
||||||
import { insertionMarkers } from './IntersectionMarker/constants';
|
import { insertionMarkers } from "./IntersectionMarker/constants";
|
||||||
import { TankControl } from './TankControl';
|
import { TankControl } from "./TankControl";
|
||||||
|
|
||||||
interface TankCardProps {
|
interface TankCardProps {
|
||||||
index: number;
|
index: number;
|
||||||
@@ -16,7 +16,6 @@ interface TankCardProps {
|
|||||||
|
|
||||||
export function TankCard({ index, tank }: TankCardProps) {
|
export function TankCard({ index, tank }: TankCardProps) {
|
||||||
const draggable = useRef<HTMLDivElement>(null);
|
const draggable = useRef<HTMLDivElement>(null);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
const { unwrap } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,8 +33,8 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
|
|
||||||
delta.set(0, 0);
|
delta.set(0, 0);
|
||||||
current.copy(initial.set(event.clientX, event.clientY));
|
current.copy(initial.set(event.clientX, event.clientY));
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -43,7 +42,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
current.set(event.clientX, event.clientY);
|
current.set(event.clientX, event.clientY);
|
||||||
delta.copy(current).sub(initial);
|
delta.copy(current).sub(initial);
|
||||||
insertionMarkers.forEach(({ element }) => {
|
insertionMarkers.forEach(({ element }) => {
|
||||||
element.style.opacity = '0';
|
element.style.opacity = "0";
|
||||||
});
|
});
|
||||||
|
|
||||||
const distances = insertionMarkers.map((insertionMarker) => {
|
const distances = insertionMarkers.map((insertionMarker) => {
|
||||||
@@ -56,34 +55,34 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const closest = distances.reduce((a, b) =>
|
const closest = distances.reduce((a, b) =>
|
||||||
a.distance < b.distance ? a : b,
|
a.distance < b.distance ? a : b
|
||||||
);
|
);
|
||||||
|
|
||||||
dropIndex = closest.index;
|
dropIndex = closest.index;
|
||||||
closest.element.style.opacity = '1';
|
closest.element.style.opacity = "1";
|
||||||
draggable.current!.style.position = 'fixed';
|
draggable.current!.style.position = "fixed";
|
||||||
draggable.current!.style.cursor = 'grabbing';
|
draggable.current!.style.cursor = "grabbing";
|
||||||
draggable.current!.style.zIndex = '1';
|
draggable.current!.style.zIndex = "1";
|
||||||
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
||||||
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
||||||
}
|
}
|
||||||
function handlePointerUp(event: PointerEvent) {
|
function handlePointerUp(event: PointerEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
draggable.current!.style.position = 'static';
|
draggable.current!.style.position = "static";
|
||||||
draggable.current!.style.zIndex = 'unset';
|
draggable.current!.style.zIndex = "unset";
|
||||||
draggable.current!.style.cursor = 'grab';
|
draggable.current!.style.cursor = "grab";
|
||||||
|
|
||||||
insertionMarkers.forEach(({ element }) => {
|
insertionMarkers.forEach(({ element }) => {
|
||||||
element.style.opacity = '0';
|
element.style.opacity = "0";
|
||||||
});
|
});
|
||||||
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
|
|
||||||
if (dropIndex === -1) return;
|
if (dropIndex === -1) return;
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
const fromIndex = index;
|
const fromIndex = index;
|
||||||
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
||||||
|
|
||||||
@@ -95,10 +94,10 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
dropIndex = -1;
|
dropIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
draggable.current?.addEventListener('pointerdown', handlePointerDown);
|
draggable.current?.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
draggable.current?.removeEventListener('pointerdown', handlePointerDown);
|
draggable.current?.removeEventListener("pointerdown", handlePointerDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -114,9 +113,9 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
justify="between"
|
justify="between"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{
|
style={{
|
||||||
userSelect: 'none',
|
userSelect: "none",
|
||||||
touchAction: 'none',
|
touchAction: "none",
|
||||||
cursor: 'grab',
|
cursor: "grab",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -126,16 +125,16 @@ export function TankCard({ index, tank }: TankCardProps) {
|
|||||||
height={64}
|
height={64}
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
maxWidth: 128,
|
maxWidth: 128,
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{unwrap(tank.name)}
|
{unwrap(tank.name)}
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
import { LinkI18n } from "../LinkI18n";
|
||||||
import { LinkI18n } from '../LinkI18n';
|
import { TankSearch } from "../TankSearch";
|
||||||
import { TankSearch } from '../TankSearch';
|
|
||||||
|
|
||||||
interface TankControlProps {
|
interface TankControlProps {
|
||||||
index: number;
|
index: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
awaitableProvisionDefinitions,
|
|
||||||
awaitableModelDefinitions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function TankControl({ index, slug }: TankControlProps) {
|
export function TankControl({ index, slug }: TankControlProps) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="3" justify="center" style={{ width: '100%' }}>
|
<Flex gap="3" justify="center" style={{ width: "100%" }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateCompareEphemeral((draft) => {
|
CompareEphemeral.mutate((draft) => {
|
||||||
draft.members.splice(index, 1);
|
draft.members.splice(index, 1);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
@@ -61,13 +56,10 @@ export function TankControl({ index, slug }: TankControlProps) {
|
|||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
CompareEphemeral.mutate((draft) => {
|
||||||
|
|
||||||
mutateCompareEphemeral((draft) => {
|
|
||||||
draft.members[index] = tankToCompareMember(
|
draft.members[index] = tankToCompareMember(
|
||||||
tank,
|
tank,
|
||||||
model,
|
provisionDefinitions
|
||||||
provisionDefinitions,
|
|
||||||
);
|
);
|
||||||
draft.sorting = undefined;
|
draft.sorting = undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { Switch } from '@radix-ui/themes';
|
import { Switch } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState } from '../../stores/embedState';
|
import { EmbedState } from "../../stores/embedState";
|
||||||
|
|
||||||
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
variant="classic"
|
variant="classic"
|
||||||
checked={EmbedState.use((state) => state[configKey] as boolean)}
|
checked={EmbedState.use((state) => state[configKey] as boolean)}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = !draft[configKey];
|
draft[configKey] = !draft[configKey];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState } from '../../stores/embedState';
|
import { EmbedState } from "../../stores/embedState";
|
||||||
import { ColorControllerRaw } from './ColorControllerRaw';
|
import { ColorControllerRaw } from "./ColorControllerRaw";
|
||||||
|
|
||||||
export function Color({ configKey }: EmbedPreviewControllerProps) {
|
export function Color({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
const color = EmbedState.use((state) => state[configKey] as string);
|
const color = EmbedState.use((state) => state[configKey] as string);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColorControllerRaw
|
<ColorControllerRaw
|
||||||
value={color}
|
value={color}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = value;
|
draft[configKey] = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Select } from '@radix-ui/themes';
|
import { Select } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState } from '../../stores/embedState';
|
import { EmbedState } from "../../stores/embedState";
|
||||||
import type {
|
import type {
|
||||||
EmbedConfigItemType,
|
EmbedConfigItemType,
|
||||||
EmbedItemType,
|
EmbedItemType,
|
||||||
} from '../../stores/embedState/constants';
|
} from "../../stores/embedState/constants";
|
||||||
|
|
||||||
export function Enum({
|
export function Enum({
|
||||||
configKey,
|
configKey,
|
||||||
@@ -13,14 +13,13 @@ export function Enum({
|
|||||||
}: EmbedPreviewControllerProps & {
|
}: EmbedPreviewControllerProps & {
|
||||||
config: EmbedConfigItemType<EmbedItemType.Enum>;
|
config: EmbedConfigItemType<EmbedItemType.Enum>;
|
||||||
}) {
|
}) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select.Root
|
<Select.Root
|
||||||
value={EmbedState.use((state) => state[configKey] as string)}
|
value={EmbedState.use((state) => state[configKey] as string)}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = value;
|
draft[configKey] = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
searchPlayersAcrossRegions,
|
searchPlayersAcrossRegions,
|
||||||
type AccountListItem,
|
type AccountListItem,
|
||||||
type AccountListWithServer,
|
type AccountListWithServer,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { CaretLeftIcon, Link2Icon, PersonIcon } from '@radix-ui/react-icons';
|
import { CaretLeftIcon, Link2Icon, PersonIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -13,15 +13,15 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { stringify } from 'urlon';
|
import { stringify } from "urlon";
|
||||||
import type { embedConfigurations } from '../../constants/embeds';
|
import type { embedConfigurations } from "../../constants/embeds";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { EmbedState, type EmbedStateStore } from '../../stores/embedState';
|
import { EmbedState, type EmbedStateStore } from "../../stores/embedState";
|
||||||
import { CopyButton } from '../CopyButton';
|
import { CopyButton } from "../CopyButton";
|
||||||
import { SearchResults } from '../SearchResults';
|
import { SearchResults } from "../SearchResults";
|
||||||
|
|
||||||
interface GenerateURLProps {
|
interface GenerateURLProps {
|
||||||
embed: keyof typeof embedConfigurations;
|
embed: keyof typeof embedConfigurations;
|
||||||
@@ -31,7 +31,6 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
const [searchDialogOpen, setSearchDialogOpen] = useState(false);
|
const [searchDialogOpen, setSearchDialogOpen] = useState(false);
|
||||||
const [copyDialogOpen, setCopyDialogOpen] = useState(false);
|
const [copyDialogOpen, setCopyDialogOpen] = useState(false);
|
||||||
const [copyUser, setCopyUser] = useState<AccountListItem | null>(null);
|
const [copyUser, setCopyUser] = useState<AccountListItem | null>(null);
|
||||||
const embedStateStore = EmbedState.useStore();
|
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [results, setResults] = useState<AccountListWithServer>([]);
|
const [results, setResults] = useState<AccountListWithServer>([]);
|
||||||
@@ -52,7 +51,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
setSearching(false);
|
setSearching(false);
|
||||||
setResults(results);
|
setResults(results);
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
if (!input.current) return;
|
if (!input.current) return;
|
||||||
@@ -94,7 +93,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
mt="4"
|
mt="4"
|
||||||
style={{ width: '20rem' }}
|
style={{ width: "20rem" }}
|
||||||
placeholder={strings.website.common.player_search.hint}
|
placeholder={strings.website.common.player_search.hint}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -136,7 +135,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Dialog.Close>
|
<Dialog.Close>
|
||||||
<Button color="red" variant="outline" style={{ width: '100%' }}>
|
<Button color="red" variant="outline" style={{ width: "100%" }}>
|
||||||
{strings.website.tools.embed.configuration.export.generate.cancel}
|
{strings.website.tools.embed.configuration.export.generate.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Close>
|
</Dialog.Close>
|
||||||
@@ -144,7 +143,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
<Dialog.Root open={copyDialogOpen} onOpenChange={setCopyDialogOpen}>
|
<Dialog.Root open={copyDialogOpen} onOpenChange={setCopyDialogOpen}>
|
||||||
<Dialog.Content style={{ width: 'fit-content' }}>
|
<Dialog.Content style={{ width: "fit-content" }}>
|
||||||
<Flex direction="column" gap="1">
|
<Flex direction="column" gap="1">
|
||||||
<Heading size="5">
|
<Heading size="5">
|
||||||
{
|
{
|
||||||
@@ -156,7 +155,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.embed.configuration.export.generate
|
strings.website.tools.embed.configuration.export.generate
|
||||||
.success.info,
|
.success.info,
|
||||||
[`${copyUser?.nickname}`],
|
[`${copyUser?.nickname}`]
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -170,7 +169,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
setSearchDialogOpen(true);
|
setSearchDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CaretLeftIcon />{' '}
|
<CaretLeftIcon />{" "}
|
||||||
{
|
{
|
||||||
strings.website.tools.embed.configuration.export.generate
|
strings.website.tools.embed.configuration.export.generate
|
||||||
.success.back
|
.success.back
|
||||||
@@ -181,12 +180,10 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
copy={() => {
|
copy={() => {
|
||||||
if (!copyUser) return;
|
if (!copyUser) return;
|
||||||
|
|
||||||
const state = embedStateStore.getState();
|
|
||||||
const initial = embedStateStore.getInitialState();
|
|
||||||
const shallowState: EmbedStateStore = {};
|
const shallowState: EmbedStateStore = {};
|
||||||
|
|
||||||
Object.entries(state).forEach(([key, value]) => {
|
Object.entries(EmbedState.state).forEach(([key, value]) => {
|
||||||
if (value !== initial[key]) {
|
if (value !== EmbedState.initial[key]) {
|
||||||
shallowState[key] = value;
|
shallowState[key] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -202,7 +199,7 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
return `${location.origin}/embed/${embed}/host?${searchParams.toString()}`;
|
return `${location.origin}/embed/${embed}/host?${searchParams.toString()}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link2Icon />{' '}
|
<Link2Icon />{" "}
|
||||||
{
|
{
|
||||||
strings.website.tools.embed.configuration.export.generate
|
strings.website.tools.embed.configuration.export.generate
|
||||||
.success.copy
|
.success.copy
|
||||||
|
|||||||
@@ -6,17 +6,16 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from "react";
|
||||||
import { parse } from 'urlon';
|
import { parse } from "urlon";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { EmbedState } from '../../stores/embedState';
|
import { EmbedState } from "../../stores/embedState";
|
||||||
|
|
||||||
export function Import() {
|
export function Import() {
|
||||||
const importInput = useRef<HTMLInputElement>(null);
|
const importInput = useRef<HTMLInputElement>(null);
|
||||||
const [showInvalidURLPrompt, setShowInvalidURLPrompt] = useState(false);
|
const [showInvalidURLPrompt, setShowInvalidURLPrompt] = useState(false);
|
||||||
const [showConfirmation, setConfirmation] = useState(false);
|
const [showConfirmation, setConfirmation] = useState(false);
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,15 +63,15 @@ export function Import() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(importInput.current.value);
|
const url = new URL(importInput.current.value);
|
||||||
const state = url.searchParams.get('state');
|
const state = url.searchParams.get("state");
|
||||||
if (state === null) throw undefined;
|
if (state === null) throw undefined;
|
||||||
const json = parse(state);
|
const json = parse(state);
|
||||||
|
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
Object.assign(draft, json);
|
Object.assign(draft, json);
|
||||||
});
|
});
|
||||||
|
|
||||||
importInput.current.value = '';
|
importInput.current.value = "";
|
||||||
setConfirmation(true);
|
setConfirmation(true);
|
||||||
setTimeout(() => setConfirmation(false), 2000);
|
setTimeout(() => setConfirmation(false), 2000);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Slider } from '@radix-ui/themes';
|
import { Slider } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState, type RadixRadius } from '../../stores/embedState';
|
import { EmbedState, type RadixRadius } from "../../stores/embedState";
|
||||||
|
|
||||||
export function Radius({ configKey }: EmbedPreviewControllerProps) {
|
export function Radius({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
variant="classic"
|
variant="classic"
|
||||||
@@ -13,12 +11,12 @@ export function Radius({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
value={[
|
value={[
|
||||||
EmbedState.use((state) => {
|
EmbedState.use((state) => {
|
||||||
const radius = state[configKey] as RadixRadius;
|
const radius = state[configKey] as RadixRadius;
|
||||||
return radius === 'full' ? 5 : parseInt(radius);
|
return radius === "full" ? 5 : parseInt(radius);
|
||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = value === 5 ? 'full' : `${value}`;
|
draft[configKey] = value === 5 ? "full" : `${value}`;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { FontBoldIcon, FontSizeIcon } from '@radix-ui/react-icons';
|
import { FontBoldIcon, FontSizeIcon } from "@radix-ui/react-icons";
|
||||||
import { Flex, Slider, Text } from '@radix-ui/themes';
|
import { Flex, Slider, Text } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState';
|
import { EmbedState, type RadixSizeWithout0 } from "../../stores/embedState";
|
||||||
import {
|
import {
|
||||||
radixTextWeights,
|
radixTextWeights,
|
||||||
type EmbedConfigItemType,
|
type EmbedConfigItemType,
|
||||||
type EmbedItemType,
|
type EmbedItemType,
|
||||||
} from '../../stores/embedState/constants';
|
} from "../../stores/embedState/constants";
|
||||||
import { ColorControllerRaw } from './ColorControllerRaw';
|
import { ColorControllerRaw } from "./ColorControllerRaw";
|
||||||
|
|
||||||
export function RichText({ configKey }: EmbedPreviewControllerProps) {
|
export function RichText({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
const state = EmbedState.use(
|
const state = EmbedState.use(
|
||||||
(state) =>
|
(state) =>
|
||||||
state[
|
state[configKey] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
|
||||||
configKey
|
|
||||||
] as EmbedConfigItemType<EmbedItemType.RichText>['default'],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -24,11 +21,11 @@ export function RichText({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
<ColorControllerRaw
|
<ColorControllerRaw
|
||||||
value={state.color}
|
value={state.color}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
(
|
(
|
||||||
draft[
|
draft[
|
||||||
configKey
|
configKey
|
||||||
] as EmbedConfigItemType<EmbedItemType.RichText>['default']
|
] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
|
||||||
).color = value;
|
).color = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -45,11 +42,11 @@ export function RichText({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
max={9}
|
max={9}
|
||||||
value={[Number(state.size)]}
|
value={[Number(state.size)]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
(
|
(
|
||||||
draft[
|
draft[
|
||||||
configKey
|
configKey
|
||||||
] as EmbedConfigItemType<EmbedItemType.RichText>['default']
|
] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
|
||||||
).size = `${value}` as RadixSizeWithout0;
|
).size = `${value}` as RadixSizeWithout0;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -66,11 +63,11 @@ export function RichText({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
max={radixTextWeights.length - 1}
|
max={radixTextWeights.length - 1}
|
||||||
value={[radixTextWeights.indexOf(state.weight)]}
|
value={[radixTextWeights.indexOf(state.weight)]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
(
|
(
|
||||||
draft[
|
draft[
|
||||||
configKey
|
configKey
|
||||||
] as EmbedConfigItemType<EmbedItemType.RichText>['default']
|
] as EmbedConfigItemType<EmbedItemType.RichText>["default"]
|
||||||
).weight = radixTextWeights[value];
|
).weight = radixTextWeights[value];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Slider } from '@radix-ui/themes';
|
import { Slider } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState, type RadixSize } from '../../stores/embedState';
|
import { EmbedState, type RadixSize } from "../../stores/embedState";
|
||||||
|
|
||||||
export function Size({ configKey }: EmbedPreviewControllerProps) {
|
export function Size({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
variant="classic"
|
variant="classic"
|
||||||
@@ -14,7 +12,7 @@ export function Size({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
|
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
|
||||||
]}
|
]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = `${value}`;
|
draft[configKey] = `${value}`;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Slider } from '@radix-ui/themes';
|
import { Slider } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState, type RadixSizeWithout0 } from '../../stores/embedState';
|
import { EmbedState, type RadixSizeWithout0 } from "../../stores/embedState";
|
||||||
|
|
||||||
export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
|
export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
variant="classic"
|
variant="classic"
|
||||||
@@ -12,11 +10,11 @@ export function SizeWithout0({ configKey }: EmbedPreviewControllerProps) {
|
|||||||
max={9}
|
max={9}
|
||||||
value={[
|
value={[
|
||||||
EmbedState.use((state) =>
|
EmbedState.use((state) =>
|
||||||
parseInt(state[configKey] as RadixSizeWithout0),
|
parseInt(state[configKey] as RadixSizeWithout0)
|
||||||
),
|
),
|
||||||
]}
|
]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = `${value}`;
|
draft[configKey] = `${value}`;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Slider as RadixSlider } from '@radix-ui/themes';
|
import { Slider as RadixSlider } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState, type RadixSize } from '../../stores/embedState';
|
import { EmbedState, type RadixSize } from "../../stores/embedState";
|
||||||
import type {
|
import type {
|
||||||
EmbedConfigItemType,
|
EmbedConfigItemType,
|
||||||
EmbedItemType,
|
EmbedItemType,
|
||||||
} from '../../stores/embedState/constants';
|
} from "../../stores/embedState/constants";
|
||||||
|
|
||||||
export function Slider({
|
export function Slider({
|
||||||
configKey,
|
configKey,
|
||||||
@@ -12,8 +12,6 @@ export function Slider({
|
|||||||
}: EmbedPreviewControllerProps & {
|
}: EmbedPreviewControllerProps & {
|
||||||
config: EmbedConfigItemType<EmbedItemType.Slider>;
|
config: EmbedConfigItemType<EmbedItemType.Slider>;
|
||||||
}) {
|
}) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadixSlider
|
<RadixSlider
|
||||||
variant="classic"
|
variant="classic"
|
||||||
@@ -23,7 +21,7 @@ export function Slider({
|
|||||||
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
|
EmbedState.use((state) => parseInt(state[configKey] as RadixSize)),
|
||||||
]}
|
]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = value;
|
draft[configKey] = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { TextField } from '@radix-ui/themes';
|
import { TextField } from "@radix-ui/themes";
|
||||||
import type { EmbedPreviewControllerProps } from '../../pages/[...locale]/embed/[embed]/_index';
|
import type { EmbedPreviewControllerProps } from "../../pages/[...locale]/embed/[embed]/_index";
|
||||||
import { EmbedState } from '../../stores/embedState';
|
import { EmbedState } from "../../stores/embedState";
|
||||||
|
|
||||||
export function TextController({ configKey }: EmbedPreviewControllerProps) {
|
export function TextController({ configKey }: EmbedPreviewControllerProps) {
|
||||||
const mutateEmbedState = EmbedState.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
placeholder="Empty"
|
placeholder="Empty"
|
||||||
value={EmbedState.use((state) => state[configKey] as string)}
|
value={EmbedState.use((state) => state[configKey] as string)}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
mutateEmbedState((draft) => {
|
EmbedState.mutate((draft) => {
|
||||||
draft[configKey] = event.target.value;
|
draft[configKey] = event.target.value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import {
|
|||||||
idToRegion,
|
idToRegion,
|
||||||
sumCompositeStats,
|
sumCompositeStats,
|
||||||
type IndividualTankStats,
|
type IndividualTankStats,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { STRINGS } from '@blitzkit/i18n';
|
import { STRINGS } from "@blitzkit/i18n";
|
||||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { ContextMenu } from '@radix-ui/themes';
|
import { ContextMenu } from "@radix-ui/themes";
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { breakdownConfig } from '../../constants/embeds';
|
import { breakdownConfig } from "../../constants/embeds";
|
||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { EmbedBreakdownPersistent } from '../../stores/embedBreakdownPersistent';
|
import { EmbedBreakdown } from "../../stores/embedBreakdown";
|
||||||
import { useEmbedStateCurry } from '../../stores/embedState/utilities';
|
import { useEmbedStateCurry } from "../../stores/embedState/utilities";
|
||||||
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from '../TanksEmbed';
|
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from "../TanksEmbed";
|
||||||
|
|
||||||
export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
|
export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
|
||||||
value,
|
value,
|
||||||
@@ -55,7 +55,7 @@ export const fakeCompositeStats = compositeStats(
|
|||||||
wins: 30,
|
wins: 30,
|
||||||
xp: 1230 * 50,
|
xp: 1230 * 50,
|
||||||
},
|
},
|
||||||
averageDefinitions.averages[7297].mu,
|
averageDefinitions.averages[7297].mu
|
||||||
);
|
);
|
||||||
|
|
||||||
export function BreakdownPreview() {
|
export function BreakdownPreview() {
|
||||||
@@ -64,11 +64,11 @@ export function BreakdownPreview() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BreakdownEmbedWrapper>
|
<BreakdownEmbedWrapper>
|
||||||
{useEmbedState('showTotal') && (
|
{useEmbedState("showTotal") && (
|
||||||
<BreakdownEmbedCard composite={fakeCompositeStats} tank={null} />
|
<BreakdownEmbedCard composite={fakeCompositeStats} tank={null} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tanks.slice(0, useEmbedState('listMaxTanks')).map((tank) => (
|
{tanks.slice(0, useEmbedState("listMaxTanks")).map((tank) => (
|
||||||
<BreakdownEmbedCard
|
<BreakdownEmbedCard
|
||||||
composite={fakeCompositeStats}
|
composite={fakeCompositeStats}
|
||||||
key={tank.id}
|
key={tank.id}
|
||||||
@@ -80,26 +80,17 @@ export function BreakdownPreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BreakdownRenderer() {
|
export function BreakdownRenderer() {
|
||||||
return (
|
|
||||||
<EmbedBreakdownPersistent.Provider>
|
|
||||||
<BreakdownRendererContent />
|
|
||||||
</EmbedBreakdownPersistent.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreakdownRendererContent() {
|
|
||||||
const { useEmbedState } = useEmbedStateCurry<typeof breakdownConfig>();
|
const { useEmbedState } = useEmbedStateCurry<typeof breakdownConfig>();
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const mutateEmbedBreakdownPersistent = EmbedBreakdownPersistent.useMutation();
|
const id = Number(params.get("id"));
|
||||||
const id = Number(params.get('id'));
|
const tankStatsA = EmbedBreakdown.use((state) => state[id] ?? []);
|
||||||
const tankStatsA = EmbedBreakdownPersistent.use((state) => state[id] ?? []);
|
|
||||||
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[]>([]);
|
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[]>([]);
|
||||||
const diff = useMemo(
|
const diff = useMemo(
|
||||||
() =>
|
() =>
|
||||||
deltaTankStats(tankStatsA, tankStatsB).sort(
|
deltaTankStats(tankStatsA, tankStatsB).sort(
|
||||||
(a, b) => b.last_battle_time - a.last_battle_time,
|
(a, b) => b.last_battle_time - a.last_battle_time
|
||||||
),
|
),
|
||||||
[tankStatsB],
|
[tankStatsB]
|
||||||
);
|
);
|
||||||
const compositeStats = diff
|
const compositeStats = diff
|
||||||
.map((diff) => {
|
.map((diff) => {
|
||||||
@@ -109,14 +100,14 @@ function BreakdownRendererContent() {
|
|||||||
|
|
||||||
const compositeStats = calculateCompositeStats(
|
const compositeStats = calculateCompositeStats(
|
||||||
{ battle_life_time: diff.battle_life_time, ...diff.all },
|
{ battle_life_time: diff.battle_life_time, ...diff.all },
|
||||||
average.mu,
|
average.mu
|
||||||
);
|
);
|
||||||
|
|
||||||
return { id: diff.tank_id, compositeStats };
|
return { id: diff.tank_id, compositeStats };
|
||||||
})
|
})
|
||||||
.filter((stats) => stats !== null);
|
.filter((stats) => stats !== null);
|
||||||
const sum = sumCompositeStats(
|
const sum = sumCompositeStats(
|
||||||
compositeStats.map(({ compositeStats }) => compositeStats),
|
compositeStats.map(({ compositeStats }) => compositeStats)
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -135,12 +126,12 @@ function BreakdownRendererContent() {
|
|||||||
<ContextMenu.Root>
|
<ContextMenu.Root>
|
||||||
<ContextMenu.Trigger>
|
<ContextMenu.Trigger>
|
||||||
<BreakdownEmbedWrapper>
|
<BreakdownEmbedWrapper>
|
||||||
{useEmbedState('showTotal') && (
|
{useEmbedState("showTotal") && (
|
||||||
<BreakdownEmbedCard composite={sum} tank={null} />
|
<BreakdownEmbedCard composite={sum} tank={null} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{compositeStats
|
{compositeStats
|
||||||
.slice(0, useEmbedState('listMaxTanks'))
|
.slice(0, useEmbedState("listMaxTanks"))
|
||||||
.map(({ id, compositeStats }) => {
|
.map(({ id, compositeStats }) => {
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
@@ -163,7 +154,7 @@ function BreakdownRendererContent() {
|
|||||||
|
|
||||||
if (newA === null) return;
|
if (newA === null) return;
|
||||||
|
|
||||||
mutateEmbedBreakdownPersistent((draft) => {
|
EmbedBreakdown.mutate((draft) => {
|
||||||
draft[id] = newA;
|
draft[id] = newA;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import locales from '@blitzkit/i18n/locales.json';
|
import locales from "@blitzkit/i18n/locales.json";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { Flex, Text } from '@radix-ui/themes';
|
import { Flex, Text } from "@radix-ui/themes";
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from "fuzzysort";
|
||||||
import { times } from 'lodash-es';
|
import { times } from "lodash-es";
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from "react";
|
||||||
import { awaitableGallery } from '../../core/awaitables/gallery';
|
import { awaitableGallery } from "../../core/awaitables/gallery";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
import { Gallery } from "../../stores/gallery";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import { InlineSkeleton } from '../InlineSkeleton';
|
import { InlineSkeleton } from "../InlineSkeleton";
|
||||||
import { GalleryCard } from './Card';
|
import { GalleryCard } from "./Card";
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -22,7 +22,7 @@ const DEFAULT_LOADED = 42;
|
|||||||
const PREVIEW_COUNT = 28;
|
const PREVIEW_COUNT = 28;
|
||||||
|
|
||||||
export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const search = GalleryEphemeral.use((state) => state.search);
|
const search = Gallery.use((state) => state.search);
|
||||||
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
|
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
|
||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@@ -35,9 +35,9 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
.go(search, gallery.avatars, {
|
.go(search, gallery.avatars, {
|
||||||
keys: [
|
keys: [
|
||||||
...locales.supported.map(
|
...locales.supported.map(
|
||||||
(supported) => `name.locales.${supported.locale}`,
|
(supported) => `name.locales.${supported.locale}`
|
||||||
),
|
),
|
||||||
'id',
|
"id",
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.map((result) => result.obj);
|
.map((result) => result.obj);
|
||||||
@@ -72,7 +72,7 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
skeleton
|
skeleton
|
||||||
onIntersection={() => setLoadedCards((state) => state + 2)}
|
onIntersection={() => setLoadedCards((state) => state + 2)}
|
||||||
/>
|
/>
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
import { TextField } from '@radix-ui/themes';
|
import { TextField } from "@radix-ui/themes";
|
||||||
import { useRef } from 'react';
|
import { useRef } from "react";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
import { Gallery } from "../../stores/gallery";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
|
|
||||||
export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const mutateGalleryEphemeral = GalleryEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,7 +16,7 @@ export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
placeholder={strings.website.tools.gallery.search.hint}
|
placeholder={strings.website.tools.gallery.search.hint}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
mutateGalleryEphemeral((draft) => {
|
Gallery.mutate((draft) => {
|
||||||
const trimmed = event.target.value.trim();
|
const trimmed = event.target.value.trim();
|
||||||
draft.search = trimmed.length === 0 ? undefined : trimmed;
|
draft.search = trimmed.length === 0 ? undefined : trimmed;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Box } from '@radix-ui/themes';
|
import { Box } from "@radix-ui/themes";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||||
import { tankToDuelMember } from '../../core/blitzkit/tankToDuelMember';
|
import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from "../../core/radix/var";
|
||||||
import { Duel } from '../../stores/duel';
|
import { Duel } from "../../stores/duel";
|
||||||
import { GuessEphemeral } from '../../stores/guessEphemeral';
|
import { Guess, GuessState } from "../../stores/guess";
|
||||||
import { TankopediaEphemeral } from '../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../stores/tankopedia";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
||||||
awaitableModelDefinitions,
|
awaitableModelDefinitions,
|
||||||
@@ -15,17 +15,14 @@ const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export function GuessBackground() {
|
export function GuessBackground() {
|
||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const tank = GuessEphemeral.use((state) => state.tank);
|
const tank = Guess.use((state) => state.tank);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateDuel((state) => {
|
Duel.mutate((state) => {
|
||||||
const model = modelDefinitions.models[tank.id];
|
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||||
state.protagonist = tankToDuelMember(tank, model, provisionDefinitions);
|
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((state) => {
|
Tankopedia.mutate((state) => {
|
||||||
state.model = modelDefinitions.models[tank.id];
|
state.model = modelDefinitions.models[tank.id];
|
||||||
});
|
});
|
||||||
}, [tank]);
|
}, [tank]);
|
||||||
@@ -39,13 +36,13 @@ export function GuessBackground() {
|
|||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
style={{
|
style={{
|
||||||
transitionDuration: '2s',
|
transitionDuration: "2s",
|
||||||
backgroundColor: Var(
|
backgroundColor: Var(
|
||||||
guessState === null
|
guessState === GuessState.NotGuessed
|
||||||
? 'accent-2'
|
? "accent-2"
|
||||||
: guessState
|
: guessState === GuessState.Correct
|
||||||
? 'jade-3'
|
? "jade-3"
|
||||||
: 'tomato-3',
|
: "tomato-3"
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -53,7 +50,7 @@ export function GuessBackground() {
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
background: `radial-gradient(circle, ${Var('color-panel-translucent')}, ${Var('color-panel-solid')})`,
|
background: `radial-gradient(circle, ${Var("color-panel-translucent")}, ${Var("color-panel-solid")})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -67,13 +64,13 @@ export function GuessBackground() {
|
|||||||
left="0"
|
left="0"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `
|
backgroundImage: `
|
||||||
radial-gradient(circle, ${Var('white-a1')} 20%, transparent 20%),
|
radial-gradient(circle, ${Var("white-a1")} 20%, transparent 20%),
|
||||||
radial-gradient(circle at 0 0, ${Var('white-a1')} 10%, transparent 10%),
|
radial-gradient(circle at 0 0, ${Var("white-a1")} 10%, transparent 10%),
|
||||||
radial-gradient(circle at 100% 0, ${Var('white-a1')} 10%, transparent 10%),
|
radial-gradient(circle at 100% 0, ${Var("white-a1")} 10%, transparent 10%),
|
||||||
radial-gradient(circle at 0 100%, ${Var('white-a1')} 10%, transparent 10%),
|
radial-gradient(circle at 0 100%, ${Var("white-a1")} 10%, transparent 10%),
|
||||||
radial-gradient(circle at 100% 100%, ${Var('white-a1')} 10%, transparent 10%)
|
radial-gradient(circle at 100% 100%, ${Var("white-a1")} 10%, transparent 10%)
|
||||||
`,
|
`,
|
||||||
backgroundSize: '4rem 4rem',
|
backgroundSize: "4rem 4rem",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
import { Guess, GuessState } from "../stores/guess";
|
||||||
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
import { SmartCanvas } from "./SmartCanvas";
|
||||||
import { SmartCanvas } from './SmartCanvas';
|
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
|
||||||
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
import { TankModel } from "./Tankopedia/HeroSection/components/TankSandbox/components/TankModel";
|
||||||
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
|
||||||
|
|
||||||
const BRIGHTNESS = 2 ** -5;
|
const BRIGHTNESS = 2 ** -5.5;
|
||||||
const CONTRAST = 2 ** 2;
|
const CONTRAST = 2 ** 2;
|
||||||
|
|
||||||
export function GuessRenderer() {
|
export function GuessRenderer() {
|
||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const isRevealed = guessState !== null;
|
const isRevealed = guessState !== GuessState.NotGuessed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartCanvas
|
<SmartCanvas
|
||||||
style={{
|
style={{
|
||||||
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
||||||
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
transitionDuration: isRevealed ? "2s" : undefined,
|
||||||
transitionDuration: isRevealed ? '2s' : undefined,
|
transitionProperty: "filter",
|
||||||
transitionProperty: 'filter',
|
|
||||||
}}
|
}}
|
||||||
camera={{ position: [-5, 5, -5] }}
|
camera={{ position: [-5, 5, -5] }}
|
||||||
>
|
>
|
||||||
<Controls distanceScale={2} zoomable={false} />
|
<Controls zoomable={false} />
|
||||||
<Lighting display={TankopediaDisplay.Model} />
|
<Lighting />
|
||||||
<TankModel />
|
<TankModel />
|
||||||
</SmartCanvas>
|
</SmartCanvas>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import {
|
|||||||
SEARCH_KEYS,
|
SEARCH_KEYS,
|
||||||
TankDefinition,
|
TankDefinition,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from "@blitzkit/i18n";
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
PaperPlaneIcon,
|
PaperPlaneIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,16 +17,16 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from "fuzzysort";
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankNames } from '../core/awaitables/tankNames';
|
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||||
import { useLocale } from '../hooks/useLocale';
|
import { useLocale } from "../hooks/useLocale";
|
||||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
import { Guess, GuessState } from "../stores/guess";
|
||||||
import { classIcons } from './ClassIcon';
|
import { classIcons } from "./ClassIcon";
|
||||||
import { SearchResults } from './SearchResults';
|
import { SearchResults } from "./SearchResults";
|
||||||
|
|
||||||
const { go } = fuzzysort;
|
const { go } = fuzzysort;
|
||||||
|
|
||||||
@@ -38,16 +38,15 @@ const [tankNames, tankDefinitions] = await Promise.all([
|
|||||||
const ids = Object.keys(tankDefinitions.tanks);
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
|
|
||||||
export function Guesser() {
|
export function Guesser() {
|
||||||
const tank = GuessEphemeral.use((state) => state.tank);
|
const tank = Guess.use((state) => state.tank);
|
||||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const correctGuesses = GuessEphemeral.use((state) => state.correctGuesses);
|
const correctGuesses = Guess.use((state) => state.correctGuesses);
|
||||||
const totalGuesses = GuessEphemeral.use((state) => state.totalGuesses);
|
const totalGuesses = Guess.use((state) => state.totalGuesses);
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
||||||
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
||||||
const mutateGuessEphemeral = GuessEphemeral.useMutation();
|
|
||||||
|
|
||||||
const requestSearch = useCallback(() => {
|
const requestSearch = useCallback(() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
@@ -72,14 +71,14 @@ export function Guesser() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setResults(
|
setResults(
|
||||||
searchResults.map((result) => tankDefinitions.tanks[result.obj.id]),
|
searchResults.map((result) => tankDefinitions.tanks[result.obj.id])
|
||||||
);
|
);
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (guessState !== null) setSelected(null);
|
if (guessState !== GuessState.NotGuessed) setSelected(null);
|
||||||
}, [guessState]);
|
}, [guessState]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -88,7 +87,7 @@ export function Guesser() {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="0"
|
bottom="0"
|
||||||
left="50%"
|
left="50%"
|
||||||
style={{ transform: 'translateX(-50%)' }}
|
style={{ transform: "translateX(-50%)" }}
|
||||||
width="100%"
|
width="100%"
|
||||||
p="4"
|
p="4"
|
||||||
maxWidth="25rem"
|
maxWidth="25rem"
|
||||||
@@ -138,7 +137,7 @@ export function Guesser() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex justify="center">
|
<Flex justify="center" style={{ userSelect: "none" }}>
|
||||||
<Text>
|
<Text>
|
||||||
{literals(strings.website.tools.guess.stats, [
|
{literals(strings.website.tools.guess.stats, [
|
||||||
`${correctGuesses}`,
|
`${correctGuesses}`,
|
||||||
@@ -149,7 +148,7 @@ export function Guesser() {
|
|||||||
|
|
||||||
<Flex gap="3">
|
<Flex gap="3">
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
disabled={guessState !== null}
|
disabled={guessState !== GuessState.NotGuessed}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={requestSearch}
|
onChange={requestSearch}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -164,13 +163,19 @@ export function Guesser() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="3"
|
size="3"
|
||||||
color={guessState === null && selected === null ? 'red' : undefined}
|
color={
|
||||||
|
guessState === GuessState.NotGuessed && selected === null
|
||||||
|
? "red"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (guessState === null) {
|
if (guessState === GuessState.NotGuessed) {
|
||||||
const correct = selected !== null && selected.id === tank.id;
|
const correct = selected !== null && selected.id === tank.id;
|
||||||
|
|
||||||
mutateGuessEphemeral((draft) => {
|
Guess.mutate((draft) => {
|
||||||
draft.guessState = correct;
|
draft.guessState = correct
|
||||||
|
? GuessState.Correct
|
||||||
|
: GuessState.Incorrect;
|
||||||
draft.totalGuesses++;
|
draft.totalGuesses++;
|
||||||
draft.correctGuesses += correct ? 1 : 0;
|
draft.correctGuesses += correct ? 1 : 0;
|
||||||
});
|
});
|
||||||
@@ -178,21 +183,22 @@ export function Guesser() {
|
|||||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||||
const tank = tankDefinitions.tanks[id];
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
mutateGuessEphemeral((draft) => {
|
Guess.mutate((draft) => {
|
||||||
if (!input.current) return;
|
if (!input.current) return;
|
||||||
|
|
||||||
draft.tank = tank;
|
draft.tank = tank;
|
||||||
draft.guessState = null;
|
draft.guessState = GuessState.NotGuessed;
|
||||||
input.current.value = '';
|
|
||||||
|
input.current.value = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{guessState === null ? (
|
{guessState === GuessState.NotGuessed ? (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
strings.website.tools.guess.search[
|
strings.website.tools.guess.search[
|
||||||
selected === null ? 'skip' : 'guess'
|
selected === null ? "skip" : "guess"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
{selected === null ? <ArrowRightIcon /> : <PaperPlaneIcon />}
|
{selected === null ? <ArrowRightIcon /> : <PaperPlaneIcon />}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { SpotLightProps } from '@react-three/fiber';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
interface Props extends SpotLightProps {
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{debug && (
|
||||||
|
<mesh position={props.position}>
|
||||||
|
<icosahedronGeometry args={[0.1, 2]} />
|
||||||
|
<meshBasicMaterial color={props.color} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<spotLight {...props} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { EyeOpenIcon } from '@radix-ui/react-icons';
|
import { EyeOpenIcon } from "@radix-ui/react-icons";
|
||||||
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
|
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
|
||||||
import { awaitablePopularTanks } from '../core/awaitables/popularTanks';
|
import { awaitablePopularTanks } from "../core/awaitables/popularTanks";
|
||||||
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
useLocale,
|
useLocale,
|
||||||
type LocaleAcceptorProps,
|
type LocaleAcceptorProps,
|
||||||
} from '../hooks/useLocale';
|
} from "../hooks/useLocale";
|
||||||
import { TankopediaPersistent } from '../stores/tankopediaPersistent';
|
import { TankCard } from "./TankCard";
|
||||||
import { TankCard } from './TankCard';
|
|
||||||
|
|
||||||
const [tankDefinitions, popularTanks] = await Promise.all([
|
const [tankDefinitions, popularTanks] = await Promise.all([
|
||||||
awaitableTankDefinitions,
|
awaitableTankDefinitions,
|
||||||
@@ -19,9 +18,7 @@ const [tankDefinitions, popularTanks] = await Promise.all([
|
|||||||
export function HomePageHotTanks({ locale }: LocaleAcceptorProps) {
|
export function HomePageHotTanks({ locale }: LocaleAcceptorProps) {
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<LocaleProvider locale={locale}>
|
||||||
<TankopediaPersistent.Provider>
|
<Content />
|
||||||
<Content />
|
|
||||||
</TankopediaPersistent.Provider>
|
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,7 +32,7 @@ function Content() {
|
|||||||
{strings.website.home.hot_tanks.title}
|
{strings.website.home.hot_tanks.title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text color="gray" align="center" mb="5" size="2">
|
<Text color="gray" align="center" mb="5" size="2">
|
||||||
{literals(strings.website.home.hot_tanks.subtitle, ['7'])}
|
{literals(strings.website.home.hot_tanks.subtitle, ["7"])}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Flex justify="center" gap="4" wrap="wrap">
|
<Flex justify="center" gap="4" wrap="wrap">
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||||
|
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||||
|
import { useModel } from "../../../hooks/useModel";
|
||||||
|
import { Mixer } from "../../../stores/mixer";
|
||||||
|
|
||||||
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
|
export function Model() {
|
||||||
|
const hull = Mixer.use((state) => state.hull);
|
||||||
|
const turret = Mixer.use((state) => state.turret);
|
||||||
|
const gun = Mixer.use((state) => state.gun);
|
||||||
|
|
||||||
|
const hullGltf = useModel(hull.id);
|
||||||
|
const turretGltf = useModel(turret.tank.id);
|
||||||
|
const gunGltf = useModel(gun.tank.id);
|
||||||
|
|
||||||
|
const hullNodes = Object.values(hullGltf.gltf.nodes);
|
||||||
|
const turretNodes = Object.values(turretGltf.gltf.nodes);
|
||||||
|
const gunNodes = Object.values(gunGltf.gltf.nodes);
|
||||||
|
|
||||||
|
const tankModel = modelDefinitions.models[hull.id];
|
||||||
|
const trackModel = tankModel.tracks[hull.tracks.at(-1)!.id];
|
||||||
|
|
||||||
|
const turretTankModel = modelDefinitions.models[turret.tank.id];
|
||||||
|
const turretTrackModel =
|
||||||
|
turretTankModel.tracks[turret.tank.tracks.at(-1)!.id];
|
||||||
|
const turretModel = turretTankModel.turrets[turret.turret.id];
|
||||||
|
|
||||||
|
const gunTankModel = modelDefinitions.models[gun.tank.id];
|
||||||
|
const gunTrackModel = gunTankModel.tracks[gun.tank.tracks.at(-1)!.id];
|
||||||
|
const gunTurretModel = gunTankModel.turrets[gun.turret.id];
|
||||||
|
const gunModel = gunTurretModel.guns[gun.gun.id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group rotation={[-Math.PI / 2, 0, 0]}>
|
||||||
|
{hullNodes.map((node) => {
|
||||||
|
const isHull = node.name === "hull";
|
||||||
|
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||||
|
const isTrack = node.name.startsWith("chassis_track_");
|
||||||
|
const isVisible = isHull || isWheel || isTrack;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(_, props, key) {
|
||||||
|
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
tankModel.turret_origin.x +
|
||||||
|
trackModel.origin.x -
|
||||||
|
turretTankModel.turret_origin.x -
|
||||||
|
turretTrackModel.origin.x,
|
||||||
|
tankModel.turret_origin.z +
|
||||||
|
trackModel.origin.z -
|
||||||
|
turretTankModel.turret_origin.z -
|
||||||
|
turretTrackModel.origin.z,
|
||||||
|
tankModel.turret_origin.y +
|
||||||
|
trackModel.origin.y -
|
||||||
|
turretTankModel.turret_origin.y -
|
||||||
|
turretTrackModel.origin.y,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{turretNodes.map((node) => {
|
||||||
|
const isCurrentTurret =
|
||||||
|
node.name ===
|
||||||
|
`turret_${turretModel.model_id.toString().padStart(2, "0")}`;
|
||||||
|
const isVisible = isCurrentTurret;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(_, props, key) {
|
||||||
|
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group
|
||||||
|
position={[
|
||||||
|
turretTankModel.turret_origin.x +
|
||||||
|
turretTrackModel.origin.x +
|
||||||
|
turretModel.gun_origin.x -
|
||||||
|
gunTankModel.turret_origin.x -
|
||||||
|
gunTrackModel.origin.x -
|
||||||
|
gunTurretModel.gun_origin.x,
|
||||||
|
turretTankModel.turret_origin.z +
|
||||||
|
turretTrackModel.origin.z +
|
||||||
|
turretModel.gun_origin.z -
|
||||||
|
gunTankModel.turret_origin.z -
|
||||||
|
gunTrackModel.origin.z -
|
||||||
|
gunTurretModel.gun_origin.z,
|
||||||
|
turretTankModel.turret_origin.y +
|
||||||
|
turretTrackModel.origin.y +
|
||||||
|
turretModel.gun_origin.y -
|
||||||
|
gunTankModel.turret_origin.y -
|
||||||
|
gunTrackModel.origin.y -
|
||||||
|
gunTurretModel.gun_origin.y,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{gunNodes.map((node) => {
|
||||||
|
const isCurrentMantlet =
|
||||||
|
node.name ===
|
||||||
|
`gun_${gunModel.model_id.toString().padStart(2, "0")}_mask`;
|
||||||
|
const isCurrentGun =
|
||||||
|
node.name ===
|
||||||
|
`gun_${gunModel.model_id.toString().padStart(2, "0")}`;
|
||||||
|
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return jsxTree(node, {
|
||||||
|
mesh(_, props, key) {
|
||||||
|
return <mesh {...props} key={key} castShadow receiveShadow />;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { OrbitControls } from "@react-three/drei";
|
||||||
|
import { Canvas } from "@react-three/fiber";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import { Lighting } from "../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||||
|
import { Model } from "./components/Model";
|
||||||
|
|
||||||
|
export function MixerScene() {
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
shadows="soft"
|
||||||
|
camera={{
|
||||||
|
fov: 45,
|
||||||
|
position: [5, 5, -5],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OrbitControls />
|
||||||
|
|
||||||
|
<Lighting />
|
||||||
|
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<Model />
|
||||||
|
</Suspense>
|
||||||
|
</Canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PersonIcon } from '@radix-ui/react-icons';
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -6,91 +6,78 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from "react";
|
||||||
import { App } from '../../stores/app';
|
import { App } from "../../stores/app";
|
||||||
import { BlitzKitTheme } from '../BlitzKitTheme';
|
import { BlitzKitTheme } from "../BlitzKitTheme";
|
||||||
import { WargamingAccountName } from '../WargamingAccountName';
|
import { WargamingAccountName } from "../WargamingAccountName";
|
||||||
import { WargamingIcon } from '../WargamingIcon';
|
import { WargamingIcon } from "../WargamingIcon";
|
||||||
import { WargamingLoginButton } from '../WargamingLoginButton';
|
import { WargamingLoginButton } from "../WargamingLoginButton";
|
||||||
|
|
||||||
export function NavbarAccount() {
|
export function NavbarAccount() {
|
||||||
return (
|
|
||||||
<App.Provider>
|
|
||||||
<Content />
|
|
||||||
</App.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Content() {
|
|
||||||
const logins = App.use((state) => state.logins);
|
const logins = App.use((state) => state.logins);
|
||||||
const mutateApp = App.useMutation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<App.Provider>
|
<Popover.Root>
|
||||||
<Popover.Root>
|
<Popover.Trigger>
|
||||||
<Popover.Trigger>
|
<IconButton variant="ghost" color="gray">
|
||||||
<IconButton variant="ghost" color="gray">
|
<PersonIcon />
|
||||||
<PersonIcon />
|
</IconButton>
|
||||||
</IconButton>
|
</Popover.Trigger>
|
||||||
</Popover.Trigger>
|
|
||||||
|
|
||||||
<BlitzKitTheme style={{ display: 'contents' }}>
|
<BlitzKitTheme style={{ display: "contents" }}>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
align="end"
|
align="end"
|
||||||
width="320px"
|
width="320px"
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
{logins.wargaming && (
|
{logins.wargaming && (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Text align="center" color="gray">
|
<Text align="center" color="gray">
|
||||||
Logged in accounts
|
Logged in accounts
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{logins.wargaming && (
|
{logins.wargaming && (
|
||||||
<Flex align="center" gap="2" justify="center">
|
<Flex align="center" gap="2" justify="center">
|
||||||
<WargamingIcon width={15} height={15} />
|
<WargamingIcon width={15} height={15} />
|
||||||
<Text>
|
<Text>
|
||||||
<Suspense
|
<Suspense fallback={<Skeleton height="1em" width="5rem" />}>
|
||||||
fallback={<Skeleton height="1em" width="5rem" />}
|
<WargamingAccountName />
|
||||||
>
|
</Suspense>
|
||||||
<WargamingAccountName />
|
</Text>
|
||||||
</Suspense>
|
<Button
|
||||||
</Text>
|
color="red"
|
||||||
<Button
|
variant="ghost"
|
||||||
color="red"
|
onClick={() => {
|
||||||
variant="ghost"
|
App.mutate((draft) => {
|
||||||
onClick={() => {
|
draft.logins.wargaming = undefined;
|
||||||
mutateApp((draft) => {
|
});
|
||||||
draft.logins.wargaming = undefined;
|
}}
|
||||||
});
|
>
|
||||||
}}
|
Logout
|
||||||
>
|
</Button>
|
||||||
Logout
|
</Flex>
|
||||||
</Button>
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* {!logins. !== !logins.wargaming && (
|
{/* {!logins. !== !logins.wargaming && (
|
||||||
<Inset side="x">
|
<Inset side="x">
|
||||||
<Separator my="4" size="4" />
|
<Separator my="4" size="4" />
|
||||||
</Inset>
|
</Inset>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{!logins.wargaming && (
|
{!logins.wargaming && (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Text align="center" color="gray">
|
<Text align="center" color="gray">
|
||||||
Log in with...
|
Log in with...
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{!logins.wargaming && <WargamingLoginButton />}
|
{!logins.wargaming && <WargamingLoginButton />}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</BlitzKitTheme>
|
</BlitzKitTheme>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
</App.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,15 @@ import {
|
|||||||
CaretDownIcon,
|
CaretDownIcon,
|
||||||
CaretSortIcon,
|
CaretSortIcon,
|
||||||
CaretUpIcon,
|
CaretUpIcon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
import { Button, Flex, Table } from '@radix-ui/themes';
|
import { Button, Flex, Table } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||||
import { TankPerformanceSortTypeNamesArray } from '../../stores/tankPerformanceSort/constants';
|
import { TankPerformanceSortTypeNamesArray } from "../../stores/tankPerformanceSort/constants";
|
||||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const sort = TankPerformanceSort.use();
|
const sort = TankPerformanceSort.use();
|
||||||
const store = TankPerformanceSort.useStore();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -34,13 +33,18 @@ export function Header() {
|
|||||||
<Flex align="center" gap="1">
|
<Flex align="center" gap="1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color={isSelected ? undefined : 'gray'}
|
color={isSelected ? undefined : "gray"}
|
||||||
highContrast={!isSelected}
|
highContrast={!isSelected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
store.setState({ direction: -sort.direction as 1 | -1 });
|
TankPerformanceSort.mutate((draft) => {
|
||||||
|
draft.direction *= -1;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
store.setState({ type, direction: -1 });
|
TankPerformanceSort.mutate((draft) => {
|
||||||
|
draft.type = type;
|
||||||
|
draft.direction = -1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { Flex, Select, Text } from '@radix-ui/themes';
|
import { Flex, Select, Text } from '@radix-ui/themes';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import {
|
import { Performance, type PlayerCountPeriod } from '../../stores/performance';
|
||||||
TankPerformanceEphemeral,
|
|
||||||
type PlayerCountPeriod,
|
|
||||||
} from '../../stores/tankPerformanceEphemeral';
|
|
||||||
|
|
||||||
export function PlayerCountControl() {
|
export function PlayerCountControl() {
|
||||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
|
||||||
(state) => state.playerCountPeriod,
|
|
||||||
);
|
|
||||||
const mutateTankPerformanceSort = TankPerformanceEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,7 +12,7 @@ export function PlayerCountControl() {
|
|||||||
<Select.Root
|
<Select.Root
|
||||||
value={playerCountPeriod}
|
value={playerCountPeriod}
|
||||||
onValueChange={(period) => {
|
onValueChange={(period) => {
|
||||||
mutateTankPerformanceSort((draft) => {
|
Performance.mutate((draft) => {
|
||||||
draft.playerCountPeriod = period as PlayerCountPeriod;
|
draft.playerCountPeriod = period as PlayerCountPeriod;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
|||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
import { Performance } from '../../stores/performance';
|
||||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
||||||
|
|
||||||
interface TankRowProps {
|
interface TankRowProps {
|
||||||
@@ -17,7 +17,7 @@ export const TankRow = memo<TankRowProps>(
|
|||||||
({ tank }) => {
|
({ tank }) => {
|
||||||
const averages = averageDefinitions.averages[tank.id];
|
const averages = averageDefinitions.averages[tank.id];
|
||||||
const ratio = useAveragesExclusionRatio();
|
const ratio = useAveragesExclusionRatio();
|
||||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
const playerCountPeriod = Performance.use(
|
||||||
(state) => state.playerCountPeriod,
|
(state) => state.playerCountPeriod,
|
||||||
);
|
);
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { Table } from "@radix-ui/themes";
|
||||||
import { Table } from '@radix-ui/themes';
|
import { times } from "lodash-es";
|
||||||
import { times } from 'lodash-es';
|
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
|
||||||
import { Fragment, Suspense, useEffect, useMemo, useState } from 'react';
|
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
import { Performance } from "../../stores/performance";
|
||||||
import { $tankFilters } from '../../stores/tankFilters';
|
import { TankFilters } from "../../stores/tankFilters";
|
||||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import { RowLoader } from "./RowLoader";
|
||||||
import { RowLoader } from './RowLoader';
|
import { TankRow } from "./TankRow";
|
||||||
import { TankRow } from './TankRow';
|
import { Total } from "./Total";
|
||||||
import { Total } from './Total';
|
|
||||||
|
|
||||||
const PREVIEW_COUNT = 10;
|
const PREVIEW_COUNT = 10;
|
||||||
const DEFAULT_LOADED_ROWS = 25;
|
const DEFAULT_LOADED_ROWS = 25;
|
||||||
@@ -25,7 +24,7 @@ const averageDefinitionsArray = Object.entries(averageDefinitions.averages).map(
|
|||||||
([id, average]) => ({
|
([id, average]) => ({
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
...average,
|
...average,
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
@@ -40,98 +39,97 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sort = TankPerformanceSort.use();
|
const sort = TankPerformanceSort.use();
|
||||||
const tankPerformancePersistentStore = TankPerformanceEphemeral.useStore();
|
|
||||||
const filters = useStore($tankFilters);
|
|
||||||
const tanksSorted = useMemo(() => {
|
const tanksSorted = useMemo(() => {
|
||||||
const { playerCountPeriod } = tankPerformancePersistentStore.getState();
|
const { playerCountPeriod } = Performance.state;
|
||||||
|
|
||||||
switch (sort.type) {
|
switch (sort.type) {
|
||||||
case 'accuracy':
|
case "accuracy":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots),
|
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots)
|
||||||
);
|
);
|
||||||
case 'capture_points':
|
case "capture_points":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.capture_points / a.mu.battles -
|
(a.mu.capture_points / a.mu.battles -
|
||||||
b.mu.capture_points / b.mu.battles),
|
b.mu.capture_points / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'damage':
|
case "damage":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.damage_dealt / a.mu.battles -
|
(a.mu.damage_dealt / a.mu.battles -
|
||||||
b.mu.damage_dealt / b.mu.battles),
|
b.mu.damage_dealt / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'damage_ratio':
|
case "damage_ratio":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.damage_dealt / a.mu.damage_received -
|
(a.mu.damage_dealt / a.mu.damage_received -
|
||||||
b.mu.damage_dealt / b.mu.damage_received),
|
b.mu.damage_dealt / b.mu.damage_received)
|
||||||
);
|
);
|
||||||
case 'damage_taken':
|
case "damage_taken":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.damage_received / a.mu.battles -
|
(a.mu.damage_received / a.mu.battles -
|
||||||
b.mu.damage_received / b.mu.battles),
|
b.mu.damage_received / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'hits':
|
case "hits":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles),
|
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'kills':
|
case "kills":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles),
|
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'shots':
|
case "shots":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles),
|
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'players':
|
case "players":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod]),
|
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
|
||||||
);
|
);
|
||||||
case 'spots':
|
case "spots":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles),
|
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'survival':
|
case "survival":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.survived_battles / a.mu.battles -
|
(a.mu.survived_battles / a.mu.battles -
|
||||||
b.mu.survived_battles / b.mu.battles),
|
b.mu.survived_battles / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'winrate':
|
case "winrate":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction *
|
sort.direction *
|
||||||
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles),
|
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles)
|
||||||
);
|
);
|
||||||
case 'xp':
|
case "xp":
|
||||||
return averageDefinitionsArray.sort(
|
return averageDefinitionsArray.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles),
|
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [sort]);
|
}, [sort]);
|
||||||
|
const filters = TankFilters.use();
|
||||||
const tanks = useMemo(() => {
|
const tanks = useMemo(() => {
|
||||||
return tanksSorted.filter((tank) =>
|
return tanksSorted.filter((tank) =>
|
||||||
filterTank(filters, tankDefinitions.tanks[tank.id]),
|
filterTank(filters, tankDefinitions.tanks[tank.id])
|
||||||
);
|
);
|
||||||
}, [filters, sort]);
|
}, [filters, sort]);
|
||||||
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { memo, useCallback } from 'react';
|
|||||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
import { Performance } from '../../stores/performance';
|
||||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||||
|
|
||||||
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
||||||
@@ -23,7 +23,7 @@ const averageDefinitions = await awaitableAverageDefinitions;
|
|||||||
|
|
||||||
export const Total = memo<TotalProps>(
|
export const Total = memo<TotalProps>(
|
||||||
({ tanks }) => {
|
({ tanks }) => {
|
||||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
const playerCountPeriod = Performance.use(
|
||||||
(state) => state.playerCountPeriod,
|
(state) => state.playerCountPeriod,
|
||||||
);
|
);
|
||||||
const { strings, locale } = useLocale();
|
const { strings, locale } = useLocale();
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { assertSecret } from '@blitzkit/core';
|
import { assertSecret } from "@blitzkit/core";
|
||||||
import { ArrowRightIcon } from '@radix-ui/react-icons';
|
import { ArrowRightIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Code, Flex, Link, Text } from '@radix-ui/themes';
|
import { Button, Code, Flex, Link, Text } from "@radix-ui/themes";
|
||||||
import { App } from '../stores/app';
|
|
||||||
|
|
||||||
export function Plugs() {
|
export function Plugs() {
|
||||||
return (
|
|
||||||
<App.Provider>
|
|
||||||
<Content />
|
|
||||||
</App.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Content() {
|
|
||||||
const promoteOpentest =
|
const promoteOpentest =
|
||||||
assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) === 'true' &&
|
assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) === "true" &&
|
||||||
assertSecret(import.meta.env.PUBLIC_BRANCH) !== 'opentest';
|
assertSecret(import.meta.env.PUBLIC_BRANCH) !== "opentest";
|
||||||
const promotions = [promoteOpentest];
|
const promotions = [promoteOpentest];
|
||||||
|
|
||||||
if (promotions.every((promotion) => !promotion)) return null;
|
if (promotions.every((promotion) => !promotion)) return null;
|
||||||
@@ -31,11 +22,11 @@ function Content() {
|
|||||||
{promoteOpentest && (
|
{promoteOpentest && (
|
||||||
<Flex direction="column" gap="3" align="center">
|
<Flex direction="column" gap="3" align="center">
|
||||||
<Text size="5">
|
<Text size="5">
|
||||||
BlitzKit{' '}
|
BlitzKit{" "}
|
||||||
<Code variant="outline" size="4" color="gray">
|
<Code variant="outline" size="4" color="gray">
|
||||||
opentest
|
opentest
|
||||||
</Code>{' '}
|
</Code>{" "}
|
||||||
is available now{' '}
|
is available now{" "}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Link href="https://opentest.blitzkit.app/" target="_blank">
|
<Link href="https://opentest.blitzkit.app/" target="_blank">
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import {
|
|||||||
fetchTankDefinitions,
|
fetchTankDefinitions,
|
||||||
TankType,
|
TankType,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { Flex, Text, type TextProps } from '@radix-ui/themes';
|
import { Flex, Text, type TextProps } from "@radix-ui/themes";
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from "lodash-es";
|
||||||
import { forwardRef, type ReactNode } from 'react';
|
import { forwardRef, type ReactNode } from "react";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||||
import { classIcons } from '../ClassIcon';
|
import { classIcons } from "../ClassIcon";
|
||||||
import { LinkI18n } from '../LinkI18n';
|
import { LinkI18n } from "../LinkI18n";
|
||||||
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants';
|
import { MAX_RECENTLY_VIEWED } from "../TankSearch/constants";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
type TankCardProps = TextProps & {
|
type TankCardProps = TextProps & {
|
||||||
tank: TankDefinition;
|
tank: TankDefinition;
|
||||||
@@ -33,11 +33,11 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
|||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
}: TankCardProps,
|
}: TankCardProps,
|
||||||
ref,
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { unwrap, locale } = useLocale();
|
const { unwrap, locale } = useLocale();
|
||||||
const provideLink = !noLink && onSelect === undefined;
|
const provideLink = !noLink && onSelect === undefined;
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
|
||||||
const Icon = classIcons[tank.class];
|
const Icon = classIcons[tank.class];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,15 +47,15 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
|||||||
size="2"
|
size="2"
|
||||||
color={
|
color={
|
||||||
tank.type === TankType.COLLECTOR
|
tank.type === TankType.COLLECTOR
|
||||||
? 'blue'
|
? "blue"
|
||||||
: tank.type === TankType.PREMIUM
|
: tank.type === TankType.PREMIUM
|
||||||
? 'amber'
|
? "amber"
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast={tank.type === TankType.RESEARCHABLE}
|
highContrast={tank.type === TankType.RESEARCHABLE}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelect?.(tank);
|
onSelect?.(tank);
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
|
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
|
||||||
.filter((id) => id in tankDefinitions.tanks)
|
.filter((id) => id in tankDefinitions.tanks)
|
||||||
.slice(0, MAX_RECENTLY_VIEWED);
|
.slice(0, MAX_RECENTLY_VIEWED);
|
||||||
@@ -73,7 +73,7 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
|||||||
locale={locale}
|
locale={locale}
|
||||||
className="link"
|
className="link"
|
||||||
underline="hover"
|
underline="hover"
|
||||||
href={provideLink ? `/tanks/${tank.slug}` : '#'}
|
href={provideLink ? `/tanks/${tank.slug}` : "#"}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (!provideLink) event.preventDefault();
|
if (!provideLink) event.preventDefault();
|
||||||
}}
|
}}
|
||||||
@@ -104,9 +104,9 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
|||||||
color="gray"
|
color="gray"
|
||||||
align="center"
|
align="center"
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: "nowrap",
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: "ellipsis",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{discriminator}
|
{discriminator}
|
||||||
@@ -115,5 +115,5 @@ export const TankCard = forwardRef<HTMLSpanElement, TankCardProps>(
|
|||||||
</LinkI18n>
|
</LinkI18n>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
@@ -4,11 +4,10 @@ import {
|
|||||||
unionBoundingBox,
|
unionBoundingBox,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||||
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
import { TankSort } from '../../../stores/tankopediaSort';
|
||||||
import { TankCard } from '../../TankCard';
|
import { TankCard } from '../../TankCard';
|
||||||
|
|
||||||
interface TankSearchCardProps {
|
interface TankSearchCardProps {
|
||||||
@@ -19,9 +18,9 @@ interface TankSearchCardProps {
|
|||||||
const modelDefinitions = await awaitableModelDefinitions;
|
const modelDefinitions = await awaitableModelDefinitions;
|
||||||
|
|
||||||
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const by = TankSort.use((state) => state.by);
|
||||||
const discriminator = useMemo(() => {
|
const discriminator = useMemo(() => {
|
||||||
if (tankopediaSort.by.startsWith('meta')) return undefined;
|
if (by.startsWith('meta')) return undefined;
|
||||||
|
|
||||||
const turret = tank.turrets.at(-1)!;
|
const turret = tank.turrets.at(-1)!;
|
||||||
const gun = turret.guns.at(-1)!;
|
const gun = turret.guns.at(-1)!;
|
||||||
@@ -34,7 +33,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
|
|
||||||
switch (tankopediaSort.by) {
|
switch (by) {
|
||||||
case 'fire.aimTime':
|
case 'fire.aimTime':
|
||||||
return gun.aim_time.toFixed(2);
|
return gun.aim_time.toFixed(2);
|
||||||
case 'fire.caliber':
|
case 'fire.caliber':
|
||||||
@@ -133,7 +132,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
|||||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [tankopediaSort.by]);
|
}, [by]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TankCard
|
<TankCard
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import {
|
|||||||
TANK_CLASSES,
|
TANK_CLASSES,
|
||||||
TankType,
|
TankType,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -14,29 +13,22 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
Popover,
|
Popover,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { isEqual, times } from 'lodash-es';
|
import { isEqual, times } from "lodash-es";
|
||||||
import { awaitableGameDefinitions } from '../../../core/awaitables/gameDefinitions';
|
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
|
||||||
import { Var } from '../../../core/radix/var';
|
import { Var } from "../../../core/radix/var";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import {
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
$tankFilters,
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
initialTankFilters,
|
import { classIcons } from "../../ClassIcon";
|
||||||
type TankFilters,
|
import { ExperimentIcon } from "../../ExperimentIcon";
|
||||||
} from '../../../stores/tankFilters';
|
import { GunAutoloaderIcon } from "../../GunAutoloaderIcon";
|
||||||
import {
|
import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon";
|
||||||
$tankopediaSort,
|
import { GunRegularIcon } from "../../GunRegularIcon";
|
||||||
initialTankopediaSort,
|
import { MissingShellIcon } from "../../MissingShellIcon";
|
||||||
} from '../../../stores/tankopediaSort';
|
import { ResearchedIcon } from "../../ResearchedIcon";
|
||||||
import { classIcons } from '../../ClassIcon';
|
import { ScienceIcon } from "../../ScienceIcon";
|
||||||
import { ExperimentIcon } from '../../ExperimentIcon';
|
import { ScienceOffIcon } from "../../ScienceOffIcon";
|
||||||
import { GunAutoloaderIcon } from '../../GunAutoloaderIcon';
|
|
||||||
import { GunAutoreloaderIcon } from '../../GunAutoreloaderIcon';
|
|
||||||
import { GunRegularIcon } from '../../GunRegularIcon';
|
|
||||||
import { MissingShellIcon } from '../../MissingShellIcon';
|
|
||||||
import { ResearchedIcon } from '../../ResearchedIcon';
|
|
||||||
import { ScienceIcon } from '../../ScienceIcon';
|
|
||||||
import { ScienceOffIcon } from '../../ScienceOffIcon';
|
|
||||||
|
|
||||||
interface FilterControlProps {
|
interface FilterControlProps {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
@@ -45,29 +37,29 @@ interface FilterControlProps {
|
|||||||
const gameDefinitions = await awaitableGameDefinitions;
|
const gameDefinitions = await awaitableGameDefinitions;
|
||||||
|
|
||||||
const shellTypeIcons: Record<ShellType, string> = {
|
const shellTypeIcons: Record<ShellType, string> = {
|
||||||
[ShellType.AP]: 'ap',
|
[ShellType.AP]: "ap",
|
||||||
[ShellType.APCR]: 'ap_cr',
|
[ShellType.APCR]: "ap_cr",
|
||||||
[ShellType.HE]: 'he',
|
[ShellType.HE]: "he",
|
||||||
[ShellType.HEAT]: 'hc',
|
[ShellType.HEAT]: "hc",
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||||
const tankFilters = useStore($tankFilters);
|
const shells = TankFilters.use((state) => state.shells);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
||||||
{tankFilters.shells[index] === null && (
|
{shells[index] === null && (
|
||||||
<Text color="gray" style={{ display: 'contents' }}>
|
<Text color="gray" style={{ display: "contents" }}>
|
||||||
<MissingShellIcon width="1em" height="1em" />
|
<MissingShellIcon width="1em" height="1em" />
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{tankFilters.shells[index] !== null && (
|
{shells[index] !== null && (
|
||||||
<img
|
<img
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(
|
src={asset(
|
||||||
`icons/shells/${shellTypeIcons[tankFilters.shells[index]]}${premium ? '_premium' : ''}.webp`,
|
`icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -78,35 +70,33 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
<Inset>
|
<Inset>
|
||||||
<Flex>
|
<Flex>
|
||||||
{Object.values(ShellType).map((shellType) => {
|
{Object.values(ShellType).map((shellType) => {
|
||||||
if (typeof shellType === 'string') return null;
|
if (typeof shellType === "string") return null;
|
||||||
|
|
||||||
const selected = tankFilters.shells[index] === shellType;
|
const selected = shells[index] === shellType;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={shellType}
|
key={shellType}
|
||||||
value={`${shellType}`}
|
value={`${shellType}`}
|
||||||
radius="none"
|
radius="none"
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const mutated = [
|
const mutated = [...shells] as TankFilters["shells"];
|
||||||
tankFilters.shells[0],
|
|
||||||
tankFilters.shells[1],
|
|
||||||
tankFilters.shells[2],
|
|
||||||
] as TankFilters['shells'];
|
|
||||||
|
|
||||||
mutated[index] = selected ? null : shellType;
|
mutated[index] = selected ? null : shellType;
|
||||||
|
|
||||||
$tankFilters.setKey('shells', mutated);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.shells = mutated;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
highContrast={selected}
|
highContrast={selected}
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={asset(
|
src={asset(
|
||||||
`icons/shells/${shellTypeIcons[shellType]}${premium ? '_premium' : ''}.webp`,
|
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp`
|
||||||
)}
|
)}
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
@@ -119,26 +109,26 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FilterControl({ compact }: FilterControlProps) {
|
export function FilterControl({ compact }: FilterControlProps) {
|
||||||
const tankFilters = useStore($tankFilters);
|
const tankFilters = TankFilters.use();
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const clearable = !isEqual(tankFilters, initialTankFilters);
|
const clearable = !isEqual(tankFilters, TankFilters.initial);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" align="center">
|
<Flex direction="column" align="center">
|
||||||
<Flex
|
<Flex
|
||||||
height="fit-content"
|
height="fit-content"
|
||||||
gap="2"
|
gap="2"
|
||||||
align={{ initial: 'start', md: 'center' }}
|
align={{ initial: "start", md: "center" }}
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
flexShrink="0"
|
flexShrink="0"
|
||||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-5)' }}
|
style={{ borderRadius: "var(--radius-5)" }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{times(5, (index) => {
|
{times(5, (index) => {
|
||||||
const tier = index + 1;
|
const tier = index + 1;
|
||||||
@@ -147,21 +137,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.tiers.includes(tier)) {
|
if (tankFilters.tiers.includes(tier)) {
|
||||||
$tankFilters.setKey(
|
TankFilters.mutate((draft) => {
|
||||||
'tiers',
|
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||||
tankFilters.tiers.filter((t) => t !== tier),
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('tiers', [
|
TankFilters.mutate((draft) => {
|
||||||
...tankFilters.tiers,
|
draft.tiers = [...draft.tiers, tier];
|
||||||
tier,
|
});
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -172,15 +160,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display={{ initial: 'none', md: 'block' }}
|
display={{ initial: "none", md: "block" }}
|
||||||
style={{
|
style={{
|
||||||
height: Var('space-1'),
|
height: Var("space-1"),
|
||||||
backgroundColor: Var('gray-a3'),
|
backgroundColor: Var("gray-a3"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{times(5, (index) => {
|
{times(5, (index) => {
|
||||||
const tier = index + 6;
|
const tier = index + 6;
|
||||||
@@ -189,21 +177,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.tiers.includes(tier)) {
|
if (tankFilters.tiers.includes(tier)) {
|
||||||
$tankFilters.setKey(
|
TankFilters.mutate((draft) => {
|
||||||
'tiers',
|
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||||
tankFilters.tiers.filter((t) => t !== tier),
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('tiers', [
|
TankFilters.mutate((draft) => {
|
||||||
...tankFilters.tiers,
|
draft.tiers = [...draft.tiers, tier];
|
||||||
tier,
|
});
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -216,12 +202,12 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
flexShrink="0"
|
flexShrink="0"
|
||||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-5)' }}
|
style={{ borderRadius: "var(--radius-5)" }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{gameDefinitions.nations
|
{gameDefinitions.nations
|
||||||
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
||||||
@@ -231,26 +217,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={nation}
|
key={nation}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.nations.includes(nation)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.nations.includes(nation)) {
|
||||||
'nations',
|
draft.nations = draft.nations.filter(
|
||||||
tankFilters.nations.filter((n) => n !== nation),
|
(n) => n !== nation
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('nations', [
|
draft.nations = [...draft.nations, nation];
|
||||||
...tankFilters.nations,
|
}
|
||||||
nation,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(`flags/circle/${nation}.webp`)}
|
src={asset(`flags/circle/${nation}.webp`)}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -259,15 +243,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display={{ initial: 'none', md: 'block' }}
|
display={{ initial: "none", md: "block" }}
|
||||||
style={{
|
style={{
|
||||||
height: Var('space-1'),
|
height: Var("space-1"),
|
||||||
backgroundColor: Var('gray-a3'),
|
backgroundColor: Var("gray-a3"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{gameDefinitions.nations
|
{gameDefinitions.nations
|
||||||
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
||||||
@@ -278,26 +262,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
key={nation}
|
key={nation}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.nations.includes(nation)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.nations.includes(nation)) {
|
||||||
'nations',
|
draft.nations = draft.nations.filter(
|
||||||
tankFilters.nations.filter((n) => n !== nation),
|
(n) => n !== nation
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('nations', [
|
draft.nations = [...draft.nations, nation];
|
||||||
...tankFilters.nations,
|
}
|
||||||
nation,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
style={{ width: '1em', height: '1em' }}
|
style={{ width: "1em", height: "1em" }}
|
||||||
src={asset(`flags/circle/${nation}.webp`)}
|
src={asset(`flags/circle/${nation}.webp`)}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -307,15 +289,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={{ initial: 'row', md: 'column' }}
|
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||||
align="start"
|
align="start"
|
||||||
gap={{ initial: '2', md: '1' }}
|
gap={{ initial: "2", md: "1" }}
|
||||||
>
|
>
|
||||||
<Flex gap="2" align="start">
|
<Flex gap="2" align="start">
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
{TANK_CLASSES.map((tankClass) => {
|
{TANK_CLASSES.map((tankClass) => {
|
||||||
const Icon = classIcons[tankClass];
|
const Icon = classIcons[tankClass];
|
||||||
@@ -324,25 +306,23 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tankClass}
|
key={tankClass}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.classes.includes(tankClass)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.classes.includes(tankClass)) {
|
||||||
'classes',
|
draft.classes = draft.classes.filter(
|
||||||
tankFilters.classes.filter((c) => c !== tankClass),
|
(c) => c !== tankClass
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('classes', [
|
draft.classes = [...draft.classes, tankClass];
|
||||||
...tankFilters.classes,
|
}
|
||||||
tankClass,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon style={{ width: '1em', height: '1em' }} />
|
<Icon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -350,96 +330,90 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes('regular') ? 'solid' : 'soft'
|
tankFilters.gunType.includes("regular") ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.gunType.includes('regular') ? undefined : 'gray'
|
tankFilters.gunType.includes("regular") ? undefined : "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.gunType.includes('regular')) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.gunType.includes("regular")) {
|
||||||
'gunType',
|
draft.gunType = draft.gunType.filter(
|
||||||
tankFilters.gunType.filter((t) => t !== 'regular'),
|
(t) => t !== "regular"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('gunType', [
|
draft.gunType = [...draft.gunType, "regular"];
|
||||||
...tankFilters.gunType,
|
}
|
||||||
'regular',
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GunRegularIcon style={{ width: '1em', height: '1em' }} />
|
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes('auto_loader') ? 'solid' : 'soft'
|
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.gunType.includes('auto_loader')
|
tankFilters.gunType.includes("auto_loader")
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.gunType.includes('auto_loader')) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.gunType.includes("auto_loader")) {
|
||||||
'gunType',
|
draft.gunType = draft.gunType.filter(
|
||||||
tankFilters.gunType.filter((t) => t !== 'auto_loader'),
|
(t) => t !== "auto_loader"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('gunType', [
|
draft.gunType = [...draft.gunType, "auto_loader"];
|
||||||
...tankFilters.gunType,
|
}
|
||||||
'auto_loader',
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GunAutoloaderIcon style={{ width: '1em', height: '1em' }} />
|
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.gunType.includes('auto_reloader')
|
tankFilters.gunType.includes("auto_reloader")
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.gunType.includes('auto_reloader')
|
tankFilters.gunType.includes("auto_reloader")
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.gunType.includes('auto_reloader')) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.gunType.includes("auto_reloader")) {
|
||||||
'gunType',
|
draft.gunType = draft.gunType.filter(
|
||||||
tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
|
(t) => t !== "auto_reloader"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('gunType', [
|
draft.gunType = [...draft.gunType, "auto_reloader"];
|
||||||
...tankFilters.gunType,
|
}
|
||||||
'auto_reloader',
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GunAutoreloaderIcon style={{ width: '1em', height: '1em' }} />
|
<GunAutoreloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<ShellFilter index={0} />
|
<ShellFilter index={0} />
|
||||||
<ShellFilter index={1} premium />
|
<ShellFilter index={1} premium />
|
||||||
@@ -450,160 +424,158 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
<Flex gap="2" align="start">
|
<Flex gap="2" align="start">
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||||
'types',
|
draft.types = draft.types.filter(
|
||||||
tankFilters.types.filter(
|
(t) => t !== TankType.RESEARCHABLE
|
||||||
(t) => t !== TankType.RESEARCHABLE,
|
);
|
||||||
),
|
} else {
|
||||||
);
|
draft.types = [...draft.types, TankType.RESEARCHABLE];
|
||||||
} else {
|
}
|
||||||
$tankFilters.setKey('types', [
|
});
|
||||||
...tankFilters.types,
|
|
||||||
TankType.RESEARCHABLE,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM)
|
tankFilters.types?.includes(TankType.PREMIUM)
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM)
|
tankFilters.types?.includes(TankType.PREMIUM)
|
||||||
? 'amber'
|
? "amber"
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||||
'types',
|
draft.types = draft.types.filter(
|
||||||
tankFilters.types.filter((t) => t !== TankType.PREMIUM),
|
(t) => t !== TankType.PREMIUM
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('types', [
|
draft.types = [...draft.types, TankType.PREMIUM];
|
||||||
...tankFilters.types,
|
}
|
||||||
TankType.PREMIUM,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.PREMIUM)
|
tankFilters.types?.includes(TankType.PREMIUM)
|
||||||
? undefined
|
? undefined
|
||||||
: 'amber'
|
: "amber"
|
||||||
}
|
}
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
style={{ display: "flex", justifyContent: "center" }}
|
||||||
>
|
>
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Text>
|
</Text>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={
|
variant={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||||
? 'solid'
|
? "solid"
|
||||||
: 'soft'
|
: "soft"
|
||||||
}
|
}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||||
? 'blue'
|
? "blue"
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||||
'types',
|
draft.types = draft.types.filter(
|
||||||
tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
|
(t) => t !== TankType.COLLECTOR
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$tankFilters.setKey('types', [
|
draft.types = [...draft.types, TankType.COLLECTOR];
|
||||||
...tankFilters.types,
|
}
|
||||||
TankType.COLLECTOR,
|
});
|
||||||
]);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||||
? undefined
|
? undefined
|
||||||
: 'blue'
|
: "blue"
|
||||||
}
|
}
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
style={{ display: "flex", justifyContent: "center" }}
|
||||||
>
|
>
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Text>
|
</Text>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={tankFilters.testing === 'include' ? 'solid' : 'soft'}
|
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === 'include' ? undefined : 'gray'}
|
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.setKey('testing', 'include');
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.testing = "include";
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExperimentIcon
|
<ExperimentIcon
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={tankFilters.testing === 'exclude' ? 'solid' : 'soft'}
|
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === 'exclude' ? undefined : 'gray'}
|
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.setKey('testing', 'exclude');
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.testing = "exclude";
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScienceOffIcon
|
<ScienceOffIcon
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={tankFilters.testing === 'only' ? 'solid' : 'soft'}
|
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={tankFilters.testing === 'only' ? undefined : 'gray'}
|
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.setKey('testing', 'only');
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.testing = "only";
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScienceIcon
|
<ScienceIcon
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -619,8 +591,8 @@ export function FilterControl({ compact }: FilterControlProps) {
|
|||||||
href="#"
|
href="#"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
$tankFilters.set(initialTankFilters);
|
TankFilters.set(TankFilters.initial);
|
||||||
$tankopediaSort.set(initialTankopediaSort);
|
TankSort.set(TankSort.initial);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{strings.website.common.tank_search.clear_filters}
|
{strings.website.common.tank_search.clear_filters}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
import { Flex, Link, Text } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
|
|
||||||
interface NoResultsProps {
|
interface NoResultsProps {
|
||||||
type?: 'filters' | 'search';
|
type?: "filters" | "search";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoResults({ type = 'filters' }: NoResultsProps) {
|
export function NoResults({ type = "filters" }: NoResultsProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexGrow="1" align="center" justify="center">
|
<Flex flexGrow="1" align="center" justify="center">
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
{strings.website.common.tank_search.no_tanks_found.body}{' '}
|
{strings.website.common.tank_search.no_tanks_found.body}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="#"
|
||||||
underline="always"
|
underline="always"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankFilters.set(initialTankFilters);
|
TankFilters.set(TankFilters.initial);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{type === 'filters'
|
{type === "filters"
|
||||||
? strings.website.common.tank_search.no_tanks_found.clear_filters
|
? strings.website.common.tank_search.no_tanks_found.clear_filters
|
||||||
: strings.website.common.tank_search.no_tanks_found.search_again}
|
: strings.website.common.tank_search.no_tanks_found.search_again}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { type TankDefinition } from '@blitzkit/core';
|
import { type TankDefinition } from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
import { CaretRightIcon } from "@radix-ui/react-icons";
|
||||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
import { Button, TextField } from "@radix-ui/themes";
|
||||||
import { Button, TextField } from '@radix-ui/themes';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../../stores/tankFilters';
|
import { LinkI18n } from "../../LinkI18n";
|
||||||
import { LinkI18n } from '../../LinkI18n';
|
|
||||||
|
|
||||||
interface QuickLinkProps {
|
interface QuickLinkProps {
|
||||||
topResult?: TankDefinition;
|
topResult?: TankDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuickLink({ topResult }: QuickLinkProps) {
|
export function QuickLink({ topResult }: QuickLinkProps) {
|
||||||
const tankFilters = useStore($tankFilters);
|
|
||||||
const { locale, unwrap } = useLocale();
|
const { locale, unwrap } = useLocale();
|
||||||
|
const search = TankFilters.use((state) => state.search);
|
||||||
|
const searching = TankFilters.use((state) => state.searching);
|
||||||
|
|
||||||
if (!tankFilters.search || !topResult || tankFilters.searching) return null;
|
if (!search || !topResult || searching) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
|
|||||||
@@ -1,38 +1,36 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { Flex, Separator, Text } from "@radix-ui/themes";
|
||||||
import { Flex, Separator, Text } from '@radix-ui/themes';
|
import { isEqual } from "lodash-es";
|
||||||
import { isEqual } from 'lodash-es';
|
import { useMemo } from "react";
|
||||||
import { useMemo } from 'react';
|
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
import { TankCard } from "../../TankCard";
|
||||||
import { TankCard } from '../../TankCard';
|
import { TankCardWrapper } from "./TankCardWrapper";
|
||||||
import { TankCardWrapper } from './TankCardWrapper';
|
|
||||||
|
|
||||||
const tankDefinitions = await awaitableTankDefinitions;
|
const tankDefinitions = await awaitableTankDefinitions;
|
||||||
|
|
||||||
export function RecentlyViewed() {
|
export function RecentlyViewed() {
|
||||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
const filters = TankFilters.use();
|
||||||
const filters = useStore($tankFilters);
|
|
||||||
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
||||||
const recentlyViewed = tankopediaPersistentStore
|
const recentlyViewed = TankopediaPersistent.state.recentlyViewed.filter(
|
||||||
.getState()
|
(id) => id in tankDefinitions.tanks
|
||||||
.recentlyViewed.filter((id) => id in tankDefinitions.tanks);
|
);
|
||||||
const sort = useStore($tankopediaSort);
|
|
||||||
const hasFilters = useMemo(
|
const hasFilters = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.entries(filters).some(([key, value]) => {
|
Object.entries(filters).some(([key, value]) => {
|
||||||
return !isEqual(
|
return !isEqual(
|
||||||
value,
|
value,
|
||||||
initialTankFilters[key as keyof typeof initialTankFilters],
|
TankFilters.initial[key as keyof typeof TankFilters.initial]
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
[filters],
|
[filters]
|
||||||
);
|
);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
const by = TankSort.use((state) => state.by);
|
||||||
|
|
||||||
if (recentlyViewed.length === 0 || sort.by !== 'meta.none' || hasFilters) {
|
if (recentlyViewed.length === 0 || by !== "meta.none" || hasFilters) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import type { TankDefinition } from '@blitzkit/core';
|
import type { TankDefinition } from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { Flex, Spinner, TextField } from "@radix-ui/themes";
|
||||||
import { Flex, Spinner, TextField } from '@radix-ui/themes';
|
import { debounce } from "lodash-es";
|
||||||
import { debounce } from 'lodash-es';
|
import { type KeyboardEventHandler, useCallback, useRef } from "react";
|
||||||
import { type KeyboardEventHandler, useCallback, useRef } from 'react';
|
import { useLocale } from "../../../hooks/useLocale";
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../../stores/tankFilters';
|
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
import { QuickLink } from "./QuickLink";
|
||||||
import { QuickLink } from './QuickLink';
|
import { Sort } from "./Sort";
|
||||||
import { Sort } from './Sort';
|
|
||||||
|
|
||||||
type SearchBarProps = MaybeSkeletonComponentProps & {
|
type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||||
topResult?: TankDefinition;
|
topResult?: TankDefinition;
|
||||||
@@ -17,30 +16,37 @@ type SearchBarProps = MaybeSkeletonComponentProps & {
|
|||||||
|
|
||||||
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const tankFilters = useStore($tankFilters);
|
const search = TankFilters.use((state) => state.search);
|
||||||
|
const searching = TankFilters.use((state) => state.searching);
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const performSearch = useCallback(
|
const performSearch = useCallback(
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
$tankFilters.setKey('searching', false);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.searching = false;
|
||||||
|
});
|
||||||
|
|
||||||
if (!input.current) return;
|
if (!input.current) return;
|
||||||
|
|
||||||
const sanitized = input.current.value.trim();
|
const sanitized = input.current.value.trim();
|
||||||
|
|
||||||
$tankFilters.setKey('search', sanitized.length === 0 ? null : sanitized);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.search = sanitized.length === 0 ? null : sanitized;
|
||||||
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
const handleChange = useCallback(() => {
|
const handleChange = useCallback(() => {
|
||||||
if (!tankFilters.searching) {
|
if (!searching) {
|
||||||
$tankFilters.setKey('searching', true);
|
TankFilters.mutate((draft) => {
|
||||||
|
draft.searching = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
performSearch();
|
performSearch();
|
||||||
}, [tankFilters]);
|
}, [searching]);
|
||||||
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||||
(event) => {
|
(event) => {
|
||||||
if (event.key !== 'Enter' || !topResult || tankFilters.searching) return;
|
if (event.key !== "Enter" || !topResult || searching) return;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -50,7 +56,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
window.location.href = `/tanks/${topResult.slug}`;
|
window.location.href = `/tanks/${topResult.slug}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[topResult],
|
[topResult]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,7 +65,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
disabled={skeleton}
|
disabled={skeleton}
|
||||||
defaultValue={tankFilters.search ?? undefined}
|
defaultValue={search ?? undefined}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
ref={input}
|
ref={input}
|
||||||
placeholder={strings.website.common.tank_search.search_bar_hint}
|
placeholder={strings.website.common.tank_search.search_bar_hint}
|
||||||
@@ -67,7 +73,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<TextField.Slot>
|
<TextField.Slot>
|
||||||
{tankFilters.searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
|
|
||||||
<QuickLink topResult={topResult} />
|
<QuickLink topResult={topResult} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||||
import { Button, DropdownMenu } from '@radix-ui/themes';
|
import { Button, DropdownMenu } from '@radix-ui/themes';
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from '../../../hooks/useLocale';
|
||||||
@@ -6,7 +5,7 @@ import type {
|
|||||||
TankopediaSortBy,
|
TankopediaSortBy,
|
||||||
TankopediaSortDirection,
|
TankopediaSortDirection,
|
||||||
} from '../../../stores/tankopediaPersistent';
|
} from '../../../stores/tankopediaPersistent';
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
import { TankSort } from '../../../stores/tankopediaSort';
|
||||||
|
|
||||||
interface ItemProps {
|
interface ItemProps {
|
||||||
by: TankopediaSortBy;
|
by: TankopediaSortBy;
|
||||||
@@ -18,13 +17,15 @@ interface ItemProps {
|
|||||||
|
|
||||||
function Item({ by }: ItemProps) {
|
function Item({ by }: ItemProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const _by = TankSort.use((state) => state.by);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={tankopediaSort.by === by}
|
checked={_by === by}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$tankopediaSort.setKey('by', by);
|
TankSort.mutate((draft) => {
|
||||||
|
draft.by = by;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{strings.website.common.tank_search.sort[by]}
|
{strings.website.common.tank_search.sort[by]}
|
||||||
@@ -33,7 +34,7 @@ function Item({ by }: ItemProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Sort() {
|
export function Sort() {
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const direction = TankSort.use((state) => state.direction);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -119,12 +120,11 @@ export function Sort() {
|
|||||||
}
|
}
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
<DropdownMenu.RadioGroup
|
<DropdownMenu.RadioGroup
|
||||||
value={tankopediaSort.direction}
|
value={direction}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
$tankopediaSort.setKey(
|
TankSort.mutate((draft) => {
|
||||||
'direction',
|
draft.direction = value as TankopediaSortDirection;
|
||||||
value as TankopediaSortDirection,
|
});
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenu.RadioItem value="ascending">
|
<DropdownMenu.RadioItem value="ascending">
|
||||||
|
|||||||
@@ -1,44 +1,43 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||||
import { useStore } from '@nanostores/react';
|
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||||
import { Flex, IconButton, Text } from '@radix-ui/themes';
|
import { times } from "lodash-es";
|
||||||
import { times } from 'lodash-es';
|
import { memo } from "react";
|
||||||
import { memo } from 'react';
|
import { TankFilters } from "../../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../../stores/tankFilters';
|
import { TankSort } from "../../../stores/tankopediaSort";
|
||||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
|
||||||
|
|
||||||
export const TierFilter = memo(() => {
|
export const TierFilter = memo(() => {
|
||||||
const tankFilters = useStore($tankFilters);
|
const by = TankSort.use((state) => state.by);
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const search = TankFilters.use((state) => state.search);
|
||||||
|
const tiers = TankFilters.use((state) => state.tiers);
|
||||||
|
|
||||||
if (tankopediaSort.by !== 'meta.none' || tankFilters.search) return null;
|
if (by !== "meta.none" || search) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
<Flex
|
<Flex
|
||||||
direction="row"
|
direction="row"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
style={{ borderRadius: 'var(--radius-full)' }}
|
style={{ borderRadius: "var(--radius-full)" }}
|
||||||
>
|
>
|
||||||
{times(10, (index) => {
|
{times(10, (index) => {
|
||||||
const tier = 10 - index;
|
const tier = 10 - index;
|
||||||
const selected = tankFilters.tiers.includes(tier);
|
const selected = tiers.includes(tier);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
key={tier}
|
key={tier}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
radius="none"
|
radius="none"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
highContrast
|
highContrast
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (tankFilters.tiers.includes(tier)) {
|
TankFilters.mutate((draft) => {
|
||||||
$tankFilters.setKey(
|
if (tiers.includes(tier)) {
|
||||||
'tiers',
|
draft.tiers = tiers.filter((t) => t !== tier);
|
||||||
tankFilters.tiers.filter((t) => t !== tier),
|
} else {
|
||||||
);
|
draft.tiers = [...tiers, tier];
|
||||||
} else {
|
}
|
||||||
$tankFilters.setKey('tiers', [...tankFilters.tiers, tier]);
|
});
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
||||||
|
|||||||
@@ -5,37 +5,36 @@ import {
|
|||||||
SEARCH_KEYS,
|
SEARCH_KEYS,
|
||||||
unionBoundingBox,
|
unionBoundingBox,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { useStore } from '@nanostores/react';
|
import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
|
||||||
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
import fuzzysort from "fuzzysort";
|
||||||
import fuzzysort from 'fuzzysort';
|
import { times, uniq } from "lodash-es";
|
||||||
import { times, uniq } from 'lodash-es';
|
import { memo, useEffect, useMemo, useState } from "react";
|
||||||
import { memo, useEffect, useMemo, useState } from 'react';
|
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||||
import { awaitableTankNames } from '../../core/awaitables/tankNames';
|
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
import { resolveReload } from "../../core/blitzkit/resolveReload";
|
||||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
import { useLocale } from "../../hooks/useLocale";
|
||||||
import { useLocale } from '../../hooks/useLocale';
|
import { TankFilters } from "../../stores/tankFilters";
|
||||||
import { $tankFilters } from '../../stores/tankFilters';
|
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
||||||
import { SORT_UNITS } from '../../stores/tankopediaPersistent/constants';
|
import { TankSort } from "../../stores/tankopediaSort";
|
||||||
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
import { ExperimentIcon } from "../ExperimentIcon";
|
||||||
import { ExperimentIcon } from '../ExperimentIcon';
|
import { TankSearchCard } from "./components/Card";
|
||||||
import { TankSearchCard } from './components/Card';
|
import { FilterControl } from "./components/FilterControl";
|
||||||
import { FilterControl } from './components/FilterControl';
|
import { NoResults } from "./components/NoResults";
|
||||||
import { NoResults } from './components/NoResults';
|
import { RecentlyViewed } from "./components/RecentlyViewed";
|
||||||
import { RecentlyViewed } from './components/RecentlyViewed';
|
import { SearchBar } from "./components/SearchBar";
|
||||||
import { SearchBar } from './components/SearchBar';
|
import { SkeletonTankCard } from "./components/SkeletonTankCard";
|
||||||
import { SkeletonTankCard } from './components/SkeletonTankCard';
|
import { TankCardWrapper } from "./components/TankCardWrapper";
|
||||||
import { TankCardWrapper } from './components/TankCardWrapper';
|
import { MAX_RECENTLY_VIEWED } from "./constants";
|
||||||
import { MAX_RECENTLY_VIEWED } from './constants';
|
|
||||||
|
|
||||||
type TankSearchProps = MaybeSkeletonComponentProps &
|
type TankSearchProps = MaybeSkeletonComponentProps &
|
||||||
Omit<FlexProps, 'onSelect'> & {
|
Omit<FlexProps, "onSelect"> & {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
onSelect?: (tank: TankDefinition) => void;
|
onSelect?: (tank: TankDefinition) => void;
|
||||||
onSelectAll?: (tanks: TankDefinition[]) => void;
|
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||||
@@ -55,23 +54,23 @@ const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
|
|||||||
export const TankSearch = memo<TankSearchProps>(
|
export const TankSearch = memo<TankSearchProps>(
|
||||||
({ compact, onSelect, onSelectAll, skeleton, ...props }) => {
|
({ compact, onSelect, onSelectAll, skeleton, ...props }) => {
|
||||||
const { strings, locale } = useLocale();
|
const { strings, locale } = useLocale();
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
|
||||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||||
const tankFilters = useStore($tankFilters);
|
const tankopediaSort = TankSort.use();
|
||||||
const tankopediaSort = useStore($tankopediaSort);
|
const tankFilters = TankFilters.use();
|
||||||
const tanksFiltered = useMemo(() => {
|
const tanksFiltered = useMemo(() => {
|
||||||
if (tankFilters.search === null) {
|
if (tankFilters.search === null) {
|
||||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||||
filterTank(tankFilters, tank),
|
filterTank(tankFilters, tank)
|
||||||
);
|
);
|
||||||
let sorted: TankDefinition[];
|
let sorted: TankDefinition[];
|
||||||
|
|
||||||
switch (tankopediaSort.by) {
|
switch (tankopediaSort.by) {
|
||||||
case 'meta.none':
|
case "meta.none":
|
||||||
sorted = metaSortTank(filtered, gameDefinitions);
|
sorted = metaSortTank(filtered, gameDefinitions);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.health': {
|
case "survivability.health": {
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aHealth = a.health + a.turrets.at(-1)!.health;
|
const aHealth = a.health + a.turrets.at(-1)!.health;
|
||||||
const bHealth = b.health + b.turrets.at(-1)!.health;
|
const bHealth = b.health + b.turrets.at(-1)!.health;
|
||||||
@@ -81,36 +80,36 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'survivability.viewRange':
|
case "survivability.viewRange":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range,
|
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.camouflageStill':
|
case "survivability.camouflageStill":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.camouflage_still - b.camouflage_still,
|
(a, b) => a.camouflage_still - b.camouflage_still
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.camouflageMoving':
|
case "survivability.camouflageMoving":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.camouflage_moving - b.camouflage_moving,
|
(a, b) => a.camouflage_moving - b.camouflage_moving
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.camouflageShooting':
|
case "survivability.camouflageShooting":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.camouflage_still *
|
a.camouflage_still *
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
||||||
b.camouflage_still *
|
b.camouflage_still *
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
|
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.volume':
|
case "survivability.volume":
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||||
@@ -121,14 +120,14 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
const aSize = normalizeBoundingBox(
|
const aSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
aTankModelDefinition.bounding_box,
|
aTankModelDefinition.bounding_box,
|
||||||
aTurretModelDefinition.bounding_box,
|
aTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const bSize = normalizeBoundingBox(
|
const bSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
bTankModelDefinition.bounding_box,
|
bTankModelDefinition.bounding_box,
|
||||||
bTurretModelDefinition.bounding_box,
|
bTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
const aVolume = aSize.x * aSize.y * aSize.z;
|
||||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
const bVolume = bSize.x * bSize.y * bSize.z;
|
||||||
@@ -137,7 +136,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'survivability.length':
|
case "survivability.length":
|
||||||
sorted = filtered.sort((a, b) => {
|
sorted = filtered.sort((a, b) => {
|
||||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||||
@@ -148,14 +147,14 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
const aSize = normalizeBoundingBox(
|
const aSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
aTankModelDefinition.bounding_box,
|
aTankModelDefinition.bounding_box,
|
||||||
aTurretModelDefinition.bounding_box,
|
aTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const bSize = normalizeBoundingBox(
|
const bSize = normalizeBoundingBox(
|
||||||
unionBoundingBox(
|
unionBoundingBox(
|
||||||
bTankModelDefinition.bounding_box,
|
bTankModelDefinition.bounding_box,
|
||||||
bTurretModelDefinition.bounding_box,
|
bTurretModelDefinition.bounding_box
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
||||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
||||||
@@ -164,147 +163,147 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.dpm':
|
case "fire.dpm":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
resolveDpm(
|
resolveDpm(
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
a.turrets.at(-1)!.guns.at(-1)!,
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0]
|
||||||
) -
|
) -
|
||||||
resolveDpm(
|
resolveDpm(
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
b.turrets.at(-1)!.guns.at(-1)!,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0]
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.dpmPremium':
|
case "fire.dpmPremium":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
? resolveDpm(
|
? resolveDpm(
|
||||||
a.turrets.at(-1)!.guns.at(-1)!,
|
a.turrets.at(-1)!.guns.at(-1)!,
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
)
|
)
|
||||||
: 0) -
|
: 0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
? resolveDpm(
|
? resolveDpm(
|
||||||
b.turrets.at(-1)!.guns.at(-1)!,
|
b.turrets.at(-1)!.guns.at(-1)!,
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||||
)
|
)
|
||||||
: 0),
|
: 0)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.reload':
|
case "fire.reload":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
|
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
|
||||||
resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
|
resolveReload(b.turrets.at(-1)!.guns.at(-1)!)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.caliber':
|
case "fire.caliber":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.standardPenetration':
|
case "fire.standardPenetration":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near -
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near,
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.premiumPenetration':
|
case "fire.premiumPenetration":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||||
0) -
|
0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||||
0),
|
0)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.tertiaryPenetration':
|
case "fire.tertiaryPenetration":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||||
0) -
|
0) -
|
||||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||||
0),
|
0)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.damage':
|
case "fire.damage":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage,
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.moduleDamage':
|
case "fire.moduleDamage":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage,
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.shellVelocity':
|
case "fire.shellVelocity":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity,
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.shellRange':
|
case "fire.shellRange":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
|
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range,
|
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.shellCapacity':
|
case "fire.shellCapacity":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
|
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity,
|
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.aimTime':
|
case "fire.aimTime":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.aim_time -
|
a.turrets.at(-1)!.guns.at(-1)!.aim_time -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.aim_time,
|
b.turrets.at(-1)!.guns.at(-1)!.aim_time
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.dispersionStill':
|
case "fire.dispersionStill":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
|
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base,
|
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.dispersionMoving':
|
case "fire.dispersionMoving":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.tracks.at(-1)!.dispersion_move -
|
a.tracks.at(-1)!.dispersion_move -
|
||||||
b.tracks.at(-1)!.dispersion_move,
|
b.tracks.at(-1)!.dispersion_move
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.gunDepression':
|
case "fire.gunDepression":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||||
@@ -314,11 +313,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
|
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
|
||||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||||
0),
|
0)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fire.gunElevation':
|
case "fire.gunElevation":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||||
@@ -328,29 +327,29 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
|
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
|
||||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||||
0),
|
0)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.forwardsSpeed':
|
case "maneuverability.forwardsSpeed":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.speed_forwards - b.speed_forwards,
|
(a, b) => a.speed_forwards - b.speed_forwards
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.backwardsSpeed':
|
case "maneuverability.backwardsSpeed":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.speed_backwards - b.speed_backwards,
|
(a, b) => a.speed_backwards - b.speed_backwards
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.power':
|
case "maneuverability.power":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
|
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.powerToWeight':
|
case "maneuverability.powerToWeight":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.engines.at(-1)!.power /
|
a.engines.at(-1)!.power /
|
||||||
@@ -364,11 +363,11 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
b.engines.at(-1)!.weight +
|
b.engines.at(-1)!.weight +
|
||||||
b.tracks.at(-1)!.weight +
|
b.tracks.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.weight +
|
b.turrets.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
b.turrets.at(-1)!.guns.at(-1)!.weight)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.weight':
|
case "maneuverability.weight":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.weight +
|
a.weight +
|
||||||
@@ -380,20 +379,20 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
b.engines.at(-1)!.weight +
|
b.engines.at(-1)!.weight +
|
||||||
b.tracks.at(-1)!.weight +
|
b.tracks.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.weight +
|
b.turrets.at(-1)!.weight +
|
||||||
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
b.turrets.at(-1)!.guns.at(-1)!.weight)
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'maneuverability.traverseSpeed':
|
case "maneuverability.traverseSpeed":
|
||||||
sorted = filtered.sort(
|
sorted = filtered.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
a.tracks.at(-1)!.traverse_speed -
|
a.tracks.at(-1)!.traverse_speed -
|
||||||
b.tracks.at(-1)!.traverse_speed,
|
b.tracks.at(-1)!.traverse_speed
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tankopediaSort.direction === 'ascending'
|
return tankopediaSort.direction === "ascending"
|
||||||
? sorted
|
? sorted
|
||||||
: sorted.reverse();
|
: sorted.reverse();
|
||||||
} else {
|
} else {
|
||||||
@@ -401,7 +400,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
keys: SEARCH_KEYS,
|
keys: SEARCH_KEYS,
|
||||||
});
|
});
|
||||||
const searchedTanks = searchedRaw.map(
|
const searchedTanks = searchedRaw.map(
|
||||||
(result) => tankDefinitions.tanks[result.obj.id],
|
(result) => tankDefinitions.tanks[result.obj.id]
|
||||||
);
|
);
|
||||||
return searchedTanks;
|
return searchedTanks;
|
||||||
}
|
}
|
||||||
@@ -444,7 +443,7 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSelectAll(tanksFiltered);
|
onSelectAll(tanksFiltered);
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.recentlyViewed = uniq([
|
draft.recentlyViewed = uniq([
|
||||||
...tanksFiltered.map(({ id }) => id),
|
...tanksFiltered.map(({ id }) => id),
|
||||||
...draft.recentlyViewed,
|
...draft.recentlyViewed,
|
||||||
@@ -459,24 +458,24 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{tankopediaSort.by !== 'meta.none' && (
|
{tankopediaSort.by !== "meta.none" && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
{literals(strings.website.common.tank_search.sorting_by, [
|
{literals(strings.website.common.tank_search.sorting_by, [
|
||||||
strings.website.common.tank_search.sort[tankopediaSort.by],
|
strings.website.common.tank_search.sort[tankopediaSort.by],
|
||||||
])}
|
])}
|
||||||
{SORT_UNITS[tankopediaSort.by] === undefined
|
{SORT_UNITS[tankopediaSort.by] === undefined
|
||||||
? ''
|
? ""
|
||||||
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
||||||
, {tankopediaSort.direction}
|
, {tankopediaSort.direction}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{tankFilters.testing === 'only' && (
|
{tankFilters.testing === "only" && (
|
||||||
<Flex justify="center" mt="4">
|
<Flex justify="center" mt="4">
|
||||||
<Callout.Root color="amber">
|
<Callout.Root color="amber">
|
||||||
<Callout.Icon>
|
<Callout.Icon>
|
||||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Callout.Icon>
|
</Callout.Icon>
|
||||||
<Callout.Text>
|
<Callout.Text>
|
||||||
{strings.website.common.warnings.test}
|
{strings.website.common.warnings.test}
|
||||||
@@ -505,12 +504,12 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
key={index}
|
key={index}
|
||||||
onIntersection={() => {
|
onIntersection={() => {
|
||||||
setLoadedCards((state) =>
|
setLoadedCards((state) =>
|
||||||
Math.min(state + 2, tanksFiltered.length),
|
Math.min(state + 2, tanksFiltered.length)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
)}
|
)}
|
||||||
</TankCardWrapper>
|
</TankCardWrapper>
|
||||||
)}
|
)}
|
||||||
@@ -523,15 +522,15 @@ export const TankSearch = memo<TankSearchProps>(
|
|||||||
<TankCardWrapper>
|
<TankCardWrapper>
|
||||||
{times(
|
{times(
|
||||||
Math.round(
|
Math.round(
|
||||||
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random(),
|
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random()
|
||||||
),
|
),
|
||||||
(index) => (
|
(index) => (
|
||||||
<SkeletonTankCard key={index} />
|
<SkeletonTankCard key={index} />
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</TankCardWrapper>
|
</TankCardWrapper>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
import { assertSecret, fetchTankDefinitions } from '@blitzkit/core';
|
import { assertSecret, fetchTankDefinitions } from "@blitzkit/core";
|
||||||
import { MinusCircledIcon } from '@radix-ui/react-icons';
|
import { MinusCircledIcon } from "@radix-ui/react-icons";
|
||||||
import { Callout, Flex } from '@radix-ui/themes';
|
import { Callout, Flex } from "@radix-ui/themes";
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from "lodash-es";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import { Duel } from '../../stores/duel';
|
import { Duel } from "../../stores/duel";
|
||||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||||
import { AesonPlug } from '../AesonPlug';
|
import { AesonPlug } from "../AesonPlug";
|
||||||
import { ExperimentIcon } from '../ExperimentIcon';
|
import { ExperimentIcon } from "../ExperimentIcon";
|
||||||
import { MAX_RECENTLY_VIEWED } from '../TankSearch/constants';
|
import { MAX_RECENTLY_VIEWED } from "../TankSearch/constants";
|
||||||
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
const tankDefinitions = await fetchTankDefinitions();
|
||||||
|
|
||||||
export function CalloutsSection() {
|
export function CalloutsSection() {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
|
||||||
const promoteAeson =
|
const promoteAeson =
|
||||||
assertSecret(import.meta.env.PUBLIC_BRANCH) === 'preview';
|
assertSecret(import.meta.env.PUBLIC_BRANCH) === "preview";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
|
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
|
||||||
.filter((id) => id in tankDefinitions.tanks)
|
.filter((id) => id in tankDefinitions.tanks)
|
||||||
.slice(0, MAX_RECENTLY_VIEWED);
|
.slice(0, MAX_RECENTLY_VIEWED);
|
||||||
@@ -45,7 +44,7 @@ export function CalloutsSection() {
|
|||||||
{tank.testing && (
|
{tank.testing && (
|
||||||
<Callout.Root color="red">
|
<Callout.Root color="red">
|
||||||
<Callout.Icon>
|
<Callout.Icon>
|
||||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||||
</Callout.Icon>
|
</Callout.Icon>
|
||||||
<Callout.Text>
|
<Callout.Text>
|
||||||
Tanks in testing are subject to change and may not represent the
|
Tanks in testing are subject to change and may not represent the
|
||||||
|
|||||||
+28
-29
@@ -1,14 +1,14 @@
|
|||||||
import { DragHandleDots2Icon } from '@radix-ui/react-icons';
|
import { DragHandleDots2Icon } from "@radix-ui/react-icons";
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { clamp } from 'three/src/math/MathUtils.js';
|
import { clamp } from "three/src/math/MathUtils.js";
|
||||||
import {
|
import {
|
||||||
fakeLogSquishyHigh,
|
fakeLogSquishyHigh,
|
||||||
fakeLogSquishyHighInverse,
|
fakeLogSquishyHighInverse,
|
||||||
} from '../../../../../../../../../core/blitzkit/fakeLog';
|
} from "../../../../../../../../../core/blitzkit/fakeLog";
|
||||||
import { Var } from '../../../../../../../../../core/radix/var';
|
import { Var } from "../../../../../../../../../core/radix/var";
|
||||||
import { Duel } from '../../../../../../../../../stores/duel';
|
import { Duel } from "../../../../../../../../../stores/duel";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
maxDistance: number;
|
maxDistance: number;
|
||||||
@@ -17,11 +17,10 @@ interface Props {
|
|||||||
|
|
||||||
export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
||||||
const assaultDistance = Duel.use(
|
const assaultDistance = Duel.use(
|
||||||
(state) => state.protagonist.assaultDistance,
|
(state) => state.protagonist.assaultDistance
|
||||||
);
|
);
|
||||||
const [x, setX] = useState(fakeLogSquishyHigh(assaultDistance / maxDistance));
|
const [x, setX] = useState(fakeLogSquishyHigh(assaultDistance / maxDistance));
|
||||||
const cursor = useRef<HTMLDivElement>(null);
|
const cursor = useRef<HTMLDivElement>(null);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let localX = x;
|
let localX = x;
|
||||||
@@ -32,8 +31,8 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
|||||||
|
|
||||||
lastX = event.clientX;
|
lastX = event.clientX;
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
window.addEventListener("pointermove", handlePointerMove);
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
window.addEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerMove(event: PointerEvent) {
|
function handlePointerMove(event: PointerEvent) {
|
||||||
@@ -54,21 +53,21 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerUp() {
|
function handlePointerUp() {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.assaultDistance =
|
draft.protagonist.assaultDistance =
|
||||||
fakeLogSquishyHighInverse(localX * maxFakeDistance) * maxDistance;
|
fakeLogSquishyHighInverse(localX * maxFakeDistance) * maxDistance;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor.current?.addEventListener('pointerdown', handlePointerDown);
|
cursor.current?.addEventListener("pointerdown", handlePointerDown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cursor.current?.removeEventListener('pointerdown', handlePointerDown);
|
cursor.current?.removeEventListener("pointerdown", handlePointerDown);
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
window.removeEventListener("pointermove", handlePointerMove);
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
window.removeEventListener("pointerup", handlePointerUp);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -82,9 +81,9 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
|||||||
width="2pt"
|
width="2pt"
|
||||||
id="assault-cursor"
|
id="assault-cursor"
|
||||||
style={{
|
style={{
|
||||||
touchAction: 'none',
|
touchAction: "none",
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
backgroundColor: Var('accent-11'),
|
backgroundColor: Var("accent-11"),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -93,19 +92,19 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
|||||||
position="absolute"
|
position="absolute"
|
||||||
left="50%"
|
left="50%"
|
||||||
bottom="100%"
|
bottom="100%"
|
||||||
style={{ transform: 'translateX(-50%)' }}
|
style={{ transform: "translateX(-50%)" }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
style={{
|
style={{
|
||||||
width: '1.5rem',
|
width: "1.5rem",
|
||||||
height: '1.5rem',
|
height: "1.5rem",
|
||||||
backgroundColor: Var('accent-9'),
|
backgroundColor: Var("accent-9"),
|
||||||
borderRadius: `50% 50% ${Var('radius-1')} ${Var('radius-1')}`,
|
borderRadius: `50% 50% ${Var("radius-1")} ${Var("radius-1")}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ display: 'flex' }} highContrast color="purple">
|
<Text style={{ display: "flex" }} highContrast color="purple">
|
||||||
<DragHandleDots2Icon />
|
<DragHandleDots2Icon />
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -114,7 +113,7 @@ export function AssaultCursor({ maxDistance, maxFakeDistance }: Props) {
|
|||||||
height="0.5rem"
|
height="0.5rem"
|
||||||
width="2pt"
|
width="2pt"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: Var('accent-11'),
|
backgroundColor: Var("accent-11"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
+12
-15
@@ -1,19 +1,16 @@
|
|||||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { Flex, SegmentedControl, Text, type FlexProps } from '@radix-ui/themes';
|
import { Flex, SegmentedControl, Text, type FlexProps } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
TankopediaRelativeAgainst,
|
TankopediaRelativeAgainst,
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
} from "../../../../../../stores/tankopedia";
|
||||||
|
|
||||||
export function CompareOptions(props: FlexProps) {
|
export function CompareOptions(props: FlexProps) {
|
||||||
const relativeAgainst = TankopediaEphemeral.use(
|
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
||||||
(state) => state.relativeAgainst,
|
|
||||||
);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +18,7 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
align="center"
|
align="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
justify="center"
|
justify="center"
|
||||||
direction={{ initial: 'column', sm: 'row' }}
|
direction={{ initial: "column", sm: "row" }}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
@@ -33,7 +30,7 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
value={`${relativeAgainst}`}
|
value={`${relativeAgainst}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -44,13 +41,13 @@ export function CompareOptions(props: FlexProps) {
|
|||||||
[
|
[
|
||||||
TIER_ROMAN_NUMERALS[tank.tier],
|
TIER_ROMAN_NUMERALS[tank.tier],
|
||||||
strings.common.tank_class_short_plural_lowercase[tank.class],
|
strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||||
],
|
]
|
||||||
)}
|
)}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
||||||
{literals(
|
{literals(
|
||||||
strings.website.tools.tankopedia.characteristics.compare.tier,
|
strings.website.tools.tankopedia.characteristics.compare.tier,
|
||||||
[TIER_ROMAN_NUMERALS[tank.tier]],
|
[TIER_ROMAN_NUMERALS[tank.tier]]
|
||||||
)}
|
)}
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
||||||
|
|||||||
+26
-56
@@ -1,61 +1,31 @@
|
|||||||
import {
|
import { asset, isExplosive } from "@blitzkit/core";
|
||||||
asset,
|
import { literals } from "@blitzkit/i18n";
|
||||||
coefficient,
|
import { Flex, Heading, IconButton, Tooltip } from "@radix-ui/themes";
|
||||||
isExplosive,
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
resolvePenetrationCoefficient,
|
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||||
} from '@blitzkit/core';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import { literals } from '@blitzkit/i18n';
|
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
|
import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
|
||||||
import { debounce } from 'lodash-es';
|
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { Info } from "./Info";
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { InfoWithDelta } from "./InfoWithDelta";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { ReloadVisualizer } from "./ReloadVisualizer";
|
||||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
import { RicochetVisualizer } from "./RicochetVisualizer";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { StatsTableWrapper } from "./StatsTableWrapper";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
import type { StatsAcceptorProps } from "./TraverseVisualizer";
|
||||||
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
|
||||||
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
|
||||||
import { Info } from './Info';
|
|
||||||
import { InfoWithDelta } from './InfoWithDelta';
|
|
||||||
import { ReloadVisualizer } from './ReloadVisualizer';
|
|
||||||
import { RicochetVisualizer } from './RicochetVisualizer';
|
|
||||||
import { StatsTableWrapper } from './StatsTableWrapper';
|
|
||||||
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
|
||||||
|
|
||||||
export function Firepower({
|
export function Firepower({
|
||||||
stats,
|
stats,
|
||||||
skeleton,
|
skeleton,
|
||||||
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
|
||||||
const setPenetrationDistanceDebounced = debounce((value: number) => {
|
|
||||||
setPenetrationDistance(value);
|
|
||||||
}, 500);
|
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
const shell = Duel.use((state) => state.protagonist.shell);
|
const shell = Duel.use((state) => state.protagonist.shell);
|
||||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
|
||||||
const hasSupercharger = useEquipment(107);
|
|
||||||
const hasCalibratedShells = useEquipment(103);
|
|
||||||
const penetrationCoefficient = coefficient([
|
|
||||||
hasCalibratedShells,
|
|
||||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
|
||||||
]);
|
|
||||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
|
||||||
hasSupercharger,
|
|
||||||
-0.6,
|
|
||||||
]);
|
|
||||||
const tankModelDefinition = useTankModelDefinition();
|
const tankModelDefinition = useTankModelDefinition();
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (penetrationDistanceInput.current) {
|
|
||||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
|
||||||
}
|
|
||||||
}, [penetrationDistance]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsTableWrapper>
|
<StatsTableWrapper>
|
||||||
<Flex align="center" gap="4">
|
<Flex align="center" gap="4">
|
||||||
@@ -70,8 +40,8 @@ export function Firepower({
|
|||||||
return (
|
return (
|
||||||
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
highContrast={selected}
|
highContrast={selected}
|
||||||
style={{
|
style={{
|
||||||
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
borderTopLeftRadius: shellIndex === 0 ? undefined : 0,
|
||||||
@@ -83,7 +53,7 @@ export function Firepower({
|
|||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.shell = thisShell;
|
draft.protagonist.shell = thisShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -108,7 +78,7 @@ export function Firepower({
|
|||||||
</Info>
|
</Info>
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="dpm" />
|
<InfoWithDelta stats={stats} decimals={0} value="dpm" />
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="damage" />
|
<InfoWithDelta stats={stats} decimals={0} value="damage" />
|
||||||
{gun.gun_type!.$case !== 'regular' && (
|
{gun.gun_type!.$case !== "regular" && (
|
||||||
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
|
<InfoWithDelta stats={stats} indent decimals={0} value="clipDamage" />
|
||||||
)}
|
)}
|
||||||
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
|
<InfoWithDelta stats={stats} decimals={0} value="moduleDamage" />
|
||||||
@@ -118,13 +88,13 @@ export function Firepower({
|
|||||||
<AssaultRangesVisualizer stats={stats} ranges={gun.assault_ranges} />
|
<AssaultRangesVisualizer stats={stats} ranges={gun.assault_ranges} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{gun.gun_type!.$case === 'auto_reloader' && (
|
{gun.gun_type!.$case === "auto_reloader" && (
|
||||||
<InfoWithDelta stats={stats} decimals={0} indent value="dpmEffective" />
|
<InfoWithDelta stats={stats} decimals={0} indent value="dpmEffective" />
|
||||||
)}
|
)}
|
||||||
{gun.gun_type!.$case !== 'regular' && (
|
{gun.gun_type!.$case !== "regular" && (
|
||||||
<InfoWithDelta stats={stats} value="shells" />
|
<InfoWithDelta stats={stats} value="shells" />
|
||||||
)}
|
)}
|
||||||
{gun.gun_type!.$case === 'auto_reloader' ? (
|
{gun.gun_type!.$case === "auto_reloader" ? (
|
||||||
<>
|
<>
|
||||||
<Info
|
<Info
|
||||||
indent
|
indent
|
||||||
@@ -149,7 +119,7 @@ export function Firepower({
|
|||||||
name={literals(
|
name={literals(
|
||||||
strings.website.tools.tankopedia.characteristics.values
|
strings.website.tools.tankopedia.characteristics.values
|
||||||
.shell_index,
|
.shell_index,
|
||||||
[`${index + 1}`],
|
[`${index + 1}`]
|
||||||
)}
|
)}
|
||||||
decimals={2}
|
decimals={2}
|
||||||
deltaType="lowerIsBetter"
|
deltaType="lowerIsBetter"
|
||||||
@@ -166,8 +136,8 @@ export function Firepower({
|
|||||||
value="shellReload"
|
value="shellReload"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(gun.gun_type!.$case === 'auto_loader' ||
|
{(gun.gun_type!.$case === "auto_loader" ||
|
||||||
gun.gun_type!.$case === 'auto_reloader') && (
|
gun.gun_type!.$case === "auto_reloader") && (
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
stats={stats}
|
stats={stats}
|
||||||
indent
|
indent
|
||||||
@@ -221,7 +191,7 @@ export function Firepower({
|
|||||||
name={literals(
|
name={literals(
|
||||||
strings.website.tools.tankopedia.characteristics.values
|
strings.website.tools.tankopedia.characteristics.values
|
||||||
.dispersion_at_distance,
|
.dispersion_at_distance,
|
||||||
['100'],
|
["100"]
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<InfoWithDelta
|
<InfoWithDelta
|
||||||
|
|||||||
+43
-48
@@ -1,22 +1,22 @@
|
|||||||
import { literals } from '@blitzkit/i18n';
|
import { literals } from "@blitzkit/i18n";
|
||||||
import { Box, Card, Flex } from '@radix-ui/themes';
|
import { Box, Card, Flex } from "@radix-ui/themes";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from "react";
|
||||||
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
import { degToRad, radToDeg } from "three/src/math/MathUtils.js";
|
||||||
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from "../../../../../../../core/awaitables/modelDefinitions";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PITCH_TRANSITION,
|
DEFAULT_PITCH_TRANSITION,
|
||||||
applyPitchYawLimits,
|
applyPitchYawLimits,
|
||||||
} from '../../../../../../../core/blitz/applyPitchYawLimits';
|
} from "../../../../../../../core/blitz/applyPitchYawLimits";
|
||||||
import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from "../../../../../../../core/blitzkit/hasEquipment";
|
||||||
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
|
import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTransform";
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from "../../../../../../../core/radix/var";
|
||||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../../stores/duel';
|
import { Duel } from "../../../../../../../stores/duel";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { VisualizerCard } from '../VisualizerCard';
|
import { VisualizerCard } from "../VisualizerCard";
|
||||||
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
import { VisualizerCornerStat } from "../VisualizerCornerStat";
|
||||||
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
import { FlexibilityCanvas } from "./components/FlexibilityCanvas";
|
||||||
|
|
||||||
const ANGLE_COEFFICIENT = 1 / 10;
|
const ANGLE_COEFFICIENT = 1 / 10;
|
||||||
|
|
||||||
@@ -40,7 +40,6 @@ export function GunFlexibilityVisualizer({
|
|||||||
}: MaybeSkeletonComponentProps) {
|
}: MaybeSkeletonComponentProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
const duelStore = Duel.useStore();
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
@@ -52,10 +51,10 @@ export function GunFlexibilityVisualizer({
|
|||||||
const [yaw, setYaw] = useState(0);
|
const [yaw, setYaw] = useState(0);
|
||||||
const [pitch, setPitch] = useState(0);
|
const [pitch, setPitch] = useState(0);
|
||||||
const [minPitch, setMinPitch] = useState(
|
const [minPitch, setMinPitch] = useState(
|
||||||
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max),
|
-degToRad(gunModel.pitch.front?.max ?? gunModel.pitch.max)
|
||||||
);
|
);
|
||||||
const [maxPitch, setMaxPitch] = useState(
|
const [maxPitch, setMaxPitch] = useState(
|
||||||
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min),
|
-degToRad(gunModel.pitch.front?.min ?? gunModel.pitch.min)
|
||||||
);
|
);
|
||||||
|
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
@@ -68,7 +67,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
const b = p.back;
|
const b = p.back;
|
||||||
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
||||||
|
|
||||||
let d = '';
|
let d = "";
|
||||||
|
|
||||||
// right
|
// right
|
||||||
const mo = mag(p.max);
|
const mo = mag(p.max);
|
||||||
@@ -169,10 +168,10 @@ export function GunFlexibilityVisualizer({
|
|||||||
return (
|
return (
|
||||||
<Flex direction="column-reverse" gap="0">
|
<Flex direction="column-reverse" gap="0">
|
||||||
<Box mt="-3" px="2">
|
<Box mt="-3" px="2">
|
||||||
<Card variant="classic" style={{ height: '10rem' }}>
|
<Card variant="classic" style={{ height: "10rem" }}>
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(var(--gray-1), var(--gray-3))',
|
background: "linear-gradient(var(--gray-1), var(--gray-3))",
|
||||||
}}
|
}}
|
||||||
position="absolute"
|
position="absolute"
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -195,8 +194,8 @@ export function GunFlexibilityVisualizer({
|
|||||||
transparent,
|
transparent,
|
||||||
transparent ${Math.PI / 2 - maxPitch}rad,
|
transparent ${Math.PI / 2 - maxPitch}rad,
|
||||||
|
|
||||||
${Var('cyan-a4')} ${Math.PI / 2 - maxPitch}rad,
|
${Var("cyan-a4")} ${Math.PI / 2 - maxPitch}rad,
|
||||||
${Var('jade-a4')} ${Math.PI / 2 - minPitch}rad,
|
${Var("jade-a4")} ${Math.PI / 2 - minPitch}rad,
|
||||||
|
|
||||||
transparent ${Math.PI / 2 - minPitch}rad,
|
transparent ${Math.PI / 2 - minPitch}rad,
|
||||||
transparent 180deg
|
transparent 180deg
|
||||||
@@ -209,7 +208,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
|
|
||||||
<VisualizerCard
|
<VisualizerCard
|
||||||
ref={container}
|
ref={container}
|
||||||
style={{ touchAction: 'none' }}
|
style={{ touchAction: "none" }}
|
||||||
onPointerMove={(event) => {
|
onPointerMove={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
@@ -221,19 +220,15 @@ export function GunFlexibilityVisualizer({
|
|||||||
const v = event.clientY - rect.top - rect.height / 2;
|
const v = event.clientY - rect.top - rect.height / 2;
|
||||||
let yaw = Math.atan2(u, -v);
|
let yaw = Math.atan2(u, -v);
|
||||||
|
|
||||||
const {
|
|
||||||
equipmentMatrix,
|
|
||||||
tank: { equipment_preset },
|
|
||||||
} = duelStore.getState().protagonist;
|
|
||||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||||
122,
|
122,
|
||||||
equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||||
124,
|
124,
|
||||||
equipment_preset,
|
Duel.state.protagonist.tank.equipment_preset,
|
||||||
equipmentMatrix,
|
Duel.state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
|
|
||||||
const min = applyPitchYawLimits(
|
const min = applyPitchYawLimits(
|
||||||
@@ -242,7 +237,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
p,
|
p,
|
||||||
y,
|
y,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
const max = applyPitchYawLimits(
|
const max = applyPitchYawLimits(
|
||||||
Math.PI,
|
Math.PI,
|
||||||
@@ -250,7 +245,7 @@ export function GunFlexibilityVisualizer({
|
|||||||
p,
|
p,
|
||||||
y,
|
y,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
|
|
||||||
yaw = min[1];
|
yaw = min[1];
|
||||||
@@ -276,8 +271,8 @@ export function GunFlexibilityVisualizer({
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: Var('radius-2'),
|
borderRadius: Var("radius-2"),
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -286,18 +281,18 @@ export function GunFlexibilityVisualizer({
|
|||||||
style={{
|
style={{
|
||||||
opacity: 2 ** -3,
|
opacity: 2 ** -3,
|
||||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
||||||
boxShadow: 'inset 0 0 4rem 1rem black',
|
boxShadow: "inset 0 0 4rem 1rem black",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: 'calc(100%)',
|
width: "calc(100%)",
|
||||||
height: 'calc(100%)',
|
height: "calc(100%)",
|
||||||
}}
|
}}
|
||||||
viewBox="-1 -1 2 2"
|
viewBox="-1 -1 2 2"
|
||||||
>
|
>
|
||||||
@@ -340,11 +335,11 @@ export function GunFlexibilityVisualizer({
|
|||||||
width="1pt"
|
width="1pt"
|
||||||
height="14rem"
|
height="14rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: Var('radius-1'),
|
borderRadius: Var("radius-1"),
|
||||||
background: `linear-gradient(${Var('gray-a11')}, ${Var('gray-a6')})`,
|
background: `linear-gradient(${Var("gray-a11")}, ${Var("gray-a6")})`,
|
||||||
boxShadow:
|
boxShadow:
|
||||||
'var(--white-a12) -0.25rem 0 1rem, var(--black-a12) 0.25rem 0 1rem',
|
"var(--white-a12) -0.25rem 0 1rem, var(--black-a12) 0.25rem 0 1rem",
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -356,9 +351,9 @@ export function GunFlexibilityVisualizer({
|
|||||||
width="0.25rem"
|
width="0.25rem"
|
||||||
height="0.25rem"
|
height="0.25rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '50%',
|
borderRadius: "50%",
|
||||||
backgroundColor: Var('gray-12'),
|
backgroundColor: Var("gray-12"),
|
||||||
transform: 'translate(-50%, 50%)',
|
transform: "translate(-50%, 50%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
+19
-19
@@ -1,10 +1,10 @@
|
|||||||
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
|
import { CaretDownIcon, CaretUpIcon } from "@radix-ui/react-icons";
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||||
import fuzzysort from 'fuzzysort';
|
import fuzzysort from "fuzzysort";
|
||||||
import { Quicklime } from 'quicklime';
|
import { Quicklime } from "quicklime";
|
||||||
import { useEffect, useRef, type ReactNode } from 'react';
|
import { useEffect, useRef, type ReactNode } from "react";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { Mark } from '../../../../../Mark';
|
import { Mark } from "../../../../../Mark";
|
||||||
|
|
||||||
export const highlightedRows = new Set<HTMLDivElement>();
|
export const highlightedRows = new Set<HTMLDivElement>();
|
||||||
export const highlightedRowsUpdate = new Quicklime();
|
export const highlightedRowsUpdate = new Quicklime();
|
||||||
@@ -16,7 +16,7 @@ export interface InfoProps {
|
|||||||
delta?: number;
|
delta?: number;
|
||||||
decimals?: number;
|
decimals?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Info({
|
export function Info({
|
||||||
@@ -26,15 +26,15 @@ export function Info({
|
|||||||
delta,
|
delta,
|
||||||
decimals,
|
decimals,
|
||||||
prefix,
|
prefix,
|
||||||
deltaType = 'higherIsBetter',
|
deltaType = "higherIsBetter",
|
||||||
}: InfoProps) {
|
}: InfoProps) {
|
||||||
const statSearch = TankopediaEphemeral.use((state) => state.statSearch);
|
const statSearch = Tankopedia.use((state) => state.statSearch);
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
let label = name;
|
let label = name;
|
||||||
let highlighted = false;
|
let highlighted = false;
|
||||||
|
|
||||||
if (statSearch !== undefined && typeof name === 'string') {
|
if (statSearch !== undefined && typeof name === "string") {
|
||||||
const result = fuzzysort.single(statSearch, name);
|
const result = fuzzysort.single(statSearch, name);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -62,7 +62,7 @@ export function Info({
|
|||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
pl={indent ? '2' : '0'}
|
pl={indent ? "2" : "0"}
|
||||||
width="100%"
|
width="100%"
|
||||||
gap="4"
|
gap="4"
|
||||||
ref={container}
|
ref={container}
|
||||||
@@ -77,9 +77,9 @@ export function Info({
|
|||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
|
delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
|
||||||
? 'green'
|
? "green"
|
||||||
: 'tomato'
|
: "tomato"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{decimals !== undefined
|
{decimals !== undefined
|
||||||
@@ -88,9 +88,9 @@ export function Info({
|
|||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
color={
|
color={
|
||||||
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
|
delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
|
||||||
? 'green'
|
? "green"
|
||||||
: 'tomato'
|
: "tomato"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
|
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
|
||||||
@@ -105,7 +105,7 @@ export function Info({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{decimals !== undefined && typeof children === 'number'
|
{decimals !== undefined && typeof children === "number"
|
||||||
? children.toFixed(decimals)
|
? children.toFixed(decimals)
|
||||||
: children}
|
: children}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
+39
-46
@@ -1,27 +1,27 @@
|
|||||||
import { createDefaultSkills } from '@blitzkit/core';
|
import { createDefaultSkills } from "@blitzkit/core";
|
||||||
import { Flex, Progress, Text } from '@radix-ui/themes';
|
import { Flex, Progress, Text } from "@radix-ui/themes";
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from "lodash-es";
|
||||||
import { memo, useMemo, type ComponentProps, type ReactNode } from 'react';
|
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
||||||
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from "../../../../../../core/awaitables/equipmentDefinitions";
|
||||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../../../../../core/awaitables/provisionDefinitions";
|
||||||
import { awaitableSkillDefinitions } from '../../../../../../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from "../../../../../../core/awaitables/skillDefinitions";
|
||||||
import { awaitableTankDefinitions } from '../../../../../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../../../../../../core/awaitables/tankDefinitions";
|
||||||
import {
|
import {
|
||||||
tankCharacteristics,
|
tankCharacteristics,
|
||||||
type TankCharacteristics,
|
type TankCharacteristics,
|
||||||
} from '../../../../../../core/blitzkit/tankCharacteristics';
|
} from "../../../../../../core/blitzkit/tankCharacteristics";
|
||||||
import { tankToDuelMember } from '../../../../../../core/blitzkit/tankToDuelMember';
|
import { tankToDuelMember } from "../../../../../../core/blitzkit/tankToDuelMember";
|
||||||
import { useDelta } from '../../../../../../hooks/useDelta';
|
import { useDelta } from "../../../../../../hooks/useDelta";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import {
|
import {
|
||||||
TankopediaEphemeral,
|
Tankopedia,
|
||||||
TankopediaRelativeAgainst,
|
TankopediaRelativeAgainst,
|
||||||
} from '../../../../../../stores/tankopediaEphemeral';
|
} from "../../../../../../stores/tankopedia";
|
||||||
import { Info, type InfoProps } from './Info';
|
import { Info, type InfoProps } from "./Info";
|
||||||
|
|
||||||
type InfoWithDeltaProps = Omit<InfoProps, 'name'> & {
|
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
||||||
stats: TankCharacteristics;
|
stats: TankCharacteristics;
|
||||||
noRanking?: boolean;
|
noRanking?: boolean;
|
||||||
} & (
|
} & (
|
||||||
@@ -51,20 +51,18 @@ const [
|
|||||||
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||||
({ stats, indent, noRanking, deltaType, ...props }) => {
|
({ stats, indent, noRanking, deltaType, ...props }) => {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const relativeAgainst = TankopediaEphemeral.use(
|
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
||||||
(state) => state.relativeAgainst,
|
|
||||||
);
|
|
||||||
const uhWhatDoICallThisVariable =
|
const uhWhatDoICallThisVariable =
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(stats)!
|
? props.value(stats)!
|
||||||
: (stats[props.value] as number);
|
: (stats[props.value] as number);
|
||||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
const delta = useDelta(uhWhatDoICallThisVariable);
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||||
const shellIndex = Duel.use((state) =>
|
const shellIndex = Duel.use((state) =>
|
||||||
state.protagonist.gun.shells.indexOf(state.protagonist.shell),
|
state.protagonist.gun.shells.indexOf(state.protagonist.shell)
|
||||||
);
|
);
|
||||||
const equipmentMatrix = Duel.use(
|
const equipmentMatrix = Duel.use(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const others = useMemo(() => {
|
const others = useMemo(() => {
|
||||||
const defaultSkills = createDefaultSkills(skillDefinitions);
|
const defaultSkills = createDefaultSkills(skillDefinitions);
|
||||||
@@ -77,15 +75,10 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
tank.class === protagonistTank.class) ||
|
tank.class === protagonistTank.class) ||
|
||||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
||||||
tank.tier === protagonistTank.tier) ||
|
tank.tier === protagonistTank.tier) ||
|
||||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
relativeAgainst === TankopediaRelativeAgainst.All
|
||||||
)
|
)
|
||||||
.map((tank) => {
|
.map((tank) => {
|
||||||
const thisTankModel = modelDefinitions.models[tank.id];
|
const member = tankToDuelMember(tank, provisionDefinitions);
|
||||||
const member = tankToDuelMember(
|
|
||||||
tank,
|
|
||||||
thisTankModel,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (member.gun.shells[shellIndex] === undefined) {
|
if (member.gun.shells[shellIndex] === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -117,14 +110,14 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
equipmentDefinitions,
|
equipmentDefinitions,
|
||||||
provisionDefinitions,
|
provisionDefinitions,
|
||||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
tankModelDefinition: modelDefinitions.models[tank.id],
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.filter((tank) => {
|
.filter((tank) => {
|
||||||
if (tank === undefined) return false;
|
if (tank === undefined) return false;
|
||||||
|
|
||||||
const othersValue =
|
const othersValue =
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(tank)!
|
? props.value(tank)!
|
||||||
: (tank[props.value] as number);
|
: (tank[props.value] as number);
|
||||||
|
|
||||||
@@ -133,22 +126,22 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
||||||
const betterTanks = others.filter((tank) => {
|
const betterTanks = others.filter((tank) => {
|
||||||
const othersValue =
|
const othersValue =
|
||||||
typeof props.value === 'function'
|
typeof props.value === "function"
|
||||||
? props.value(tank)!
|
? props.value(tank)!
|
||||||
: (tank[props.value] as number);
|
: (tank[props.value] as number);
|
||||||
|
|
||||||
if (othersValue === undefined) return false;
|
if (othersValue === undefined) return false;
|
||||||
return deltaType === 'lowerIsBetter'
|
return deltaType === "lowerIsBetter"
|
||||||
? othersValue < uhWhatDoICallThisVariable
|
? othersValue < uhWhatDoICallThisVariable
|
||||||
: othersValue > uhWhatDoICallThisVariable;
|
: othersValue > uhWhatDoICallThisVariable;
|
||||||
});
|
});
|
||||||
const goodness = (others.length - betterTanks.length) / others.length;
|
const goodness = (others.length - betterTanks.length) / others.length;
|
||||||
let color: ComponentProps<typeof Progress>['color'];
|
let color: ComponentProps<typeof Progress>["color"];
|
||||||
|
|
||||||
if (goodness <= 0.25) color = 'red';
|
if (goodness <= 0.25) color = "red";
|
||||||
else if (goodness <= 0.5) color = 'orange';
|
else if (goodness <= 0.5) color = "orange";
|
||||||
else if (goodness <= 0.75) color = 'yellow';
|
else if (goodness <= 0.75) color = "yellow";
|
||||||
else color = 'green';
|
else color = "green";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column">
|
<Flex direction="column">
|
||||||
@@ -156,7 +149,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
deltaType={deltaType}
|
deltaType={deltaType}
|
||||||
indent={indent}
|
indent={indent}
|
||||||
name={
|
name={
|
||||||
'name' in props
|
"name" in props
|
||||||
? props.name
|
? props.name
|
||||||
: strings.website.tools.tankopedia.characteristics.values[
|
: strings.website.tools.tankopedia.characteristics.values[
|
||||||
props.value
|
props.value
|
||||||
@@ -169,17 +162,17 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
{!noRanking && (
|
{!noRanking && (
|
||||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2">
|
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
||||||
<Progress
|
<Progress
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size="1"
|
size="1"
|
||||||
value={goodness * 100}
|
value={goodness * 100}
|
||||||
color={color}
|
color={color}
|
||||||
style={{ height: '0.125rem', opacity: 0.5 }}
|
style={{ height: "0.125rem", opacity: 0.5 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text color="gray" size="1">
|
<Text color="gray" size="1">
|
||||||
{clamp(betterTanks.length + 1, 1, others.length)} /{' '}
|
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
||||||
{others.length}
|
{others.length}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -189,6 +182,6 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
|||||||
},
|
},
|
||||||
|
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(typeof a.value === 'function' ? a.value(a.stats) : a.stats[a.value]) ===
|
(typeof a.value === "function" ? a.value(a.stats) : a.stats[a.value]) ===
|
||||||
(typeof b.value === 'function' ? b.value(b.stats) : b.stats[b.value]),
|
(typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value])
|
||||||
);
|
);
|
||||||
+28
-5
@@ -1,8 +1,10 @@
|
|||||||
import { Box, Code } from '@radix-ui/themes';
|
import { DoubleArrowRightIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Box, Button, Code } from '@radix-ui/themes';
|
||||||
import { times } from 'lodash-es';
|
import { times } from 'lodash-es';
|
||||||
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Var } from '../../../../../../../core/radix/var';
|
import { Var } from '../../../../../../../core/radix/var';
|
||||||
|
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||||
import { VisualizerCard } from '../VisualizerCard';
|
import { VisualizerCard } from '../VisualizerCard';
|
||||||
import { Shell } from './components/Shell';
|
import { Shell } from './components/Shell';
|
||||||
@@ -19,6 +21,8 @@ export interface ReloadUpdateData {
|
|||||||
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
||||||
|
|
||||||
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||||
|
const { strings } = useLocale();
|
||||||
|
|
||||||
const reloadCircle = useRef<HTMLDivElement>(null);
|
const reloadCircle = useRef<HTMLDivElement>(null);
|
||||||
const reloadCore = useRef<HTMLDivElement>(null);
|
const reloadCore = useRef<HTMLDivElement>(null);
|
||||||
const reloadGlow = useRef<HTMLDivElement>(null);
|
const reloadGlow = useRef<HTMLDivElement>(null);
|
||||||
@@ -246,7 +250,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
|
|
||||||
<Code
|
<Code
|
||||||
ref={progressTime}
|
ref={progressTime}
|
||||||
size="1"
|
size="2"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
color="gray"
|
color="gray"
|
||||||
@@ -255,14 +259,14 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
right: 'calc(50% + var(--space-8))',
|
right: 'calc(50% + var(--space-8))',
|
||||||
transform: 'translateX(50%)',
|
transform: 'translate(50%, -50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
0.00
|
0.00
|
||||||
</Code>
|
</Code>
|
||||||
<Code
|
<Code
|
||||||
ref={totalTime}
|
ref={totalTime}
|
||||||
size="1"
|
size="2"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
color="gray"
|
color="gray"
|
||||||
@@ -270,7 +274,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: 'calc(50% + var(--space-8))',
|
left: 'calc(50% + var(--space-8))',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
0.00
|
0.00
|
||||||
@@ -290,6 +294,25 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="1"
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
highContrast
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '68%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
state.current = Math.floor(state.current + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strings.website.tools.tankopedia.visualizers.reload.skip}
|
||||||
|
<DoubleArrowRightIcon />
|
||||||
|
</Button>
|
||||||
</VisualizerCard>
|
</VisualizerCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+11
-11
@@ -1,12 +1,12 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from "@blitzkit/core";
|
||||||
import { Flex, IconButton, Tooltip } from '@radix-ui/themes';
|
import { Flex, IconButton, Tooltip } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../../../stores/duel';
|
import { Duel } from "../../../../../../../../stores/duel";
|
||||||
|
|
||||||
export function ShellSwitcher() {
|
export function ShellSwitcher() {
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
const shell = Duel.use((state) => state.protagonist.shell);
|
const shell = Duel.use((state) => state.protagonist.shell);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const { unwrap } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -18,26 +18,26 @@ export function ShellSwitcher() {
|
|||||||
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
<Tooltip content={unwrap(thisShell.name)} key={thisShell.id}>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="3"
|
size="3"
|
||||||
color={selected ? undefined : 'gray'}
|
color={selected ? undefined : "gray"}
|
||||||
variant={selected ? 'solid' : 'soft'}
|
variant={selected ? "solid" : "soft"}
|
||||||
highContrast={selected}
|
highContrast={selected}
|
||||||
style={{
|
style={{
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
borderBottomLeftRadius: shellIndex === 0 ? '50%' : 0,
|
borderBottomLeftRadius: shellIndex === 0 ? "50%" : 0,
|
||||||
borderTopRightRadius: 0,
|
borderTopRightRadius: 0,
|
||||||
borderBottomRightRadius:
|
borderBottomRightRadius:
|
||||||
shellIndex === gun.shells.length - 1 ? '50%' : 0,
|
shellIndex === gun.shells.length - 1 ? "50%" : 0,
|
||||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.shell = thisShell;
|
draft.protagonist.shell = thisShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt={unwrap(thisShell.name)}
|
alt={unwrap(thisShell.name)}
|
||||||
style={{ width: '1.125rem', height: '1.125rem' }}
|
style={{ width: "1.125rem", height: "1.125rem" }}
|
||||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
+19
-20
@@ -1,16 +1,15 @@
|
|||||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
import { Flex, Spinner, TextField, type FlexProps } from '@radix-ui/themes';
|
import { Flex, Spinner, TextField, type FlexProps } from "@radix-ui/themes";
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { highlightedRows, highlightedRowsUpdate } from './Info';
|
import { highlightedRows, highlightedRowsUpdate } from "./Info";
|
||||||
|
|
||||||
export function StatSearch(props: FlexProps) {
|
export function StatSearch(props: FlexProps) {
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const count = useRef<HTMLDivElement>(null);
|
const count = useRef<HTMLDivElement>(null);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const search = useCallback(() => {
|
const search = useCallback(() => {
|
||||||
@@ -20,7 +19,7 @@ export function StatSearch(props: FlexProps) {
|
|||||||
setSearching(true);
|
setSearching(true);
|
||||||
} else {
|
} else {
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
count.current.textContent = '';
|
count.current.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -31,21 +30,21 @@ export function StatSearch(props: FlexProps) {
|
|||||||
if (!input.current || !count.current) return;
|
if (!input.current || !count.current) return;
|
||||||
|
|
||||||
if (input.current.value.length === 0) {
|
if (input.current.value.length === 0) {
|
||||||
count.current.textContent = '';
|
count.current.textContent = "";
|
||||||
|
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.statSearch = undefined;
|
draft.statSearch = undefined;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSearching(false);
|
setSearching(false);
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.statSearch = input.current!.value;
|
draft.statSearch = input.current!.value;
|
||||||
});
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[],
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,7 +52,7 @@ export function StatSearch(props: FlexProps) {
|
|||||||
if (!count.current || !input.current) return;
|
if (!count.current || !input.current) return;
|
||||||
|
|
||||||
count.current.textContent =
|
count.current.textContent =
|
||||||
input.current.value.length === 0 ? '' : `0 / ${highlightedRows.size}`;
|
input.current.value.length === 0 ? "" : `0 / ${highlightedRows.size}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
highlightedRowsUpdate.on(handleHighlightedRowsUpdate);
|
highlightedRowsUpdate.on(handleHighlightedRowsUpdate);
|
||||||
@@ -69,23 +68,23 @@ export function StatSearch(props: FlexProps) {
|
|||||||
ref={input}
|
ref={input}
|
||||||
placeholder={strings.website.tools.tankopedia.search}
|
placeholder={strings.website.tools.tankopedia.search}
|
||||||
variant="classic"
|
variant="classic"
|
||||||
style={{ width: '100%' }}
|
style={{ width: "100%" }}
|
||||||
onChange={search}
|
onChange={search}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (!count.current) return;
|
if (!count.current) return;
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
const newIndex = (index + 1) % highlightedRows.size;
|
const newIndex = (index + 1) % highlightedRows.size;
|
||||||
count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`;
|
count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`;
|
||||||
const array = [...highlightedRows];
|
const array = [...highlightedRows];
|
||||||
|
|
||||||
array.forEach((element) => element.classList.remove('focused'));
|
array.forEach((element) => element.classList.remove("focused"));
|
||||||
|
|
||||||
if (array[newIndex]) {
|
if (array[newIndex]) {
|
||||||
array[newIndex].classList.add('focused');
|
array[newIndex].classList.add("focused");
|
||||||
array[newIndex].scrollIntoView({
|
array[newIndex].scrollIntoView({
|
||||||
block: 'center',
|
block: "center",
|
||||||
inline: 'center',
|
inline: "center",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-31
@@ -1,19 +1,19 @@
|
|||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from "@radix-ui/themes";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from "../../../../../core/awaitables/equipmentDefinitions";
|
||||||
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../../../../core/awaitables/provisionDefinitions";
|
||||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
||||||
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
|
||||||
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
import { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
|
||||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from "../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
||||||
import { Crew } from './components/Crew';
|
import { Crew } from "./components/Crew";
|
||||||
import { Firepower } from './components/Firepower';
|
import { Firepower } from "./components/Firepower";
|
||||||
import { Maneuverability } from './components/Maneuverability';
|
import { Maneuverability } from "./components/Maneuverability";
|
||||||
import { Survivability } from './components/Survivability';
|
import { Survivability } from "./components/Survivability";
|
||||||
|
|
||||||
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||||
awaitableEquipmentDefinitions,
|
awaitableEquipmentDefinitions,
|
||||||
@@ -21,19 +21,18 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const mutateDuel = Duel.useMutation();
|
const crewSkills = Tankopedia.use((state) => state.skills);
|
||||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
|
||||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||||
const assaultDistance = Duel.use(
|
const assaultDistance = Duel.use(
|
||||||
(state) => state.protagonist.assaultDistance,
|
(state) => state.protagonist.assaultDistance
|
||||||
);
|
);
|
||||||
const equipmentMatrix = Duel.use(
|
const equipmentMatrix = Duel.use(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const { tank, turret, gun, engine, track, shell } = Duel.use(
|
const { tank, turret, gun, engine, track, shell } = Duel.use(
|
||||||
(state) => state.protagonist,
|
(state) => state.protagonist
|
||||||
);
|
);
|
||||||
const stockEngine = tank.engines[0];
|
const stockEngine = tank.engines[0];
|
||||||
const stockTrack = tank.tracks[0];
|
const stockTrack = tank.tracks[0];
|
||||||
@@ -68,7 +67,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
tankModelDefinition,
|
tankModelDefinition,
|
||||||
equipmentDefinitions: equipmentDefinitions,
|
equipmentDefinitions: equipmentDefinitions,
|
||||||
provisionDefinitions: provisionDefinitions,
|
provisionDefinitions: provisionDefinitions,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||||
@@ -81,7 +80,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
gunModelDefinition.pitch,
|
gunModelDefinition.pitch,
|
||||||
turretModelDefinition.yaw,
|
turretModelDefinition.yaw,
|
||||||
hasImprovedVerticalStabilizer,
|
hasImprovedVerticalStabilizer,
|
||||||
hasDownImprovedVerticalStabilizer,
|
hasDownImprovedVerticalStabilizer
|
||||||
);
|
);
|
||||||
|
|
||||||
modelTransformEvent.dispatch({ pitch, yaw });
|
modelTransformEvent.dispatch({ pitch, yaw });
|
||||||
@@ -90,25 +89,25 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
direction={{ initial: 'column', md: 'row' }}
|
direction={{ initial: "column", md: "row" }}
|
||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: "center", sm: "start" }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: "100%", sm: "auto" }}
|
||||||
>
|
>
|
||||||
<Firepower skeleton={skeleton} stats={stats} />
|
<Firepower skeleton={skeleton} stats={stats} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
direction={{ initial: 'column', lg: 'row' }}
|
direction={{ initial: "column", lg: "row" }}
|
||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: "center", sm: "start" }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: "100%", sm: "auto" }}
|
||||||
>
|
>
|
||||||
<Maneuverability stats={stats} />
|
<Maneuverability stats={stats} />
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
direction="column"
|
direction="column"
|
||||||
align={{ initial: 'center', sm: 'start' }}
|
align={{ initial: "center", sm: "start" }}
|
||||||
width={{ initial: '100%', sm: 'auto' }}
|
width={{ initial: "100%", sm: "auto" }}
|
||||||
>
|
>
|
||||||
<Survivability stats={stats} />
|
<Survivability stats={stats} />
|
||||||
<Crew stats={stats} />
|
<Crew stats={stats} />
|
||||||
|
|||||||
+16
-17
@@ -1,5 +1,5 @@
|
|||||||
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
|
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
||||||
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -7,31 +7,30 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Popover,
|
Popover,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { awaitableConsumableDefinitions } from '../../../../core/awaitables/consumableDefinitions';
|
import { awaitableConsumableDefinitions } from "../../../../core/awaitables/consumableDefinitions";
|
||||||
import { useEquipment } from '../../../../hooks/useEquipment';
|
import { useEquipment } from "../../../../hooks/useEquipment";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import { ConsumablesManager } from '../../../ConsumablesManager';
|
import { ConsumablesManager } from "../../../ConsumablesManager";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
const consumableDefinitions = await awaitableConsumableDefinitions;
|
const consumableDefinitions = await awaitableConsumableDefinitions;
|
||||||
|
|
||||||
export function Consumables() {
|
export function Consumables() {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const protagonist = Duel.use((state) => state.protagonist);
|
const protagonist = Duel.use((state) => state.protagonist);
|
||||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||||
const consumablesList = Object.values(
|
const consumablesList = Object.values(
|
||||||
consumableDefinitions.consumables,
|
consumableDefinitions.consumables
|
||||||
).filter((consumable) =>
|
).filter((consumable) =>
|
||||||
checkConsumableProvisionInclusivity(
|
checkConsumableProvisionInclusivity(
|
||||||
consumable,
|
consumable,
|
||||||
protagonist.tank,
|
protagonist.tank,
|
||||||
protagonist.gun,
|
protagonist.gun
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
const cooldownBooster = Duel.use(
|
const cooldownBooster = Duel.use(
|
||||||
(state) => state.protagonist.cooldownBooster,
|
(state) => state.protagonist.cooldownBooster
|
||||||
);
|
);
|
||||||
const hasConsumableDeliverySystem = useEquipment(118);
|
const hasConsumableDeliverySystem = useEquipment(118);
|
||||||
const hasHighEndConsumables = useEquipment(101);
|
const hasHighEndConsumables = useEquipment(101);
|
||||||
@@ -67,7 +66,7 @@ export function Consumables() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.consumables = [];
|
draft.protagonist.consumables = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -85,11 +84,11 @@ export function Consumables() {
|
|||||||
hasHighEndConsumables={hasHighEndConsumables}
|
hasHighEndConsumables={hasHighEndConsumables}
|
||||||
timers
|
timers
|
||||||
onConsumablesChange={(consumables) => {
|
onConsumablesChange={(consumables) => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.consumables = consumables;
|
draft.protagonist.consumables = consumables;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
justify={{ initial: 'center', sm: 'start' }}
|
justify={{ initial: "center", sm: "start" }}
|
||||||
/>
|
/>
|
||||||
</ConfigurationChildWrapper>
|
</ConfigurationChildWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
+9
-10
@@ -1,19 +1,18 @@
|
|||||||
import { Button, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||||
import { awaitableEquipmentDefinitions } from '../../../../core/awaitables/equipmentDefinitions';
|
import { awaitableEquipmentDefinitions } from "../../../../core/awaitables/equipmentDefinitions";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import { EquipmentManager } from '../../../EquipmentManager';
|
import { EquipmentManager } from "../../../EquipmentManager";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
||||||
|
|
||||||
export function Equipment() {
|
export function Equipment() {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const protagonist = Duel.use((state) => state.protagonist);
|
const protagonist = Duel.use((state) => state.protagonist);
|
||||||
const equipmentPreset =
|
const equipmentPreset =
|
||||||
equipmentDefinitions.presets[protagonist.tank.equipment_preset];
|
equipmentDefinitions.presets[protagonist.tank.equipment_preset];
|
||||||
const equipmentMatrix = Duel.use(
|
const equipmentMatrix = Duel.use(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
(state) => state.protagonist.equipmentMatrix
|
||||||
);
|
);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ export function Equipment() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.equipmentMatrix.forEach((row) => {
|
draft.protagonist.equipmentMatrix.forEach((row) => {
|
||||||
row.forEach((_, index) => {
|
row.forEach((_, index) => {
|
||||||
row[index] = 0;
|
row[index] = 0;
|
||||||
@@ -44,7 +43,7 @@ export function Equipment() {
|
|||||||
matrix={equipmentMatrix}
|
matrix={equipmentMatrix}
|
||||||
preset={equipmentPreset}
|
preset={equipmentPreset}
|
||||||
onChange={(matrix) => {
|
onChange={(matrix) => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.equipmentMatrix = matrix;
|
draft.protagonist.equipmentMatrix = matrix;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+18
-19
@@ -1,15 +1,14 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from "@blitzkit/core";
|
||||||
import { Flex, Heading } from '@radix-ui/themes';
|
import { Flex, Heading } from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import { GenericTankComponentButton } from '../../../ModuleButtons/GenericTankComponentButton';
|
import { GenericTankComponentButton } from "../../../ModuleButtons/GenericTankComponentButton";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
export function Miscellaneous() {
|
export function Miscellaneous() {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||||
const cooldownBooster = Duel.use(
|
const cooldownBooster = Duel.use(
|
||||||
(state) => state.protagonist.cooldownBooster,
|
(state) => state.protagonist.cooldownBooster
|
||||||
);
|
);
|
||||||
const { tank } = Duel.use((state) => state.protagonist);
|
const { tank } = Duel.use((state) => state.protagonist);
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
@@ -23,37 +22,37 @@ export function Miscellaneous() {
|
|||||||
<Flex gap="2" align="center">
|
<Flex gap="2" align="center">
|
||||||
{!tank.fixed_camouflage && (
|
{!tank.fixed_camouflage && (
|
||||||
<GenericTankComponentButton
|
<GenericTankComponentButton
|
||||||
icon={asset('icons/camo.webp')}
|
icon={asset("icons/camo.webp")}
|
||||||
selected={camouflage}
|
selected={camouflage}
|
||||||
iconStyles={{
|
iconStyles={{
|
||||||
top: '50%',
|
top: "50%",
|
||||||
left: '50%',
|
left: "50%",
|
||||||
transform: 'translate(calc(-50% + 4px), calc(-50% + 4px))',
|
transform: "translate(calc(-50% + 4px), calc(-50% + 4px))",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.camouflage = !camouflage;
|
draft.protagonist.camouflage = !camouflage;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<GenericTankComponentButton
|
<GenericTankComponentButton
|
||||||
icon={asset('icons/boosters/equipment.webp')}
|
icon={asset("icons/boosters/equipment.webp")}
|
||||||
selected={cooldownBooster > 0}
|
selected={cooldownBooster > 0}
|
||||||
banner={
|
banner={
|
||||||
cooldownBooster === 0
|
cooldownBooster === 0
|
||||||
? undefined
|
? undefined
|
||||||
: cooldownBooster === 1
|
: cooldownBooster === 1
|
||||||
? '#afb0abc0'
|
? "#afb0abc0"
|
||||||
: cooldownBooster === 2
|
: cooldownBooster === 2
|
||||||
? '#5f72cbc0'
|
? "#5f72cbc0"
|
||||||
: '#9b3cc0c0'
|
: "#9b3cc0c0"
|
||||||
}
|
}
|
||||||
iconStyles={{
|
iconStyles={{
|
||||||
transform: 'translate(-50%, -50%) scale(0.8)',
|
transform: "translate(-50%, -50%) scale(0.8)",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.cooldownBooster++;
|
draft.protagonist.cooldownBooster++;
|
||||||
if (draft.protagonist.cooldownBooster === 4)
|
if (draft.protagonist.cooldownBooster === 4)
|
||||||
draft.protagonist.cooldownBooster = 0;
|
draft.protagonist.cooldownBooster = 0;
|
||||||
|
|||||||
+44
-45
@@ -10,7 +10,7 @@ import {
|
|||||||
TurretDefinition,
|
TurretDefinition,
|
||||||
type TankDefinition,
|
type TankDefinition,
|
||||||
type Unlock,
|
type Unlock,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
@@ -18,12 +18,12 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import { LinkI18n } from '../../../LinkI18n';
|
import { LinkI18n } from "../../../LinkI18n";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
type ModuleDefinition =
|
type ModuleDefinition =
|
||||||
| TurretDefinition
|
| TurretDefinition
|
||||||
@@ -32,7 +32,7 @@ type ModuleDefinition =
|
|||||||
| EngineDefinition;
|
| EngineDefinition;
|
||||||
|
|
||||||
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
function extractModule(raw: ModuleDefinition | TankDefinition) {
|
||||||
if ('gun_type' in raw) {
|
if ("gun_type" in raw) {
|
||||||
return raw;
|
return raw;
|
||||||
} else
|
} else
|
||||||
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
return raw as Exclude<ModuleDefinition | TankDefinition, GunDefinition>;
|
||||||
@@ -43,11 +43,11 @@ interface ModuleWithUnlocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const moduleTypeTypeIconName: Record<ModuleType, string> = {
|
const moduleTypeTypeIconName: Record<ModuleType, string> = {
|
||||||
[ModuleType.VEHICLE]: '',
|
[ModuleType.VEHICLE]: "",
|
||||||
[ModuleType.ENGINE]: 'engine',
|
[ModuleType.ENGINE]: "engine",
|
||||||
[ModuleType.TRACKS]: 'chassis',
|
[ModuleType.TRACKS]: "chassis",
|
||||||
[ModuleType.TURRET]: 'turret',
|
[ModuleType.TURRET]: "turret",
|
||||||
[ModuleType.GUN]: 'gun',
|
[ModuleType.GUN]: "gun",
|
||||||
};
|
};
|
||||||
|
|
||||||
function ModuleButton({
|
function ModuleButton({
|
||||||
@@ -70,11 +70,11 @@ function ModuleButton({
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="4"
|
size="4"
|
||||||
radius="small"
|
radius="small"
|
||||||
variant={selected ? 'surface' : 'soft'}
|
variant={selected ? "surface" : "soft"}
|
||||||
color={top ? 'amber' : selected ? undefined : 'gray'}
|
color={top ? "amber" : selected ? undefined : "gray"}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
width: isTank ? 72 : undefined,
|
width: isTank ? 72 : undefined,
|
||||||
height: isTank ? 72 : undefined,
|
height: isTank ? 72 : undefined,
|
||||||
}}
|
}}
|
||||||
@@ -83,7 +83,7 @@ function ModuleButton({
|
|||||||
size="1"
|
size="1"
|
||||||
color="gray"
|
color="gray"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
left: isTank ? 8 : 4,
|
left: isTank ? 8 : 4,
|
||||||
top: isTank ? 8 : 4,
|
top: isTank ? 8 : 4,
|
||||||
}}
|
}}
|
||||||
@@ -94,13 +94,13 @@ function ModuleButton({
|
|||||||
{unlock.cost.value > 0 && (
|
{unlock.cost.value > 0 && (
|
||||||
<Flex
|
<Flex
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
left: '50%',
|
left: "50%",
|
||||||
bottom: 4,
|
bottom: 4,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
}}
|
}}
|
||||||
align="center"
|
align="center"
|
||||||
gap={isTank ? '1' : undefined}
|
gap={isTank ? "1" : undefined}
|
||||||
>
|
>
|
||||||
<Text size="1" color="gray">
|
<Text size="1" color="gray">
|
||||||
{formatCompact(locale, unlock.cost.value)}
|
{formatCompact(locale, unlock.cost.value)}
|
||||||
@@ -112,7 +112,7 @@ function ModuleButton({
|
|||||||
style={{
|
style={{
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -128,7 +128,7 @@ function ModuleButton({
|
|||||||
style={{
|
style={{
|
||||||
width: isTank ? 64 : 32,
|
width: isTank ? 64 : 32,
|
||||||
height: isTank ? 64 : 32,
|
height: isTank ? 64 : 32,
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -149,7 +149,6 @@ function ModuleButton({
|
|||||||
const tankDefinitions = await awaitableTankDefinitions;
|
const tankDefinitions = await awaitableTankDefinitions;
|
||||||
|
|
||||||
export function Modules() {
|
export function Modules() {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const hasUpgrades =
|
const hasUpgrades =
|
||||||
tank.turrets.length > 1 ||
|
tank.turrets.length > 1 ||
|
||||||
@@ -171,15 +170,15 @@ export function Modules() {
|
|||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
function setByUnlock(unlock: Unlock) {
|
function setByUnlock(unlock: Unlock) {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
if (unlock.type === ModuleType.TURRET) {
|
if (unlock.type === ModuleType.TURRET) {
|
||||||
draft.protagonist.turret = draft.protagonist.tank.turrets.find(
|
draft.protagonist.turret = draft.protagonist.tank.turrets.find(
|
||||||
(turret) => turret.id === unlock.id,
|
(turret) => turret.id === unlock.id
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!draft.protagonist.turret.guns.some(
|
!draft.protagonist.turret.guns.some(
|
||||||
(gun) => gun.id === draft.protagonist.gun.id,
|
(gun) => gun.id === draft.protagonist.gun.id
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||||
@@ -187,7 +186,7 @@ export function Modules() {
|
|||||||
}
|
}
|
||||||
} else if (unlock.type === ModuleType.GUN) {
|
} else if (unlock.type === ModuleType.GUN) {
|
||||||
const gunInTurret = draft.protagonist.turret.guns.find(
|
const gunInTurret = draft.protagonist.turret.guns.find(
|
||||||
(gun) => gun.id === unlock.id,
|
(gun) => gun.id === unlock.id
|
||||||
);
|
);
|
||||||
if (gunInTurret) {
|
if (gunInTurret) {
|
||||||
draft.protagonist.gun = gunInTurret;
|
draft.protagonist.gun = gunInTurret;
|
||||||
@@ -195,10 +194,10 @@ export function Modules() {
|
|||||||
} else {
|
} else {
|
||||||
// TODO: warn somehow?
|
// TODO: warn somehow?
|
||||||
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
const suitableTurret = draft.protagonist.tank.turrets.find((turret) =>
|
||||||
turret.guns.some((gun) => gun.id === unlock.id),
|
turret.guns.some((gun) => gun.id === unlock.id)
|
||||||
)!;
|
)!;
|
||||||
const gunInSuitableTurret = suitableTurret.guns.find(
|
const gunInSuitableTurret = suitableTurret.guns.find(
|
||||||
(gun) => gun.id === unlock.id,
|
(gun) => gun.id === unlock.id
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
draft.protagonist.turret = suitableTurret;
|
draft.protagonist.turret = suitableTurret;
|
||||||
@@ -207,11 +206,11 @@ export function Modules() {
|
|||||||
}
|
}
|
||||||
} else if (unlock.type === ModuleType.ENGINE) {
|
} else if (unlock.type === ModuleType.ENGINE) {
|
||||||
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
draft.protagonist.engine = draft.protagonist.tank.engines.find(
|
||||||
(engine) => engine.id === unlock.id,
|
(engine) => engine.id === unlock.id
|
||||||
)!;
|
)!;
|
||||||
} else if (unlock.type === ModuleType.TRACKS) {
|
} else if (unlock.type === ModuleType.TRACKS) {
|
||||||
draft.protagonist.track = draft.protagonist.tank.tracks.find(
|
draft.protagonist.track = draft.protagonist.tank.tracks.find(
|
||||||
(track) => track.id === unlock.id,
|
(track) => track.id === unlock.id
|
||||||
)!;
|
)!;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -235,7 +234,7 @@ export function Modules() {
|
|||||||
} else if (unlock.type === ModuleType.GUN) {
|
} else if (unlock.type === ModuleType.GUN) {
|
||||||
module = tank.turrets
|
module = tank.turrets
|
||||||
.find((turret) =>
|
.find((turret) =>
|
||||||
turret.guns.some((gun) => gun.id === unlock.id),
|
turret.guns.some((gun) => gun.id === unlock.id)
|
||||||
)!
|
)!
|
||||||
.guns.find((gun) => gun.id === unlock.id)!;
|
.guns.find((gun) => gun.id === unlock.id)!;
|
||||||
} else if (unlock.type === ModuleType.VEHICLE) {
|
} else if (unlock.type === ModuleType.VEHICLE) {
|
||||||
@@ -257,12 +256,12 @@ export function Modules() {
|
|||||||
{unlocks.length > 1 && (
|
{unlocks.length > 1 && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'currentcolor',
|
backgroundColor: "currentcolor",
|
||||||
width: `calc(${central ? 100 : 50}% + ${
|
width: `calc(${central ? 100 : 50}% + ${
|
||||||
central ? 8 : 4
|
central ? 8 : 4
|
||||||
}px)`,
|
}px)`,
|
||||||
height: 1,
|
height: 1,
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
right: first ? 0 : undefined,
|
right: first ? 0 : undefined,
|
||||||
left: last ? 0 : undefined,
|
left: last ? 0 : undefined,
|
||||||
transform: `translateX(${first ? 4 : last ? -4 : 0}px)`,
|
transform: `translateX(${first ? 4 : last ? -4 : 0}px)`,
|
||||||
@@ -279,14 +278,14 @@ export function Modules() {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'currentcolor',
|
backgroundColor: "currentcolor",
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 8,
|
height: 8,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateY(-7px)',
|
transform: "translateY(-7px)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -326,7 +325,7 @@ export function Modules() {
|
|||||||
1 && (
|
1 && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'currentcolor',
|
backgroundColor: "currentcolor",
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 8,
|
height: 8,
|
||||||
marginBottom: -8,
|
marginBottom: -8,
|
||||||
@@ -337,7 +336,7 @@ export function Modules() {
|
|||||||
{(extractModule(module) as ModuleWithUnlocks).unlocks &&
|
{(extractModule(module) as ModuleWithUnlocks).unlocks &&
|
||||||
tree(
|
tree(
|
||||||
type,
|
type,
|
||||||
(extractModule(module) as ModuleWithUnlocks).unlocks!,
|
(extractModule(module) as ModuleWithUnlocks).unlocks!
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@@ -359,7 +358,7 @@ export function Modules() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
draft.protagonist.turret = draft.protagonist.tank.turrets[0];
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
draft.protagonist.gun = draft.protagonist.turret.guns[0];
|
||||||
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
draft.protagonist.shell = draft.protagonist.gun.shells[0];
|
||||||
@@ -373,7 +372,7 @@ export function Modules() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.turret =
|
draft.protagonist.turret =
|
||||||
draft.protagonist.tank.turrets.at(-1)!;
|
draft.protagonist.tank.turrets.at(-1)!;
|
||||||
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
draft.protagonist.gun = draft.protagonist.turret.guns.at(-1)!;
|
||||||
@@ -394,28 +393,28 @@ export function Modules() {
|
|||||||
<Flex gap="2" wrap="wrap" gapY="6">
|
<Flex gap="2" wrap="wrap" gapY="6">
|
||||||
{tree(ModuleType.ENGINE, [
|
{tree(ModuleType.ENGINE, [
|
||||||
{
|
{
|
||||||
cost: { type: 'xp', value: 0 },
|
cost: { type: "xp", value: 0 },
|
||||||
id: engine0.id,
|
id: engine0.id,
|
||||||
type: ModuleType.ENGINE,
|
type: ModuleType.ENGINE,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
{tree(ModuleType.TRACKS, [
|
{tree(ModuleType.TRACKS, [
|
||||||
{
|
{
|
||||||
cost: { type: 'xp', value: 0 },
|
cost: { type: "xp", value: 0 },
|
||||||
id: track0.id,
|
id: track0.id,
|
||||||
type: ModuleType.TRACKS,
|
type: ModuleType.TRACKS,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
{tree(ModuleType.TURRET, [
|
{tree(ModuleType.TURRET, [
|
||||||
{
|
{
|
||||||
cost: { type: 'xp', value: 0 },
|
cost: { type: "xp", value: 0 },
|
||||||
id: turret0.id,
|
id: turret0.id,
|
||||||
type: ModuleType.TURRET,
|
type: ModuleType.TURRET,
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
{tree(ModuleType.GUN, [
|
{tree(ModuleType.GUN, [
|
||||||
{
|
{
|
||||||
cost: { type: 'xp', value: 0 },
|
cost: { type: "xp", value: 0 },
|
||||||
id: gun0.id,
|
id: gun0.id,
|
||||||
type: ModuleType.GUN,
|
type: ModuleType.GUN,
|
||||||
},
|
},
|
||||||
|
|||||||
+9
-10
@@ -1,15 +1,14 @@
|
|||||||
import { availableProvisions } from '@blitzkit/core';
|
import { availableProvisions } from "@blitzkit/core";
|
||||||
import { Button, Flex, Heading } from '@radix-ui/themes';
|
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||||
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from "../../../../core/awaitables/provisionDefinitions";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from "../../../../stores/duel";
|
||||||
import { ProvisionsManager } from '../../../ProvisionsManager';
|
import { ProvisionsManager } from "../../../ProvisionsManager";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||||
|
|
||||||
export function Provisions() {
|
export function Provisions() {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const { tank, gun } = Duel.use((state) => state.protagonist);
|
const { tank, gun } = Duel.use((state) => state.protagonist);
|
||||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||||
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
|
const provisionsList = availableProvisions(tank, gun, provisionDefinitions);
|
||||||
@@ -25,7 +24,7 @@ export function Provisions() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.provisions = [];
|
draft.protagonist.provisions = [];
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -39,7 +38,7 @@ export function Provisions() {
|
|||||||
selected={provisions}
|
selected={provisions}
|
||||||
disabled={tank.max_provisions === provisions.length}
|
disabled={tank.max_provisions === provisions.length}
|
||||||
onChange={(provisions) => {
|
onChange={(provisions) => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.provisions = provisions;
|
draft.protagonist.provisions = provisions;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+10
-11
@@ -1,4 +1,4 @@
|
|||||||
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -6,15 +6,14 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Popover,
|
Popover,
|
||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { useLocale } from '../../../../hooks/useLocale';
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||||
import { CrewSkillManager } from '../../../CrewSkillManager';
|
import { CrewSkillManager } from "../../../CrewSkillManager";
|
||||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||||
|
|
||||||
export function Skills() {
|
export function Skills() {
|
||||||
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
const skillLevels = Tankopedia.use((state) => state.skills);
|
||||||
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,7 +45,7 @@ export function Skills() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 0;
|
draft.skills[skill] = 0;
|
||||||
});
|
});
|
||||||
@@ -58,7 +57,7 @@ export function Skills() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
Object.keys(draft.skills).forEach((skill) => {
|
Object.keys(draft.skills).forEach((skill) => {
|
||||||
draft.skills[skill] = 7;
|
draft.skills[skill] = 7;
|
||||||
});
|
});
|
||||||
@@ -72,7 +71,7 @@ export function Skills() {
|
|||||||
<CrewSkillManager
|
<CrewSkillManager
|
||||||
skillLevels={skillLevels}
|
skillLevels={skillLevels}
|
||||||
onChange={(skills) => {
|
onChange={(skills) => {
|
||||||
mutateTankopediaTemporary((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.skills = skills;
|
draft.skills = skills;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+34
-35
@@ -1,5 +1,5 @@
|
|||||||
import { ShellType } from '@blitzkit/core';
|
import { ShellType } from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -9,18 +9,17 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from "lodash-es";
|
||||||
import { type ChangeEvent } from 'react';
|
import { type ChangeEvent } from "react";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
|
|
||||||
export function CustomShellButton() {
|
export function CustomShellButton() {
|
||||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
const customShell = Tankopedia.use((state) => state.customShell);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,24 +27,24 @@ export function CustomShellButton() {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open || customShell) return;
|
if (!open || customShell) return;
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell = antagonistShell;
|
draft.customShell = antagonistShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={customShell ? undefined : 'gray'}
|
color={customShell ? undefined : "gray"}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size={{ initial: '2', sm: '3' }}
|
size={{ initial: "2", sm: "3" }}
|
||||||
radius="none"
|
radius="none"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="custom shell"
|
alt="custom shell"
|
||||||
src="/assets/images/icons/custom-shell.png"
|
src="/assets/images/icons/custom-shell.png"
|
||||||
style={{
|
style={{
|
||||||
width: '50%',
|
width: "50%",
|
||||||
height: '50%',
|
height: "50%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -91,13 +90,13 @@ export function CustomShellButton() {
|
|||||||
modified.ricochet = undefined;
|
modified.ricochet = undefined;
|
||||||
modified.explosion_radius =
|
modified.explosion_radius =
|
||||||
gun.shells.find(
|
gun.shells.find(
|
||||||
(shell) => shell.type === ShellType.HE,
|
(shell) => shell.type === ShellType.HE
|
||||||
)?.explosion_radius ?? 1;
|
)?.explosion_radius ?? 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell = modified;
|
draft.customShell = modified;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -130,11 +129,11 @@ export function CustomShellButton() {
|
|||||||
</Text>
|
</Text>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
style={{ maxWidth: '6rem' }}
|
style={{ maxWidth: "6rem" }}
|
||||||
defaultValue={customShell.caliber}
|
defaultValue={customShell.caliber}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell!.caliber = event.target.valueAsNumber;
|
draft.customShell!.caliber = event.target.valueAsNumber;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -142,9 +141,9 @@ export function CustomShellButton() {
|
|||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
(event.target as HTMLInputElement).blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === "Escape") {
|
||||||
(event.target as HTMLInputElement).valueAsNumber =
|
(event.target as HTMLInputElement).valueAsNumber =
|
||||||
customShell.caliber;
|
customShell.caliber;
|
||||||
(event.target as HTMLInputElement).blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
@@ -152,7 +151,7 @@ export function CustomShellButton() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField.Slot side="right">
|
<TextField.Slot side="right">
|
||||||
{literals(strings.common.units.mm, [''])}
|
{literals(strings.common.units.mm, [""])}
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -166,11 +165,11 @@ export function CustomShellButton() {
|
|||||||
</Text>
|
</Text>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
style={{ maxWidth: '6rem' }}
|
style={{ maxWidth: "6rem" }}
|
||||||
defaultValue={customShell.penetration.near}
|
defaultValue={customShell.penetration.near}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell!.penetration.near =
|
draft.customShell!.penetration.near =
|
||||||
event.target.valueAsNumber;
|
event.target.valueAsNumber;
|
||||||
});
|
});
|
||||||
@@ -179,9 +178,9 @@ export function CustomShellButton() {
|
|||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
(event.target as HTMLInputElement).blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === "Escape") {
|
||||||
(event.target as HTMLInputElement).valueAsNumber =
|
(event.target as HTMLInputElement).valueAsNumber =
|
||||||
customShell.penetration.near;
|
customShell.penetration.near;
|
||||||
(event.target as HTMLInputElement).blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
@@ -189,7 +188,7 @@ export function CustomShellButton() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextField.Slot side="right">
|
<TextField.Slot side="right">
|
||||||
{literals(strings.common.units.mm, [''])}
|
{literals(strings.common.units.mm, [""])}
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -224,9 +223,9 @@ export function CustomShellButton() {
|
|||||||
step={Number.EPSILON}
|
step={Number.EPSILON}
|
||||||
defaultValue={[customShell.normalization ?? 0]}
|
defaultValue={[customShell.normalization ?? 0]}
|
||||||
onValueChange={([value]) => {
|
onValueChange={([value]) => {
|
||||||
console.log('asd');
|
console.log("asd");
|
||||||
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell!.normalization = value;
|
draft.customShell!.normalization = value;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -255,7 +254,7 @@ export function CustomShellButton() {
|
|||||||
step={Number.EPSILON}
|
step={Number.EPSILON}
|
||||||
defaultValue={[customShell.ricochet ?? 90]}
|
defaultValue={[customShell.ricochet ?? 90]}
|
||||||
onValueChange={throttle(([value]) => {
|
onValueChange={throttle(([value]) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell!.ricochet = value;
|
draft.customShell!.ricochet = value;
|
||||||
});
|
});
|
||||||
}, 1000)}
|
}, 1000)}
|
||||||
@@ -270,11 +269,11 @@ export function CustomShellButton() {
|
|||||||
<Text>Explosion radius</Text>
|
<Text>Explosion radius</Text>
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
style={{ maxWidth: '6rem' }}
|
style={{ maxWidth: "6rem" }}
|
||||||
defaultValue={customShell.explosion_radius ?? 0}
|
defaultValue={customShell.explosion_radius ?? 0}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell!.explosion_radius =
|
draft.customShell!.explosion_radius =
|
||||||
event.target.valueAsNumber;
|
event.target.valueAsNumber;
|
||||||
});
|
});
|
||||||
@@ -283,9 +282,9 @@ export function CustomShellButton() {
|
|||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
if (event.key === "Enter") {
|
||||||
(event.target as HTMLInputElement).blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === "Escape") {
|
||||||
(event.target as HTMLInputElement).valueAsNumber =
|
(event.target as HTMLInputElement).valueAsNumber =
|
||||||
customShell.explosion_radius ?? 1;
|
customShell.explosion_radius ?? 1;
|
||||||
(event.target as HTMLInputElement).blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
@@ -310,7 +309,7 @@ export function CustomShellButton() {
|
|||||||
<Button
|
<Button
|
||||||
color="red"
|
color="red"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.customShell = antagonistShell;
|
draft.customShell = antagonistShell;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
+13
-15
@@ -1,15 +1,13 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from "@blitzkit/core";
|
||||||
import { IconButton } from '@radix-ui/themes';
|
import { IconButton } from "@radix-ui/themes";
|
||||||
import { useArmor } from '../../../../../../hooks/useArmor';
|
import { useArmor } from "../../../../../../hooks/useArmor";
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from "../../../../../../stores/duel";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
|
|
||||||
export function DynamicArmorSwitcher() {
|
export function DynamicArmorSwitcher() {
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const isDynamicArmorActive = Duel.use((state) =>
|
const isDynamicArmorActive = Duel.use((state) =>
|
||||||
state.protagonist.consumables.includes(73),
|
state.protagonist.consumables.includes(73)
|
||||||
);
|
);
|
||||||
const { hasDynamicArmor } = useArmor(tank.id);
|
const { hasDynamicArmor } = useArmor(tank.id);
|
||||||
|
|
||||||
@@ -17,11 +15,11 @@ export function DynamicArmorSwitcher() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
color={isDynamicArmorActive ? undefined : 'gray'}
|
color={isDynamicArmorActive ? undefined : "gray"}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size={{ initial: '2', sm: '3' }}
|
size={{ initial: "2", sm: "3" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
if (draft.protagonist.consumables.includes(73)) {
|
if (draft.protagonist.consumables.includes(73)) {
|
||||||
draft.protagonist.consumables =
|
draft.protagonist.consumables =
|
||||||
draft.protagonist.consumables.filter((id) => id !== 73);
|
draft.protagonist.consumables.filter((id) => id !== 73);
|
||||||
@@ -33,17 +31,17 @@ export function DynamicArmorSwitcher() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Dynamic Armor"
|
alt="Dynamic Armor"
|
||||||
src={asset('icons/consumables/73.webp')}
|
src={asset("icons/consumables/73.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '50%',
|
width: "50%",
|
||||||
height: '50%',
|
height: "50%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
-1
@@ -14,7 +14,6 @@ import { Duel } from '../../../../../../../stores/duel';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
export function QuickInputs() {
|
export function QuickInputs() {
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const protagonist = Duel.use((state) => state.protagonist);
|
const protagonist = Duel.use((state) => state.protagonist);
|
||||||
const yawInput = useRef<HTMLInputElement>(null);
|
const yawInput = useRef<HTMLInputElement>(null);
|
||||||
const pitchInput = useRef<HTMLInputElement>(null);
|
const pitchInput = useRef<HTMLInputElement>(null);
|
||||||
|
|||||||
+44
-47
@@ -1,46 +1,43 @@
|
|||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
|
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { Button, Checkbox, Flex, Text } from '@radix-ui/themes';
|
import { Button, Checkbox, Flex, Text } from "@radix-ui/themes";
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from "react";
|
||||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||||
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
|
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
|
||||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||||
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
|
import type { ThicknessRange } from "../../../../../Armor/components/StaticArmor";
|
||||||
import { DynamicArmorSwitcher } from './DynamicArmorSwitcher';
|
import { DynamicArmorSwitcher } from "./DynamicArmorSwitcher";
|
||||||
|
|
||||||
type ThicknessesProps = MaybeSkeletonComponentProps & {
|
type ThicknessesProps = MaybeSkeletonComponentProps & {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||||
const display = TankopediaEphemeral.use((state) => state.display);
|
const display = Tankopedia.use((state) => state.display);
|
||||||
const showExternalModules = TankopediaPersistent.use(
|
const showExternalModules = TankopediaPersistent.use(
|
||||||
(state) => state.showExternalModules,
|
(state) => state.showExternalModules
|
||||||
);
|
);
|
||||||
const showSpacedArmor = TankopediaPersistent.use(
|
const showSpacedArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showSpacedArmor,
|
(state) => state.showSpacedArmor
|
||||||
);
|
);
|
||||||
const showPrimaryArmor = TankopediaPersistent.use(
|
const showPrimaryArmor = TankopediaPersistent.use(
|
||||||
(state) => state.showPrimaryArmor,
|
(state) => state.showPrimaryArmor
|
||||||
);
|
);
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
const editStatic = Tankopedia.use((state) => state.editStatic);
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
|
||||||
const { strings } = useLocale();
|
const { strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
right={display === TankopediaDisplay.StaticArmor ? '0' : '-6rem'}
|
right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"}
|
||||||
top="50%"
|
top="50%"
|
||||||
mr="4"
|
mr="4"
|
||||||
style={{
|
style={{
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
userSelect: 'none',
|
userSelect: "none",
|
||||||
transitionDuration: '200ms',
|
transitionDuration: "200ms",
|
||||||
}}
|
}}
|
||||||
direction="column"
|
direction="column"
|
||||||
gap="5"
|
gap="5"
|
||||||
@@ -50,12 +47,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
<Flex
|
<Flex
|
||||||
height="64px"
|
height="64px"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
|
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -70,7 +67,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
{strings.website.tools.tankopedia.sandbox.static.primary}
|
{strings.website.tools.tankopedia.sandbox.static.primary}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color="gray" size="1">
|
<Text color="gray" size="1">
|
||||||
{literals(strings.common.units.mm, ['0'])}
|
{literals(strings.common.units.mm, ["0"])}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@@ -78,10 +75,10 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
align="center"
|
align="center"
|
||||||
width="1rem"
|
width="1rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 'var(--radius-full)',
|
borderRadius: "var(--radius-full)",
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
|
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
|
||||||
color: 'black',
|
color: "black",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||||
@@ -91,12 +88,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
<Flex
|
<Flex
|
||||||
height="64px"
|
height="64px"
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
|
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -111,7 +108,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
{strings.website.tools.tankopedia.sandbox.static.spaced}
|
{strings.website.tools.tankopedia.sandbox.static.spaced}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color="gray" size="1">
|
<Text color="gray" size="1">
|
||||||
{literals(strings.common.units.mm, ['0'])}
|
{literals(strings.common.units.mm, ["0"])}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@@ -119,8 +116,8 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
align="center"
|
align="center"
|
||||||
width="1rem"
|
width="1rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 'var(--radius-full)',
|
borderRadius: "var(--radius-full)",
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
|
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -130,12 +127,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
|
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.showExternalModules = !draft.showExternalModules;
|
draft.showExternalModules = !draft.showExternalModules;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -151,10 +148,10 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
width="1rem"
|
width="1rem"
|
||||||
height="1rem"
|
height="1rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 'var(--radius-full)',
|
borderRadius: "var(--radius-full)",
|
||||||
overflow: 'hidden',
|
overflow: "hidden",
|
||||||
backgroundColor: '#c0c0c0',
|
backgroundColor: "#c0c0c0",
|
||||||
color: 'black',
|
color: "black",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||||
@@ -166,9 +163,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
<Flex align="center" gap="2">
|
<Flex align="center" gap="2">
|
||||||
<Flex
|
<Flex
|
||||||
gap="2"
|
gap="2"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.editStatic = !draft.editStatic;
|
draft.editStatic = !draft.editStatic;
|
||||||
draft.highlightArmor = undefined;
|
draft.highlightArmor = undefined;
|
||||||
});
|
});
|
||||||
@@ -186,9 +183,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
|||||||
size="1"
|
size="1"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.editStatic = false;
|
draft.editStatic = false;
|
||||||
draft.model = tankopediaEphemeralStore.getInitialState().model;
|
draft.model = Tankopedia.initial.model;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+111
-212
@@ -3,8 +3,8 @@ import {
|
|||||||
resolvePenetrationCoefficient,
|
resolvePenetrationCoefficient,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
uniqueGuns,
|
uniqueGuns,
|
||||||
} from '@blitzkit/core';
|
} from "@blitzkit/core";
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from "@blitzkit/i18n/src/literals";
|
||||||
import {
|
import {
|
||||||
CameraIcon,
|
CameraIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
EnterFullScreenIcon,
|
EnterFullScreenIcon,
|
||||||
ExitFullScreenIcon,
|
ExitFullScreenIcon,
|
||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
GearIcon,
|
} from "@radix-ui/react-icons";
|
||||||
} from '@radix-ui/react-icons';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -26,27 +25,27 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@radix-ui/themes';
|
} from "@radix-ui/themes";
|
||||||
import { Suspense, useState, type RefObject } from 'react';
|
import { Suspense, useState, type RefObject } from "react";
|
||||||
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
|
||||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||||
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
import { useFullScreen } from "../../../../../hooks/useFullScreen";
|
||||||
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
|
import { useFullscreenAvailability } from "../../../../../hooks/useFullscreenAvailability";
|
||||||
import { useLocale } from '../../../../../hooks/useLocale';
|
import { useLocale } from "../../../../../hooks/useLocale";
|
||||||
import { App } from '../../../../../stores/app';
|
import { Duel } from "../../../../../stores/duel";
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
|
||||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
|
||||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
||||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
|
||||||
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
|
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
|
||||||
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
|
import { SmallTankIcon } from "../../../../SmallTankIcon";
|
||||||
import { SmallTankIcon } from '../../../../SmallTankIcon';
|
import { TankSearch } from "../../../../TankSearch";
|
||||||
import { TankSearch } from '../../../../TankSearch';
|
import { screenshotReadyEvent } from "../TankSandbox/components/SceneProps";
|
||||||
import { CustomShellButton } from './components/CustomShellButton';
|
import { CustomShellButton } from "./components/CustomShellButton";
|
||||||
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
|
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||||
import { QuickInputs } from './components/QuickInputs';
|
import { QuickInputs } from "./components/QuickInputs";
|
||||||
import { Thicknesses } from './components/Thicknesses';
|
import { Thicknesses } from "./components/Thicknesses";
|
||||||
|
|
||||||
type OptionsProps = MaybeSkeletonComponentProps & {
|
type OptionsProps = MaybeSkeletonComponentProps & {
|
||||||
thicknessRange: ThicknessRange;
|
thicknessRange: ThicknessRange;
|
||||||
@@ -54,40 +53,27 @@ type OptionsProps = MaybeSkeletonComponentProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||||
const hasCustomShell = TankopediaEphemeral.use(
|
const hasCustomShell = Tankopedia.use(
|
||||||
(state) => state.customShell !== undefined,
|
(state) => state.customShell !== undefined
|
||||||
);
|
);
|
||||||
const display = TankopediaEphemeral.use((state) => state.display);
|
const display = Tankopedia.use((state) => state.display);
|
||||||
const isFullScreen = useFullScreen();
|
const isFullScreen = useFullScreen();
|
||||||
const showGrid = TankopediaPersistent.use((state) => state.showGrid);
|
|
||||||
const greenPenetration = TankopediaPersistent.use(
|
|
||||||
(state) => state.greenPenetration,
|
|
||||||
);
|
|
||||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
|
||||||
(state) => state.hideTankModelUnderArmor,
|
|
||||||
);
|
|
||||||
const advancedHighlighting = TankopediaPersistent.use(
|
const advancedHighlighting = TankopediaPersistent.use(
|
||||||
(state) => state.advancedHighlighting,
|
(state) => state.advancedHighlighting
|
||||||
);
|
);
|
||||||
const wireframe = TankopediaPersistent.use((state) => state.wireframe);
|
|
||||||
const opaque = TankopediaPersistent.use((state) => state.opaque);
|
|
||||||
const fullScreenAvailable = useFullscreenAvailability(true);
|
const fullScreenAvailable = useFullscreenAvailability(true);
|
||||||
const developerMode = App.use((state) => state.developerMode);
|
|
||||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||||
const antagonistGun = Duel.use((state) => state.antagonist.gun);
|
const antagonistGun = Duel.use((state) => state.antagonist.gun);
|
||||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||||
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
|
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
|
||||||
const antagonistTank = Duel.use((state) => state.antagonist.tank);
|
const antagonistTank = Duel.use((state) => state.antagonist.tank);
|
||||||
const hasCalibratedShells = useEquipment(103, true);
|
const hasCalibratedShells = useEquipment(103, true);
|
||||||
const [tab, setTab] = useState('search');
|
const [tab, setTab] = useState("search");
|
||||||
const mutateDuel = Duel.useMutation();
|
|
||||||
const hasEnhancedArmor = useEquipment(110);
|
const hasEnhancedArmor = useEquipment(110);
|
||||||
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
const revealed = Tankopedia.use((state) => state.revealed);
|
||||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,21 +85,21 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
gap="2"
|
gap="2"
|
||||||
direction="column"
|
direction="column"
|
||||||
top="50%"
|
top="50%"
|
||||||
right={display === TankopediaDisplay.DynamicArmor ? '3' : '-4rem'}
|
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
transform: 'translateY(-50%)',
|
transform: "translateY(-50%)",
|
||||||
transitionDuration: '200ms',
|
transitionDuration: "200ms",
|
||||||
}}
|
}}
|
||||||
align="end"
|
align="end"
|
||||||
>
|
>
|
||||||
{!hasCustomShell && (
|
{!hasCustomShell && (
|
||||||
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
<Text color="gray" size={{ initial: "1", sm: "2" }}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
(
|
(
|
||||||
resolvePenetrationCoefficient(
|
resolvePenetrationCoefficient(
|
||||||
hasCalibratedShells,
|
hasCalibratedShells,
|
||||||
antagonistShell.type,
|
antagonistShell.type
|
||||||
) * antagonistShell.penetration.near
|
) * antagonistShell.penetration.near
|
||||||
).toFixed(0),
|
).toFixed(0),
|
||||||
])}
|
])}
|
||||||
@@ -127,14 +113,14 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
radius="full"
|
radius="full"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
color="gray"
|
color="gray"
|
||||||
size={{ initial: '2', sm: '3' }}
|
size={{ initial: "2", sm: "3" }}
|
||||||
style={{ position: 'relative' }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
size="1"
|
size="1"
|
||||||
color="gray"
|
color="gray"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
}}
|
}}
|
||||||
@@ -146,11 +132,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
|
|
||||||
<img
|
<img
|
||||||
alt="Antagonist Gun"
|
alt="Antagonist Gun"
|
||||||
src={asset('icons/modules/gun.webp')}
|
src={asset("icons/modules/gun.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '65%',
|
width: "65%",
|
||||||
height: '65%',
|
height: "65%",
|
||||||
objectFit: 'contain',
|
objectFit: "contain",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -163,7 +149,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
module="gun"
|
module="gun"
|
||||||
key={gun.id}
|
key={gun.id}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.antagonist.turret = turret;
|
draft.antagonist.turret = turret;
|
||||||
draft.antagonist.gun = gun;
|
draft.antagonist.gun = gun;
|
||||||
draft.antagonist.shell = gun.shells[0];
|
draft.antagonist.shell = gun.shells[0];
|
||||||
@@ -172,7 +158,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
selected={gun.id === antagonistGun.id}
|
selected={gun.id === antagonistGun.id}
|
||||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||||
secondaryDiscriminator={
|
secondaryDiscriminator={
|
||||||
<Text style={{ fontSize: '0.75em' }}>
|
<Text style={{ fontSize: "0.75em" }}>
|
||||||
{literals(strings.common.units.mm, [
|
{literals(strings.common.units.mm, [
|
||||||
gun.shells[0].caliber.toFixed(0),
|
gun.shells[0].caliber.toFixed(0),
|
||||||
])}
|
])}
|
||||||
@@ -188,7 +174,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 'var(--radius-full)',
|
borderRadius: "var(--radius-full)",
|
||||||
}}
|
}}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
@@ -197,17 +183,17 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
color={
|
color={
|
||||||
thisShell.id === antagonistShell.id && !hasCustomShell
|
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||||
? undefined
|
? undefined
|
||||||
: 'gray'
|
: "gray"
|
||||||
}
|
}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
key={thisShell.id}
|
key={thisShell.id}
|
||||||
size={{ initial: '2', sm: '3' }}
|
size={{ initial: "2", sm: "3" }}
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.antagonist.shell = thisShell;
|
draft.antagonist.shell = thisShell;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
draft.customShell = undefined;
|
draft.customShell = undefined;
|
||||||
});
|
});
|
||||||
@@ -217,8 +203,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
alt={unwrap(thisShell.name)}
|
alt={unwrap(thisShell.name)}
|
||||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||||
style={{
|
style={{
|
||||||
width: '50%',
|
width: "50%",
|
||||||
height: '50%',
|
height: "50%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -230,58 +216,58 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: "auto",
|
||||||
borderRadius: 'var(--radius-full)',
|
borderRadius: "var(--radius-full)",
|
||||||
}}
|
}}
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={hasCalibratedShells ? undefined : 'gray'}
|
color={hasCalibratedShells ? undefined : "gray"}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size={{ initial: '2', sm: '3' }}
|
size={{ initial: "2", sm: "3" }}
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells
|
draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells
|
||||||
? 0
|
? 0
|
||||||
: 1;
|
: 1;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Calibrated Shells"
|
alt="Calibrated Shells"
|
||||||
src={asset('icons/equipment/103.webp')}
|
src={asset("icons/equipment/103.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '50%',
|
width: "50%",
|
||||||
height: '50%',
|
height: "50%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
color={hasEnhancedArmor ? undefined : 'gray'}
|
color={hasEnhancedArmor ? undefined : "gray"}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
size={{ initial: '2', sm: '3' }}
|
size={{ initial: "2", sm: "3" }}
|
||||||
radius="none"
|
radius="none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor
|
draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor
|
||||||
? 0
|
? 0
|
||||||
: -1;
|
: -1;
|
||||||
});
|
});
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.shot = undefined;
|
draft.shot = undefined;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Enhanced Armor"
|
alt="Enhanced Armor"
|
||||||
src={asset('icons/equipment/110.webp')}
|
src={asset("icons/equipment/110.webp")}
|
||||||
style={{
|
style={{
|
||||||
width: '50%',
|
width: "50%",
|
||||||
height: '50%',
|
height: "50%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -298,24 +284,24 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom={revealed ? '4' : '-100%'}
|
bottom={revealed ? "4" : "-100%"}
|
||||||
left="50%"
|
left="50%"
|
||||||
style={{ transform: 'translateX(-50%)', transitionDuration: '200ms' }}
|
style={{ transform: "translateX(-50%)", transitionDuration: "200ms" }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
style={{ transitionDuration: '200ms' }}
|
style={{ transitionDuration: "200ms" }}
|
||||||
position="relative"
|
position="relative"
|
||||||
bottom={display === TankopediaDisplay.DynamicArmor ? '0' : '-7rem'}
|
bottom={display === TankopediaDisplay.DynamicArmor ? "0" : "-7rem"}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
mb="1"
|
mb="1"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: "pointer" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
TankopediaPersistent.mutate((draft) => {
|
||||||
draft.advancedHighlighting = !draft.advancedHighlighting;
|
draft.advancedHighlighting = !draft.advancedHighlighting;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -330,6 +316,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Text color="gray" size="2">
|
<Text color="gray" size="2">
|
||||||
{strings.website.tools.tankopedia.sandbox.dynamic.shooter}
|
{strings.website.tools.tankopedia.sandbox.dynamic.shooter}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
open={antagonistSelectorOpen}
|
open={antagonistSelectorOpen}
|
||||||
onOpenChange={setAntagonistSelectorOpen}
|
onOpenChange={setAntagonistSelectorOpen}
|
||||||
@@ -349,12 +336,12 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Tabs.Root
|
<Tabs.Root
|
||||||
value={tab}
|
value={tab}
|
||||||
onValueChange={setTab}
|
onValueChange={setTab}
|
||||||
style={{ position: 'relative' }}
|
style={{ position: "relative" }}
|
||||||
>
|
>
|
||||||
<TankSearch
|
<TankSearch
|
||||||
compact
|
compact
|
||||||
onSelect={(tank) => {
|
onSelect={(tank) => {
|
||||||
mutateDuel((draft) => {
|
Duel.mutate((draft) => {
|
||||||
draft.antagonist.tank = tank;
|
draft.antagonist.tank = tank;
|
||||||
draft.antagonist.engine = tank.engines.at(-1)!;
|
draft.antagonist.engine = tank.engines.at(-1)!;
|
||||||
draft.antagonist.track = tank.tracks.at(-1)!;
|
draft.antagonist.track = tank.tracks.at(-1)!;
|
||||||
@@ -377,7 +364,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
variant="classic"
|
variant="classic"
|
||||||
value={`${disturbed ? display : -1}`}
|
value={`${disturbed ? display : -1}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
Tankopedia.mutate((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
draft.display = Number(value);
|
draft.display = Number(value);
|
||||||
});
|
});
|
||||||
@@ -390,7 +377,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex height="100%" align="center">
|
<Flex height="100%" align="center">
|
||||||
<img
|
<img
|
||||||
src="/assets/images/icons/tankopedia-model.png"
|
src="/assets/images/icons/tankopedia-model.png"
|
||||||
style={{ height: '1.25em' }}
|
style={{ height: "1.25em" }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -402,7 +389,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex height="100%" align="center">
|
<Flex height="100%" align="center">
|
||||||
<img
|
<img
|
||||||
src="/assets/images/icons/tankopedia-dynamic-armor.png"
|
src="/assets/images/icons/tankopedia-dynamic-armor.png"
|
||||||
style={{ height: '1.25em' }}
|
style={{ height: "1.25em" }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -414,7 +401,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex height="100%" align="center">
|
<Flex height="100%" align="center">
|
||||||
<img
|
<img
|
||||||
src="/assets/images/icons/tankopedia-static-armor.png"
|
src="/assets/images/icons/tankopedia-static-armor.png"
|
||||||
style={{ height: '1.25em' }}
|
style={{ height: "1.25em" }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -451,106 +438,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<IconButton variant="ghost" color="gray">
|
|
||||||
<GearIcon />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
{display === TankopediaDisplay.DynamicArmor && (
|
|
||||||
<>
|
|
||||||
<DropdownMenu.Label>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.armor
|
|
||||||
}
|
|
||||||
</DropdownMenu.Label>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={greenPenetration}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.greenPenetration = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.green_penetration
|
|
||||||
}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={hideTankModelUnderArmor}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.hideTankModelUnderArmor = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.hide_model_under_armor
|
|
||||||
}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={opaque}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.opaque = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.opaque
|
|
||||||
}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
|
|
||||||
{developerMode && (
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={wireframe}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.wireframe = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.dev_wireframe
|
|
||||||
}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenu.Label>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.environment
|
|
||||||
}
|
|
||||||
</DropdownMenu.Label>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={showGrid}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.showGrid = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.show_grid
|
|
||||||
}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
<IconButton color="gray" variant="ghost">
|
<IconButton color="gray" variant="ghost">
|
||||||
@@ -563,19 +450,25 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Popover.Close>
|
<Popover.Close>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!canvas.current) return;
|
screenshotReadyEvent.dispatch(true);
|
||||||
|
|
||||||
const anchor = document.createElement('a');
|
requestAnimationFrame(() => {
|
||||||
|
if (!canvas.current) return;
|
||||||
|
|
||||||
anchor.setAttribute(
|
const anchor = document.createElement("a");
|
||||||
'download',
|
|
||||||
`${protagonistTank.name}.png`,
|
anchor.setAttribute(
|
||||||
);
|
"download",
|
||||||
anchor.setAttribute(
|
`${protagonistTank.name}.png`
|
||||||
'href',
|
);
|
||||||
canvas.current.toDataURL('image/png'),
|
anchor.setAttribute(
|
||||||
);
|
"href",
|
||||||
anchor.click();
|
canvas.current.toDataURL("image/png")
|
||||||
|
);
|
||||||
|
anchor.click();
|
||||||
|
|
||||||
|
screenshotReadyEvent.dispatch(false);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
@@ -589,14 +482,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!canvas.current) return;
|
screenshotReadyEvent.dispatch(true);
|
||||||
|
|
||||||
canvas.current.toBlob((blob) => {
|
requestAnimationFrame(() => {
|
||||||
if (!blob) return;
|
if (!canvas.current) return;
|
||||||
|
|
||||||
navigator.clipboard.write([
|
canvas.current.toBlob((blob) => {
|
||||||
new ClipboardItem({ 'image/png': blob }),
|
if (!blob) return;
|
||||||
]);
|
|
||||||
|
navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ "image/png": blob }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
screenshotReadyEvent.dispatch(false);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+10
-10
@@ -1,10 +1,10 @@
|
|||||||
import { Box } from '@radix-ui/themes';
|
import { Box } from "@radix-ui/themes";
|
||||||
import { Var } from '../../../../../core/radix/var';
|
import { Var } from "../../../../../core/radix/var";
|
||||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
|
|
||||||
export function ScrollHint() {
|
export function ScrollHint() {
|
||||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||||
|
|
||||||
if (disturbed) return null;
|
if (disturbed) return null;
|
||||||
|
|
||||||
@@ -17,9 +17,9 @@ export function ScrollHint() {
|
|||||||
height="2rem"
|
height="2rem"
|
||||||
width="1rem"
|
width="1rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '0.5rem',
|
borderRadius: "0.5rem",
|
||||||
outline: `2px solid ${Var('gray-11')}`,
|
outline: `2px solid ${Var("gray-11")}`,
|
||||||
transform: 'translateX(-50%)',
|
transform: "translateX(-50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -28,8 +28,8 @@ export function ScrollHint() {
|
|||||||
height="0.75rem"
|
height="0.75rem"
|
||||||
m="0.125rem"
|
m="0.125rem"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '50%',
|
borderRadius: "50%",
|
||||||
backgroundColor: Var('gray-12'),
|
backgroundColor: Var("gray-12"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user