mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-11 19:56:10 +01:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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_ASSET_FAST_USER: ${{ secrets.PUBLIC_ASSET_FAST_USER }}
|
||||
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:
|
||||
host:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
@@ -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",
|
||||
"private": true,
|
||||
"version": "1.27.17",
|
||||
"version": "1.27.19",
|
||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||
"type": "module",
|
||||
"types": "./types.d.ts",
|
||||
|
||||
@@ -861,6 +861,13 @@
|
||||
},
|
||||
|
||||
"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": {
|
||||
"name": "Guess the tank",
|
||||
"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.",
|
||||
|
||||
"visualizers": {
|
||||
"reload": {
|
||||
"skip": "Skip"
|
||||
},
|
||||
|
||||
"assault": {
|
||||
"title": "Assault shells"
|
||||
},
|
||||
@@ -935,9 +946,7 @@
|
||||
"green_penetration": "Green penetration",
|
||||
"hide_model_under_armor": "Hide model under armor",
|
||||
"opaque": "Opaque",
|
||||
"dev_wireframe": "DEV: wireframe",
|
||||
"environment": "Environment",
|
||||
"show_grid": "Show grid"
|
||||
"dev_wireframe": "DEV: wireframe"
|
||||
},
|
||||
|
||||
"screenshot": {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "varuna",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { produce } from "immer";
|
||||
import { merge } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ProviderProps<Arguments> {
|
||||
args: Arguments;
|
||||
}
|
||||
|
||||
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 dehydrated = localStorage.getItem(this.persistence);
|
||||
|
||||
if (dehydrated) {
|
||||
const rehydrated = JSON.parse(dehydrated) as unknown;
|
||||
data = merge(data, rehydrated);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const last = slicer(this.state);
|
||||
|
||||
function listener(state: Type) {
|
||||
const next = slicer(state);
|
||||
if (next !== last) callback(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("State 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
|
||||
) {
|
||||
const [slice, setSlice] = useState(slicer(this.state));
|
||||
useEffect(() => this.on(slicer, setSlice), []);
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
"@astrolib/seo": "^1.0.0-beta.8",
|
||||
"@blitzkit/core": "workspace:*",
|
||||
"@blitzkit/i18n": "workspace:*",
|
||||
"varuna": "workspace:*",
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@nanostores/persistent": "^0.10.2",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 490 KiB After Width: | Height: | Size: 669 KiB |
@@ -21,11 +21,9 @@ export function ArmorPlateDisplay() {
|
||||
const highlightArmor = TankopediaEphemeral.use(
|
||||
(state) => state.highlightArmor,
|
||||
);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||
const duelStore = Duel.useStore();
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
|
||||
if (highlightArmor === undefined) return null;
|
||||
|
||||
@@ -95,7 +93,7 @@ export function ArmorPlateDisplay() {
|
||||
if (event.key === 'Enter') {
|
||||
input.current?.blur();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
const { protagonist } = duelStore.getState();
|
||||
const tank = draft.model;
|
||||
@@ -119,7 +117,7 @@ export function ArmorPlateDisplay() {
|
||||
});
|
||||
} else if (event.key === 'Escape') {
|
||||
input.current?.blur();
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}
|
||||
@@ -137,7 +135,7 @@ export function ArmorPlateDisplay() {
|
||||
mt="2"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
if (!highlightArmor.editingPlate) return;
|
||||
|
||||
const index = resolveArmorIndex(highlightArmor.name);
|
||||
@@ -145,8 +143,7 @@ export function ArmorPlateDisplay() {
|
||||
if (index === undefined) return;
|
||||
|
||||
draft.highlightArmor = undefined;
|
||||
const initialState =
|
||||
tankopediaEphemeralStore.getInitialState();
|
||||
const initialState = TankopediaEphemeral.initial;
|
||||
const { protagonist } = duelStore.getState();
|
||||
const tank = draft.model;
|
||||
const initialTank = initialState.model;
|
||||
|
||||
@@ -14,7 +14,6 @@ export const PrimaryArmorScene = memo(() => {
|
||||
const wrapper = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
// const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
|
||||
+10
-20
@@ -43,7 +43,6 @@ export function PrimaryArmorSceneComponent({
|
||||
}: PrimaryArmorSceneComponentProps) {
|
||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||
const duelStore = Duel.useStore();
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const material = new ShaderMaterial({
|
||||
fragmentShader,
|
||||
vertexShader,
|
||||
@@ -80,9 +79,9 @@ export function PrimaryArmorSceneComponent({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function handleShellChange() {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
|
||||
material.uniforms.caliber.value = shell.caliber;
|
||||
@@ -97,14 +96,8 @@ export function PrimaryArmorSceneComponent({
|
||||
material.uniforms.damage.value = shell.armor_damage;
|
||||
material.uniforms.explosionRadius.value = shell.explosion_radius;
|
||||
|
||||
await handleProtagonistEquipmentChange(
|
||||
duel.protagonist.equipmentMatrix,
|
||||
true,
|
||||
);
|
||||
await handleAntagonistEquipmentChange(
|
||||
duel.antagonist.equipmentMatrix,
|
||||
true,
|
||||
);
|
||||
handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix, true);
|
||||
handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix, true);
|
||||
|
||||
invalidate();
|
||||
}
|
||||
@@ -121,12 +114,12 @@ export function PrimaryArmorSceneComponent({
|
||||
function handleWireframeChange(wireframe: boolean) {
|
||||
material.wireframe = wireframe;
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
noInvalidate = false,
|
||||
) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -137,15 +130,15 @@ export function PrimaryArmorSceneComponent({
|
||||
|
||||
if (!noInvalidate) invalidate();
|
||||
}
|
||||
async function handleAntagonistEquipmentChange(
|
||||
function handleAntagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
noInvalidate = false,
|
||||
) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -174,10 +167,7 @@ export function PrimaryArmorSceneComponent({
|
||||
|
||||
const unsubscribes = [
|
||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||
tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.customShell,
|
||||
handleShellChange,
|
||||
),
|
||||
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||
tankopediaPersistentStore.subscribe(
|
||||
(state) => state.greenPenetration,
|
||||
handleGreenPenetrationChange,
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
|
||||
|
||||
vec3 getPenetrationColor(bool isThreeCalibersRule, bool 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);
|
||||
@@ -107,7 +107,7 @@ void main() {
|
||||
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
||||
|
||||
if (advancedHighlighting && didRicochet) {
|
||||
baseColor = vec3(1.0, baseColor.g, 0.5);
|
||||
baseColor = vec3(1.0, baseColor.g, 1.0);
|
||||
}
|
||||
|
||||
if (greenPenetration || advancedHighlighting) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Path,
|
||||
Quaternion,
|
||||
} from 'three';
|
||||
import { isHalloween } from '../../../core/blitzkit/isHalloween';
|
||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
@@ -72,11 +73,8 @@ export function ShotDisplay() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const data = new Date();
|
||||
const isHalloween = data.getMonth() === 9 && data.getDate() === 31;
|
||||
|
||||
if (
|
||||
isHalloween &&
|
||||
isHalloween() &&
|
||||
shot?.splashRadius !== undefined &&
|
||||
shot.in.status === 'penetration'
|
||||
) {
|
||||
|
||||
@@ -37,44 +37,38 @@ function LayerEntry({
|
||||
const { locale, strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex align="center" gap="2">
|
||||
<Text size="1" color="gray" style={{ width: 12 }}>
|
||||
{layerIndex}
|
||||
</Text>
|
||||
<Text size="2" color={shotStatusColor} style={{ width: 48 }}>
|
||||
{layerName}
|
||||
</Text>
|
||||
<tr>
|
||||
<td>
|
||||
<Text size="1" color="gray">
|
||||
{layerIndex}
|
||||
</Text>
|
||||
</td>
|
||||
|
||||
{layer.type === null && (
|
||||
<>
|
||||
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
|
||||
<td>
|
||||
<Text size="2" color={shotStatusColor}>
|
||||
{layerName}
|
||||
</Text>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{layer.type === null && (
|
||||
<Text size="2" color={shotStatusColor}>
|
||||
{literals(strings.common.units.mm, [
|
||||
Math.round(layer.distance * 1000).toLocaleString(locale),
|
||||
])}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<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)],
|
||||
)}
|
||||
{layer.type === ArmorType.External && (
|
||||
<Text size="2" color={shotStatusColor}>
|
||||
{literals(strings.common.units.mm, [
|
||||
Math.round(layer.thickness).toLocaleString(locale),
|
||||
])}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
{layer.type === ArmorType.External && (
|
||||
<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 }}>
|
||||
)}
|
||||
{(layer.type === ArmorType.Primary ||
|
||||
layer.type === ArmorType.Spaced) && (
|
||||
<Text size="2" color={shotStatusColor}>
|
||||
{literals(
|
||||
angle === undefined
|
||||
? strings.common.units.mm
|
||||
@@ -86,7 +80,22 @@ function LayerEntry({
|
||||
],
|
||||
)}
|
||||
</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">
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||
@@ -94,9 +103,9 @@ function LayerEntry({
|
||||
[Math.round(layer.thickness).toLocaleString(locale)],
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
const inTitleColor = shotStatusColors[shot.in.status];
|
||||
|
||||
return (
|
||||
<Card variant="classic" style={{ width: 300 }} {...props}>
|
||||
<Card variant="classic" {...props}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text color={inTitleColor} weight="bold">
|
||||
@@ -127,7 +136,12 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Flex direction="column">
|
||||
<table
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
borderSpacing: 'var(--space-1) 0',
|
||||
}}
|
||||
>
|
||||
{shot.in.layers.map((layer, index) => {
|
||||
const layerName =
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</table>
|
||||
</Flex>
|
||||
|
||||
{shot.out && shot.out.layers.length > 0 && (
|
||||
|
||||
+8
-13
@@ -29,7 +29,6 @@ export function SpacedArmorSubExternal({
|
||||
variant,
|
||||
clip,
|
||||
}: SpacedArmorSubExternalProps) {
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const duelStore = Duel.useStore();
|
||||
const material = new ShaderMaterial({
|
||||
fragmentShader,
|
||||
@@ -50,15 +49,13 @@ export function SpacedArmorSubExternal({
|
||||
useEffect(() => {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
material.uniforms.penetration.value = shell.penetration.near;
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
) {
|
||||
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -67,12 +64,12 @@ export function SpacedArmorSubExternal({
|
||||
? thickness * 1.03
|
||||
: thickness;
|
||||
}
|
||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -93,10 +90,7 @@ export function SpacedArmorSubExternal({
|
||||
|
||||
const unsubscribes = [
|
||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||
tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.customShell,
|
||||
handleShellChange,
|
||||
),
|
||||
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||
duelStore.subscribe(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
handleProtagonistEquipmentChange,
|
||||
@@ -125,6 +119,7 @@ export function SpacedArmorSubExternal({
|
||||
return (
|
||||
<mesh
|
||||
{...props}
|
||||
onClick={() => {}}
|
||||
key={`${key}-spaced-sub-external-exclude`}
|
||||
renderOrder={3}
|
||||
material={
|
||||
|
||||
+7
-13
@@ -30,7 +30,6 @@ export function SpacedArmorSubSpaced({
|
||||
node,
|
||||
thickness,
|
||||
}: SpacedArmorSubSpacedProps) {
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const duelStore = Duel.useStore();
|
||||
const material = new ShaderMaterial({
|
||||
fragmentShader,
|
||||
@@ -52,7 +51,7 @@ export function SpacedArmorSubSpaced({
|
||||
useEffect(() => {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
|
||||
material.uniforms.penetration.value = shell.penetration.near;
|
||||
@@ -64,11 +63,9 @@ export function SpacedArmorSubSpaced({
|
||||
shell.normalization ?? 0,
|
||||
);
|
||||
}
|
||||
async function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
) {
|
||||
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -77,12 +74,12 @@ export function SpacedArmorSubSpaced({
|
||||
? thickness * 1.03
|
||||
: thickness;
|
||||
}
|
||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
@@ -103,10 +100,7 @@ export function SpacedArmorSubSpaced({
|
||||
|
||||
const unsubscribes = [
|
||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||
tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.customShell,
|
||||
handleShellChange,
|
||||
),
|
||||
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||
duelStore.subscribe(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
handleProtagonistEquipmentChange,
|
||||
|
||||
+12
-13
@@ -76,19 +76,18 @@ export function SpacedArmorSceneComponent({
|
||||
clip,
|
||||
...props
|
||||
}: SpacedArmorSceneComponentProps) {
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const duelStore = Duel.useStore();
|
||||
const camera = useThree((state) => state.camera);
|
||||
|
||||
const shoot = useCallback(
|
||||
async (
|
||||
(
|
||||
point: Vector3,
|
||||
intersections: Intersection[],
|
||||
allowRicochet: boolean,
|
||||
remainingPenetrationInput?: number,
|
||||
) => {
|
||||
const { antagonist, protagonist } = duelStore.getState();
|
||||
const { customShell } = tankopediaEphemeralStore.getState();
|
||||
const { customShell } = TankopediaEphemeral.state;
|
||||
const shell = customShell ?? antagonist.shell;
|
||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||
const shot: Shot = {
|
||||
@@ -103,12 +102,12 @@ export function SpacedArmorSceneComponent({
|
||||
},
|
||||
};
|
||||
|
||||
const hasCalibratedShells = await hasEquipment(
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
antagonist.tank.equipment_preset,
|
||||
antagonist.equipmentMatrix,
|
||||
);
|
||||
const hasEnhancedArmor = await hasEquipment(
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
protagonist.tank.equipment_preset,
|
||||
protagonist.equipmentMatrix,
|
||||
@@ -340,7 +339,7 @@ export function SpacedArmorSceneComponent({
|
||||
props.scene.children,
|
||||
true,
|
||||
);
|
||||
const outShot = await shoot(
|
||||
const outShot = shoot(
|
||||
lastLayer.point,
|
||||
ricochetIntersections,
|
||||
false,
|
||||
@@ -386,14 +385,14 @@ export function SpacedArmorSceneComponent({
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
onClick={async (event) => {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const shot = (await shoot(
|
||||
event.point,
|
||||
event.intersections,
|
||||
true,
|
||||
))!;
|
||||
tankopediaEphemeralStore.setState({ shot });
|
||||
|
||||
const shot = shoot(event.point, event.intersections, true)!;
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = shot;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
const wrapper = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
@@ -46,9 +45,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
const hullOrigin = correctZYTuple(trackModelDefinition.origin);
|
||||
const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin);
|
||||
const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const duelStore = Duel.useStore();
|
||||
const maskOrigin =
|
||||
gunModelDefinition.mask === undefined
|
||||
@@ -157,24 +154,24 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -196,7 +193,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
@@ -209,7 +206,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
<StaticArmorSceneComponent
|
||||
name={node.name}
|
||||
thicknessRange={thicknessRange}
|
||||
key={node.uuid}
|
||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
||||
thickness={thickness}
|
||||
node={node}
|
||||
@@ -248,10 +244,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -261,14 +257,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -290,7 +286,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
@@ -330,10 +326,10 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -342,14 +338,14 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -371,7 +367,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
FrontSide,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshStandardMaterial,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Plane,
|
||||
Quaternion,
|
||||
@@ -69,8 +69,6 @@ export function StaticArmorSceneComponent({
|
||||
onPointerDown,
|
||||
...props
|
||||
}: StaticArmorSceneComponentProps) {
|
||||
const mutateTankopediaEphemeralStore = TankopediaEphemeral.useMutation();
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const camera = useThree((state) => state.camera);
|
||||
const x = thickness / thicknessRange.value;
|
||||
const xClamped = clamp(x, 0, 1);
|
||||
@@ -108,7 +106,7 @@ export function StaticArmorSceneComponent({
|
||||
|
||||
const surfaceMaterial = useMemo(
|
||||
() =>
|
||||
new MeshStandardMaterial({
|
||||
new MeshBasicMaterial({
|
||||
color,
|
||||
opacity,
|
||||
transparent: opacity < 1,
|
||||
@@ -166,7 +164,7 @@ export function StaticArmorSceneComponent({
|
||||
}
|
||||
|
||||
const unsubscribes = [
|
||||
tankopediaEphemeralStore.subscribe(
|
||||
TankopediaEphemeral.on(
|
||||
(state) => state.highlightArmor?.name,
|
||||
handleHighlightArmor,
|
||||
),
|
||||
@@ -238,7 +236,7 @@ export function StaticArmorSceneComponent({
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const { editStatic } = tankopediaEphemeralStore.getState();
|
||||
const { editStatic } = TankopediaEphemeral.state;
|
||||
|
||||
const bounds = new Box3().setFromObject(event.object);
|
||||
const point = bounds.min
|
||||
@@ -259,7 +257,7 @@ export function StaticArmorSceneComponent({
|
||||
const thicknessAngled =
|
||||
thickness / Math.sin(Math.PI / 2 - angle);
|
||||
|
||||
mutateTankopediaEphemeralStore((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
@@ -16,10 +15,7 @@ interface ControlsProps {
|
||||
onAddTankDialogOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function Controls({
|
||||
addTankDialogOpen,
|
||||
@@ -60,11 +56,9 @@ export function Controls({
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members.push(
|
||||
tankToCompareMember(tank, model, provisionDefinitions),
|
||||
tankToCompareMember(tank, provisionDefinitions),
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -74,13 +68,7 @@ export function Controls({
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members.push(
|
||||
...tanks.map((tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
return tankToCompareMember(
|
||||
tank,
|
||||
model,
|
||||
provisionDefinitions,
|
||||
);
|
||||
return tankToCompareMember(tank, provisionDefinitions);
|
||||
}),
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
@@ -14,10 +13,7 @@ interface TankControlProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const [provisionDefinitions, modelDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
|
||||
export function TankControl({ index, slug }: TankControlProps) {
|
||||
const { locale } = useLocale();
|
||||
@@ -61,12 +57,9 @@ export function TankControl({ index, slug }: TankControlProps) {
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
draft.members[index] = tankToCompareMember(
|
||||
tank,
|
||||
model,
|
||||
provisionDefinitions,
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Box } from '@radix-ui/themes';
|
||||
import { useEffect } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToDuelMember } from '../../core/blitzkit/tankToDuelMember';
|
||||
import { Var } from '../../core/radix/var';
|
||||
import { Duel } from '../../stores/duel';
|
||||
import { GuessEphemeral } from '../../stores/guessEphemeral';
|
||||
import { TankopediaEphemeral } from '../../stores/tankopediaEphemeral';
|
||||
import './index.css';
|
||||
import { Box } from "@radix-ui/themes";
|
||||
import { useEffect } from "react";
|
||||
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||
import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
|
||||
import { Var } from "../../core/radix/var";
|
||||
import { Duel } from "../../stores/duel";
|
||||
import { Guess } from "../../stores/guessEphemeral";
|
||||
import { TankopediaEphemeral } from "../../stores/tankopediaEphemeral";
|
||||
import "./index.css";
|
||||
|
||||
const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
||||
awaitableModelDefinitions,
|
||||
@@ -15,17 +15,15 @@ const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function GuessBackground() {
|
||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||
const tank = GuessEphemeral.use((state) => state.tank);
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
mutateDuel((state) => {
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
state.protagonist = tankToDuelMember(tank, model, provisionDefinitions);
|
||||
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||
});
|
||||
mutateTankopediaEphemeral((state) => {
|
||||
TankopediaEphemeral.mutate((state) => {
|
||||
state.model = modelDefinitions.models[tank.id];
|
||||
});
|
||||
}, [tank]);
|
||||
@@ -39,13 +37,13 @@ export function GuessBackground() {
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transitionDuration: '2s',
|
||||
transitionDuration: "2s",
|
||||
backgroundColor: Var(
|
||||
guessState === null
|
||||
? 'accent-2'
|
||||
? "accent-2"
|
||||
: guessState
|
||||
? 'jade-3'
|
||||
: 'tomato-3',
|
||||
? "jade-3"
|
||||
: "tomato-3"
|
||||
),
|
||||
}}
|
||||
>
|
||||
@@ -53,7 +51,7 @@ export function GuessBackground() {
|
||||
width="100%"
|
||||
height="100%"
|
||||
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>
|
||||
@@ -67,13 +65,13 @@ export function GuessBackground() {
|
||||
left="0"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
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 100% 0, ${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, ${Var("white-a1")} 20%, transparent 20%),
|
||||
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 0 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 { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
||||
import { SmartCanvas } from './SmartCanvas';
|
||||
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
||||
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
||||
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
||||
import { Guess } from "../stores/guessEphemeral";
|
||||
import { SmartCanvas } from "./SmartCanvas";
|
||||
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
|
||||
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||
import { TankModel } from "./Tankopedia/HeroSection/components/TankSandbox/components/TankModel";
|
||||
|
||||
const BRIGHTNESS = 2 ** -5;
|
||||
const BRIGHTNESS = 2 ** -5.5;
|
||||
const CONTRAST = 2 ** 2;
|
||||
|
||||
export function GuessRenderer() {
|
||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const isRevealed = guessState !== null;
|
||||
|
||||
return (
|
||||
<SmartCanvas
|
||||
style={{
|
||||
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,
|
||||
transitionProperty: 'filter',
|
||||
transitionDuration: isRevealed ? "2s" : undefined,
|
||||
transitionProperty: "filter",
|
||||
}}
|
||||
camera={{ position: [-5, 5, -5] }}
|
||||
>
|
||||
<Controls distanceScale={2} zoomable={false} />
|
||||
<Lighting display={TankopediaDisplay.Model} />
|
||||
<Controls zoomable={false} />
|
||||
<Lighting />
|
||||
<TankModel />
|
||||
</SmartCanvas>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,13 @@ import {
|
||||
SEARCH_KEYS,
|
||||
TankDefinition,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
MagnifyingGlassIcon,
|
||||
PaperPlaneIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -17,16 +17,16 @@ import {
|
||||
Spinner,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions';
|
||||
import { awaitableTankNames } from '../core/awaitables/tankNames';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
||||
import { classIcons } from './ClassIcon';
|
||||
import { SearchResults } from './SearchResults';
|
||||
} from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { Guess } from "../stores/guessEphemeral";
|
||||
import { classIcons } from "./ClassIcon";
|
||||
import { SearchResults } from "./SearchResults";
|
||||
|
||||
const { go } = fuzzysort;
|
||||
|
||||
@@ -38,16 +38,15 @@ const [tankNames, tankDefinitions] = await Promise.all([
|
||||
const ids = Object.keys(tankDefinitions.tanks);
|
||||
|
||||
export function Guesser() {
|
||||
const tank = GuessEphemeral.use((state) => state.tank);
|
||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||
const correctGuesses = GuessEphemeral.use((state) => state.correctGuesses);
|
||||
const totalGuesses = GuessEphemeral.use((state) => state.totalGuesses);
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const correctGuesses = Guess.use((state) => state.correctGuesses);
|
||||
const totalGuesses = Guess.use((state) => state.totalGuesses);
|
||||
const { strings, unwrap } = useLocale();
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
||||
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
||||
const mutateGuessEphemeral = GuessEphemeral.useMutation();
|
||||
|
||||
const requestSearch = useCallback(() => {
|
||||
setSelected(null);
|
||||
@@ -72,10 +71,10 @@ export function Guesser() {
|
||||
});
|
||||
|
||||
setResults(
|
||||
searchResults.map((result) => tankDefinitions.tanks[result.obj.id]),
|
||||
searchResults.map((result) => tankDefinitions.tanks[result.obj.id])
|
||||
);
|
||||
}, 500),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,7 +87,7 @@ export function Guesser() {
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
left="50%"
|
||||
style={{ transform: 'translateX(-50%)' }}
|
||||
style={{ transform: "translateX(-50%)" }}
|
||||
width="100%"
|
||||
p="4"
|
||||
maxWidth="25rem"
|
||||
@@ -138,7 +137,7 @@ export function Guesser() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Flex justify="center">
|
||||
<Flex justify="center" style={{ userSelect: "none" }}>
|
||||
<Text>
|
||||
{literals(strings.website.tools.guess.stats, [
|
||||
`${correctGuesses}`,
|
||||
@@ -164,12 +163,12 @@ export function Guesser() {
|
||||
|
||||
<Button
|
||||
size="3"
|
||||
color={guessState === null && selected === null ? 'red' : undefined}
|
||||
color={guessState === null && selected === null ? "red" : undefined}
|
||||
onClick={() => {
|
||||
if (guessState === null) {
|
||||
const correct = selected !== null && selected.id === tank.id;
|
||||
|
||||
mutateGuessEphemeral((draft) => {
|
||||
Guess.mutate((draft) => {
|
||||
draft.guessState = correct;
|
||||
draft.totalGuesses++;
|
||||
draft.correctGuesses += correct ? 1 : 0;
|
||||
@@ -178,12 +177,12 @@ export function Guesser() {
|
||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
mutateGuessEphemeral((draft) => {
|
||||
Guess.mutate((draft) => {
|
||||
if (!input.current) return;
|
||||
|
||||
draft.tank = tank;
|
||||
draft.guessState = null;
|
||||
input.current.value = '';
|
||||
input.current.value = "";
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -192,7 +191,7 @@ export function Guesser() {
|
||||
<>
|
||||
{
|
||||
strings.website.tools.guess.search[
|
||||
selected === null ? 'skip' : 'guess'
|
||||
selected === null ? "skip" : "guess"
|
||||
]
|
||||
}
|
||||
{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} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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,15 +1,9 @@
|
||||
import { Flex, Select, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import {
|
||||
TankPerformanceEphemeral,
|
||||
type PlayerCountPeriod,
|
||||
} from '../../stores/tankPerformanceEphemeral';
|
||||
import { Performance, type PlayerCountPeriod } from '../../stores/performance';
|
||||
|
||||
export function PlayerCountControl() {
|
||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const mutateTankPerformanceSort = TankPerformanceEphemeral.useMutation();
|
||||
const playerCountPeriod = Performance.use((state) => state.playerCountPeriod);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -18,7 +12,7 @@ export function PlayerCountControl() {
|
||||
<Select.Root
|
||||
value={playerCountPeriod}
|
||||
onValueChange={(period) => {
|
||||
mutateTankPerformanceSort((draft) => {
|
||||
Performance.mutate((draft) => {
|
||||
draft.playerCountPeriod = period as PlayerCountPeriod;
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
||||
import { Performance } from '../../stores/performance';
|
||||
import { TankRowHeaderCell } from '../TankRowHeaderCell';
|
||||
|
||||
interface TankRowProps {
|
||||
@@ -17,7 +17,7 @@ export const TankRow = memo<TankRowProps>(
|
||||
({ tank }) => {
|
||||
const averages = averageDefinitions.averages[tank.id];
|
||||
const ratio = useAveragesExclusionRatio();
|
||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||
const playerCountPeriod = Performance.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const { locale } = useLocale();
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Fragment, Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { RowLoader } from './RowLoader';
|
||||
import { TankRow } from './TankRow';
|
||||
import { Total } from './Total';
|
||||
import { Table } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { Performance } from "../../stores/performance";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { RowLoader } from "./RowLoader";
|
||||
import { TankRow } from "./TankRow";
|
||||
import { Total } from "./Total";
|
||||
|
||||
const PREVIEW_COUNT = 10;
|
||||
const DEFAULT_LOADED_ROWS = 25;
|
||||
@@ -25,7 +24,7 @@ const averageDefinitionsArray = Object.entries(averageDefinitions.averages).map(
|
||||
([id, average]) => ({
|
||||
id: Number(id),
|
||||
...average,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
@@ -40,98 +39,97 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
}
|
||||
|
||||
const sort = TankPerformanceSort.use();
|
||||
const tankPerformancePersistentStore = TankPerformanceEphemeral.useStore();
|
||||
const filters = useStore($tankFilters);
|
||||
const tanksSorted = useMemo(() => {
|
||||
const { playerCountPeriod } = tankPerformancePersistentStore.getState();
|
||||
const { playerCountPeriod } = Performance.state;
|
||||
|
||||
switch (sort.type) {
|
||||
case 'accuracy':
|
||||
case "accuracy":
|
||||
return averageDefinitionsArray.sort(
|
||||
(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(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(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(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(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(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(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(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(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(
|
||||
(a, b) =>
|
||||
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(
|
||||
(a, b) =>
|
||||
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(
|
||||
(a, b) =>
|
||||
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(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod]),
|
||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
|
||||
);
|
||||
case 'spots':
|
||||
case "spots":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
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(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(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(
|
||||
(a, b) =>
|
||||
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(
|
||||
(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]);
|
||||
const filters = TankFilters.use();
|
||||
const tanks = useMemo(() => {
|
||||
return tanksSorted.filter((tank) =>
|
||||
filterTank(filters, tankDefinitions.tanks[tank.id]),
|
||||
filterTank(filters, tankDefinitions.tanks[tank.id])
|
||||
);
|
||||
}, [filters, sort]);
|
||||
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { memo, useCallback } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { useAveragesExclusionRatio } from '../../hooks/useAveragesExclusionRatio';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { TankPerformanceEphemeral } from '../../stores/tankPerformanceEphemeral';
|
||||
import { Performance } from '../../stores/performance';
|
||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||
|
||||
interface AverageDefinitionsEntryWithId extends AverageDefinitionsEntry {
|
||||
@@ -23,7 +23,7 @@ const averageDefinitions = await awaitableAverageDefinitions;
|
||||
|
||||
export const Total = memo<TotalProps>(
|
||||
({ tanks }) => {
|
||||
const playerCountPeriod = TankPerformanceEphemeral.use(
|
||||
const playerCountPeriod = Performance.use(
|
||||
(state) => state.playerCountPeriod,
|
||||
);
|
||||
const { strings, locale } = useLocale();
|
||||
|
||||
@@ -4,11 +4,10 @@ import {
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useMemo } from 'react';
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { resolveReload } from '../../../core/blitzkit/resolveReload';
|
||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
import { TankCard } from '../../TankCard';
|
||||
|
||||
interface TankSearchCardProps {
|
||||
@@ -19,9 +18,9 @@ interface TankSearchCardProps {
|
||||
const modelDefinitions = await awaitableModelDefinitions;
|
||||
|
||||
export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
const by = TankSort.use((state) => state.by);
|
||||
const discriminator = useMemo(() => {
|
||||
if (tankopediaSort.by.startsWith('meta')) return undefined;
|
||||
if (by.startsWith('meta')) return undefined;
|
||||
|
||||
const turret = tank.turrets.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 gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
switch (tankopediaSort.by) {
|
||||
switch (by) {
|
||||
case 'fire.aimTime':
|
||||
return gun.aim_time.toFixed(2);
|
||||
case 'fire.caliber':
|
||||
@@ -133,7 +132,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
||||
}
|
||||
}
|
||||
}, [tankopediaSort.by]);
|
||||
}, [by]);
|
||||
|
||||
return (
|
||||
<TankCard
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
TANK_CLASSES,
|
||||
TankType,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
} from "@blitzkit/core";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -14,29 +13,22 @@ import {
|
||||
Link,
|
||||
Popover,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { isEqual, times } from 'lodash-es';
|
||||
import { awaitableGameDefinitions } from '../../../core/awaitables/gameDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import {
|
||||
$tankFilters,
|
||||
initialTankFilters,
|
||||
type TankFilters,
|
||||
} from '../../../stores/tankFilters';
|
||||
import {
|
||||
$tankopediaSort,
|
||||
initialTankopediaSort,
|
||||
} from '../../../stores/tankopediaSort';
|
||||
import { classIcons } from '../../ClassIcon';
|
||||
import { ExperimentIcon } from '../../ExperimentIcon';
|
||||
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';
|
||||
} from "@radix-ui/themes";
|
||||
import { isEqual, times } from "lodash-es";
|
||||
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
|
||||
import { Var } from "../../../core/radix/var";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { classIcons } from "../../ClassIcon";
|
||||
import { ExperimentIcon } from "../../ExperimentIcon";
|
||||
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 {
|
||||
compact?: boolean;
|
||||
@@ -45,29 +37,29 @@ interface FilterControlProps {
|
||||
const gameDefinitions = await awaitableGameDefinitions;
|
||||
|
||||
const shellTypeIcons: Record<ShellType, string> = {
|
||||
[ShellType.AP]: 'ap',
|
||||
[ShellType.APCR]: 'ap_cr',
|
||||
[ShellType.HE]: 'he',
|
||||
[ShellType.HEAT]: 'hc',
|
||||
[ShellType.AP]: "ap",
|
||||
[ShellType.APCR]: "ap_cr",
|
||||
[ShellType.HE]: "he",
|
||||
[ShellType.HEAT]: "hc",
|
||||
};
|
||||
|
||||
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const shells = TankFilters.use((state) => state.shells);
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
||||
{tankFilters.shells[index] === null && (
|
||||
<Text color="gray" style={{ display: 'contents' }}>
|
||||
{shells[index] === null && (
|
||||
<Text color="gray" style={{ display: "contents" }}>
|
||||
<MissingShellIcon width="1em" height="1em" />
|
||||
</Text>
|
||||
)}
|
||||
{tankFilters.shells[index] !== null && (
|
||||
{shells[index] !== null && (
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
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>
|
||||
<Flex>
|
||||
{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 (
|
||||
<IconButton
|
||||
key={shellType}
|
||||
value={`${shellType}`}
|
||||
radius="none"
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
onClick={() => {
|
||||
const mutated = [
|
||||
tankFilters.shells[0],
|
||||
tankFilters.shells[1],
|
||||
tankFilters.shells[2],
|
||||
] as TankFilters['shells'];
|
||||
const mutated = [...shells] as TankFilters["shells"];
|
||||
|
||||
mutated[index] = selected ? null : shellType;
|
||||
|
||||
$tankFilters.setKey('shells', mutated);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.shells = mutated;
|
||||
});
|
||||
}}
|
||||
highContrast={selected}
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
>
|
||||
<img
|
||||
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>
|
||||
);
|
||||
@@ -119,26 +109,26 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
}
|
||||
|
||||
export function FilterControl({ compact }: FilterControlProps) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const tankFilters = TankFilters.use();
|
||||
const { strings } = useLocale();
|
||||
const clearable = !isEqual(tankFilters, initialTankFilters);
|
||||
const clearable = !isEqual(tankFilters, TankFilters.initial);
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center">
|
||||
<Flex
|
||||
height="fit-content"
|
||||
gap="2"
|
||||
align={{ initial: 'start', md: 'center' }}
|
||||
align={{ initial: "start", md: "center" }}
|
||||
justify="center"
|
||||
>
|
||||
<Flex
|
||||
flexShrink="0"
|
||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-5)' }}
|
||||
style={{ borderRadius: "var(--radius-5)" }}
|
||||
>
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{times(5, (index) => {
|
||||
const tier = index + 1;
|
||||
@@ -147,21 +137,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={tier}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.tiers.includes(tier)) {
|
||||
$tankFilters.setKey(
|
||||
'tiers',
|
||||
tankFilters.tiers.filter((t) => t !== tier),
|
||||
);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||
});
|
||||
} else {
|
||||
$tankFilters.setKey('tiers', [
|
||||
...tankFilters.tiers,
|
||||
tier,
|
||||
]);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = [...draft.tiers, tier];
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -172,15 +160,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
display={{ initial: 'none', md: 'block' }}
|
||||
display={{ initial: "none", md: "block" }}
|
||||
style={{
|
||||
height: Var('space-1'),
|
||||
backgroundColor: Var('gray-a3'),
|
||||
height: Var("space-1"),
|
||||
backgroundColor: Var("gray-a3"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{times(5, (index) => {
|
||||
const tier = index + 6;
|
||||
@@ -189,21 +177,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={tier}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.tiers.includes(tier)) {
|
||||
$tankFilters.setKey(
|
||||
'tiers',
|
||||
tankFilters.tiers.filter((t) => t !== tier),
|
||||
);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||
});
|
||||
} else {
|
||||
$tankFilters.setKey('tiers', [
|
||||
...tankFilters.tiers,
|
||||
tier,
|
||||
]);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = [...draft.tiers, tier];
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -216,12 +202,12 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
|
||||
<Flex
|
||||
flexShrink="0"
|
||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-5)' }}
|
||||
style={{ borderRadius: "var(--radius-5)" }}
|
||||
>
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{gameDefinitions.nations
|
||||
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
||||
@@ -231,26 +217,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={nation}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
color={selected ? undefined : 'gray'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
$tankFilters.setKey(
|
||||
'nations',
|
||||
tankFilters.nations.filter((n) => n !== nation),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('nations', [
|
||||
...tankFilters.nations,
|
||||
nation,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
draft.nations = draft.nations.filter(
|
||||
(n) => n !== nation
|
||||
);
|
||||
} else {
|
||||
draft.nations = [...draft.nations, nation];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(`flags/circle/${nation}.webp`)}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -259,15 +243,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
display={{ initial: 'none', md: 'block' }}
|
||||
display={{ initial: "none", md: "block" }}
|
||||
style={{
|
||||
height: Var('space-1'),
|
||||
backgroundColor: Var('gray-a3'),
|
||||
height: Var("space-1"),
|
||||
backgroundColor: Var("gray-a3"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{gameDefinitions.nations
|
||||
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
||||
@@ -278,26 +262,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
<IconButton
|
||||
key={nation}
|
||||
style={{ flex: 1 }}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
color={selected ? undefined : 'gray'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
$tankFilters.setKey(
|
||||
'nations',
|
||||
tankFilters.nations.filter((n) => n !== nation),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('nations', [
|
||||
...tankFilters.nations,
|
||||
nation,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
draft.nations = draft.nations.filter(
|
||||
(n) => n !== nation
|
||||
);
|
||||
} else {
|
||||
draft.nations = [...draft.nations, nation];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(`flags/circle/${nation}.webp`)}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -307,15 +289,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
direction={{ initial: 'row', md: 'column' }}
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
align="start"
|
||||
gap={{ initial: '2', md: '1' }}
|
||||
gap={{ initial: "2", md: "1" }}
|
||||
>
|
||||
<Flex gap="2" align="start">
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{TANK_CLASSES.map((tankClass) => {
|
||||
const Icon = classIcons[tankClass];
|
||||
@@ -324,25 +306,23 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={tankClass}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.classes.includes(tankClass)) {
|
||||
$tankFilters.setKey(
|
||||
'classes',
|
||||
tankFilters.classes.filter((c) => c !== tankClass),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('classes', [
|
||||
...tankFilters.classes,
|
||||
tankClass,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.classes.includes(tankClass)) {
|
||||
draft.classes = draft.classes.filter(
|
||||
(c) => c !== tankClass
|
||||
);
|
||||
} else {
|
||||
draft.classes = [...draft.classes, tankClass];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon style={{ width: '1em', height: '1em' }} />
|
||||
<Icon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
@@ -350,96 +330,90 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes('regular') ? 'solid' : 'soft'
|
||||
tankFilters.gunType.includes("regular") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes('regular') ? undefined : 'gray'
|
||||
tankFilters.gunType.includes("regular") ? undefined : "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.gunType.includes('regular')) {
|
||||
$tankFilters.setKey(
|
||||
'gunType',
|
||||
tankFilters.gunType.filter((t) => t !== 'regular'),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('gunType', [
|
||||
...tankFilters.gunType,
|
||||
'regular',
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("regular")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "regular"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "regular"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunRegularIcon style={{ width: '1em', height: '1em' }} />
|
||||
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes('auto_loader') ? 'solid' : 'soft'
|
||||
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes('auto_loader')
|
||||
tankFilters.gunType.includes("auto_loader")
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.gunType.includes('auto_loader')) {
|
||||
$tankFilters.setKey(
|
||||
'gunType',
|
||||
tankFilters.gunType.filter((t) => t !== 'auto_loader'),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('gunType', [
|
||||
...tankFilters.gunType,
|
||||
'auto_loader',
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_loader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_loader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_loader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoloaderIcon style={{ width: '1em', height: '1em' }} />
|
||||
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes('auto_reloader')
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
tankFilters.gunType.includes("auto_reloader")
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes('auto_reloader')
|
||||
tankFilters.gunType.includes("auto_reloader")
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.gunType.includes('auto_reloader')) {
|
||||
$tankFilters.setKey(
|
||||
'gunType',
|
||||
tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('gunType', [
|
||||
...tankFilters.gunType,
|
||||
'auto_reloader',
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_reloader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_reloader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_reloader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoreloaderIcon style={{ width: '1em', height: '1em' }} />
|
||||
<GunAutoreloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<ShellFilter index={0} />
|
||||
<ShellFilter index={1} premium />
|
||||
@@ -450,160 +424,158 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
<Flex gap="2" align="start">
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||
$tankFilters.setKey(
|
||||
'types',
|
||||
tankFilters.types.filter(
|
||||
(t) => t !== TankType.RESEARCHABLE,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('types', [
|
||||
...tankFilters.types,
|
||||
TankType.RESEARCHABLE,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.RESEARCHABLE
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.RESEARCHABLE];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? 'amber'
|
||||
: 'gray'
|
||||
? "amber"
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||
$tankFilters.setKey(
|
||||
'types',
|
||||
tankFilters.types.filter((t) => t !== TankType.PREMIUM),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('types', [
|
||||
...tankFilters.types,
|
||||
TankType.PREMIUM,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.PREMIUM
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.PREMIUM];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? 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>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? 'blue'
|
||||
: 'gray'
|
||||
? "blue"
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||
$tankFilters.setKey(
|
||||
'types',
|
||||
tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('types', [
|
||||
...tankFilters.types,
|
||||
TankType.COLLECTOR,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.COLLECTOR
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.COLLECTOR];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? 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>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === 'include' ? 'solid' : 'soft'}
|
||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === 'include' ? undefined : 'gray'}
|
||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
$tankFilters.setKey('testing', 'include');
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "include";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ExperimentIcon
|
||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === 'exclude' ? 'solid' : 'soft'}
|
||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === 'exclude' ? undefined : 'gray'}
|
||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
$tankFilters.setKey('testing', 'exclude');
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "exclude";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceOffIcon
|
||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === 'only' ? 'solid' : 'soft'}
|
||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === 'only' ? undefined : 'gray'}
|
||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
$tankFilters.setKey('testing', 'only');
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "only";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceIcon
|
||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
@@ -619,8 +591,8 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
$tankFilters.set(initialTankFilters);
|
||||
$tankopediaSort.set(initialTankopediaSort);
|
||||
TankFilters.set(TankFilters.initial);
|
||||
TankSort.set(TankSort.initial);
|
||||
}}
|
||||
>
|
||||
{strings.website.common.tank_search.clear_filters}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
||||
import { Flex, Link, Text } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
|
||||
interface NoResultsProps {
|
||||
type?: 'filters' | 'search';
|
||||
type?: "filters" | "search";
|
||||
}
|
||||
|
||||
export function NoResults({ type = 'filters' }: NoResultsProps) {
|
||||
export function NoResults({ type = "filters" }: NoResultsProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" align="center" justify="center">
|
||||
<Text color="gray">
|
||||
{strings.website.common.tank_search.no_tanks_found.body}{' '}
|
||||
{strings.website.common.tank_search.no_tanks_found.body}{" "}
|
||||
<Link
|
||||
href="#"
|
||||
underline="always"
|
||||
color="red"
|
||||
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.search_again}
|
||||
</Link>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { type TankDefinition } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, TextField } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import { LinkI18n } from '../../LinkI18n';
|
||||
import { type TankDefinition } from "@blitzkit/core";
|
||||
import { CaretRightIcon } from "@radix-ui/react-icons";
|
||||
import { Button, TextField } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { LinkI18n } from "../../LinkI18n";
|
||||
|
||||
interface QuickLinkProps {
|
||||
topResult?: TankDefinition;
|
||||
}
|
||||
|
||||
export function QuickLink({ topResult }: QuickLinkProps) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
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 (
|
||||
<TextField.Slot>
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Flex, Separator, Text } from '@radix-ui/themes';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||
import { TankCard } from '../../TankCard';
|
||||
import { TankCardWrapper } from './TankCardWrapper';
|
||||
import { Flex, Separator, Text } from "@radix-ui/themes";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useMemo } from "react";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { TankCard } from "../../TankCard";
|
||||
import { TankCardWrapper } from "./TankCardWrapper";
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export function RecentlyViewed() {
|
||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||
const filters = useStore($tankFilters);
|
||||
const filters = TankFilters.use();
|
||||
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
||||
const recentlyViewed = tankopediaPersistentStore
|
||||
.getState()
|
||||
.recentlyViewed.filter((id) => id in tankDefinitions.tanks);
|
||||
const sort = useStore($tankopediaSort);
|
||||
const hasFilters = useMemo(
|
||||
() =>
|
||||
Object.entries(filters).some(([key, value]) => {
|
||||
return !isEqual(
|
||||
value,
|
||||
initialTankFilters[key as keyof typeof initialTankFilters],
|
||||
TankFilters.initial[key as keyof typeof TankFilters.initial]
|
||||
);
|
||||
}),
|
||||
[filters],
|
||||
[filters]
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { TankDefinition } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Spinner, TextField } from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { type KeyboardEventHandler, useCallback, useRef } from 'react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
import { QuickLink } from './QuickLink';
|
||||
import { Sort } from './Sort';
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { Flex, Spinner, TextField } from "@radix-ui/themes";
|
||||
import { debounce } from "lodash-es";
|
||||
import { type KeyboardEventHandler, useCallback, useRef } from "react";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
import { QuickLink } from "./QuickLink";
|
||||
import { Sort } from "./Sort";
|
||||
|
||||
type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||
topResult?: TankDefinition;
|
||||
@@ -17,30 +16,37 @@ type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||
|
||||
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
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 performSearch = useCallback(
|
||||
debounce(() => {
|
||||
$tankFilters.setKey('searching', false);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.searching = false;
|
||||
});
|
||||
|
||||
if (!input.current) return;
|
||||
|
||||
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),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
const handleChange = useCallback(() => {
|
||||
if (!tankFilters.searching) {
|
||||
$tankFilters.setKey('searching', true);
|
||||
if (!searching) {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.searching = true;
|
||||
});
|
||||
}
|
||||
|
||||
performSearch();
|
||||
}, [tankFilters]);
|
||||
}, [searching]);
|
||||
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||
(event) => {
|
||||
if (event.key !== 'Enter' || !topResult || tankFilters.searching) return;
|
||||
if (event.key !== "Enter" || !topResult || searching) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -50,7 +56,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
window.location.href = `/tanks/${topResult.slug}`;
|
||||
}
|
||||
},
|
||||
[topResult],
|
||||
[topResult]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -59,7 +65,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
disabled={skeleton}
|
||||
defaultValue={tankFilters.search ?? undefined}
|
||||
defaultValue={search ?? undefined}
|
||||
style={{ flex: 1 }}
|
||||
ref={input}
|
||||
placeholder={strings.website.common.tank_search.search_bar_hint}
|
||||
@@ -67,7 +73,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<TextField.Slot>
|
||||
{tankFilters.searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||
</TextField.Slot>
|
||||
|
||||
<QuickLink topResult={topResult} />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||
import { Button, DropdownMenu } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
@@ -6,7 +5,7 @@ import type {
|
||||
TankopediaSortBy,
|
||||
TankopediaSortDirection,
|
||||
} from '../../../stores/tankopediaPersistent';
|
||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
|
||||
interface ItemProps {
|
||||
by: TankopediaSortBy;
|
||||
@@ -18,13 +17,15 @@ interface ItemProps {
|
||||
|
||||
function Item({ by }: ItemProps) {
|
||||
const { strings } = useLocale();
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
const _by = TankSort.use((state) => state.by);
|
||||
|
||||
return (
|
||||
<DropdownMenu.CheckboxItem
|
||||
checked={tankopediaSort.by === by}
|
||||
checked={_by === by}
|
||||
onClick={() => {
|
||||
$tankopediaSort.setKey('by', by);
|
||||
TankSort.mutate((draft) => {
|
||||
draft.by = by;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{strings.website.common.tank_search.sort[by]}
|
||||
@@ -33,7 +34,7 @@ function Item({ by }: ItemProps) {
|
||||
}
|
||||
|
||||
export function Sort() {
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
const direction = TankSort.use((state) => state.direction);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -119,12 +120,11 @@ export function Sort() {
|
||||
}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaSort.direction}
|
||||
value={direction}
|
||||
onValueChange={(value) => {
|
||||
$tankopediaSort.setKey(
|
||||
'direction',
|
||||
value as TankopediaSortDirection,
|
||||
);
|
||||
TankSort.mutate((draft) => {
|
||||
draft.direction = value as TankopediaSortDirection;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="ascending">
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Flex, IconButton, Text } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import { $tankopediaSort } from '../../../stores/tankopediaSort';
|
||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { memo } from "react";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
|
||||
export const TierFilter = memo(() => {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
const by = TankSort.use((state) => state.by);
|
||||
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 (
|
||||
<Flex justify="center">
|
||||
<Flex
|
||||
direction="row"
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
>
|
||||
{times(10, (index) => {
|
||||
const tier = 10 - index;
|
||||
const selected = tankFilters.tiers.includes(tier);
|
||||
const selected = tiers.includes(tier);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
key={tier}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.tiers.includes(tier)) {
|
||||
$tankFilters.setKey(
|
||||
'tiers',
|
||||
tankFilters.tiers.filter((t) => t !== tier),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('tiers', [...tankFilters.tiers, tier]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tiers.includes(tier)) {
|
||||
draft.tiers = tiers.filter((t) => t !== tier);
|
||||
} else {
|
||||
draft.tiers = [...tiers, tier];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
||||
|
||||
@@ -5,37 +5,36 @@ import {
|
||||
SEARCH_KEYS,
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { times, uniq } from 'lodash-es';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { awaitableTankNames } from '../../core/awaitables/tankNames';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
||||
import { SORT_UNITS } from '../../stores/tankopediaPersistent/constants';
|
||||
import { $tankopediaSort } from '../../stores/tankopediaSort';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { ExperimentIcon } from '../ExperimentIcon';
|
||||
import { TankSearchCard } from './components/Card';
|
||||
import { FilterControl } from './components/FilterControl';
|
||||
import { NoResults } from './components/NoResults';
|
||||
import { RecentlyViewed } from './components/RecentlyViewed';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { SkeletonTankCard } from './components/SkeletonTankCard';
|
||||
import { TankCardWrapper } from './components/TankCardWrapper';
|
||||
import { MAX_RECENTLY_VIEWED } from './constants';
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { times, uniq } from "lodash-es";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { resolveReload } from "../../core/blitzkit/resolveReload";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
||||
import { TankSort } from "../../stores/tankopediaSort";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { ExperimentIcon } from "../ExperimentIcon";
|
||||
import { TankSearchCard } from "./components/Card";
|
||||
import { FilterControl } from "./components/FilterControl";
|
||||
import { NoResults } from "./components/NoResults";
|
||||
import { RecentlyViewed } from "./components/RecentlyViewed";
|
||||
import { SearchBar } from "./components/SearchBar";
|
||||
import { SkeletonTankCard } from "./components/SkeletonTankCard";
|
||||
import { TankCardWrapper } from "./components/TankCardWrapper";
|
||||
import { MAX_RECENTLY_VIEWED } from "./constants";
|
||||
|
||||
type TankSearchProps = MaybeSkeletonComponentProps &
|
||||
Omit<FlexProps, 'onSelect'> & {
|
||||
Omit<FlexProps, "onSelect"> & {
|
||||
compact?: boolean;
|
||||
onSelect?: (tank: TankDefinition) => void;
|
||||
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||
@@ -57,21 +56,21 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const { strings, locale } = useLocale();
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const tankopediaSort = useStore($tankopediaSort);
|
||||
const tankopediaSort = TankSort.use();
|
||||
const tankFilters = TankFilters.use();
|
||||
const tanksFiltered = useMemo(() => {
|
||||
if (tankFilters.search === null) {
|
||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||
filterTank(tankFilters, tank),
|
||||
filterTank(tankFilters, tank)
|
||||
);
|
||||
let sorted: TankDefinition[];
|
||||
|
||||
switch (tankopediaSort.by) {
|
||||
case 'meta.none':
|
||||
case "meta.none":
|
||||
sorted = metaSortTank(filtered, gameDefinitions);
|
||||
break;
|
||||
|
||||
case 'survivability.health': {
|
||||
case "survivability.health": {
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const aHealth = a.health + a.turrets.at(-1)!.health;
|
||||
const bHealth = b.health + b.turrets.at(-1)!.health;
|
||||
@@ -81,36 +80,36 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
break;
|
||||
}
|
||||
|
||||
case 'survivability.viewRange':
|
||||
case "survivability.viewRange":
|
||||
sorted = filtered.sort(
|
||||
(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;
|
||||
|
||||
case 'survivability.camouflageStill':
|
||||
case "survivability.camouflageStill":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.camouflage_still - b.camouflage_still,
|
||||
(a, b) => a.camouflage_still - b.camouflage_still
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.camouflageMoving':
|
||||
case "survivability.camouflageMoving":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.camouflage_moving - b.camouflage_moving,
|
||||
(a, b) => a.camouflage_moving - b.camouflage_moving
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.camouflageShooting':
|
||||
case "survivability.camouflageShooting":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.camouflage_still *
|
||||
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
||||
b.camouflage_still *
|
||||
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.volume':
|
||||
case "survivability.volume":
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||
@@ -121,14 +120,14 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const aSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box,
|
||||
aTurretModelDefinition.bounding_box,
|
||||
),
|
||||
aTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box,
|
||||
bTurretModelDefinition.bounding_box,
|
||||
),
|
||||
bTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
||||
@@ -137,7 +136,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
});
|
||||
break;
|
||||
|
||||
case 'survivability.length':
|
||||
case "survivability.length":
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||
@@ -148,14 +147,14 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const aSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box,
|
||||
aTurretModelDefinition.bounding_box,
|
||||
),
|
||||
aTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box,
|
||||
bTurretModelDefinition.bounding_box,
|
||||
),
|
||||
bTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
||||
@@ -164,147 +163,147 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
});
|
||||
break;
|
||||
|
||||
case 'fire.dpm':
|
||||
case "fire.dpm":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
resolveDpm(
|
||||
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(
|
||||
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;
|
||||
|
||||
case 'fire.dpmPremium':
|
||||
case "fire.dpmPremium":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||
? resolveDpm(
|
||||
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) -
|
||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||
? resolveDpm(
|
||||
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;
|
||||
|
||||
case 'fire.reload':
|
||||
case "fire.reload":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.caliber':
|
||||
case "fire.caliber":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.standardPenetration':
|
||||
case "fire.standardPenetration":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.premiumPenetration':
|
||||
case "fire.premiumPenetration":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||
0) -
|
||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.tertiaryPenetration':
|
||||
case "fire.tertiaryPenetration":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||
0) -
|
||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.damage':
|
||||
case "fire.damage":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.moduleDamage':
|
||||
case "fire.moduleDamage":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.shellVelocity':
|
||||
case "fire.shellVelocity":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.shellRange':
|
||||
case "fire.shellRange":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.shellCapacity':
|
||||
case "fire.shellCapacity":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.aimTime':
|
||||
case "fire.aimTime":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.dispersionStill':
|
||||
case "fire.dispersionStill":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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;
|
||||
|
||||
case 'fire.dispersionMoving':
|
||||
case "fire.dispersionMoving":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.tracks.at(-1)!.dispersion_move -
|
||||
b.tracks.at(-1)!.dispersion_move,
|
||||
b.tracks.at(-1)!.dispersion_move
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.gunDepression':
|
||||
case "fire.gunDepression":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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]
|
||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
|
||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.gunElevation':
|
||||
case "fire.gunElevation":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
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]
|
||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
|
||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.forwardsSpeed':
|
||||
case "maneuverability.forwardsSpeed":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.speed_forwards - b.speed_forwards,
|
||||
(a, b) => a.speed_forwards - b.speed_forwards
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.backwardsSpeed':
|
||||
case "maneuverability.backwardsSpeed":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.speed_backwards - b.speed_backwards,
|
||||
(a, b) => a.speed_backwards - b.speed_backwards
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.power':
|
||||
case "maneuverability.power":
|
||||
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;
|
||||
|
||||
case 'maneuverability.powerToWeight':
|
||||
case "maneuverability.powerToWeight":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.engines.at(-1)!.power /
|
||||
@@ -364,11 +363,11 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
b.engines.at(-1)!.weight +
|
||||
b.tracks.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;
|
||||
|
||||
case 'maneuverability.weight':
|
||||
case "maneuverability.weight":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.weight +
|
||||
@@ -380,20 +379,20 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
b.engines.at(-1)!.weight +
|
||||
b.tracks.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;
|
||||
|
||||
case 'maneuverability.traverseSpeed':
|
||||
case "maneuverability.traverseSpeed":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.tracks.at(-1)!.traverse_speed -
|
||||
b.tracks.at(-1)!.traverse_speed,
|
||||
b.tracks.at(-1)!.traverse_speed
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return tankopediaSort.direction === 'ascending'
|
||||
return tankopediaSort.direction === "ascending"
|
||||
? sorted
|
||||
: sorted.reverse();
|
||||
} else {
|
||||
@@ -401,7 +400,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
keys: SEARCH_KEYS,
|
||||
});
|
||||
const searchedTanks = searchedRaw.map(
|
||||
(result) => tankDefinitions.tanks[result.obj.id],
|
||||
(result) => tankDefinitions.tanks[result.obj.id]
|
||||
);
|
||||
return searchedTanks;
|
||||
}
|
||||
@@ -459,24 +458,24 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{tankopediaSort.by !== 'meta.none' && (
|
||||
{tankopediaSort.by !== "meta.none" && (
|
||||
<Text color="gray">
|
||||
{literals(strings.website.common.tank_search.sorting_by, [
|
||||
strings.website.common.tank_search.sort[tankopediaSort.by],
|
||||
])}
|
||||
{SORT_UNITS[tankopediaSort.by] === undefined
|
||||
? ''
|
||||
? ""
|
||||
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
||||
, {tankopediaSort.direction}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{tankFilters.testing === 'only' && (
|
||||
{tankFilters.testing === "only" && (
|
||||
<Flex justify="center" mt="4">
|
||||
<Callout.Root color="amber">
|
||||
<Callout.Icon>
|
||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>
|
||||
{strings.website.common.warnings.test}
|
||||
@@ -505,12 +504,12 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
key={index}
|
||||
onIntersection={() => {
|
||||
setLoadedCards((state) =>
|
||||
Math.min(state + 2, tanksFiltered.length),
|
||||
Math.min(state + 2, tanksFiltered.length)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}
|
||||
)}
|
||||
</TankCardWrapper>
|
||||
)}
|
||||
@@ -523,15 +522,15 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
<TankCardWrapper>
|
||||
{times(
|
||||
Math.round(
|
||||
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random(),
|
||||
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random()
|
||||
),
|
||||
(index) => (
|
||||
<SkeletonTankCard key={index} />
|
||||
),
|
||||
)
|
||||
)}
|
||||
</TankCardWrapper>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
+1
-2
@@ -13,7 +13,6 @@ export function CompareOptions(props: FlexProps) {
|
||||
(state) => state.relativeAgainst,
|
||||
);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -33,7 +32,7 @@ export function CompareOptions(props: FlexProps) {
|
||||
size="1"
|
||||
value={`${relativeAgainst}`}
|
||||
onValueChange={(value) => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
||||
});
|
||||
}}
|
||||
|
||||
+1
-30
@@ -1,14 +1,6 @@
|
||||
import {
|
||||
asset,
|
||||
coefficient,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { asset, isExplosive } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { Flex, Heading, IconButton, Tooltip } from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
@@ -28,34 +20,13 @@ export function Firepower({
|
||||
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
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 gun = Duel.use((state) => state.protagonist.gun);
|
||||
const shell = Duel.use((state) => state.protagonist.shell);
|
||||
const penetrationDistanceInput = useRef<HTMLInputElement>(null);
|
||||
const hasSupercharger = useEquipment(107);
|
||||
const hasCalibratedShells = useEquipment(103);
|
||||
const penetrationCoefficient = coefficient([
|
||||
hasCalibratedShells,
|
||||
resolvePenetrationCoefficient(true, shell.type) - 1,
|
||||
]);
|
||||
const penetrationLossOverDistanceCoefficient = coefficient([
|
||||
hasSupercharger,
|
||||
-0.6,
|
||||
]);
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||
|
||||
useEffect(() => {
|
||||
if (penetrationDistanceInput.current) {
|
||||
penetrationDistanceInput.current.value = `${penetrationDistance}`;
|
||||
}
|
||||
}, [penetrationDistance]);
|
||||
|
||||
return (
|
||||
<StatsTableWrapper>
|
||||
<Flex align="center" gap="4">
|
||||
|
||||
+1
-6
@@ -80,12 +80,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
||||
)
|
||||
.map((tank) => {
|
||||
const thisTankModel = modelDefinitions.models[tank.id];
|
||||
const member = tankToDuelMember(
|
||||
tank,
|
||||
thisTankModel,
|
||||
provisionDefinitions,
|
||||
);
|
||||
const member = tankToDuelMember(tank, provisionDefinitions);
|
||||
|
||||
if (member.gun.shells[shellIndex] === undefined) {
|
||||
return undefined;
|
||||
|
||||
+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 { Quicklime, type QuicklimeCallback } from 'quicklime';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Var } from '../../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||
import { VisualizerCard } from '../VisualizerCard';
|
||||
import { Shell } from './components/Shell';
|
||||
@@ -19,6 +21,8 @@ export interface ReloadUpdateData {
|
||||
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
|
||||
|
||||
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
const reloadCircle = useRef<HTMLDivElement>(null);
|
||||
const reloadCore = useRef<HTMLDivElement>(null);
|
||||
const reloadGlow = useRef<HTMLDivElement>(null);
|
||||
@@ -246,7 +250,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
|
||||
<Code
|
||||
ref={progressTime}
|
||||
size="1"
|
||||
size="2"
|
||||
variant="ghost"
|
||||
weight="bold"
|
||||
color="gray"
|
||||
@@ -255,14 +259,14 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
right: 'calc(50% + var(--space-8))',
|
||||
transform: 'translateX(50%)',
|
||||
transform: 'translate(50%, -50%)',
|
||||
}}
|
||||
>
|
||||
0.00
|
||||
</Code>
|
||||
<Code
|
||||
ref={totalTime}
|
||||
size="1"
|
||||
size="2"
|
||||
variant="ghost"
|
||||
weight="bold"
|
||||
color="gray"
|
||||
@@ -270,7 +274,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: 'calc(50% + var(--space-8))',
|
||||
transform: 'translateX(-50%)',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
0.00
|
||||
@@ -290,6 +294,25 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
width="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>
|
||||
);
|
||||
}
|
||||
+2
-3
@@ -10,7 +10,6 @@ export function StatSearch(props: FlexProps) {
|
||||
const { strings } = useLocale();
|
||||
const [searching, setSearching] = useState(false);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const count = useRef<HTMLDivElement>(null);
|
||||
const [index, setIndex] = useState(0);
|
||||
const search = useCallback(() => {
|
||||
@@ -34,14 +33,14 @@ export function StatSearch(props: FlexProps) {
|
||||
count.current.textContent = '';
|
||||
|
||||
setSearching(false);
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.statSearch = undefined;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSearching(false);
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.statSearch = input.current!.value;
|
||||
});
|
||||
}, 500),
|
||||
|
||||
-1
@@ -21,7 +21,6 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||
|
||||
+3
-4
@@ -14,7 +14,6 @@ import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
|
||||
export function Skills() {
|
||||
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
||||
const mutateTankopediaTemporary = TankopediaEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -46,7 +45,7 @@ export function Skills() {
|
||||
variant="ghost"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Object.keys(draft.skills).forEach((skill) => {
|
||||
draft.skills[skill] = 0;
|
||||
});
|
||||
@@ -58,7 +57,7 @@ export function Skills() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Object.keys(draft.skills).forEach((skill) => {
|
||||
draft.skills[skill] = 7;
|
||||
});
|
||||
@@ -72,7 +71,7 @@ export function Skills() {
|
||||
<CrewSkillManager
|
||||
skillLevels={skillLevels}
|
||||
onChange={(skills) => {
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.skills = skills;
|
||||
});
|
||||
}}
|
||||
|
||||
+8
-9
@@ -20,7 +20,6 @@ export function CustomShellButton() {
|
||||
const customShell = TankopediaEphemeral.use((state) => state.customShell);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -28,7 +27,7 @@ export function CustomShellButton() {
|
||||
onOpenChange={(open) => {
|
||||
if (!open || customShell) return;
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell = antagonistShell;
|
||||
});
|
||||
}}
|
||||
@@ -97,7 +96,7 @@ export function CustomShellButton() {
|
||||
}
|
||||
}
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell = modified;
|
||||
});
|
||||
}}
|
||||
@@ -134,7 +133,7 @@ export function CustomShellButton() {
|
||||
defaultValue={customShell.caliber}
|
||||
type="number"
|
||||
onChange={(event) => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell!.caliber = event.target.valueAsNumber;
|
||||
});
|
||||
}}
|
||||
@@ -170,7 +169,7 @@ export function CustomShellButton() {
|
||||
defaultValue={customShell.penetration.near}
|
||||
type="number"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell!.penetration.near =
|
||||
event.target.valueAsNumber;
|
||||
});
|
||||
@@ -226,7 +225,7 @@ export function CustomShellButton() {
|
||||
onValueChange={([value]) => {
|
||||
console.log('asd');
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell!.normalization = value;
|
||||
});
|
||||
}}
|
||||
@@ -255,7 +254,7 @@ export function CustomShellButton() {
|
||||
step={Number.EPSILON}
|
||||
defaultValue={[customShell.ricochet ?? 90]}
|
||||
onValueChange={throttle(([value]) => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell!.ricochet = value;
|
||||
});
|
||||
}, 1000)}
|
||||
@@ -274,7 +273,7 @@ export function CustomShellButton() {
|
||||
defaultValue={customShell.explosion_radius ?? 0}
|
||||
type="number"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell!.explosion_radius =
|
||||
event.target.valueAsNumber;
|
||||
});
|
||||
@@ -310,7 +309,7 @@ export function CustomShellButton() {
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell = antagonistShell;
|
||||
});
|
||||
}}
|
||||
|
||||
+1
-2
@@ -7,7 +7,6 @@ import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemera
|
||||
export function DynamicArmorSwitcher() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const isDynamicArmorActive = Duel.use((state) =>
|
||||
state.protagonist.consumables.includes(73),
|
||||
);
|
||||
@@ -33,7 +32,7 @@ export function DynamicArmorSwitcher() {
|
||||
}
|
||||
}
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
|
||||
-1
@@ -14,7 +14,6 @@ import { Duel } from '../../../../../../../stores/duel';
|
||||
import './index.css';
|
||||
|
||||
export function QuickInputs() {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const protagonist = Duel.use((state) => state.protagonist);
|
||||
const yawInput = useRef<HTMLInputElement>(null);
|
||||
const pitchInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
+6
-8
@@ -26,9 +26,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
(state) => state.showPrimaryArmor,
|
||||
);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -55,7 +53,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -96,7 +94,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -135,7 +133,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showExternalModules = !draft.showExternalModules;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -168,7 +166,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
gap="2"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.editStatic = !draft.editStatic;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -186,9 +184,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
size="1"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.editStatic = false;
|
||||
draft.model = tankopediaEphemeralStore.getInitialState().model;
|
||||
draft.model = TankopediaEphemeral.initial.model;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+35
-134
@@ -12,7 +12,6 @@ import {
|
||||
EnterFullScreenIcon,
|
||||
ExitFullScreenIcon,
|
||||
EyeOpenIcon,
|
||||
GearIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import {
|
||||
Button,
|
||||
@@ -33,7 +32,6 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
||||
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { App } from '../../../../../stores/app';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||
@@ -43,6 +41,7 @@ import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
|
||||
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
|
||||
import { SmallTankIcon } from '../../../../SmallTankIcon';
|
||||
import { TankSearch } from '../../../../TankSearch';
|
||||
import { screenshotReadyEvent } from '../TankSandbox/components/SceneProps';
|
||||
import { CustomShellButton } from './components/CustomShellButton';
|
||||
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
|
||||
import { QuickInputs } from './components/QuickInputs';
|
||||
@@ -59,20 +58,10 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
);
|
||||
const display = TankopediaEphemeral.use((state) => state.display);
|
||||
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(
|
||||
(state) => state.advancedHighlighting,
|
||||
);
|
||||
const wireframe = TankopediaPersistent.use((state) => state.wireframe);
|
||||
const opaque = TankopediaPersistent.use((state) => state.opaque);
|
||||
const fullScreenAvailable = useFullscreenAvailability(true);
|
||||
const developerMode = App.use((state) => state.developerMode);
|
||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||
const antagonistGun = Duel.use((state) => state.antagonist.gun);
|
||||
const antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||
@@ -84,7 +73,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
const hasEnhancedArmor = useEquipment(110);
|
||||
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
@@ -207,7 +195,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
mutateDuel((draft) => {
|
||||
draft.antagonist.shell = thisShell;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
@@ -246,7 +234,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
? 0
|
||||
: 1;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -271,7 +259,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
? 0
|
||||
: -1;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -330,6 +318,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Text color="gray" size="2">
|
||||
{strings.website.tools.tankopedia.sandbox.dynamic.shooter}
|
||||
</Text>
|
||||
|
||||
<Dialog.Root
|
||||
open={antagonistSelectorOpen}
|
||||
onOpenChange={setAntagonistSelectorOpen}
|
||||
@@ -377,7 +366,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
variant="classic"
|
||||
value={`${disturbed ? display : -1}`}
|
||||
onValueChange={(value) => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.disturbed = true;
|
||||
draft.display = Number(value);
|
||||
});
|
||||
@@ -451,106 +440,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
</DropdownMenu.Content>
|
||||
</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.Trigger>
|
||||
<IconButton color="gray" variant="ghost">
|
||||
@@ -563,19 +452,25 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Popover.Close>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!canvas.current) return;
|
||||
screenshotReadyEvent.dispatch(true);
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
requestAnimationFrame(() => {
|
||||
if (!canvas.current) return;
|
||||
|
||||
anchor.setAttribute(
|
||||
'download',
|
||||
`${protagonistTank.name}.png`,
|
||||
);
|
||||
anchor.setAttribute(
|
||||
'href',
|
||||
canvas.current.toDataURL('image/png'),
|
||||
);
|
||||
anchor.click();
|
||||
const anchor = document.createElement('a');
|
||||
|
||||
anchor.setAttribute(
|
||||
'download',
|
||||
`${protagonistTank.name}.png`,
|
||||
);
|
||||
anchor.setAttribute(
|
||||
'href',
|
||||
canvas.current.toDataURL('image/png'),
|
||||
);
|
||||
anchor.click();
|
||||
|
||||
screenshotReadyEvent.dispatch(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DownloadIcon />
|
||||
@@ -589,14 +484,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (!canvas.current) return;
|
||||
screenshotReadyEvent.dispatch(true);
|
||||
|
||||
canvas.current.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
requestAnimationFrame(() => {
|
||||
if (!canvas.current) return;
|
||||
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({ 'image/png': blob }),
|
||||
]);
|
||||
canvas.current.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({ 'image/png': blob }),
|
||||
]);
|
||||
});
|
||||
|
||||
screenshotReadyEvent.dispatch(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ export function Tracker({ fontSize }: Props) {
|
||||
<Box
|
||||
position="absolute"
|
||||
left="50%"
|
||||
bottom="0"
|
||||
top="50%"
|
||||
width="min(100%, 16rem)"
|
||||
style={{
|
||||
transform: `translate(-50%, calc(${fontSize} / 2))`,
|
||||
transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
|
||||
}}
|
||||
>
|
||||
<Progress color="gray" value={progress} />
|
||||
|
||||
+12
-87
@@ -1,113 +1,39 @@
|
||||
import { TankType } from '@blitzkit/core';
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { Var } from '../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { RadixColor } from '../../../../../../stores/embedState';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { Tracker } from './components/Tracker';
|
||||
|
||||
interface TitleProps {
|
||||
foreground?: boolean;
|
||||
}
|
||||
|
||||
export const NATION_COLORS: Record<
|
||||
string,
|
||||
{
|
||||
text: RadixColor;
|
||||
background: RadixColor[];
|
||||
tint: RadixColor;
|
||||
}
|
||||
> = {
|
||||
france: {
|
||||
text: 'mint',
|
||||
tint: 'purple',
|
||||
background: ['mint', 'cyan', 'jade'],
|
||||
},
|
||||
germany: {
|
||||
text: 'sage',
|
||||
tint: 'blue',
|
||||
background: ['slate', 'sage', 'mauve'],
|
||||
},
|
||||
usa: {
|
||||
text: 'gold',
|
||||
tint: 'red',
|
||||
background: ['brown', 'gold', 'gray'],
|
||||
},
|
||||
european: {
|
||||
text: 'sky',
|
||||
tint: 'violet',
|
||||
background: ['cyan', 'blue', 'indigo'],
|
||||
},
|
||||
ussr: {
|
||||
text: 'bronze',
|
||||
tint: 'red',
|
||||
background: ['bronze', 'tomato', 'bronze'],
|
||||
},
|
||||
uk: {
|
||||
text: 'gold',
|
||||
tint: 'red',
|
||||
background: ['bronze', 'gold', 'brown'],
|
||||
},
|
||||
japan: {
|
||||
text: 'mint',
|
||||
tint: 'violet',
|
||||
background: ['green', 'teal', 'jade'],
|
||||
},
|
||||
china: {
|
||||
text: 'mint',
|
||||
tint: 'pink',
|
||||
background: ['green', 'teal', 'jade'],
|
||||
},
|
||||
other: {
|
||||
text: 'amber',
|
||||
tint: 'sky',
|
||||
background: ['amber', 'yellow', 'gray'],
|
||||
},
|
||||
};
|
||||
|
||||
export function Title({ foreground }: TitleProps) {
|
||||
export function Title() {
|
||||
const { unwrap } = useLocale();
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||
const nationColors = NATION_COLORS[protagonist.nation];
|
||||
const color = Var(
|
||||
disturbed
|
||||
? protagonist.type === TankType.COLLECTOR
|
||||
? 'blue-11'
|
||||
: protagonist.type === TankType.PREMIUM
|
||||
? 'amber-11'
|
||||
: 'gray-12'
|
||||
: `${nationColors.text}-12`,
|
||||
);
|
||||
const name = unwrap(protagonist.name);
|
||||
const fontSize = revealed
|
||||
? disturbed
|
||||
? revealed
|
||||
? '2rem'
|
||||
: `min(65vh, ${125 / name.length}vw)`
|
||||
: `min(48vh, ${75 / name.length}vw)`;
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
: `min(16vh, ${125 / name.length}vw)`
|
||||
: `min(12vh, ${75 / name.length}vw)`;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.disturbed = true;
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
}}
|
||||
direction="column"
|
||||
position="absolute"
|
||||
align="center"
|
||||
justify="center"
|
||||
top={disturbed ? '6rem' : '50%'}
|
||||
top={revealed ? '6rem' : '50%'}
|
||||
left="50%"
|
||||
width="100%"
|
||||
height={disturbed ? fontSize : '100%'}
|
||||
height={revealed ? fontSize : '100%'}
|
||||
style={{
|
||||
pointerEvents: disturbed ? 'none' : undefined,
|
||||
pointerEvents: revealed ? 'none' : undefined,
|
||||
transitionDuration: '1s',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
@@ -119,11 +45,10 @@ export function Title({ foreground }: TitleProps) {
|
||||
pointerEvents: 'none',
|
||||
fontSize,
|
||||
whiteSpace: 'nowrap',
|
||||
color: foreground ? Var('gray-a10') : color,
|
||||
opacity: foreground && disturbed ? 0 : 1,
|
||||
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
||||
opacity: revealed ? 1 : 0.5,
|
||||
letterSpacing: revealed || !revealed ? 0 : '-0.03em',
|
||||
transition: `
|
||||
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||
letter-spacing 1.5s ${revealed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||
font-size 1s,
|
||||
-webkit-text-stroke 2s,
|
||||
opacity 1s
|
||||
|
||||
+13
-13
@@ -24,7 +24,6 @@ interface ControlsProps {
|
||||
autoRotate?: boolean;
|
||||
zoomable?: boolean;
|
||||
enableRotate?: boolean;
|
||||
distanceScale?: number;
|
||||
}
|
||||
|
||||
export function Controls({
|
||||
@@ -32,11 +31,8 @@ export function Controls({
|
||||
autoRotate = true,
|
||||
zoomable = true,
|
||||
enableRotate = true,
|
||||
distanceScale = 1,
|
||||
}: ControlsProps) {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const duelStore = Duel.useStore();
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const orbitControls = useRef<OrbitControlsClass>(null);
|
||||
@@ -84,9 +80,9 @@ export function Controls({
|
||||
protagonistGunOrigin.y;
|
||||
const inspectModeInitialPosition = new Vector3(
|
||||
-8,
|
||||
gunHeight + (naked ? 10 : 2),
|
||||
naked ? gunHeight + 10 : 2,
|
||||
-13,
|
||||
).multiplyScalar(distanceScale);
|
||||
);
|
||||
|
||||
const t0 = useRef(Date.now() / 1000);
|
||||
useFrame(() => {
|
||||
@@ -104,21 +100,21 @@ export function Controls({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
||||
const unsubscribeTankopediaEphemeral = TankopediaEphemeral.on(
|
||||
(state) => state.controlsEnabled,
|
||||
(enabled) => {
|
||||
if (orbitControls.current) orbitControls.current.enabled = enabled;
|
||||
},
|
||||
);
|
||||
|
||||
async function handlePoseEvent(event: Pose) {
|
||||
function handlePoseEvent(event: Pose) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -224,7 +220,7 @@ export function Controls({
|
||||
}
|
||||
|
||||
function disturb() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.disturbed = true;
|
||||
});
|
||||
}
|
||||
@@ -240,7 +236,11 @@ export function Controls({
|
||||
orbitControls.current.rotateSpeed = 0.25;
|
||||
|
||||
camera.position.copy(inspectModeInitialPosition);
|
||||
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
||||
orbitControls.current.target.set(
|
||||
0,
|
||||
gunHeight * (naked ? 1 / 4 : 3 / 4),
|
||||
0,
|
||||
);
|
||||
orbitControls.current.enablePan = true;
|
||||
orbitControls.current.enableZoom = true;
|
||||
camera.fov = naked ? 20 : 25;
|
||||
@@ -266,7 +266,7 @@ export function Controls({
|
||||
enableZoom={zoomable}
|
||||
zoomSpeed={zoomable ? undefined : 0}
|
||||
ref={orbitControls}
|
||||
enabled={tankopediaEphemeralStore.getState().controlsEnabled}
|
||||
enabled={TankopediaEphemeral.state.controlsEnabled}
|
||||
enableDamping={false}
|
||||
autoRotateSpeed={1 / 4}
|
||||
/>
|
||||
|
||||
+1
-2
@@ -6,10 +6,9 @@ import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemera
|
||||
|
||||
export function InitialFogReveal() {
|
||||
const fog = useThree((state) => state.scene.fog as Fog | null);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { Environment } from '@react-three/drei';
|
||||
import { Euler } from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { useModel } from '../../../../../../hooks/useModel';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||
|
||||
const LIGHTS_COUNT = 5;
|
||||
|
||||
interface LightingProps {
|
||||
display: TankopediaDisplay;
|
||||
}
|
||||
|
||||
export function Lighting({ display }: LightingProps) {
|
||||
const protagonist = Duel.use((state) => state.protagonist);
|
||||
const { hasPbr } = useModel(protagonist.tank.id);
|
||||
const isBrighterLighting =
|
||||
!hasPbr && display !== TankopediaDisplay.StaticArmor;
|
||||
|
||||
return (
|
||||
<>
|
||||
{display !== TankopediaDisplay.StaticArmor && (
|
||||
<Environment
|
||||
files="https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/winter_river_1k.hdr"
|
||||
environmentIntensity={isBrighterLighting ? 1.5 : 1.25}
|
||||
environmentRotation={new Euler(0, degToRad(180), 0)}
|
||||
/>
|
||||
)}
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<Environment preset="lobby" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { useHelper } from '@react-three/drei';
|
||||
import { times } from 'lodash-es';
|
||||
import { useRef } from 'react';
|
||||
import { Color, SpotLight, SpotLightHelper } from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { isHalloween } from '../../../../../../../core/blitzkit/isHalloween';
|
||||
import { HelpingSpotLight } from '../../../../../../HelpingSpotLight';
|
||||
|
||||
const LIGHTS_COUNT = 5;
|
||||
const LIGHT_DISTANCE = 13;
|
||||
const LIGHT_HEIGHT_0 = 4;
|
||||
const LIGHT_HEIGHT_1 = 2;
|
||||
const INTENSITY = 30;
|
||||
|
||||
export function Lighting() {
|
||||
const topLight = useRef<SpotLight>(null!);
|
||||
const frontLight = useRef<SpotLight>(null!);
|
||||
|
||||
useHelper(topLight, SpotLightHelper, 'red');
|
||||
useHelper(frontLight, SpotLightHelper, 'green');
|
||||
|
||||
return (
|
||||
<>
|
||||
{times(LIGHTS_COUNT, (index) => {
|
||||
const x = index / (LIGHTS_COUNT - 1);
|
||||
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152);
|
||||
const position = [
|
||||
LIGHT_DISTANCE * Math.sin(theta),
|
||||
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
|
||||
LIGHT_DISTANCE * Math.cos(theta),
|
||||
] as const;
|
||||
const color = new Color();
|
||||
|
||||
if (isHalloween()) color.set(1, 0.25, 0.25);
|
||||
|
||||
return (
|
||||
<HelpingSpotLight
|
||||
position={position}
|
||||
intensity={INTENSITY}
|
||||
penumbra={1}
|
||||
castShadow
|
||||
decay={1}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+30
-220
@@ -1,240 +1,50 @@
|
||||
import { asset, I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { useFrame, useLoader } from '@react-three/fiber';
|
||||
import { invalidate, useFrame } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
Group,
|
||||
MeshStandardMaterial,
|
||||
PerspectiveCamera,
|
||||
TextureLoader,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { GLTFLoader } from 'three/examples/jsm/Addons.js';
|
||||
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions';
|
||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTransform';
|
||||
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { Duel } from '../../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { aimTarget } from './constants';
|
||||
import { Quicklime, type QuicklimeEvent } from 'quicklime';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Mesh, MeshStandardMaterial } from 'three';
|
||||
|
||||
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
|
||||
await Promise.all([
|
||||
awaitableModelDefinitions,
|
||||
awaitableEquipmentDefinitions,
|
||||
awaitableProvisionDefinitions,
|
||||
]);
|
||||
const SIZE = 2 ** 5;
|
||||
|
||||
const emptyVector = new Vector2();
|
||||
export const screenshotReadyEvent = new Quicklime(false);
|
||||
|
||||
export function SceneProps() {
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const targetCircleWrapper = useRef<Group>(null);
|
||||
const clientTargetCircle = useRef<HTMLDivElement>(null);
|
||||
const serverTargetCircle = useRef<HTMLDivElement>(null);
|
||||
const mesh = useRef<Mesh>(null!);
|
||||
const material = useRef<MeshStandardMaterial>(null);
|
||||
const playground = useRef<Group>(null);
|
||||
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
const shell = Duel.use((state) => state.protagonist.shell);
|
||||
const engine = Duel.use((state) => state.protagonist.engine);
|
||||
const tankModelDefinition = modelDefinitions.models[tank.id];
|
||||
const trackModelDefinition =
|
||||
modelDefinitions.models[tank.id].tracks[track.id];
|
||||
const gunModelDefinition =
|
||||
tankModelDefinition.turrets[turret.id].guns[gun.id];
|
||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||
const hullOrigin = new Vector3(
|
||||
trackModelDefinition.origin.x,
|
||||
trackModelDefinition.origin.y,
|
||||
-trackModelDefinition.origin.z,
|
||||
);
|
||||
const turretOrigin = new Vector3(
|
||||
tankModelDefinition.turret_origin.x,
|
||||
tankModelDefinition.turret_origin.y,
|
||||
-tankModelDefinition.turret_origin.z,
|
||||
);
|
||||
const gunOriginOnlyY = new Vector3(0, turretModelDefinition.gun_origin.y, 0);
|
||||
const shellOrigin = hullOrigin.clone().add(turretOrigin).add(gunOriginOnlyY);
|
||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const consumables = Duel.use((state) => state.protagonist.consumables);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
const assaultDistance = Duel.use(
|
||||
(state) => state.protagonist.assaultDistance,
|
||||
);
|
||||
const equipmentMatrix = Duel.use(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
);
|
||||
const characteristics = tankCharacteristics(
|
||||
{
|
||||
tank,
|
||||
turret,
|
||||
gun,
|
||||
shell,
|
||||
consumables,
|
||||
applyDynamicArmor: false,
|
||||
applyReactiveArmor: false,
|
||||
applySpallLiner: false,
|
||||
camouflage,
|
||||
crewSkills,
|
||||
engine,
|
||||
equipmentMatrix,
|
||||
provisions,
|
||||
stockEngine: tank.engines[0],
|
||||
stockGun: tank.turrets[0].guns[0],
|
||||
stockTurret: tank.turrets[0],
|
||||
stockTrack: tank.tracks[0],
|
||||
track,
|
||||
assaultDistance,
|
||||
},
|
||||
{ equipmentDefinitions, provisionDefinitions, tankModelDefinition },
|
||||
);
|
||||
let { dispersion } = characteristics;
|
||||
const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed);
|
||||
const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`));
|
||||
const texture = useLoader(TextureLoader, '/assets/images/3d/grid.png');
|
||||
const path = new Vector3();
|
||||
const direction = new Vector3();
|
||||
texture.anisotropy = 2;
|
||||
|
||||
useFrame(({ camera }) => {
|
||||
if (!material.current) return;
|
||||
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
|
||||
});
|
||||
|
||||
let lastTime = 0;
|
||||
let DEBUG_disable = false;
|
||||
|
||||
useFrame(({ raycaster, camera, clock, gl }) => {
|
||||
if (
|
||||
!playground.current ||
|
||||
!targetCircleWrapper.current ||
|
||||
DEBUG_disable ||
|
||||
!clientTargetCircle.current ||
|
||||
!serverTargetCircle.current
|
||||
) {
|
||||
return;
|
||||
useEffect(() => {
|
||||
function handleScreenshotReady(event: QuicklimeEvent<boolean>) {
|
||||
mesh.current.visible = !event.data;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
raycaster.setFromCamera(emptyVector, camera);
|
||||
screenshotReadyEvent.on(handleScreenshotReady);
|
||||
|
||||
const cameraIntersections = raycaster.intersectObjects(
|
||||
playground.current.children,
|
||||
true,
|
||||
);
|
||||
|
||||
let length: number;
|
||||
|
||||
if (cameraIntersections.length === 0) {
|
||||
length = characteristics.shellRange;
|
||||
direction.set(0, 0, -1).applyQuaternion(camera.quaternion);
|
||||
aimTarget.copy(shellOrigin).addScaledVector(direction, length);
|
||||
} else {
|
||||
aimTarget.copy(cameraIntersections[0].point);
|
||||
path.copy(aimTarget).sub(shellOrigin);
|
||||
length = path.length();
|
||||
direction.copy(path).normalize();
|
||||
}
|
||||
|
||||
const deltaT = clock.elapsedTime - lastTime;
|
||||
lastTime = clock.elapsedTime;
|
||||
|
||||
const desiredPitch = Math.asin(direction.y);
|
||||
const desiredYaw = Math.atan2(-direction.x, -direction.z);
|
||||
const deltaPitch = desiredPitch - modelTransformEvent.last!.pitch;
|
||||
const deltaYaw = desiredYaw - modelTransformEvent.last!.yaw;
|
||||
const deltaAngle = Math.sqrt(deltaPitch ** 2 + deltaYaw ** 2);
|
||||
const maxPossibleRotationInTime = gunRotationSpeed * deltaT;
|
||||
const clampedDeltaAngle = Math.min(maxPossibleRotationInTime, deltaAngle);
|
||||
const clampedDeltaPitch =
|
||||
deltaAngle === 0 ? 0 : deltaPitch * (clampedDeltaAngle / deltaAngle);
|
||||
const clampedDeltaYaw =
|
||||
deltaAngle === 0 ? 0 : deltaYaw * (clampedDeltaAngle / deltaAngle);
|
||||
|
||||
const [pitch, yaw] = applyPitchYawLimits(
|
||||
modelTransformEvent.last!.pitch + clampedDeltaPitch,
|
||||
modelTransformEvent.last!.yaw + clampedDeltaYaw,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
);
|
||||
|
||||
const actualDeltaPitch = pitch - modelTransformEvent.last!.pitch;
|
||||
const actualDeltaYaw = yaw - modelTransformEvent.last!.yaw;
|
||||
const deltaRotation = Math.abs(actualDeltaPitch) + Math.abs(actualDeltaYaw);
|
||||
const rotationSpeed = deltaRotation / deltaT;
|
||||
const turretTraversePenalty =
|
||||
characteristics.dispersionTurretTraversing * radToDeg(rotationSpeed);
|
||||
|
||||
const penalty = Math.sqrt(1 + turretTraversePenalty ** 2);
|
||||
|
||||
if (penalty === 1) {
|
||||
const dispersionMod = dispersion - characteristics.dispersion;
|
||||
const dispersionModDischarged =
|
||||
dispersionMod * Math.exp(-deltaT / characteristics.aimTime) -
|
||||
dispersionMod;
|
||||
|
||||
dispersion += dispersionModDischarged;
|
||||
} else {
|
||||
dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
|
||||
}
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
|
||||
const gunDirection = new Vector3(0, 0, -1)
|
||||
.applyAxisAngle(I_HAT, pitch)
|
||||
.applyAxisAngle(J_HAT, yaw);
|
||||
|
||||
raycaster.set(shellOrigin, gunDirection);
|
||||
|
||||
const gunIntersections = raycaster.intersectObjects(
|
||||
playground.current.children,
|
||||
true,
|
||||
);
|
||||
|
||||
let gunTarget: Vector3;
|
||||
|
||||
if (gunIntersections.length === 0) {
|
||||
gunTarget = shellOrigin
|
||||
.clone()
|
||||
.add(gunDirection.multiplyScalar(characteristics.shellRange));
|
||||
} else {
|
||||
gunTarget = gunIntersections[0].point;
|
||||
}
|
||||
|
||||
targetCircleWrapper.current.position.copy(gunTarget);
|
||||
|
||||
const distanceToTarget = gunTarget.distanceTo(shellOrigin);
|
||||
const dispersionRadius = dispersion * (distanceToTarget / 100);
|
||||
const dispersionSize = 2 * dispersionRadius;
|
||||
const targetDistanceFromCamera = gunTarget.distanceTo(camera.position);
|
||||
const angularSize =
|
||||
2 * Math.atan(dispersionSize / (2 * targetDistanceFromCamera));
|
||||
const verticalScreenSize =
|
||||
2 * Math.tan(degToRad((camera as PerspectiveCamera).fov) / 2);
|
||||
const screenSpaceSize =
|
||||
gl.domElement.clientHeight * (angularSize / verticalScreenSize);
|
||||
const targetCircleDotsSize = `${screenSpaceSize}px`;
|
||||
|
||||
clientTargetCircle.current.style.width = targetCircleDotsSize;
|
||||
clientTargetCircle.current.style.height = targetCircleDotsSize;
|
||||
serverTargetCircle.current.style.width = targetCircleDotsSize;
|
||||
serverTargetCircle.current.style.height = targetCircleDotsSize;
|
||||
});
|
||||
|
||||
if (!disturbed) return null;
|
||||
return () => {
|
||||
screenshotReadyEvent.off(handleScreenshotReady);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial ref={material} map={texture} transparent />
|
||||
<mesh
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
receiveShadow
|
||||
visible={!screenshotReadyEvent.last!}
|
||||
ref={mesh}
|
||||
>
|
||||
<planeGeometry args={[SIZE, SIZE]} />
|
||||
<meshStandardMaterial
|
||||
transparent
|
||||
ref={material}
|
||||
color="black"
|
||||
roughness={1}
|
||||
metalness={1}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
+18
-19
@@ -14,17 +14,14 @@ import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent
|
||||
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
|
||||
|
||||
export function TankModel() {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const duelStore = Duel.useStore();
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const canvas = useThree((state) => state.gl.domElement);
|
||||
const hullContainer = useRef<Group>(null);
|
||||
const turretContainer = useRef<Group>(null);
|
||||
const gunContainer = useRef<Group>(null);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonist.turret.id];
|
||||
@@ -64,13 +61,12 @@ export function TankModel() {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
if (
|
||||
isTrack &&
|
||||
tankopediaEphemeralStore.getState().display ===
|
||||
TankopediaDisplay.Model
|
||||
TankopediaEphemeral.state.display === TankopediaDisplay.Model
|
||||
) {
|
||||
position.set(event.clientX, event.clientY);
|
||||
event.stopPropagation();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
|
||||
@@ -89,7 +85,7 @@ export function TankModel() {
|
||||
translateTexture(deltaX + deltaY);
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
|
||||
@@ -105,6 +101,7 @@ export function TankModel() {
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -130,24 +127,24 @@ export function TankModel() {
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -169,7 +166,7 @@ export function TankModel() {
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
@@ -184,6 +181,7 @@ export function TankModel() {
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -212,10 +210,10 @@ export function TankModel() {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -227,14 +225,14 @@ export function TankModel() {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
async function handlePointerMove(event: PointerEvent) {
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = await hasEquipment(
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
@@ -254,7 +252,7 @@ export function TankModel() {
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
@@ -269,6 +267,7 @@ export function TankModel() {
|
||||
key={key}
|
||||
onPointerDown={onPointerDown}
|
||||
castShadow
|
||||
receiveShadow
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ const skeletonMaterial = new ShaderMaterial({
|
||||
|
||||
export function TransitionSkeleton() {
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const track = Duel.use((state) => state.protagonist.track);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const hullContainer = useRef<Group>(null);
|
||||
@@ -34,7 +33,7 @@ export function TransitionSkeleton() {
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
const { gltf } = useModel(protagonist.tank.id);
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
const revealed = tankopediaEphemeralStore((state) => state.revealed);
|
||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
|
||||
+5
-7
@@ -49,13 +49,11 @@ export const fogAnimationTime = 2.5;
|
||||
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
({ thicknessRange, naked }, ref) => {
|
||||
const fog = useRef(new Fog('black', forNear1, fogFar1));
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
const protagonistGun = Duel.use((state) => state.protagonist.gun);
|
||||
const protagonistTurret = Duel.use((state) => state.protagonist.turret);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonistTurret.id];
|
||||
@@ -178,13 +176,13 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
|
||||
useEffect(() => {
|
||||
if (display !== TankopediaDisplay.DynamicArmor) {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (display !== TankopediaDisplay.StaticArmor) {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}
|
||||
@@ -199,10 +197,10 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
localClippingEnabled: true,
|
||||
preserveDrawingBuffer: true,
|
||||
}}
|
||||
shadows
|
||||
shadows="soft"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMissed={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -232,7 +230,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<StaticArmor thicknessRange={thicknessRange} />
|
||||
)}
|
||||
<Lighting display={display} />
|
||||
<Lighting />
|
||||
</Suspense>
|
||||
|
||||
<InitialAligner />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Box, Flex } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Suspense, useEffect, useMemo, useRef } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import { useFullScreen } from '../../../hooks/useFullScreen';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
@@ -10,7 +9,7 @@ import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonCo
|
||||
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
||||
import { Options } from './components/Options';
|
||||
import { TankSandbox } from './components/TankSandbox';
|
||||
import { NATION_COLORS, Title } from './components/TankSandbox/Title';
|
||||
import { Title } from './components/TankSandbox/Title';
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
@@ -39,8 +38,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
}, [protagonist]);
|
||||
const duelStore = Duel.useStore();
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const nationColors = NATION_COLORS[protagonist.nation];
|
||||
|
||||
useEffect(() => {
|
||||
if (disturbed) {
|
||||
@@ -57,7 +54,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
mutateDuel((draft) => {
|
||||
draft.antagonist.shell = shells[index];
|
||||
});
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
}
|
||||
@@ -85,9 +82,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
left="0"
|
||||
style={{
|
||||
opacity: disturbed ? 0 : 1,
|
||||
background: `linear-gradient(${nationColors.background
|
||||
.map((color) => Var(`${color}-2`))
|
||||
.join(',')})`,
|
||||
background: 'black',
|
||||
transitionDuration: '1s',
|
||||
}}
|
||||
/>
|
||||
@@ -101,8 +96,16 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
transitionDuration: '1s',
|
||||
background: isFullScreen ? 'black' : undefined,
|
||||
}}
|
||||
height={!isFullScreen ? 'calc(100svh - 6rem)' : '100vh'}
|
||||
maxHeight={isFullScreen ? undefined : '60rem'}
|
||||
height={
|
||||
isFullScreen
|
||||
? '100vh'
|
||||
: disturbed
|
||||
? 'calc(100svh - 6rem)'
|
||||
: {
|
||||
initial: '28rem',
|
||||
md: 'calc(100svh - 12rem)',
|
||||
}
|
||||
}
|
||||
maxWidth={isFullScreen ? undefined : '120rem'}
|
||||
flexGrow="1"
|
||||
width={isFullScreen ? '100vw' : undefined}
|
||||
@@ -118,8 +121,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
position="relative"
|
||||
>
|
||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||
<Title />
|
||||
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -145,7 +146,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Title foreground />
|
||||
<Title />
|
||||
|
||||
<Options
|
||||
skeleton={skeleton}
|
||||
|
||||
@@ -21,7 +21,6 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { locale, strings } = useLocale();
|
||||
const master = Duel.use((state) => state.protagonist.tank);
|
||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const lines = useMemo(() => {
|
||||
function extend(line: Line): Line[] {
|
||||
@@ -148,7 +147,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
draft.xpMultiplier = multiplier;
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { TierList, tierListInitialState } from '../../../stores/tierList';
|
||||
import { TierList } from '../../../stores/tierList';
|
||||
import { Share } from './components/Share';
|
||||
|
||||
export function TierListControls() {
|
||||
const mutateTierList = TierList.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -13,8 +12,8 @@ export function TierListControls() {
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
mutateTierList((draft) => {
|
||||
Object.assign(draft, tierListInitialState);
|
||||
TierList.mutate((draft) => {
|
||||
Object.assign(draft, TierList.initial);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -26,7 +26,6 @@ export function Header({ index }: HeaderProps) {
|
||||
const rowStyle = tierListRows[index];
|
||||
const name = TierList.use((state) => state.rows[index].name);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const mutateTierList = TierList.useMutation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -67,9 +66,8 @@ export function Header({ index }: HeaderProps) {
|
||||
onBlur={() => {
|
||||
if (!input.current) return;
|
||||
|
||||
mutateTierList((draft) => {
|
||||
TierList.mutate((draft) => {
|
||||
if (!input.current) return;
|
||||
|
||||
draft.rows[index].name = input.current.value;
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ type TierListTileProps = {
|
||||
|
||||
export function TierListTile(props: TierListTileProps) {
|
||||
const card = useRef<HTMLDivElement>(null);
|
||||
const mutateTierList = TierList.useMutation();
|
||||
const lastPosition = useRef({ x: 0, y: 0 });
|
||||
const handlePointerDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLSpanElement>) => {
|
||||
@@ -48,7 +47,7 @@ export function TierListTile(props: TierListTileProps) {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
|
||||
mutateTierList((draft) => {
|
||||
TierList.mutate((draft) => {
|
||||
draft.dragging = true;
|
||||
});
|
||||
},
|
||||
@@ -104,7 +103,7 @@ export function TierListTile(props: TierListTileProps) {
|
||||
|
||||
const tileIndex = Number(tile.dataset.tileIndex);
|
||||
|
||||
mutateTierList((draft) => {
|
||||
TierList.mutate((draft) => {
|
||||
draft.dragging = false;
|
||||
draft.rows.forEach((row) => {
|
||||
row.tanks = row.tanks.filter((tankId) => tankId !== props.tank.id);
|
||||
@@ -118,7 +117,7 @@ export function TierListTile(props: TierListTileProps) {
|
||||
break rowLoop;
|
||||
}
|
||||
|
||||
mutateTierList((draft) => {
|
||||
TierList.mutate((draft) => {
|
||||
draft.dragging = false;
|
||||
draft.rows.forEach((row) => {
|
||||
row.tanks = row.tanks.filter((tankId) => tankId !== props.tank.id);
|
||||
@@ -133,7 +132,7 @@ export function TierListTile(props: TierListTileProps) {
|
||||
}
|
||||
|
||||
if (!wasPlaced) {
|
||||
mutateTierList((draft) => {
|
||||
TierList.mutate((draft) => {
|
||||
draft.dragging = false;
|
||||
draft.rows.forEach((row) => {
|
||||
row.tanks = row.tanks.filter((tankId) => tankId !== props.tank.id);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { metaSortTank } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { times } from 'lodash-es';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TierList } from '../../stores/tierList';
|
||||
import { SkeletonTankCard } from '../TankSearch/components/SkeletonTankCard';
|
||||
import { TankCardWrapper } from '../TankSearch/components/TankCardWrapper';
|
||||
import { TierListTile } from './Tile';
|
||||
import { metaSortTank } from "@blitzkit/core";
|
||||
import { times } from "lodash-es";
|
||||
import { useMemo, useState } from "react";
|
||||
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TierList } from "../../stores/tierList";
|
||||
import { SkeletonTankCard } from "../TankSearch/components/SkeletonTankCard";
|
||||
import { TankCardWrapper } from "../TankSearch/components/TankCardWrapper";
|
||||
import { TierListTile } from "./Tile";
|
||||
|
||||
const [tankDefinitions, gameDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
@@ -22,17 +21,17 @@ const PREVIEW_COUNT = 32;
|
||||
const DEFAULT_LOADED_CARDS = 75;
|
||||
|
||||
export function TierListTiles() {
|
||||
const filters = useStore($tankFilters);
|
||||
const filters = TankFilters.use();
|
||||
const placedTanks = TierList.use((state) => state.placedTanks);
|
||||
const sorted = useMemo(
|
||||
() =>
|
||||
metaSortTank(
|
||||
tanks.filter(
|
||||
(tank) => filterTank(filters, tank) && !placedTanks.has(tank.id),
|
||||
(tank) => filterTank(filters, tank) && !placedTanks.has(tank.id)
|
||||
),
|
||||
gameDefinitions,
|
||||
gameDefinitions
|
||||
).reverse(),
|
||||
[filters, placedTanks],
|
||||
[filters, placedTanks]
|
||||
);
|
||||
const [loadedTiles, setLoadedTiles] = useState(DEFAULT_LOADED_CARDS);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export function URLManager() {
|
||||
const rows = TierList.use((state) => state.rows);
|
||||
const mutateTierList = TierList.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -20,7 +19,7 @@ export function URLManager() {
|
||||
.map(Number)
|
||||
.filter((id) => id in tankDefinitions.tanks);
|
||||
|
||||
mutateTierList((draft) => {
|
||||
TierList.mutate((draft) => {
|
||||
draft.rows[index].tanks = values;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,11 +127,18 @@ export const guessTool: Tool = {
|
||||
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||
};
|
||||
|
||||
export const mixerTool: Tool = {
|
||||
id: 'mixer',
|
||||
button: { color: 'red' },
|
||||
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||
};
|
||||
|
||||
export const TOOLS: Tool[] = [
|
||||
tankopediaTool,
|
||||
compareTool,
|
||||
performanceTool,
|
||||
// guessTool,
|
||||
guessTool,
|
||||
// mixerTool,
|
||||
// chartsTool,
|
||||
galleryTool,
|
||||
sessionTool,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function isHalloween() {
|
||||
const data = new Date();
|
||||
return data.getMonth() === 9 && data.getDate() === 31;
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
import type {
|
||||
ModelDefinition,
|
||||
ProvisionDefinitions,
|
||||
TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import type { ProvisionDefinitions, TankDefinition } from '@blitzkit/core';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
import type { CompareMember } from '../../stores/compareEphemeral';
|
||||
import { tankToDuelMember } from './tankToDuelMember';
|
||||
|
||||
export function tankToCompareMember(
|
||||
tank: TankDefinition,
|
||||
model: ModelDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
return {
|
||||
...tankToDuelMember(tank, model, provisionDefinitions),
|
||||
...tankToDuelMember(tank, provisionDefinitions),
|
||||
key: uniqueId(),
|
||||
} satisfies CompareMember;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
createDefaultProvisions,
|
||||
ModelDefinition,
|
||||
type ProvisionDefinitions,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
@@ -9,7 +8,6 @@ import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
|
||||
|
||||
export function tankToDuelMember(
|
||||
tank: TankDefinition,
|
||||
model: ModelDefinition,
|
||||
provisionDefinitions: ProvisionDefinitions,
|
||||
) {
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
modelTransformEvent,
|
||||
type ModelTransformEventData,
|
||||
} from '../core/blitzkit/modelTransform';
|
||||
import { Duel } from '../stores/duel';
|
||||
import { useTankModelDefinition } from './useTankModelDefinition';
|
||||
|
||||
export function useTankTransform(
|
||||
@@ -25,7 +24,6 @@ export function useTankTransform(
|
||||
gunContainer: RefObject<Group>,
|
||||
) {
|
||||
const tankModelDefinition = useTankModelDefinition();
|
||||
const duelStore = Duel.useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
---
|
||||
import { DiscordLogoIcon, PaperPlaneIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||
import { PageWrapper } from '../components/PageWrapper';
|
||||
import { getStrings } from '../core/i18n/getStrings';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
export interface Props {
|
||||
path: string;
|
||||
@@ -16,30 +12,29 @@ const strings = getStrings(locale);
|
||||
const resolvedPath = `/${locale === undefined ? '' : `${locale}/`}${path}`;
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<link
|
||||
slot="head"
|
||||
rel="canonical"
|
||||
href={`${Astro.url.origin}${resolvedPath}`}
|
||||
/>
|
||||
<meta slot="head" http-equiv="refresh" content={`0; url=${resolvedPath}`} />
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="canonical" href={`${Astro.url.origin}${resolvedPath}`} />
|
||||
<meta http-equiv="refresh" content={`0; url=${resolvedPath}`} />
|
||||
</head>
|
||||
|
||||
<PageWrapper align="center" justify="center" gap="0">
|
||||
<Heading>{strings.website.redirect.title}</Heading>
|
||||
<Text color="gray">{strings.website.redirect.body}</Text>
|
||||
|
||||
<Flex mt="4" gap="2">
|
||||
<Link href={resolvedPath}>
|
||||
<Button>
|
||||
<PaperPlaneIcon /> Redirect
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href="https://discord.gg/nDt7AjGJQH">
|
||||
<Button variant="outline">
|
||||
<DiscordLogoIcon /> Discord
|
||||
</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
</BaseLayout>
|
||||
<body>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
fontFamily: 'sans-serif',
|
||||
}}
|
||||
>
|
||||
<h2>{strings.website.redirect.title}</h2>
|
||||
<span>{strings.website.redirect.body}</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -114,11 +114,7 @@ function Content() {
|
||||
|
||||
if (tank === undefined) return;
|
||||
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
draft.members.push(
|
||||
tankToCompareMember(tank, model, provisionDefinitions),
|
||||
);
|
||||
draft.members.push(tankToCompareMember(tank, provisionDefinitions));
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { Box, Flex, Heading } from '@radix-ui/themes';
|
||||
import { Suspense } from 'react';
|
||||
import { GuessBackground } from '../../../components/GuessBackground';
|
||||
import { Guesser } from '../../../components/Guesser';
|
||||
import { GuessRenderer } from '../../../components/GuessRenderer';
|
||||
import { GuessRendererLoader } from '../../../components/GuessRendererLoader';
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import { Box, Flex, Heading } from "@radix-ui/themes";
|
||||
import { Suspense } from "react";
|
||||
import { GuessBackground } from "../../../components/GuessBackground";
|
||||
import { Guesser } from "../../../components/Guesser";
|
||||
import { GuessRenderer } from "../../../components/GuessRenderer";
|
||||
import { GuessRendererLoader } from "../../../components/GuessRendererLoader";
|
||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../../core/awaitables/provisionDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { GuessEphemeral } from '../../../stores/guessEphemeral';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Guess } from "../../../stores/guessEphemeral";
|
||||
import { TankopediaEphemeral } from "../../../stores/tankopediaEphemeral";
|
||||
|
||||
const [tankDefinitions, modelDefinitions, provisionDefinitions] =
|
||||
await Promise.all([
|
||||
@@ -27,95 +26,92 @@ const [tankDefinitions, modelDefinitions, provisionDefinitions] =
|
||||
const ids = Object.keys(tankDefinitions.tanks);
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
const initialId = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||
const initialTank = tankDefinitions.tanks[initialId];
|
||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
Guess.useInitialization(tank);
|
||||
|
||||
return (
|
||||
<GuessEphemeral.Provider data={initialTank}>
|
||||
<LocaleProvider locale={locale}>
|
||||
<Page2 />
|
||||
</LocaleProvider>
|
||||
</GuessEphemeral.Provider>
|
||||
<LocaleProvider locale={locale}>
|
||||
<Container />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Page2() {
|
||||
const tank = GuessEphemeral.use((state) => state.tank);
|
||||
function Container() {
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
TankopediaEphemeral.useInitialization(model);
|
||||
|
||||
return (
|
||||
<TankopediaEphemeral.Provider data={model}>
|
||||
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
|
||||
<Content />
|
||||
</Duel.Provider>
|
||||
</TankopediaEphemeral.Provider>
|
||||
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
|
||||
<Content />
|
||||
</Duel.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
const { unwrap } = useLocale();
|
||||
const tank = GuessEphemeral.use((state) => state.tank);
|
||||
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const isRevealed = guessState !== null;
|
||||
const name = unwrap(tank.name);
|
||||
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
||||
const transitionDuration = isRevealed ? "2s" : undefined;
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||
<GuessBackground />
|
||||
|
||||
<Flex
|
||||
position="absolute"
|
||||
left="50%"
|
||||
top="50%"
|
||||
ml="9"
|
||||
<Heading
|
||||
style={{
|
||||
transitionDuration,
|
||||
opacity: isRevealed ? 1 : 0,
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
transform: 'translateY(-50%)',
|
||||
fontWeight: 900,
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
fontSize,
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Heading weight="medium" size="9">
|
||||
{unwrap(tank.name)}
|
||||
</Heading>
|
||||
</Flex>
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
top="0"
|
||||
right={isRevealed ? '16rem' : '0'}
|
||||
style={{
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
}}
|
||||
style={{ transitionDuration }}
|
||||
>
|
||||
<Suspense fallback={<GuessRendererLoader />}>
|
||||
<GuessRenderer />
|
||||
</Suspense>
|
||||
</Box>
|
||||
|
||||
<Flex
|
||||
position="absolute"
|
||||
left="50%"
|
||||
top="50%"
|
||||
ml="9"
|
||||
<Heading
|
||||
style={{
|
||||
opacity: isRevealed ? 1 : 0,
|
||||
transitionDuration: isRevealed ? '2s' : undefined,
|
||||
transform: 'translateY(-50%)',
|
||||
color: 'transparent',
|
||||
WebkitTextStroke: `1px ${Var('gray-12')}`,
|
||||
transitionDuration,
|
||||
opacity: isRevealed ? 0.5 : 0,
|
||||
fontWeight: 900,
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
fontSize,
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
WebkitTextStroke: `1px inherit`,
|
||||
}}
|
||||
weight="medium"
|
||||
size="9"
|
||||
>
|
||||
{unwrap(tank.name)}
|
||||
</Heading>
|
||||
</Flex>
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
<Guesser />
|
||||
</Flex>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Box, Flex, Heading, Link } from '@radix-ui/themes';
|
||||
import type { GetStaticPaths } from 'astro';
|
||||
import { AesonPlug } from '../../components/AesonPlug';
|
||||
import HomeHero from '../../components/HomeHero.astro';
|
||||
import { HomePageHotTanks } from '../../components/HomePageHotTanks';
|
||||
import { Plugs } from '../../components/Plugs';
|
||||
import ToolCard from '../../components/ToolCard.astro';
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
discordTool,
|
||||
embedTool,
|
||||
galleryTool,
|
||||
guessTool,
|
||||
moreTool,
|
||||
performanceTool,
|
||||
sessionTool,
|
||||
@@ -25,7 +25,6 @@ import { getStrings } from '../../core/i18n/getStrings';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
|
||||
const PUBLIC_ASSET_BRANCH = assertSecret(import.meta.env.PUBLIC_BRANCH);
|
||||
const MODE = assertSecret(import.meta.env.MODE);
|
||||
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
||||
const strings = getStrings(Astro.params.locale);
|
||||
|
||||
@@ -110,22 +109,22 @@ export const getStaticPaths = (() => {
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<!-- <ToolCard tool={chartsTool} /> -->
|
||||
<!-- <ToolCard tool={guessTool} /> -->
|
||||
<ToolCard tool={guessTool} />
|
||||
<ToolCard tool={galleryTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={sessionTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={discordTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={tierListTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={embedTool} />
|
||||
<ToolCard tool={moreTool} />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||
<ToolCard tool={moreTool} />
|
||||
<Box flexGrow="1" />
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Box } from "@radix-ui/themes";
|
||||
import { useRef } from "react";
|
||||
import { MixerScene } from "../../../components/MixerScene";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Mixer } from "../../../stores/mixer";
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
const tanks = Object.values(tankDefinitions.tanks);
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
const hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||
const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||
const turret = useRef(
|
||||
turretTank.current.turrets[
|
||||
Math.floor(Math.random() * turretTank.current.turrets.length)
|
||||
]
|
||||
);
|
||||
const gunTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]);
|
||||
const gunTurret = useRef(
|
||||
gunTank.current.turrets[
|
||||
Math.floor(Math.random() * gunTank.current.turrets.length)
|
||||
]
|
||||
);
|
||||
const gun = useRef(
|
||||
gunTurret.current.guns[
|
||||
Math.floor(Math.random() * gunTurret.current.guns.length)
|
||||
]
|
||||
);
|
||||
|
||||
Mixer.useInitialization({
|
||||
hull: hull.current,
|
||||
turret: { tank: turretTank.current, turret: turret.current },
|
||||
gun: {
|
||||
tank: gunTank.current,
|
||||
turret: gunTurret.current,
|
||||
gun: gun.current,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Content() {
|
||||
return (
|
||||
<PageWrapper p="0">
|
||||
<Box flexGrow="1" position="relative">
|
||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||
<MixerScene />
|
||||
</Box>
|
||||
</Box>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import { Page } from './_index';
|
||||
|
||||
export { getStaticPaths } from '../index.astro';
|
||||
|
||||
const { locale } = Astro.params;
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<PageWrapper p="0" maxWidth="unset">
|
||||
<Page {locale} client:load />
|
||||
</PageWrapper>
|
||||
</BaseLayout>
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { TankPerformanceEphemeral } from '../../../stores/tankPerformanceEphemeral';
|
||||
import { TankPerformanceSort } from '../../../stores/tankPerformanceSort';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
|
||||
@@ -17,9 +16,7 @@ export function Page({ skeleton, locale }: PageProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<TankPerformanceSort.Provider>
|
||||
<TankPerformanceEphemeral.Provider>
|
||||
<Content skeleton={skeleton} />
|
||||
</TankPerformanceEphemeral.Provider>
|
||||
<Content skeleton={skeleton} />
|
||||
</TankPerformanceSort.Provider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
idToRegion,
|
||||
searchPlayersAcrossRegions,
|
||||
sumCompositeStats,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
AlertDialog,
|
||||
Button,
|
||||
@@ -30,21 +30,21 @@ import {
|
||||
Table,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import { StickyRowHeaderCell } from '../../../components/StickyRowHeaderCell';
|
||||
import { StickyTableRoot } from '../../../components/StickyTableRoot';
|
||||
import { TankRowHeaderCell } from '../../../components/TankRowHeaderCell';
|
||||
import { awaitableAverageDefinitions } from '../../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
} from "@radix-ui/themes";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { StickyRowHeaderCell } from "../../../components/StickyRowHeaderCell";
|
||||
import { StickyTableRoot } from "../../../components/StickyTableRoot";
|
||||
import { TankRowHeaderCell } from "../../../components/TankRowHeaderCell";
|
||||
import { awaitableAverageDefinitions } from "../../../core/awaitables/averageDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { Session, type SessionTracking } from '../../../stores/session';
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Session, type SessionTracking } from "../../../stores/session";
|
||||
|
||||
const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
@@ -54,9 +54,7 @@ const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<Session.Provider>
|
||||
<Content />
|
||||
</Session.Provider>
|
||||
<Content />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -70,9 +68,8 @@ function Content() {
|
||||
useState(false);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const session = Session.use();
|
||||
const mutateSession = Session.useMutation();
|
||||
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[] | null>(
|
||||
null,
|
||||
null
|
||||
);
|
||||
const [accountInfo, setAccountInfo] = useState<
|
||||
IndividualAccountInfo | null | undefined
|
||||
@@ -84,7 +81,7 @@ function Content() {
|
||||
if (!id) return setAccountInfo(null);
|
||||
|
||||
setAccountInfo(
|
||||
session.tracking ? await getAccountInfo(idToRegion(id), id) : null,
|
||||
session.tracking ? await getAccountInfo(idToRegion(id), id) : null
|
||||
);
|
||||
})();
|
||||
}, [session.tracking && session.player.id]);
|
||||
@@ -98,20 +95,20 @@ function Content() {
|
||||
const average = averageDefinitions.averages[tank.id];
|
||||
const composite = compositeStats(
|
||||
{ ...entry.all, battle_life_time: entry.battle_life_time },
|
||||
average?.mu,
|
||||
average?.mu
|
||||
);
|
||||
|
||||
return { tank, composite };
|
||||
})
|
||||
: undefined,
|
||||
[session.tracking && session.player, tankStatsB],
|
||||
[session.tracking && session.player, tankStatsB]
|
||||
);
|
||||
const total = useMemo(
|
||||
() =>
|
||||
delta
|
||||
? sumCompositeStats(delta.map(({ composite }) => composite))
|
||||
: undefined,
|
||||
[delta],
|
||||
[delta]
|
||||
);
|
||||
const search = debounce(async () => {
|
||||
if (!input.current) return;
|
||||
@@ -131,7 +128,8 @@ function Content() {
|
||||
}
|
||||
|
||||
setShowSearch(false);
|
||||
mutateSession((draft) => {
|
||||
|
||||
Session.mutate((draft) => {
|
||||
if (!input.current) return;
|
||||
|
||||
draft.tracking = true;
|
||||
@@ -152,7 +150,7 @@ function Content() {
|
||||
|
||||
const tankStats = await getTankStats(
|
||||
idToRegion(session.player.id),
|
||||
session.player.id,
|
||||
session.player.id
|
||||
);
|
||||
setTankStatsB(tankStats);
|
||||
}
|
||||
@@ -206,8 +204,8 @@ function Content() {
|
||||
variant="classic"
|
||||
size="3"
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '75vw',
|
||||
position: "relative",
|
||||
width: "75vw",
|
||||
maxWidth: 480,
|
||||
}}
|
||||
ref={input}
|
||||
@@ -272,7 +270,7 @@ function Content() {
|
||||
<Flex
|
||||
mt="2"
|
||||
mb="2"
|
||||
direction={{ initial: 'column', sm: 'row' }}
|
||||
direction={{ initial: "column", sm: "row" }}
|
||||
align="center"
|
||||
justify="between"
|
||||
gap="2"
|
||||
@@ -296,7 +294,7 @@ function Content() {
|
||||
onClick={async () => {
|
||||
const stats = await getTankStats(
|
||||
session.player.region,
|
||||
session.player.id,
|
||||
session.player.id
|
||||
);
|
||||
|
||||
if (stats === null) {
|
||||
@@ -304,7 +302,7 @@ function Content() {
|
||||
return;
|
||||
}
|
||||
|
||||
mutateSession((draft) => {
|
||||
Session.mutate((draft) => {
|
||||
(draft as SessionTracking).player.stats = stats;
|
||||
});
|
||||
}}
|
||||
@@ -313,7 +311,7 @@ function Content() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutateSession((draft) => {
|
||||
Session.mutate((draft) => {
|
||||
draft.tracking = false;
|
||||
});
|
||||
}}
|
||||
@@ -338,7 +336,7 @@ function Content() {
|
||||
align="center"
|
||||
width="0"
|
||||
key={column}
|
||||
style={{ position: 'relative' }}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
{index === 0 && session.columns.length < 4 && (
|
||||
<DropdownMenu.Root>
|
||||
@@ -347,10 +345,10 @@ function Content() {
|
||||
size="1"
|
||||
variant="surface"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: '50%',
|
||||
transform: 'translate(-100%, -50%)',
|
||||
top: "50%",
|
||||
transform: "translate(-100%, -50%)",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
@@ -365,9 +363,9 @@ function Content() {
|
||||
<DropdownMenu.Item
|
||||
key={key}
|
||||
onClick={() => {
|
||||
mutateSession((draft) => {
|
||||
Session.mutate((draft) => {
|
||||
(draft as SessionTracking).columns.unshift(
|
||||
key,
|
||||
key
|
||||
);
|
||||
});
|
||||
}}
|
||||
@@ -382,8 +380,8 @@ function Content() {
|
||||
<Select.Root
|
||||
value={column}
|
||||
onValueChange={(value) => {
|
||||
mutateSession((draft) => {
|
||||
if (value === 'remove') {
|
||||
Session.mutate((draft) => {
|
||||
if (value === "remove") {
|
||||
(draft as SessionTracking).columns.splice(index, 1);
|
||||
} else {
|
||||
(draft as SessionTracking).columns[index] =
|
||||
@@ -404,8 +402,7 @@ function Content() {
|
||||
{compositeStatsKeys
|
||||
.filter(
|
||||
(item) =>
|
||||
!session.columns.includes(item) ||
|
||||
item === column,
|
||||
!session.columns.includes(item) || item === column
|
||||
)
|
||||
.map((key) => (
|
||||
<Select.Item key={key} value={key}>
|
||||
@@ -422,14 +419,14 @@ function Content() {
|
||||
<Table.Body>
|
||||
<Table.Row
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<StickyRowHeaderCell
|
||||
style={{
|
||||
paddingLeft: 32,
|
||||
position: 'relative',
|
||||
overflowY: 'hidden',
|
||||
position: "relative",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
{strings.website.tools.session.table.total}
|
||||
@@ -446,7 +443,7 @@ function Content() {
|
||||
<Table.Row
|
||||
key={tank.id}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<TankRowHeaderCell tank={tank} />
|
||||
@@ -455,7 +452,7 @@ function Content() {
|
||||
{formatCompositeStat(
|
||||
composite[column],
|
||||
column,
|
||||
composite,
|
||||
composite
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { TankType } from '@blitzkit/core';
|
||||
import { PageWrapper } from '../../../../components/PageWrapper';
|
||||
import { CalloutsSection } from '../../../../components/Tankopedia/CalloutsSection';
|
||||
import { CharacteristicsSection } from '../../../../components/Tankopedia/CharacteristicsSection';
|
||||
import { GameModeSection } from '../../../../components/Tankopedia/GameModeSection';
|
||||
import { GuideSection } from '../../../../components/Tankopedia/GuideSection';
|
||||
import { HeroSection } from '../../../../components/Tankopedia/HeroSection';
|
||||
import { MetaSection } from '../../../../components/Tankopedia/MetaSection';
|
||||
import { TechTreeSection } from '../../../../components/Tankopedia/TechTreeSection';
|
||||
import { VideoSection } from '../../../../components/Tankopedia/VideoSection';
|
||||
import { awaitableModelDefinitions } from '../../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
||||
import { TankType } from "@blitzkit/core";
|
||||
import { PageWrapper } from "../../../../components/PageWrapper";
|
||||
import { CalloutsSection } from "../../../../components/Tankopedia/CalloutsSection";
|
||||
import { CharacteristicsSection } from "../../../../components/Tankopedia/CharacteristicsSection";
|
||||
import { GameModeSection } from "../../../../components/Tankopedia/GameModeSection";
|
||||
import { GuideSection } from "../../../../components/Tankopedia/GuideSection";
|
||||
import { HeroSection } from "../../../../components/Tankopedia/HeroSection";
|
||||
import { MetaSection } from "../../../../components/Tankopedia/MetaSection";
|
||||
import { TechTreeSection } from "../../../../components/Tankopedia/TechTreeSection";
|
||||
import { VideoSection } from "../../../../components/Tankopedia/VideoSection";
|
||||
import { awaitableModelDefinitions } from "../../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../../../core/awaitables/provisionDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from '../../../../hooks/useLocale';
|
||||
import { App } from '../../../../stores/app';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||
import type { TankGuide } from './index.astro';
|
||||
} from "../../../../hooks/useLocale";
|
||||
import { App } from "../../../../stores/app";
|
||||
import { Duel } from "../../../../stores/duel";
|
||||
import { TankopediaEphemeral } from "../../../../stores/tankopediaEphemeral";
|
||||
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
|
||||
import type { TankGuide } from "./index.astro";
|
||||
|
||||
type PageProps = MaybeSkeletonComponentProps &
|
||||
LocaleAcceptorProps & {
|
||||
@@ -39,30 +39,30 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const model = modelDefinitions.models[id];
|
||||
|
||||
TankopediaEphemeral.useInitialization(model);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<TankopediaEphemeral.Provider data={model}>
|
||||
<App.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Duel.Provider
|
||||
data={{ tank, model, provisionDefinitions: provisionDefinitions }}
|
||||
>
|
||||
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
||||
<HeroSection skeleton={skeleton} />
|
||||
<MetaSection />
|
||||
<CalloutsSection />
|
||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||
<TechTreeSection skeleton={skeleton} />
|
||||
)}
|
||||
<CharacteristicsSection skeleton={skeleton} />
|
||||
<GameModeSection />
|
||||
{guide && <GuideSection guide={guide} />}
|
||||
<VideoSection skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
</Duel.Provider>
|
||||
</TankopediaPersistent.Provider>
|
||||
</App.Provider>
|
||||
</TankopediaEphemeral.Provider>
|
||||
<App.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Duel.Provider
|
||||
data={{ tank, model, provisionDefinitions: provisionDefinitions }}
|
||||
>
|
||||
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
||||
<HeroSection skeleton={skeleton} />
|
||||
<MetaSection />
|
||||
<CalloutsSection />
|
||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||
<TechTreeSection skeleton={skeleton} />
|
||||
)}
|
||||
<CharacteristicsSection skeleton={skeleton} />
|
||||
<GameModeSection />
|
||||
{guide && <GuideSection guide={guide} />}
|
||||
<VideoSection skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
</Duel.Provider>
|
||||
</TankopediaPersistent.Provider>
|
||||
</App.Provider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
@@ -9,16 +9,13 @@ import {
|
||||
type LocaleAcceptorProps,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
import { TierList } from '../../../stores/tierList';
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<TierList.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Content />
|
||||
</TankopediaPersistent.Provider>
|
||||
</TierList.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Content />
|
||||
</TankopediaPersistent.Provider>
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { TankSandbox } from '../../../../../components/Tankopedia/HeroSection/components/TankSandbox';
|
||||
import { awaitableModelDefinitions } from '../../../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions';
|
||||
import { LocaleProvider } from '../../../../../hooks/useLocale';
|
||||
import { App } from '../../../../../stores/app';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||
import { TankSandbox } from "../../../../../components/Tankopedia/HeroSection/components/TankSandbox";
|
||||
import { awaitableModelDefinitions } from "../../../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../../../../core/awaitables/provisionDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../../../core/awaitables/tankDefinitions";
|
||||
import { LocaleProvider } from "../../../../../hooks/useLocale";
|
||||
import { App } from "../../../../../stores/app";
|
||||
import { Duel } from "../../../../../stores/duel";
|
||||
import { TankopediaEphemeral } from "../../../../../stores/tankopediaEphemeral";
|
||||
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
|
||||
|
||||
const [provisionDefinitions, tankDefinitions, modelDefinitions] =
|
||||
await Promise.all([
|
||||
@@ -23,14 +23,14 @@ export function Page({ id }: PageProps) {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const model = modelDefinitions.models[id];
|
||||
|
||||
TankopediaEphemeral.useInitialization(model);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale="en">
|
||||
<App.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<Duel.Provider data={{ provisionDefinitions, model, tank }}>
|
||||
<TankopediaEphemeral.Provider data={model}>
|
||||
<Content />
|
||||
</TankopediaEphemeral.Provider>
|
||||
<Content />
|
||||
</Duel.Provider>
|
||||
</TankopediaPersistent.Provider>
|
||||
</App.Provider>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const Duel = createContextualStore(
|
||||
model: ModelDefinition;
|
||||
provisionDefinitions: ProvisionDefinitions;
|
||||
}) => {
|
||||
const protagonist = tankToDuelMember(tank, model, provisionDefinitions);
|
||||
const protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||
|
||||
return create<DuelStore>()(
|
||||
subscribeWithSelector<DuelStore>(() => ({
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { TankDefinition } from '@blitzkit/core';
|
||||
import { create } from 'zustand';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export interface GuessEphemeral {
|
||||
export interface Guess {
|
||||
tank: TankDefinition;
|
||||
// true = correct, false = incorrect, null = not guessed
|
||||
/**
|
||||
* true = correct, false = incorrect, null = not guessed
|
||||
*/
|
||||
guessState: boolean | null;
|
||||
totalGuesses: number;
|
||||
correctGuesses: number;
|
||||
}
|
||||
|
||||
export const GuessEphemeral = createContextualStore((tank: TankDefinition) =>
|
||||
create<GuessEphemeral>()(() => ({
|
||||
tank,
|
||||
guessState: null,
|
||||
totalGuesses: 0,
|
||||
correctGuesses: 0,
|
||||
})),
|
||||
);
|
||||
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
||||
tank,
|
||||
guessState: null,
|
||||
totalGuesses: 0,
|
||||
correctGuesses: 0,
|
||||
}));
|
||||
@@ -0,0 +1,21 @@
|
||||
import type {
|
||||
GunDefinition,
|
||||
TankDefinition,
|
||||
TurretDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export interface Mixer {
|
||||
hull: TankDefinition;
|
||||
turret: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
};
|
||||
gun: {
|
||||
tank: TankDefinition;
|
||||
turret: TurretDefinition;
|
||||
gun: GunDefinition;
|
||||
};
|
||||
}
|
||||
|
||||
export const Mixer = new Varuna<Mixer, Mixer>((data) => data);
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Samples } from '@blitzkit/core';
|
||||
import { Varuna } from 'varuna';
|
||||
|
||||
export interface Performance {
|
||||
playerCountPeriod: PlayerCountPeriod;
|
||||
}
|
||||
|
||||
export type PlayerCountPeriod = keyof Samples;
|
||||
|
||||
export const Performance = new Varuna<Performance>({
|
||||
playerCountPeriod: 'd_30',
|
||||
});
|
||||
@@ -2,11 +2,8 @@ import type {
|
||||
CompositeStatsKey,
|
||||
IndividualTankStats,
|
||||
Region,
|
||||
} from '@blitzkit/core';
|
||||
import { merge } from 'lodash-es';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
} from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
interface SessionBase {
|
||||
columns: CompositeStatsKey[];
|
||||
@@ -28,19 +25,15 @@ interface SessionNotTracking extends SessionBase {
|
||||
|
||||
type Session = SessionTracking | SessionNotTracking;
|
||||
|
||||
export const Session = createContextualStore(() =>
|
||||
create<Session>()(
|
||||
persist(
|
||||
(_) => ({
|
||||
columns: [
|
||||
'cumulative_battles',
|
||||
'normalized_wins',
|
||||
'cumulative_wn8',
|
||||
'normalized_damage_dealt',
|
||||
],
|
||||
tracking: false,
|
||||
}),
|
||||
{ name: 'session-3', merge: (a, b) => merge(b, a) },
|
||||
),
|
||||
),
|
||||
export const Session = new Varuna<Session>(
|
||||
{
|
||||
columns: [
|
||||
"cumulative_battles",
|
||||
"normalized_wins",
|
||||
"cumulative_wn8",
|
||||
"normalized_damage_dealt",
|
||||
],
|
||||
tracking: false,
|
||||
},
|
||||
"session-4"
|
||||
);
|
||||
@@ -3,8 +3,8 @@ import type {
|
||||
ShellType,
|
||||
TankClass,
|
||||
TankType,
|
||||
} from '@blitzkit/core';
|
||||
import { map } from 'nanostores';
|
||||
} from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export type CaseType<T> = T extends {
|
||||
gun_type?: { $case: infer U; value: any };
|
||||
@@ -17,23 +17,21 @@ export interface TankFilters {
|
||||
nations: string[];
|
||||
classes: TankClass[];
|
||||
types: TankType[];
|
||||
testing: 'include' | 'exclude' | 'only';
|
||||
testing: "include" | "exclude" | "only";
|
||||
search: string | null;
|
||||
searching: boolean;
|
||||
gunType: CaseType<GunDefinition>[];
|
||||
shells: [ShellType | null, ShellType | null, ShellType | null];
|
||||
}
|
||||
|
||||
export const initialTankFilters: TankFilters = {
|
||||
export const TankFilters = new Varuna<TankFilters>({
|
||||
tiers: [],
|
||||
nations: [],
|
||||
classes: [],
|
||||
types: [],
|
||||
testing: 'include',
|
||||
testing: "include",
|
||||
search: null,
|
||||
searching: false,
|
||||
gunType: [],
|
||||
shells: [null, null, null],
|
||||
};
|
||||
|
||||
export const $tankFilters = map<TankFilters>(initialTankFilters);
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Samples } from '@blitzkit/core';
|
||||
import { create } from 'zustand';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
|
||||
export interface TankPerformanceEphemeral {
|
||||
playerCountPeriod: PlayerCountPeriod;
|
||||
}
|
||||
|
||||
export type PlayerCountPeriod = keyof Samples;
|
||||
|
||||
export const TankPerformanceEphemeral = createContextualStore(() =>
|
||||
create<TankPerformanceEphemeral>()(() => ({
|
||||
playerCountPeriod: 'd_30',
|
||||
})),
|
||||
);
|
||||
@@ -4,13 +4,11 @@ import {
|
||||
type ModelDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import type { Vector3 } from 'three';
|
||||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { Varuna } from 'varuna';
|
||||
import type { ArmorType } from '../components/Armor/components/SpacedArmorScene';
|
||||
import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent';
|
||||
import type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection';
|
||||
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
import { TankopediaDisplay } from './tankopediaPersistent/constants';
|
||||
|
||||
export interface ShotLayerBase {
|
||||
@@ -109,20 +107,17 @@ interface TankopediaEphemeral {
|
||||
|
||||
const skillDefinitions = await awaitableSkillDefinitions;
|
||||
|
||||
export const TankopediaEphemeral = createContextualStore(
|
||||
(model: ModelDefinition) => {
|
||||
return create<TankopediaEphemeral>()(
|
||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
||||
disturbed: false,
|
||||
revealed: false,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
editStatic: false,
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
model,
|
||||
controlsEnabled: true,
|
||||
xpMultiplier: 1,
|
||||
display: TankopediaDisplay.Model,
|
||||
})),
|
||||
);
|
||||
},
|
||||
);
|
||||
export const TankopediaEphemeral = new Varuna<
|
||||
TankopediaEphemeral,
|
||||
ModelDefinition
|
||||
>((model) => ({
|
||||
disturbed: false,
|
||||
revealed: false,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
editStatic: false,
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
model,
|
||||
controlsEnabled: true,
|
||||
xpMultiplier: 1,
|
||||
display: TankopediaDisplay.Model,
|
||||
}));
|
||||
@@ -12,7 +12,6 @@ export interface TankopediaPersistentStore {
|
||||
wireframe: boolean;
|
||||
opaque: boolean;
|
||||
advancedHighlighting: boolean;
|
||||
showGrid: boolean;
|
||||
greenPenetration: boolean;
|
||||
showSpacedArmor: boolean;
|
||||
showExternalModules: boolean;
|
||||
@@ -34,7 +33,6 @@ export const TankopediaPersistent = createContextualStore(() =>
|
||||
wireframe: false,
|
||||
opaque: false,
|
||||
advancedHighlighting: false,
|
||||
showGrid: true,
|
||||
greenPenetration: false,
|
||||
showSpacedArmor: true,
|
||||
showExternalModules: true,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { map } from 'nanostores';
|
||||
import { Varuna } from 'varuna';
|
||||
import {
|
||||
type TankopediaSortBy,
|
||||
type TankopediaSortDirection,
|
||||
} from './tankopediaPersistent';
|
||||
|
||||
export interface TankopediaSort {
|
||||
export interface TankSort {
|
||||
by: TankopediaSortBy;
|
||||
direction: TankopediaSortDirection;
|
||||
}
|
||||
|
||||
export const initialTankopediaSort: TankopediaSort = {
|
||||
export const TankSort = new Varuna<TankSort>({
|
||||
by: 'meta.none',
|
||||
direction: 'descending',
|
||||
};
|
||||
|
||||
export const $tankopediaSort = map<TankopediaSort>(initialTankopediaSort);
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
import { enableMapSet } from 'immer';
|
||||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { Varuna } from 'varuna';
|
||||
import { tierListRows } from '../components/TierList/Table/constants';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
|
||||
export interface TierList {
|
||||
dragging: boolean;
|
||||
@@ -10,19 +8,10 @@ export interface TierList {
|
||||
placedTanks: Set<number>;
|
||||
}
|
||||
|
||||
enableMapSet();
|
||||
|
||||
export const tierListInitialState: TierList = {
|
||||
export const TierList = new Varuna<TierList>({
|
||||
dragging: false,
|
||||
rows: tierListRows.map((row) => ({
|
||||
name: row.name,
|
||||
tanks: [],
|
||||
})),
|
||||
rows: tierListRows.map((row) => ({ name: row.name, tanks: [] })),
|
||||
placedTanks: new Set(),
|
||||
};
|
||||
});
|
||||
|
||||
export const TierList = createContextualStore(() =>
|
||||
create<TierList>()(
|
||||
subscribeWithSelector<TierList>(() => tierListInitialState),
|
||||
),
|
||||
);
|
||||
enableMapSet();
|
||||
Reference in new issue
Block a user