feat: renaming

This commit is contained in:
TrèsAbhi committed 2025-09-01 22:56:39 -05:00
1 parent 8b527f2333
commit 96482d4268
38 files changed
+931 -943

No files matched your search

@@ -1,4 +1,4 @@
import { ReloadIcon } from '@radix-ui/react-icons'; import { ReloadIcon } from "@radix-ui/react-icons";
import { import {
Card, Card,
Code, Code,
@@ -6,21 +6,19 @@ import {
IconButton, IconButton,
Text, Text,
TextField, TextField,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { Html } from '@react-three/drei'; import { Html } from "@react-three/drei";
import { useRef } from 'react'; import { useRef } from "react";
import { radToDeg } from 'three/src/math/MathUtils.js'; import { radToDeg } from "three/src/math/MathUtils.js";
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex'; import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
import { App } from '../../../stores/app'; import { App } from "../../../stores/app";
import { Duel } from '../../../stores/duel'; import { Duel } from "../../../stores/duel";
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../stores/tankopedia";
import { layerTypeNames } from './ShotDisplayCard'; import { layerTypeNames } from "./ShotDisplayCard";
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from "./SpacedArmorScene";
export function ArmorPlateDisplay() { export function ArmorPlateDisplay() {
const highlightArmor = TankopediaEphemeral.use( const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
(state) => state.highlightArmor,
);
const developerMode = App.useDeferred((state) => state.developerMode, false); const developerMode = App.useDeferred((state) => state.developerMode, false);
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
@@ -31,20 +29,20 @@ export function ArmorPlateDisplay() {
<group position={highlightArmor.point}> <group position={highlightArmor.point}>
<Html <Html
center center
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }} style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
> >
<Card <Card
variant="classic" variant="classic"
mb="4" mb="4"
style={{ style={{
whiteSpace: 'nowrap', whiteSpace: "nowrap",
color: highlightArmor.color, color: highlightArmor.color,
transform: 'translateY(-50%)', transform: "translateY(-50%)",
}} }}
> >
<Flex direction="column"> <Flex direction="column">
<Text weight="bold"> <Text weight="bold">
{layerTypeNames[highlightArmor.type]}{' '} {layerTypeNames[highlightArmor.type]}{" "}
{highlightArmor.thickness.toFixed(0)} {highlightArmor.thickness.toFixed(0)}
<Text size="1" weight="regular"> <Text size="1" weight="regular">
mm mm
@@ -90,10 +88,10 @@ export function ArmorPlateDisplay() {
if (isNaN(thickness) || thickness < 0) return; if (isNaN(thickness) || thickness < 0) return;
if (event.key === 'Enter') { if (event.key === "Enter") {
input.current?.blur(); input.current?.blur();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
const { protagonist } = duelStore.getState(); const { protagonist } = duelStore.getState();
const tank = draft.model; const tank = draft.model;
@@ -101,23 +99,23 @@ export function ArmorPlateDisplay() {
const gun = turret.guns[protagonist.gun.id]; const gun = turret.guns[protagonist.gun.id];
const track = tank.tracks[protagonist.track.id]; const track = tank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith('hull_')) { if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] = thickness; tank.armor.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith('turret_')) { } else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] = thickness; turret.armor.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith('gun_')) { } else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes('_armor_')) { if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] = thickness; gun.armor.thickness[index] = thickness;
} else { } else {
gun.thickness = thickness; gun.thickness = thickness;
} }
} else if (highlightArmor.name.startsWith('chassis_')) { } else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = thickness; track.thickness = thickness;
} }
}); });
} else if (event.key === 'Escape') { } else if (event.key === "Escape") {
input.current?.blur(); input.current?.blur();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
} }
@@ -135,7 +133,7 @@ export function ArmorPlateDisplay() {
mt="2" mt="2"
color="red" color="red"
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
if (!highlightArmor.editingPlate) return; if (!highlightArmor.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name); const index = resolveArmorIndex(highlightArmor.name);
@@ -143,7 +141,7 @@ export function ArmorPlateDisplay() {
if (index === undefined) return; if (index === undefined) return;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
const initialState = TankopediaEphemeral.initial; const initialState = Tankopedia.initial;
const { protagonist } = duelStore.getState(); const { protagonist } = duelStore.getState();
const tank = draft.model; const tank = draft.model;
const initialTank = initialState.model; const initialTank = initialState.model;
@@ -156,20 +154,20 @@ export function ArmorPlateDisplay() {
const initialTrack = const initialTrack =
initialTank.tracks[protagonist.track.id]; initialTank.tracks[protagonist.track.id];
if (highlightArmor.name.startsWith('hull_')) { if (highlightArmor.name.startsWith("hull_")) {
tank.armor.thickness[index] = tank.armor.thickness[index] =
initialTank.armor.thickness[index]; initialTank.armor.thickness[index];
} else if (highlightArmor.name.startsWith('turret_')) { } else if (highlightArmor.name.startsWith("turret_")) {
turret.armor.thickness[index] = turret.armor.thickness[index] =
initialTurret.armor.thickness[index]; initialTurret.armor.thickness[index];
} else if (highlightArmor.name.startsWith('gun_')) { } else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes('_armor_')) { if (highlightArmor.name.includes("_armor_")) {
gun.armor.thickness[index] = gun.armor.thickness[index] =
initialGun.armor.thickness[index]; initialGun.armor.thickness[index];
} else { } else {
gun.thickness = initialGun.thickness; gun.thickness = initialGun.thickness;
} }
} else if (highlightArmor.name.startsWith('chassis_')) { } else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = initialTrack.thickness; track.thickness = initialTrack.thickness;
} }
}); });
@@ -2,9 +2,9 @@ import {
canSplash, canSplash,
isExplosive, isExplosive,
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { invalidate, useFrame } from '@react-three/fiber'; import { invalidate, useFrame } from "@react-three/fiber";
import { useEffect } from 'react'; import { useEffect } from "react";
import { import {
MeshBasicMaterial, MeshBasicMaterial,
NormalBlending, NormalBlending,
@@ -14,16 +14,16 @@ import {
UniformsLib, UniformsLib,
UniformsUtils, UniformsUtils,
Vector2, Vector2,
} from 'three'; } from "three";
import { degToRad } from 'three/src/math/MathUtils.js'; import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from '../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from '../../../../stores/duel'; import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import fragmentShader from './shaders/fragment.glsl?raw'; import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from './shaders/vertex.glsl?raw'; import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from './target'; import { spacedArmorRenderTarget } from "./target";
interface PrimaryArmorSceneComponentProps { interface PrimaryArmorSceneComponentProps {
thickness: number; thickness: number;
@@ -81,15 +81,15 @@ export function PrimaryArmorSceneComponent({
useEffect(() => { useEffect(() => {
function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = TankopediaEphemeral.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
material.uniforms.caliber.value = shell.caliber; material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad( material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!, isExplosive(shell.type) ? 90 : shell.ricochet!
); );
material.uniforms.normalization.value = degToRad( material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0, shell.normalization ?? 0
); );
material.uniforms.isExplosive.value = isExplosive(shell.type); material.uniforms.isExplosive.value = isExplosive(shell.type);
material.uniforms.canSplash.value = canSplash(shell.type); material.uniforms.canSplash.value = canSplash(shell.type);
@@ -116,13 +116,13 @@ export function PrimaryArmorSceneComponent({
} }
function handleProtagonistEquipmentChange( function handleProtagonistEquipmentChange(
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false, noInvalidate = false
) { ) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
equipment, equipment
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03 ? thickness * 1.03
@@ -132,16 +132,16 @@ export function PrimaryArmorSceneComponent({
} }
function handleAntagonistEquipmentChange( function handleAntagonistEquipmentChange(
equipment: EquipmentMatrix, equipment: EquipmentMatrix,
noInvalidate = false, noInvalidate = false
) { ) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = TankopediaEphemeral.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, duel.antagonist.tank.equipment_preset,
equipment, equipment
); );
material.uniforms.penetration.value = material.uniforms.penetration.value =
@@ -159,34 +159,34 @@ export function PrimaryArmorSceneComponent({
handleOpaqueChange(initialState.opaque); handleOpaqueChange(initialState.opaque);
handleWireframeChange(initialState.wireframe); handleWireframeChange(initialState.wireframe);
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix, duelStore.getState().protagonist.equipmentMatrix
); );
handleAntagonistEquipmentChange( handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix, duelStore.getState().antagonist.equipmentMatrix
); );
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
TankopediaEphemeral.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
tankopediaPersistentStore.subscribe( tankopediaPersistentStore.subscribe(
(state) => state.greenPenetration, (state) => state.greenPenetration,
handleGreenPenetrationChange, handleGreenPenetrationChange
), ),
tankopediaPersistentStore.subscribe( tankopediaPersistentStore.subscribe(
(state) => state.advancedHighlighting, (state) => state.advancedHighlighting,
handleAdvancedHighlightingChange, handleAdvancedHighlightingChange
), ),
tankopediaPersistentStore.subscribe( tankopediaPersistentStore.subscribe(
(state) => state.opaque, (state) => state.opaque,
handleOpaqueChange, handleOpaqueChange
), ),
duelStore.subscribe( duelStore.subscribe(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
(equipment) => handleProtagonistEquipmentChange(equipment), (equipment) => handleProtagonistEquipmentChange(equipment)
), ),
duelStore.subscribe( duelStore.subscribe(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment), (equipment) => handleAntagonistEquipmentChange(equipment)
), ),
]; ];
@@ -197,7 +197,7 @@ export function PrimaryArmorSceneComponent({
useFrame(({ gl, camera }) => { useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar( gl.getSize(material.uniforms.resolution.value).multiplyScalar(
gl.getPixelRatio(), gl.getPixelRatio()
); );
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture; material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
material.uniforms.spacedArmorDepth.value = material.uniforms.spacedArmorDepth.value =
@@ -1,9 +1,9 @@
import { J_HAT, K_HAT } from '@blitzkit/core'; import { J_HAT, K_HAT } from "@blitzkit/core";
import { Box, Flex, Text } from '@radix-ui/themes'; import { Box, Flex, Text } from "@radix-ui/themes";
import { Html } from '@react-three/drei'; import { Html } from "@react-three/drei";
import { invalidate, useFrame } from '@react-three/fiber'; import { invalidate, useFrame } from "@react-three/fiber";
import { clamp } from 'lodash-es'; import { clamp } from "lodash-es";
import { useEffect, useRef, type ComponentProps } from 'react'; import { useEffect, useRef, type ComponentProps } from "react";
import { import {
BufferGeometry, BufferGeometry,
DoubleSide, DoubleSide,
@@ -15,24 +15,24 @@ import {
MeshBasicMaterial, MeshBasicMaterial,
Path, Path,
Quaternion, Quaternion,
} from 'three'; } from "three";
import { isHalloween } from '../../../core/blitzkit/isHalloween'; import { isHalloween } from "../../../core/blitzkit/isHalloween";
import { LocaleProvider, useLocale } from '../../../hooks/useLocale'; import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
import { import {
TankopediaEphemeral, Tankopedia,
type ShotLayerNonExternal, type ShotLayerNonExternal,
type ShotStatus, type ShotStatus,
} from '../../../stores/tankopediaEphemeral'; } from "../../../stores/tankopedia";
import { ShotDisplayCard } from './ShotDisplayCard'; import { ShotDisplayCard } from "./ShotDisplayCard";
export const shotStatusColors: Record< export const shotStatusColors: Record<
ShotStatus, ShotStatus,
ComponentProps<typeof Text>['color'] ComponentProps<typeof Text>["color"]
> = { > = {
blocked: 'red', blocked: "red",
penetration: 'green', penetration: "green",
ricochet: 'yellow', ricochet: "yellow",
splash: 'orange', splash: "orange",
}; };
const SURFACE_POINT_SIZE = 12; const SURFACE_POINT_SIZE = 12;
@@ -41,7 +41,7 @@ const TRACER_THICK = 1 / 32;
const TRACER_THIN = TRACER_THICK / 2; const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() { export function ShotDisplay() {
const shot = TankopediaEphemeral.use((state) => state.shot); const shot = Tankopedia.use((state) => state.shot);
const inTracer = useRef<Mesh>(null); const inTracer = useRef<Mesh>(null);
const outTracer = useRef<Mesh>(null); const outTracer = useRef<Mesh>(null);
const splashRadiusWrapper = useRef<Group>(null); const splashRadiusWrapper = useRef<Group>(null);
@@ -52,16 +52,16 @@ export function ShotDisplay() {
useEffect(() => { useEffect(() => {
if (shot?.splashRadius !== undefined) { if (shot?.splashRadius !== undefined) {
const outlineGeometry = new BufferGeometry().setFromPoints( const outlineGeometry = new BufferGeometry().setFromPoints(
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50), new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50)
); );
const outlineMaterial = new LineDashedMaterial({ const outlineMaterial = new LineDashedMaterial({
color: 'orange', color: "orange",
dashSize: 0.05, dashSize: 0.05,
gapSize: 0.025, gapSize: 0.025,
}); });
const outline = new Line( const outline = new Line(
outlineGeometry, outlineGeometry,
outlineMaterial, outlineMaterial
).computeLineDistances(); ).computeLineDistances();
splashRadiusWrapper.current?.add(outline); splashRadiusWrapper.current?.add(outline);
@@ -76,9 +76,9 @@ export function ShotDisplay() {
if ( if (
isHalloween() && isHalloween() &&
shot?.splashRadius !== undefined && shot?.splashRadius !== undefined &&
shot.in.status === 'penetration' shot.in.status === "penetration"
) { ) {
const audio = new Audio('/assets/audio/lotta-damage.mp3'); const audio = new Audio("/assets/audio/lotta-damage.mp3");
audio.currentTime = 0.7; audio.currentTime = 0.7;
audio.volume = 0.25; audio.volume = 0.25;
@@ -134,11 +134,11 @@ export function ShotDisplay() {
const outLength = const outLength =
shot.out && shot.out.layers.length > 0 shot.out && shot.out.layers.length > 0
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo( ? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
(shot.out.layers.at(-1) as ShotLayerNonExternal).point, (shot.out.layers.at(-1) as ShotLayerNonExternal).point
) )
: SHOT_DISPLAY_LENGTH_INFINITY; : SHOT_DISPLAY_LENGTH_INFINITY;
const inLast = shot.in.layers.findLast( const inLast = shot.in.layers.findLast(
(layer) => layer.type !== null, (layer) => layer.type !== null
) as ShotLayerNonExternal; ) as ShotLayerNonExternal;
const outTitleColor = shot.out const outTitleColor = shot.out
? shotStatusColors[shot.out.status] ? shotStatusColors[shot.out.status]
@@ -154,7 +154,7 @@ export function ShotDisplay() {
position={inLast.point} position={inLast.point}
ref={splashRadiusWrapper} ref={splashRadiusWrapper}
rotation={new Euler().setFromQuaternion( rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal), new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
)} )}
> >
<mesh> <mesh>
@@ -172,13 +172,13 @@ export function ShotDisplay() {
<Html position={inLast.point} center> <Html position={inLast.point} center>
<Box <Box
style={{ style={{
pointerEvents: 'none', pointerEvents: "none",
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`, transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
}} }}
mb={tracerGoingUp ? '0' : '9'} mb={tracerGoingUp ? "0" : "9"}
pb={tracerGoingUp ? '0' : '4'} pb={tracerGoingUp ? "0" : "4"}
mt={tracerGoingUp ? '9' : '0'} mt={tracerGoingUp ? "9" : "0"}
pt={tracerGoingUp ? '4' : '0'} pt={tracerGoingUp ? "4" : "0"}
> >
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
<ShotDisplayCard shot={shot} /> <ShotDisplayCard shot={shot} />
@@ -190,7 +190,7 @@ export function ShotDisplay() {
if (layer.type === null) return null; if (layer.type === null) return null;
const shotStatusColor = const shotStatusColor =
shot.in.status === 'splash' shot.in.status === "splash"
? shotStatusColors.splash ? shotStatusColors.splash
: shotStatusColors[layer.status]; : shotStatusColors[layer.status];
@@ -199,8 +199,8 @@ export function ShotDisplay() {
position={layer.point} position={layer.point}
key={index} key={index}
style={{ style={{
pointerEvents: 'none', pointerEvents: "none",
transform: 'translateY(-50%)', transform: "translateY(-50%)",
}} }}
> >
<Flex <Flex
@@ -225,7 +225,7 @@ export function ShotDisplay() {
<group <group
position={inLast.point} position={inLast.point}
rotation={new Euler().setFromQuaternion( rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal), new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal)
)} )}
> >
<mesh position={[0, inLength / 2, 0]} renderOrder={2}> <mesh position={[0, inLength / 2, 0]} renderOrder={2}>
@@ -253,8 +253,8 @@ export function ShotDisplay() {
rotation={new Euler().setFromQuaternion( rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors( new Quaternion().setFromUnitVectors(
J_HAT, J_HAT,
shot.out.surfaceNormal.clone(), shot.out.surfaceNormal.clone()
), )
)} )}
> >
<mesh position={[0, outLength / 2, 0]} renderOrder={2}> <mesh position={[0, outLength / 2, 0]} renderOrder={2}>
@@ -1,4 +1,4 @@
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { import {
Card, Card,
Flex, Flex,
@@ -6,19 +6,19 @@ import {
Separator, Separator,
Text, Text,
type CardProps, type CardProps,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import type { ComponentProps } from 'react'; import type { ComponentProps } from "react";
import { radToDeg } from 'three/src/math/MathUtils.js'; import { radToDeg } from "three/src/math/MathUtils.js";
import { useLocale } from '../../../hooks/useLocale'; import { useLocale } from "../../../hooks/useLocale";
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral'; import type { Shot, ShotLayer } from "../../../stores/tankopedia";
import { shotStatusColors } from './ShotDisplay'; import { shotStatusColors } from "./ShotDisplay";
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from "./SpacedArmorScene";
export const layerTypeNames = { export const layerTypeNames = {
[ArmorType.Primary]: 'primary', [ArmorType.Primary]: "primary",
[ArmorType.Spaced]: 'spaced', [ArmorType.Spaced]: "spaced",
[ArmorType.External]: 'external', [ArmorType.External]: "external",
null: 'gap', null: "gap",
} as const; } as const;
function LayerEntry({ function LayerEntry({
@@ -29,7 +29,7 @@ function LayerEntry({
angle, angle,
}: { }: {
layerIndex?: string; layerIndex?: string;
shotStatusColor: ComponentProps<typeof Text>['color']; shotStatusColor: ComponentProps<typeof Text>["color"];
layerName: string; layerName: string;
layer: ShotLayer; layer: ShotLayer;
angle?: number; angle?: number;
@@ -77,7 +77,7 @@ function LayerEntry({
[ [
Math.round(layer.thicknessAngled).toLocaleString(locale), Math.round(layer.thicknessAngled).toLocaleString(locale),
radToDeg(angle ?? 0).toFixed(0), radToDeg(angle ?? 0).toFixed(0),
], ]
)} )}
</Text> </Text>
)} )}
@@ -89,7 +89,7 @@ function LayerEntry({
{literals( {literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.ricochet_loss, .ricochet_loss,
[Math.max(-100, -50 * layer.distance).toFixed(0)], [Math.max(-100, -50 * layer.distance).toFixed(0)]
)} )}
</Text> </Text>
)} )}
@@ -100,7 +100,7 @@ function LayerEntry({
{literals( {literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.nominal, .nominal,
[Math.round(layer.thickness).toLocaleString(locale)], [Math.round(layer.thickness).toLocaleString(locale)]
)} )}
</Text> </Text>
)} )}
@@ -132,14 +132,14 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[ strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
shot.in.status shot.in.status
], ],
[Math.round(shot.damage).toLocaleString(locale)], [Math.round(shot.damage).toLocaleString(locale)]
)} )}
</Text> </Text>
<table <table
style={{ style={{
whiteSpace: 'nowrap', whiteSpace: "nowrap",
borderSpacing: 'var(--space-1) 0', borderSpacing: "var(--space-1) 0",
}} }}
> >
{shot.in.layers.map((layer, index) => { {shot.in.layers.map((layer, index) => {
@@ -155,7 +155,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
? undefined ? undefined
: `${(shot.containsGaps ? index / 2 : index) + 1}.`; : `${(shot.containsGaps ? index / 2 : index) + 1}.`;
const shotStatusColor = const shotStatusColor =
shot.in.status === 'splash' shot.in.status === "splash"
? shotStatusColors.splash ? shotStatusColors.splash
: shotStatusColors[layer.status]; : shotStatusColors[layer.status];
@@ -202,7 +202,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
{literals( {literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.status[shot.out.status], .status[shot.out.status],
[Math.round(shot.damage).toLocaleString(locale)], [Math.round(shot.damage).toLocaleString(locale)]
)} )}
</Text> </Text>
@@ -224,7 +224,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
1 1
}.`; }.`;
const shotStatusColor = const shotStatusColor =
shot.out!.status === 'splash' shot.out!.status === "splash"
? shotStatusColors.splash ? shotStatusColors.splash
: shotStatusColors[layer.status]; : shotStatusColors[layer.status];
@@ -1,20 +1,20 @@
import { resolvePenetrationCoefficient } from '@blitzkit/core'; import { resolvePenetrationCoefficient } from "@blitzkit/core";
import { useEffect } from 'react'; import { useEffect } from "react";
import { import {
AdditiveBlending, AdditiveBlending,
MeshBasicMaterial, MeshBasicMaterial,
Object3D, Object3D,
Plane, Plane,
ShaderMaterial, ShaderMaterial,
} from 'three'; } from "three";
import type { ArmorUserData, ExternalModuleVariant } from '../..'; import type { ArmorUserData, ExternalModuleVariant } from "../..";
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel'; import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { ArmorType } from '../../../SpacedArmorScene'; import { ArmorType } from "../../../SpacedArmorScene";
import fragmentShader from './shaders/fragment.glsl?raw'; import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from './shaders/vertex.glsl?raw'; import vertexShader from "./shaders/vertex.glsl?raw";
interface SpacedArmorSubExternalProps { interface SpacedArmorSubExternalProps {
node: Object3D; node: Object3D;
@@ -49,7 +49,7 @@ export function SpacedArmorSubExternal({
useEffect(() => { useEffect(() => {
function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = TankopediaEphemeral.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration.near;
} }
@@ -58,7 +58,7 @@ export function SpacedArmorSubExternal({
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
equipment, equipment
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03 ? thickness * 1.03
@@ -66,13 +66,13 @@ export function SpacedArmorSubExternal({
} }
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = TankopediaEphemeral.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, duel.antagonist.tank.equipment_preset,
equipment, equipment
); );
material.uniforms.penetration.value = material.uniforms.penetration.value =
@@ -82,22 +82,22 @@ export function SpacedArmorSubExternal({
handleShellChange(); handleShellChange();
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix, duelStore.getState().protagonist.equipmentMatrix
); );
handleAntagonistEquipmentChange( handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix, duelStore.getState().antagonist.equipmentMatrix
); );
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
TankopediaEphemeral.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
duelStore.subscribe( duelStore.subscribe(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange, handleProtagonistEquipmentChange
), ),
duelStore.subscribe( duelStore.subscribe(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange, handleAntagonistEquipmentChange
), ),
]; ];
@@ -1,20 +1,20 @@
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core'; import { isExplosive, resolvePenetrationCoefficient } from "@blitzkit/core";
import { useEffect } from 'react'; import { useEffect } from "react";
import { import {
AdditiveBlending, AdditiveBlending,
MeshBasicMaterial, MeshBasicMaterial,
ShaderMaterial, ShaderMaterial,
type Object3D, type Object3D,
} from 'three'; } from "three";
import { degToRad } from 'three/src/math/MathUtils.js'; import { degToRad } from "three/src/math/MathUtils.js";
import type { ArmorUserData } from '../..'; import type { ArmorUserData } from "../..";
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel'; import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { ArmorType } from '../../../SpacedArmorScene'; import { ArmorType } from "../../../SpacedArmorScene";
import fragmentShader from './shaders/fragment.glsl?raw'; import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from './shaders/vertex.glsl?raw'; import vertexShader from "./shaders/vertex.glsl?raw";
interface SpacedArmorSubSpacedProps { interface SpacedArmorSubSpacedProps {
node: Object3D; node: Object3D;
@@ -51,16 +51,16 @@ export function SpacedArmorSubSpaced({
useEffect(() => { useEffect(() => {
function handleShellChange() { function handleShellChange() {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = TankopediaEphemeral.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
material.uniforms.penetration.value = shell.penetration.near; material.uniforms.penetration.value = shell.penetration.near;
material.uniforms.caliber.value = shell.caliber; material.uniforms.caliber.value = shell.caliber;
material.uniforms.ricochet.value = degToRad( material.uniforms.ricochet.value = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!, isExplosive(shell.type) ? 90 : shell.ricochet!
); );
material.uniforms.normalization.value = degToRad( material.uniforms.normalization.value = degToRad(
shell.normalization ?? 0, shell.normalization ?? 0
); );
} }
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) { function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
@@ -68,7 +68,7 @@ export function SpacedArmorSubSpaced({
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
equipment, equipment
); );
material.uniforms.thickness.value = hasEnhancedArmor material.uniforms.thickness.value = hasEnhancedArmor
? thickness * 1.03 ? thickness * 1.03
@@ -76,13 +76,13 @@ export function SpacedArmorSubSpaced({
} }
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const tankopediaEphemeral = TankopediaEphemeral.state; const tankopediaEphemeral = Tankopedia.state;
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
const penetration = shell.penetration.near; const penetration = shell.penetration.near;
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
duel.antagonist.tank.equipment_preset, duel.antagonist.tank.equipment_preset,
equipment, equipment
); );
material.uniforms.penetration.value = material.uniforms.penetration.value =
@@ -92,22 +92,22 @@ export function SpacedArmorSubSpaced({
handleShellChange(); handleShellChange();
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix, duelStore.getState().protagonist.equipmentMatrix
); );
handleAntagonistEquipmentChange( handleAntagonistEquipmentChange(
duelStore.getState().antagonist.equipmentMatrix, duelStore.getState().antagonist.equipmentMatrix
); );
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
TankopediaEphemeral.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
duelStore.subscribe( duelStore.subscribe(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange, handleProtagonistEquipmentChange
), ),
duelStore.subscribe( duelStore.subscribe(
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
handleAntagonistEquipmentChange, handleAntagonistEquipmentChange
), ),
]; ];
@@ -2,9 +2,9 @@ import {
ShellType, ShellType,
isExplosive, isExplosive,
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { useThree } from '@react-three/fiber'; import { useThree } from "@react-three/fiber";
import { useCallback } from 'react'; import { useCallback } from "react";
import { import {
type Intersection, type Intersection,
MeshBasicMaterial, MeshBasicMaterial,
@@ -14,21 +14,21 @@ import {
Raycaster, Raycaster,
Scene, Scene,
Vector3, Vector3,
} from 'three'; } from "three";
import { degToRad } from 'three/src/math/MathUtils.js'; import { degToRad } from "three/src/math/MathUtils.js";
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
import { jsxTree } from '../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane'; import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
import { Duel } from '../../../../stores/duel'; import { Duel } from "../../../../stores/duel";
import { import {
type Shot, type Shot,
type ShotLayerBase, type ShotLayerBase,
type ShotLayerNonExternal, type ShotLayerNonExternal,
TankopediaEphemeral, Tankopedia,
} from '../../../../stores/tankopediaEphemeral'; } from "../../../../stores/tankopedia";
import { ArmorType } from '../SpacedArmorScene'; import { ArmorType } from "../SpacedArmorScene";
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubExternal } from "./components/SpacedArmorSubExternal";
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; import { SpacedArmorSubSpaced } from "./components/SpacedArmorSubSpaced";
type SpacedArmorSceneComponentProps = { type SpacedArmorSceneComponentProps = {
node: Object3D; node: Object3D;
@@ -45,7 +45,7 @@ type SpacedArmorSceneComponentProps = {
} }
); );
export type ExternalModuleVariant = 'gun' | 'track'; export type ExternalModuleVariant = "gun" | "track";
export type ArmorUserData = { export type ArmorUserData = {
thickness: number; thickness: number;
} & ( } & (
@@ -84,10 +84,10 @@ export function SpacedArmorSceneComponent({
point: Vector3, point: Vector3,
intersections: Intersection[], intersections: Intersection[],
allowRicochet: boolean, allowRicochet: boolean,
remainingPenetrationInput?: number, remainingPenetrationInput?: number
) => { ) => {
const { antagonist, protagonist } = duelStore.getState(); const { antagonist, protagonist } = duelStore.getState();
const { customShell } = TankopediaEphemeral.state; const { customShell } = Tankopedia.state;
const shell = customShell ?? antagonist.shell; const shell = customShell ?? antagonist.shell;
const cameraNormal = camera.position.clone().sub(point).normalize(); const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: Shot = { const shot: Shot = {
@@ -98,19 +98,19 @@ export function SpacedArmorSceneComponent({
in: { in: {
surfaceNormal: cameraNormal, surfaceNormal: cameraNormal,
layers: [], layers: [],
status: 'blocked', status: "blocked",
}, },
}; };
const hasCalibratedShells = hasEquipment( const hasCalibratedShells = hasEquipment(
103, 103,
antagonist.tank.equipment_preset, antagonist.tank.equipment_preset,
antagonist.equipmentMatrix, antagonist.equipmentMatrix
); );
const hasEnhancedArmor = hasEquipment( const hasEnhancedArmor = hasEquipment(
110, 110,
protagonist.tank.equipment_preset, protagonist.tank.equipment_preset,
protagonist.equipmentMatrix, protagonist.equipmentMatrix
); );
const penetration = const penetration =
shell.penetration.near * shell.penetration.near *
@@ -118,8 +118,8 @@ export function SpacedArmorSceneComponent({
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1; const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
const filteredIntersections = intersections.filter( const filteredIntersections = intersections.filter(
(intersection) => (intersection) =>
'type' in intersection.object.userData && "type" in intersection.object.userData &&
!discardClippingPlane(intersection.object, intersection.point), !discardClippingPlane(intersection.object, intersection.point)
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[]; ) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
const encounteredExternalModuleVariants: ExternalModuleVariant[] = []; const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
const noDuplicateIntersections: typeof filteredIntersections = []; const noDuplicateIntersections: typeof filteredIntersections = [];
@@ -128,12 +128,12 @@ export function SpacedArmorSceneComponent({
if (intersection.object.userData.type === ArmorType.External) { if (intersection.object.userData.type === ArmorType.External) {
if ( if (
!encounteredExternalModuleVariants.includes( !encounteredExternalModuleVariants.includes(
intersection.object.userData.variant, intersection.object.userData.variant
) )
) { ) {
noDuplicateIntersections.push(intersection); noDuplicateIntersections.push(intersection);
encounteredExternalModuleVariants.push( encounteredExternalModuleVariants.push(
intersection.object.userData.variant, intersection.object.userData.variant
); );
} }
} else { } else {
@@ -161,7 +161,7 @@ export function SpacedArmorSceneComponent({
const surfaceNormal = intersection const surfaceNormal = intersection
.normal!.clone() .normal!.clone()
.applyQuaternion( .applyQuaternion(
intersection.object.getWorldQuaternion(new Quaternion()), intersection.object.getWorldQuaternion(new Quaternion())
); );
if (shell.type === ShellType.HEAT && index > 0) { if (shell.type === ShellType.HEAT && index > 0) {
@@ -173,7 +173,7 @@ export function SpacedArmorSceneComponent({
shot.in.layers.push({ shot.in.layers.push({
type: null, type: null,
distance, distance,
status: blocked ? 'blocked' : 'penetration', status: blocked ? "blocked" : "penetration",
}); });
if (blocked) break; if (blocked) break;
@@ -193,16 +193,16 @@ export function SpacedArmorSceneComponent({
surfaceNormal, surfaceNormal,
status: status:
shell.type === ShellType.HE shell.type === ShellType.HE
? 'blocked' ? "blocked"
: blocked : blocked
? 'blocked' ? "blocked"
: 'penetration', : "penetration",
variant: layer.variant, variant: layer.variant,
}); });
} else { } else {
const thickness = layer.thickness * thicknessCoefficient; const thickness = layer.thickness * thicknessCoefficient;
const ricochet = degToRad( const ricochet = degToRad(
isExplosive(shell.type) ? 90 : shell.ricochet!, isExplosive(shell.type) ? 90 : shell.ricochet!
); );
const angle = surfaceNormal.angleTo(cameraNormal); const angle = surfaceNormal.angleTo(cameraNormal);
@@ -226,7 +226,7 @@ export function SpacedArmorSceneComponent({
thicknessAngled: finalThickness, thicknessAngled: finalThickness,
point: intersection.point, point: intersection.point,
surfaceNormal, surfaceNormal,
status: 'ricochet', status: "ricochet",
angle, angle,
}); });
@@ -244,10 +244,10 @@ export function SpacedArmorSceneComponent({
surfaceNormal: intersection.face!.normal, surfaceNormal: intersection.face!.normal,
status: status:
shell.type === ShellType.HE && layer.type !== ArmorType.Primary shell.type === ShellType.HE && layer.type !== ArmorType.Primary
? 'blocked' ? "blocked"
: blocked : blocked
? 'blocked' ? "blocked"
: 'penetration', : "penetration",
angle, angle,
}); });
} }
@@ -281,10 +281,10 @@ export function SpacedArmorSceneComponent({
return acc + layer.thicknessAngled; return acc + layer.thicknessAngled;
}, },
0, 0
); );
const distanceFromSpacedArmor = firstLayer.point.distanceTo( const distanceFromSpacedArmor = firstLayer.point.distanceTo(
lastLayer.point, lastLayer.point
); );
const finalDamage = Math.max( const finalDamage = Math.max(
0, 0,
@@ -293,7 +293,7 @@ export function SpacedArmorSceneComponent({
(1 - distanceFromSpacedArmor / shell.explosion_radius!) - (1 - distanceFromSpacedArmor / shell.explosion_radius!) -
1.1 * 1.1 *
(lastLayer.thicknessAngled + (lastLayer.thicknessAngled +
Math.min(penetration, totalSpacedArmorThickness)), Math.min(penetration, totalSpacedArmorThickness))
); );
/** /**
@@ -307,19 +307,19 @@ export function SpacedArmorSceneComponent({
* for parity's sake, I am capping the value here. * for parity's sake, I am capping the value here.
*/ */
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') { if (shot.in.layers.length > 1 || lastLayer.status === "blocked") {
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked'; shot.in.status = finalDamage > 0 ? "splash" : "blocked";
shot.damage = finalDamage; shot.damage = finalDamage;
} else { } else {
shot.in.status = 'penetration'; shot.in.status = "penetration";
shot.damage = shell.armor_damage; shot.damage = shell.armor_damage;
} }
} else { } else {
if (lastLayer.status === 'blocked') { if (lastLayer.status === "blocked") {
shot.in.status = 'blocked'; shot.in.status = "blocked";
shot.damage = 0; shot.damage = 0;
} else if (lastLayer.status === 'penetration') { } else if (lastLayer.status === "penetration") {
shot.in.status = 'penetration'; shot.in.status = "penetration";
shot.damage = shell.armor_damage; shot.damage = shell.armor_damage;
} else { } else {
const caster = new Raycaster(); const caster = new Raycaster();
@@ -330,29 +330,29 @@ export function SpacedArmorSceneComponent({
.sub( .sub(
lastLayer.surfaceNormal lastLayer.surfaceNormal
.clone() .clone()
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)), .multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
); );
caster.set(lastLayer.point, ricochetNormal); caster.set(lastLayer.point, ricochetNormal);
const ricochetIntersections = caster.intersectObjects( const ricochetIntersections = caster.intersectObjects(
props.scene.children, props.scene.children,
true, true
); );
const outShot = shoot( const outShot = shoot(
lastLayer.point, lastLayer.point,
ricochetIntersections, ricochetIntersections,
false, false,
remainingPenetration, remainingPenetration
); );
shot.in.status = 'ricochet'; shot.in.status = "ricochet";
if (outShot === null) { if (outShot === null) {
shot.damage = 0; shot.damage = 0;
shot.out = { shot.out = {
surfaceNormal: ricochetNormal, surfaceNormal: ricochetNormal,
layers: [], layers: [],
status: 'ricochet', status: "ricochet",
}; };
} else { } else {
shot.damage = outShot.damage; shot.damage = outShot.damage;
@@ -365,7 +365,7 @@ export function SpacedArmorSceneComponent({
return shot; return shot;
}, },
[camera], [camera]
); );
return ( return (
@@ -390,7 +390,7 @@ export function SpacedArmorSceneComponent({
const shot = shoot(event.point, event.intersections, true)!; const shot = shoot(event.point, event.intersections, true)!;
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = shot; draft.shot = shot;
}); });
}} }}
@@ -1,22 +1,22 @@
import { useThree, type ThreeEvent } from '@react-three/fiber'; import { useThree, type ThreeEvent } from "@react-three/fiber";
import { memo, useRef } from 'react'; import { memo, useRef } from "react";
import { Group, Plane, Vector2, Vector3 } from 'three'; import { Group, Plane, Vector2, Vector3 } from "three";
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from "../../../core/blitz/applyPitchYawLimits";
import { correctZYTuple } from '../../../core/blitz/correctZYTuple'; import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
import { hasEquipment } from '../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../core/blitzkit/hasEquipment";
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform'; import { modelTransformEvent } from "../../../core/blitzkit/modelTransform";
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId'; import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
import { resolveArmor } from '../../../core/blitzkit/resolveThickness'; import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
import { useArmor } from '../../../hooks/useArmor'; import { useArmor } from "../../../hooks/useArmor";
import { useModel } from '../../../hooks/useModel'; import { useModel } from "../../../hooks/useModel";
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
import { useTankTransform } from '../../../hooks/useTankTransform'; import { useTankTransform } from "../../../hooks/useTankTransform";
import { Duel } from '../../../stores/duel'; import { Duel } from "../../../stores/duel";
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../stores/tankopedia";
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
import { ModelTankWrapper } from './ModelTankWrapper'; import { ModelTankWrapper } from "./ModelTankWrapper";
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from "./SpacedArmorScene";
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent'; import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
interface ArmorSceneProps { interface ArmorSceneProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -52,16 +52,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
? undefined ? undefined
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
const showPrimaryArmor = TankopediaPersistent.use( const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor, (state) => state.showPrimaryArmor
); );
const showSpacedArmor = TankopediaPersistent.use( const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor, (state) => state.showSpacedArmor
); );
const showExternalModules = TankopediaPersistent.use( const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules, (state) => state.showExternalModules
); );
const isDynamicArmorActive = Duel.use((state) => const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73), state.protagonist.consumables.includes(73)
); );
useTankTransform(track, turret, turretContainer, gunContainer); useTankTransform(track, turret, turretContainer, gunContainer);
@@ -70,12 +70,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<ModelTankWrapper ref={wrapper}> <ModelTankWrapper ref={wrapper}>
<group position={hullOrigin}> <group position={hullOrigin}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isHull = node.name.startsWith('hull_'); const isHull = node.name.startsWith("hull_");
const isVisible = isHull; const isVisible = isHull;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
tankModelDefinition.armor, tankModelDefinition.armor,
armorId, armorId
); );
if ( if (
@@ -83,8 +83,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
thickness === undefined || thickness === undefined ||
(!showPrimaryArmor && !spaced) || (!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) || (!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) || (isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes('state_01')) (!isDynamicArmorActive && node.name.includes("state_01"))
) )
return null; return null;
@@ -103,8 +103,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
</group> </group>
{modelNodes.map((node) => { {modelNodes.map((node) => {
const isWheel = node.name.startsWith('chassis_wheel_'); const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith('chassis_track_'); const isTrack = node.name.startsWith("chassis_track_");
const isVisible = isWheel || isTrack; const isVisible = isWheel || isTrack;
if (!isVisible || !showExternalModules) return null; if (!isVisible || !showExternalModules) return null;
@@ -126,13 +126,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={turretContainer}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`, `turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
turretModelDefinition.armor, turretModelDefinition.armor,
armorId, armorId
); );
if ( if (
@@ -140,8 +140,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
thickness === undefined || thickness === undefined ||
(!showPrimaryArmor && !spaced) || (!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) || (!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) || (isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes('state_01')) (!isDynamicArmorActive && node.name.includes("state_01"))
) { ) {
return null; return null;
} }
@@ -154,27 +154,27 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
position.set(event.clientX, event.clientY); position.set(event.clientX, event.clientY);
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = false; draft.controlsEnabled = false;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
@@ -188,16 +188,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
return ( return (
@@ -219,13 +219,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<group ref={gunContainer}> <group ref={gunContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentGun = node.name.startsWith( const isCurrentGun = node.name.startsWith(
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`, `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`
); );
const isVisible = isCurrentGun; const isVisible = isCurrentGun;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
const { spaced, thickness } = resolveArmor( const { spaced, thickness } = resolveArmor(
gunModelDefinition.armor, gunModelDefinition.armor,
armorId, armorId
); );
if ( if (
@@ -233,8 +233,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
thickness === undefined || thickness === undefined ||
(!showPrimaryArmor && !spaced) || (!showPrimaryArmor && !spaced) ||
(!showSpacedArmor && spaced) || (!showSpacedArmor && spaced) ||
(isDynamicArmorActive && node.name.includes('state_00')) || (isDynamicArmorActive && node.name.includes("state_00")) ||
(!isDynamicArmorActive && node.name.includes('state_01')) (!isDynamicArmorActive && node.name.includes("state_01"))
) )
return null; return null;
@@ -244,30 +244,30 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
function onPointerDown(event: ThreeEvent<PointerEvent>) { function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = false; draft.controlsEnabled = false;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
position.set(event.clientX, event.clientY); position.set(event.clientX, event.clientY);
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -281,16 +281,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
return ( return (
@@ -312,7 +312,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
{modelNodes.map((node) => { {modelNodes.map((node) => {
const gunString = `gun_${gunModelDefinition.model_id const gunString = `gun_${gunModelDefinition.model_id
.toString() .toString()
.padStart(2, '0')}`; .padStart(2, "0")}`;
const isCurrentGun = gunModelDefinition.mask const isCurrentGun = gunModelDefinition.mask
? node.name.startsWith(gunString) ? node.name.startsWith(gunString)
: node.name === gunString; : node.name === gunString;
@@ -326,29 +326,29 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
function onPointerDown(event: ThreeEvent<PointerEvent>) { function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = false; draft.controlsEnabled = false;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
position.set(event.clientX, event.clientY); position.set(event.clientX, event.clientY);
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -362,17 +362,17 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
return ( return (
@@ -1,8 +1,8 @@
import { I_HAT, J_HAT } from '@blitzkit/core'; import { I_HAT, J_HAT } from "@blitzkit/core";
import { type MeshProps, useThree } from '@react-three/fiber'; import { type MeshProps, useThree } from "@react-three/fiber";
import { clamp } from 'lodash-es'; import { clamp } from "lodash-es";
import type { QuicklimeEvent } from 'quicklime'; import type { QuicklimeEvent } from "quicklime";
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from "react";
import { import {
Box3, Box3,
Color, Color,
@@ -15,28 +15,28 @@ import {
Plane, Plane,
Quaternion, Quaternion,
Vector3, Vector3,
} from 'three'; } from "three";
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector'; import { unrotateDavaVector } from "../../../core/blitz/unrotateDavaVector";
import { jsxTree } from '../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../core/blitzkit/jsxTree";
import { import {
type ModelTransformEventData, type ModelTransformEventData,
modelTransformEvent, modelTransformEvent,
} from '../../../core/blitzkit/modelTransform'; } from "../../../core/blitzkit/modelTransform";
import { discardClippingPlane } from '../../../core/three/discardClippingPlane'; import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../stores/tankopedia";
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from "./SpacedArmorScene";
import type { import type {
ArmorUserData, ArmorUserData,
ExternalModuleVariant, ExternalModuleVariant,
} from './SpacedArmorSceneComponent'; } from "./SpacedArmorSceneComponent";
import type { ThicknessRange } from './StaticArmor'; import type { ThicknessRange } from "./StaticArmor";
type StaticArmorSceneComponentProps = { type StaticArmorSceneComponentProps = {
name: string; name: string;
thickness: number; thickness: number;
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
node: Object3D; node: Object3D;
onPointerDown?: MeshProps['onPointerDown']; onPointerDown?: MeshProps["onPointerDown"];
} & ( } & (
| { | {
type: Exclude<ArmorType, ArmorType.External>; type: Exclude<ArmorType, ArmorType.External>;
@@ -90,7 +90,7 @@ export function StaticArmorSceneComponent({
color = new Color( color = new Color(
clamp(1 - (7 / 8) * xClamped, 0, 1), clamp(1 - (7 / 8) * xClamped, 0, 1),
0, 0,
clamp(1 - (1 / 8) * xClamped, 0, 1), clamp(1 - (1 / 8) * xClamped, 0, 1)
); );
opacity = clamp(x + 1 / 2, 0, 1); opacity = clamp(x + 1 / 2, 0, 1);
break; break;
@@ -113,7 +113,7 @@ export function StaticArmorSceneComponent({
depthWrite, depthWrite,
...(props.clip ? { clippingPlanes: [props.clip] } : {}), ...(props.clip ? { clippingPlanes: [props.clip] } : {}),
}), }),
[thickness], [thickness]
); );
const outlineMaterial = useMemo( const outlineMaterial = useMemo(
() => () =>
@@ -122,7 +122,7 @@ export function StaticArmorSceneComponent({
.clone() .clone()
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1), .multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
}), }),
[thickness], [thickness]
); );
useEffect(() => { useEffect(() => {
@@ -137,11 +137,11 @@ export function StaticArmorSceneComponent({
outlineMaterial.visible = true; outlineMaterial.visible = true;
} else if ( } else if (
selectedName === name || selectedName === name ||
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) || (name.startsWith("chassis_") && selectedName.startsWith("chassis_")) ||
(name.startsWith('gun_') && (name.startsWith("gun_") &&
selectedName.startsWith('gun_') && selectedName.startsWith("gun_") &&
!name.includes('_armor_') && !name.includes("_armor_") &&
!selectedName.includes('_armor_')) !selectedName.includes("_armor_"))
) { ) {
// this selected, stand out! // this selected, stand out!
surfaceMaterial.opacity = 1; surfaceMaterial.opacity = 1;
@@ -164,9 +164,9 @@ export function StaticArmorSceneComponent({
} }
const unsubscribes = [ const unsubscribes = [
TankopediaEphemeral.on( Tankopedia.on(
(state) => state.highlightArmor?.name, (state) => state.highlightArmor?.name,
handleHighlightArmor, handleHighlightArmor
), ),
]; ];
@@ -179,14 +179,14 @@ export function StaticArmorSceneComponent({
const { clip } = props; const { clip } = props;
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone()); const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add( const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
unrotateDavaVector(props.turretOrigin.clone()), unrotateDavaVector(props.turretOrigin.clone())
); );
const barrelOrigin = neckOrigin.clone().add(gunOrigin); const barrelOrigin = neckOrigin.clone().add(gunOrigin);
const distanceToBarrel = clip.distanceToPoint(barrelOrigin); const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
const point = new Vector3(); const point = new Vector3();
function handleModelTransform( function handleModelTransform(
event: QuicklimeEvent<ModelTransformEventData>, event: QuicklimeEvent<ModelTransformEventData>
) { ) {
clip.normal clip.normal
.set(0, 0, -1) .set(0, 0, -1)
@@ -227,7 +227,7 @@ export function StaticArmorSceneComponent({
{ {
type: props.type, type: props.type,
variant: variant:
props.type === ArmorType.External ? props.variant : 'gun', props.type === ArmorType.External ? props.variant : "gun",
thickness, thickness,
} satisfies ArmorUserData } satisfies ArmorUserData
} }
@@ -236,7 +236,7 @@ export function StaticArmorSceneComponent({
event.stopPropagation(); event.stopPropagation();
const { editStatic } = TankopediaEphemeral.state; const { editStatic } = Tankopedia.state;
const bounds = new Box3().setFromObject(event.object); const bounds = new Box3().setFromObject(event.object);
const point = bounds.min const point = bounds.min
@@ -251,13 +251,13 @@ export function StaticArmorSceneComponent({
const surfaceNormal = event const surfaceNormal = event
.normal!.clone() .normal!.clone()
.applyQuaternion( .applyQuaternion(
event.object.getWorldQuaternion(new Quaternion()), event.object.getWorldQuaternion(new Quaternion())
); );
const angle = surfaceNormal.angleTo(cameraNormal); const angle = surfaceNormal.angleTo(cameraNormal);
const thicknessAngled = const thicknessAngled =
thickness / Math.sin(Math.PI / 2 - angle); thickness / Math.sin(Math.PI / 2 - angle);
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = { draft.highlightArmor = {
type: props.type, type: props.type,
name, name,
@@ -5,8 +5,8 @@ import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDe
import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember"; import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
import { Var } from "../../core/radix/var"; import { Var } from "../../core/radix/var";
import { Duel } from "../../stores/duel"; import { Duel } from "../../stores/duel";
import { Guess, GuessState } from "../../stores/guessEphemeral"; import { Guess, GuessState } from "../../stores/guess";
import { TankopediaEphemeral } from "../../stores/tankopediaEphemeral"; import { Tankopedia } from "../../stores/tankopedia";
import "./index.css"; import "./index.css";
const [modelDefinitions, provisionDefinitions] = await Promise.all([ const [modelDefinitions, provisionDefinitions] = await Promise.all([
@@ -23,7 +23,7 @@ export function GuessBackground() {
mutateDuel((state) => { mutateDuel((state) => {
state.protagonist = tankToDuelMember(tank, provisionDefinitions); state.protagonist = tankToDuelMember(tank, provisionDefinitions);
}); });
TankopediaEphemeral.mutate((state) => { Tankopedia.mutate((state) => {
state.model = modelDefinitions.models[tank.id]; state.model = modelDefinitions.models[tank.id];
}); });
}, [tank]); }, [tank]);
@@ -1,4 +1,4 @@
import { Guess, GuessState } from "../stores/guessEphemeral"; import { Guess, GuessState } from "../stores/guess";
import { SmartCanvas } from "./SmartCanvas"; import { SmartCanvas } from "./SmartCanvas";
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control"; import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting"; import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
+1 -1
View File
@@ -24,7 +24,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
import { awaitableTankNames } from "../core/awaitables/tankNames"; import { awaitableTankNames } from "../core/awaitables/tankNames";
import { useLocale } from "../hooks/useLocale"; import { useLocale } from "../hooks/useLocale";
import { Guess, GuessState } from "../stores/guessEphemeral"; import { Guess, GuessState } from "../stores/guess";
import { classIcons } from "./ClassIcon"; import { classIcons } from "./ClassIcon";
import { SearchResults } from "./SearchResults"; import { SearchResults } from "./SearchResults";
@@ -1,17 +1,15 @@
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core'; import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { Flex, SegmentedControl, Text, type FlexProps } from '@radix-ui/themes'; import { Flex, SegmentedControl, Text, type FlexProps } from "@radix-ui/themes";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { import {
TankopediaEphemeral, Tankopedia,
TankopediaRelativeAgainst, TankopediaRelativeAgainst,
} from '../../../../../../stores/tankopediaEphemeral'; } from "../../../../../../stores/tankopedia";
export function CompareOptions(props: FlexProps) { export function CompareOptions(props: FlexProps) {
const relativeAgainst = TankopediaEphemeral.use( const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
(state) => state.relativeAgainst,
);
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const { strings } = useLocale(); const { strings } = useLocale();
@@ -20,7 +18,7 @@ export function CompareOptions(props: FlexProps) {
align="center" align="center"
gap="2" gap="2"
justify="center" justify="center"
direction={{ initial: 'column', sm: 'row' }} direction={{ initial: "column", sm: "row" }}
{...props} {...props}
> >
<Text color="gray"> <Text color="gray">
@@ -32,7 +30,7 @@ export function CompareOptions(props: FlexProps) {
size="1" size="1"
value={`${relativeAgainst}`} value={`${relativeAgainst}`}
onValueChange={(value) => { onValueChange={(value) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst; draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
}); });
}} }}
@@ -43,13 +41,13 @@ export function CompareOptions(props: FlexProps) {
[ [
TIER_ROMAN_NUMERALS[tank.tier], TIER_ROMAN_NUMERALS[tank.tier],
strings.common.tank_class_short_plural_lowercase[tank.class], strings.common.tank_class_short_plural_lowercase[tank.class],
], ]
)} )}
</SegmentedControl.Item> </SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}> <SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
{literals( {literals(
strings.website.tools.tankopedia.characteristics.compare.tier, strings.website.tools.tankopedia.characteristics.compare.tier,
[TIER_ROMAN_NUMERALS[tank.tier]], [TIER_ROMAN_NUMERALS[tank.tier]]
)} )}
</SegmentedControl.Item> </SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}> <SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
@@ -1,10 +1,10 @@
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons'; import { CaretDownIcon, CaretUpIcon } from "@radix-ui/react-icons";
import { Box, Flex, Text } from '@radix-ui/themes'; import { Box, Flex, Text } from "@radix-ui/themes";
import fuzzysort from 'fuzzysort'; import fuzzysort from "fuzzysort";
import { Quicklime } from 'quicklime'; import { Quicklime } from "quicklime";
import { useEffect, useRef, type ReactNode } from 'react'; import { useEffect, useRef, type ReactNode } from "react";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { Mark } from '../../../../../Mark'; import { Mark } from "../../../../../Mark";
export const highlightedRows = new Set<HTMLDivElement>(); export const highlightedRows = new Set<HTMLDivElement>();
export const highlightedRowsUpdate = new Quicklime(); export const highlightedRowsUpdate = new Quicklime();
@@ -16,7 +16,7 @@ export interface InfoProps {
delta?: number; delta?: number;
decimals?: number; decimals?: number;
prefix?: string; prefix?: string;
deltaType?: 'higherIsBetter' | 'lowerIsBetter'; deltaType?: "higherIsBetter" | "lowerIsBetter";
} }
export function Info({ export function Info({
@@ -26,15 +26,15 @@ export function Info({
delta, delta,
decimals, decimals,
prefix, prefix,
deltaType = 'higherIsBetter', deltaType = "higherIsBetter",
}: InfoProps) { }: InfoProps) {
const statSearch = TankopediaEphemeral.use((state) => state.statSearch); const statSearch = Tankopedia.use((state) => state.statSearch);
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
let label = name; let label = name;
let highlighted = false; let highlighted = false;
if (statSearch !== undefined && typeof name === 'string') { if (statSearch !== undefined && typeof name === "string") {
const result = fuzzysort.single(statSearch, name); const result = fuzzysort.single(statSearch, name);
if (result) { if (result) {
@@ -62,7 +62,7 @@ export function Info({
return ( return (
<Flex <Flex
align="center" align="center"
pl={indent ? '2' : '0'} pl={indent ? "2" : "0"}
width="100%" width="100%"
gap="4" gap="4"
ref={container} ref={container}
@@ -77,9 +77,9 @@ export function Info({
<> <>
<Text <Text
color={ color={
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0 delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
? 'green' ? "green"
: 'tomato' : "tomato"
} }
> >
{decimals !== undefined {decimals !== undefined
@@ -88,9 +88,9 @@ export function Info({
</Text> </Text>
<Text <Text
color={ color={
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0 delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
? 'green' ? "green"
: 'tomato' : "tomato"
} }
> >
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />} {delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
@@ -105,7 +105,7 @@ export function Info({
</Text> </Text>
)} )}
{decimals !== undefined && typeof children === 'number' {decimals !== undefined && typeof children === "number"
? children.toFixed(decimals) ? children.toFixed(decimals)
: children} : children}
</Text> </Text>
@@ -1,27 +1,27 @@
import { createDefaultSkills } from '@blitzkit/core'; import { createDefaultSkills } from "@blitzkit/core";
import { Flex, Progress, Text } from '@radix-ui/themes'; import { Flex, Progress, Text } from "@radix-ui/themes";
import { clamp } from 'lodash-es'; import { clamp } from "lodash-es";
import { memo, useMemo, type ComponentProps, type ReactNode } from 'react'; import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions'; import { awaitableEquipmentDefinitions } from "../../../../../../core/awaitables/equipmentDefinitions";
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from "../../../../../../core/awaitables/provisionDefinitions";
import { awaitableSkillDefinitions } from '../../../../../../core/awaitables/skillDefinitions'; import { awaitableSkillDefinitions } from "../../../../../../core/awaitables/skillDefinitions";
import { awaitableTankDefinitions } from '../../../../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from "../../../../../../core/awaitables/tankDefinitions";
import { import {
tankCharacteristics, tankCharacteristics,
type TankCharacteristics, type TankCharacteristics,
} from '../../../../../../core/blitzkit/tankCharacteristics'; } from "../../../../../../core/blitzkit/tankCharacteristics";
import { tankToDuelMember } from '../../../../../../core/blitzkit/tankToDuelMember'; import { tankToDuelMember } from "../../../../../../core/blitzkit/tankToDuelMember";
import { useDelta } from '../../../../../../hooks/useDelta'; import { useDelta } from "../../../../../../hooks/useDelta";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { import {
TankopediaEphemeral, Tankopedia,
TankopediaRelativeAgainst, TankopediaRelativeAgainst,
} from '../../../../../../stores/tankopediaEphemeral'; } from "../../../../../../stores/tankopedia";
import { Info, type InfoProps } from './Info'; import { Info, type InfoProps } from "./Info";
type InfoWithDeltaProps = Omit<InfoProps, 'name'> & { type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
stats: TankCharacteristics; stats: TankCharacteristics;
noRanking?: boolean; noRanking?: boolean;
} & ( } & (
@@ -51,20 +51,18 @@ const [
export const InfoWithDelta = memo<InfoWithDeltaProps>( export const InfoWithDelta = memo<InfoWithDeltaProps>(
({ stats, indent, noRanking, deltaType, ...props }) => { ({ stats, indent, noRanking, deltaType, ...props }) => {
const { strings } = useLocale(); const { strings } = useLocale();
const relativeAgainst = TankopediaEphemeral.use( const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
(state) => state.relativeAgainst,
);
const uhWhatDoICallThisVariable = const uhWhatDoICallThisVariable =
typeof props.value === 'function' typeof props.value === "function"
? props.value(stats)! ? props.value(stats)!
: (stats[props.value] as number); : (stats[props.value] as number);
const delta = useDelta(uhWhatDoICallThisVariable); const delta = useDelta(uhWhatDoICallThisVariable);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
const shellIndex = Duel.use((state) => const shellIndex = Duel.use((state) =>
state.protagonist.gun.shells.indexOf(state.protagonist.shell), state.protagonist.gun.shells.indexOf(state.protagonist.shell)
); );
const equipmentMatrix = Duel.use( const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix
); );
const others = useMemo(() => { const others = useMemo(() => {
const defaultSkills = createDefaultSkills(skillDefinitions); const defaultSkills = createDefaultSkills(skillDefinitions);
@@ -77,7 +75,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
tank.class === protagonistTank.class) || tank.class === protagonistTank.class) ||
(relativeAgainst === TankopediaRelativeAgainst.Tier && (relativeAgainst === TankopediaRelativeAgainst.Tier &&
tank.tier === protagonistTank.tier) || tank.tier === protagonistTank.tier) ||
relativeAgainst === TankopediaRelativeAgainst.All, relativeAgainst === TankopediaRelativeAgainst.All
) )
.map((tank) => { .map((tank) => {
const member = tankToDuelMember(tank, provisionDefinitions); const member = tankToDuelMember(tank, provisionDefinitions);
@@ -112,14 +110,14 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
equipmentDefinitions, equipmentDefinitions,
provisionDefinitions, provisionDefinitions,
tankModelDefinition: modelDefinitions.models[tank.id], tankModelDefinition: modelDefinitions.models[tank.id],
}, }
); );
}) })
.filter((tank) => { .filter((tank) => {
if (tank === undefined) return false; if (tank === undefined) return false;
const othersValue = const othersValue =
typeof props.value === 'function' typeof props.value === "function"
? props.value(tank)! ? props.value(tank)!
: (tank[props.value] as number); : (tank[props.value] as number);
@@ -128,22 +126,22 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
}, [relativeAgainst, shellIndex, equipmentMatrix]); }, [relativeAgainst, shellIndex, equipmentMatrix]);
const betterTanks = others.filter((tank) => { const betterTanks = others.filter((tank) => {
const othersValue = const othersValue =
typeof props.value === 'function' typeof props.value === "function"
? props.value(tank)! ? props.value(tank)!
: (tank[props.value] as number); : (tank[props.value] as number);
if (othersValue === undefined) return false; if (othersValue === undefined) return false;
return deltaType === 'lowerIsBetter' return deltaType === "lowerIsBetter"
? othersValue < uhWhatDoICallThisVariable ? othersValue < uhWhatDoICallThisVariable
: othersValue > uhWhatDoICallThisVariable; : othersValue > uhWhatDoICallThisVariable;
}); });
const goodness = (others.length - betterTanks.length) / others.length; const goodness = (others.length - betterTanks.length) / others.length;
let color: ComponentProps<typeof Progress>['color']; let color: ComponentProps<typeof Progress>["color"];
if (goodness <= 0.25) color = 'red'; if (goodness <= 0.25) color = "red";
else if (goodness <= 0.5) color = 'orange'; else if (goodness <= 0.5) color = "orange";
else if (goodness <= 0.75) color = 'yellow'; else if (goodness <= 0.75) color = "yellow";
else color = 'green'; else color = "green";
return ( return (
<Flex direction="column"> <Flex direction="column">
@@ -151,7 +149,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
deltaType={deltaType} deltaType={deltaType}
indent={indent} indent={indent}
name={ name={
'name' in props "name" in props
? props.name ? props.name
: strings.website.tools.tankopedia.characteristics.values[ : strings.website.tools.tankopedia.characteristics.values[
props.value props.value
@@ -164,17 +162,17 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
</Info> </Info>
{!noRanking && ( {!noRanking && (
<Flex pl={indent ? '2' : '0'} align="center" gap="2"> <Flex pl={indent ? "2" : "0"} align="center" gap="2">
<Progress <Progress
variant="soft" variant="soft"
size="1" size="1"
value={goodness * 100} value={goodness * 100}
color={color} color={color}
style={{ height: '0.125rem', opacity: 0.5 }} style={{ height: "0.125rem", opacity: 0.5 }}
/> />
<Text color="gray" size="1"> <Text color="gray" size="1">
{clamp(betterTanks.length + 1, 1, others.length)} /{' '} {clamp(betterTanks.length + 1, 1, others.length)} /{" "}
{others.length} {others.length}
</Text> </Text>
</Flex> </Flex>
@@ -184,6 +182,6 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
}, },
(a, b) => (a, b) =>
(typeof a.value === 'function' ? a.value(a.stats) : a.stats[a.value]) === (typeof a.value === "function" ? a.value(a.stats) : a.stats[a.value]) ===
(typeof b.value === 'function' ? b.value(b.stats) : b.stats[b.value]), (typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value])
); );
@@ -1,10 +1,10 @@
import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { Flex, Spinner, TextField, type FlexProps } from '@radix-ui/themes'; import { Flex, Spinner, TextField, type FlexProps } from "@radix-ui/themes";
import { debounce } from 'lodash-es'; import { debounce } from "lodash-es";
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from "react";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { highlightedRows, highlightedRowsUpdate } from './Info'; import { highlightedRows, highlightedRowsUpdate } from "./Info";
export function StatSearch(props: FlexProps) { export function StatSearch(props: FlexProps) {
const { strings } = useLocale(); const { strings } = useLocale();
@@ -19,7 +19,7 @@ export function StatSearch(props: FlexProps) {
setSearching(true); setSearching(true);
} else { } else {
setSearching(false); setSearching(false);
count.current.textContent = ''; count.current.textContent = "";
} }
requestUpdate(); requestUpdate();
@@ -30,21 +30,21 @@ export function StatSearch(props: FlexProps) {
if (!input.current || !count.current) return; if (!input.current || !count.current) return;
if (input.current.value.length === 0) { if (input.current.value.length === 0) {
count.current.textContent = ''; count.current.textContent = "";
setSearching(false); setSearching(false);
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.statSearch = undefined; draft.statSearch = undefined;
}); });
return; return;
} }
setSearching(false); setSearching(false);
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.statSearch = input.current!.value; draft.statSearch = input.current!.value;
}); });
}, 500), }, 500),
[], []
); );
useEffect(() => { useEffect(() => {
@@ -52,7 +52,7 @@ export function StatSearch(props: FlexProps) {
if (!count.current || !input.current) return; if (!count.current || !input.current) return;
count.current.textContent = count.current.textContent =
input.current.value.length === 0 ? '' : `0 / ${highlightedRows.size}`; input.current.value.length === 0 ? "" : `0 / ${highlightedRows.size}`;
}; };
highlightedRowsUpdate.on(handleHighlightedRowsUpdate); highlightedRowsUpdate.on(handleHighlightedRowsUpdate);
@@ -68,23 +68,23 @@ export function StatSearch(props: FlexProps) {
ref={input} ref={input}
placeholder={strings.website.tools.tankopedia.search} placeholder={strings.website.tools.tankopedia.search}
variant="classic" variant="classic"
style={{ width: '100%' }} style={{ width: "100%" }}
onChange={search} onChange={search}
onKeyDown={(event) => { onKeyDown={(event) => {
if (!count.current) return; if (!count.current) return;
if (event.key === 'Enter') { if (event.key === "Enter") {
const newIndex = (index + 1) % highlightedRows.size; const newIndex = (index + 1) % highlightedRows.size;
count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`; count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`;
const array = [...highlightedRows]; const array = [...highlightedRows];
array.forEach((element) => element.classList.remove('focused')); array.forEach((element) => element.classList.remove("focused"));
if (array[newIndex]) { if (array[newIndex]) {
array[newIndex].classList.add('focused'); array[newIndex].classList.add("focused");
array[newIndex].scrollIntoView({ array[newIndex].scrollIntoView({
block: 'center', block: "center",
inline: 'center', inline: "center",
}); });
} }
@@ -1,19 +1,19 @@
import { Flex } from '@radix-ui/themes'; import { Flex } from "@radix-ui/themes";
import { useEffect } from 'react'; import { useEffect } from "react";
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions'; import { awaitableEquipmentDefinitions } from "../../../../../core/awaitables/equipmentDefinitions";
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from "../../../../../core/awaitables/provisionDefinitions";
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform'; import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics'; import { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
import { useEquipment } from '../../../../../hooks/useEquipment'; import { useEquipment } from "../../../../../hooks/useEquipment";
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
import { Duel } from '../../../../../stores/duel'; import { Duel } from "../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import { Crew } from './components/Crew'; import { Crew } from "./components/Crew";
import { Firepower } from './components/Firepower'; import { Firepower } from "./components/Firepower";
import { Maneuverability } from './components/Maneuverability'; import { Maneuverability } from "./components/Maneuverability";
import { Survivability } from './components/Survivability'; import { Survivability } from "./components/Survivability";
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
awaitableEquipmentDefinitions, awaitableEquipmentDefinitions,
@@ -21,18 +21,18 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
]); ]);
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) { export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
const crewSkills = TankopediaEphemeral.use((state) => state.skills); const crewSkills = Tankopedia.use((state) => state.skills);
const provisions = Duel.use((state) => state.protagonist.provisions); const provisions = Duel.use((state) => state.protagonist.provisions);
const consumables = Duel.use((state) => state.protagonist.consumables); const consumables = Duel.use((state) => state.protagonist.consumables);
const camouflage = Duel.use((state) => state.protagonist.camouflage); const camouflage = Duel.use((state) => state.protagonist.camouflage);
const assaultDistance = Duel.use( const assaultDistance = Duel.use(
(state) => state.protagonist.assaultDistance, (state) => state.protagonist.assaultDistance
); );
const equipmentMatrix = Duel.use( const equipmentMatrix = Duel.use(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix
); );
const { tank, turret, gun, engine, track, shell } = Duel.use( const { tank, turret, gun, engine, track, shell } = Duel.use(
(state) => state.protagonist, (state) => state.protagonist
); );
const stockEngine = tank.engines[0]; const stockEngine = tank.engines[0];
const stockTrack = tank.tracks[0]; const stockTrack = tank.tracks[0];
@@ -67,7 +67,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
tankModelDefinition, tankModelDefinition,
equipmentDefinitions: equipmentDefinitions, equipmentDefinitions: equipmentDefinitions,
provisionDefinitions: provisionDefinitions, provisionDefinitions: provisionDefinitions,
}, }
); );
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
@@ -80,7 +80,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
@@ -89,25 +89,25 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
return ( return (
<Flex <Flex
gap="8" gap="8"
direction={{ initial: 'column', md: 'row' }} direction={{ initial: "column", md: "row" }}
align={{ initial: 'center', sm: 'start' }} align={{ initial: "center", sm: "start" }}
width={{ initial: '100%', sm: 'auto' }} width={{ initial: "100%", sm: "auto" }}
> >
<Firepower skeleton={skeleton} stats={stats} /> <Firepower skeleton={skeleton} stats={stats} />
<Flex <Flex
gap="8" gap="8"
direction={{ initial: 'column', lg: 'row' }} direction={{ initial: "column", lg: "row" }}
align={{ initial: 'center', sm: 'start' }} align={{ initial: "center", sm: "start" }}
width={{ initial: '100%', sm: 'auto' }} width={{ initial: "100%", sm: "auto" }}
> >
<Maneuverability stats={stats} /> <Maneuverability stats={stats} />
<Flex <Flex
gap="8" gap="8"
direction="column" direction="column"
align={{ initial: 'center', sm: 'start' }} align={{ initial: "center", sm: "start" }}
width={{ initial: '100%', sm: 'auto' }} width={{ initial: "100%", sm: "auto" }}
> >
<Survivability stats={stats} /> <Survivability stats={stats} />
<Crew stats={stats} /> <Crew stats={stats} />
@@ -1,4 +1,4 @@
import { InfoCircledIcon } from '@radix-ui/react-icons'; import { InfoCircledIcon } from "@radix-ui/react-icons";
import { import {
Button, Button,
Flex, Flex,
@@ -6,14 +6,14 @@ import {
IconButton, IconButton,
Popover, Popover,
Text, Text,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../stores/tankopedia";
import { CrewSkillManager } from '../../../CrewSkillManager'; import { CrewSkillManager } from "../../../CrewSkillManager";
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper'; import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
export function Skills() { export function Skills() {
const skillLevels = TankopediaEphemeral.use((state) => state.skills); const skillLevels = Tankopedia.use((state) => state.skills);
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
@@ -45,7 +45,7 @@ export function Skills() {
variant="ghost" variant="ghost"
color="red" color="red"
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
Object.keys(draft.skills).forEach((skill) => { Object.keys(draft.skills).forEach((skill) => {
draft.skills[skill] = 0; draft.skills[skill] = 0;
}); });
@@ -57,7 +57,7 @@ export function Skills() {
<Button <Button
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
Object.keys(draft.skills).forEach((skill) => { Object.keys(draft.skills).forEach((skill) => {
draft.skills[skill] = 7; draft.skills[skill] = 7;
}); });
@@ -71,7 +71,7 @@ export function Skills() {
<CrewSkillManager <CrewSkillManager
skillLevels={skillLevels} skillLevels={skillLevels}
onChange={(skills) => { onChange={(skills) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.skills = skills; draft.skills = skills;
}); });
}} }}
@@ -1,5 +1,5 @@
import { ShellType } from '@blitzkit/core'; import { ShellType } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { import {
Button, Button,
Flex, Flex,
@@ -9,15 +9,15 @@ import {
Slider, Slider,
Text, Text,
TextField, TextField,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { throttle } from 'lodash-es'; import { throttle } from "lodash-es";
import { type ChangeEvent } from 'react'; import { type ChangeEvent } from "react";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
export function CustomShellButton() { export function CustomShellButton() {
const customShell = TankopediaEphemeral.use((state) => state.customShell); const customShell = Tankopedia.use((state) => state.customShell);
const gun = Duel.use((state) => state.protagonist.gun); const gun = Duel.use((state) => state.protagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell); const antagonistShell = Duel.use((state) => state.antagonist.shell);
const { strings } = useLocale(); const { strings } = useLocale();
@@ -27,24 +27,24 @@ export function CustomShellButton() {
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open || customShell) return; if (!open || customShell) return;
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell; draft.customShell = antagonistShell;
}); });
}} }}
> >
<Popover.Trigger> <Popover.Trigger>
<IconButton <IconButton
color={customShell ? undefined : 'gray'} color={customShell ? undefined : "gray"}
variant="soft" variant="soft"
size={{ initial: '2', sm: '3' }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
> >
<img <img
alt="custom shell" alt="custom shell"
src="/assets/images/icons/custom-shell.png" src="/assets/images/icons/custom-shell.png"
style={{ style={{
width: '50%', width: "50%",
height: '50%', height: "50%",
}} }}
/> />
</IconButton> </IconButton>
@@ -90,13 +90,13 @@ export function CustomShellButton() {
modified.ricochet = undefined; modified.ricochet = undefined;
modified.explosion_radius = modified.explosion_radius =
gun.shells.find( gun.shells.find(
(shell) => shell.type === ShellType.HE, (shell) => shell.type === ShellType.HE
)?.explosion_radius ?? 1; )?.explosion_radius ?? 1;
break; break;
} }
} }
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell = modified; draft.customShell = modified;
}); });
}} }}
@@ -129,11 +129,11 @@ export function CustomShellButton() {
</Text> </Text>
<TextField.Root <TextField.Root
variant="classic" variant="classic"
style={{ maxWidth: '6rem' }} style={{ maxWidth: "6rem" }}
defaultValue={customShell.caliber} defaultValue={customShell.caliber}
type="number" type="number"
onChange={(event) => { onChange={(event) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell!.caliber = event.target.valueAsNumber; draft.customShell!.caliber = event.target.valueAsNumber;
}); });
}} }}
@@ -141,9 +141,9 @@ export function CustomShellButton() {
onKeyDown={(event) => { onKeyDown={(event) => {
event.stopPropagation(); event.stopPropagation();
if (event.key === 'Enter') { if (event.key === "Enter") {
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
} else if (event.key === 'Escape') { } else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber = (event.target as HTMLInputElement).valueAsNumber =
customShell.caliber; customShell.caliber;
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
@@ -151,7 +151,7 @@ export function CustomShellButton() {
}} }}
> >
<TextField.Slot side="right"> <TextField.Slot side="right">
{literals(strings.common.units.mm, [''])} {literals(strings.common.units.mm, [""])}
</TextField.Slot> </TextField.Slot>
</TextField.Root> </TextField.Root>
</Flex> </Flex>
@@ -165,11 +165,11 @@ export function CustomShellButton() {
</Text> </Text>
<TextField.Root <TextField.Root
variant="classic" variant="classic"
style={{ maxWidth: '6rem' }} style={{ maxWidth: "6rem" }}
defaultValue={customShell.penetration.near} defaultValue={customShell.penetration.near}
type="number" type="number"
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell!.penetration.near = draft.customShell!.penetration.near =
event.target.valueAsNumber; event.target.valueAsNumber;
}); });
@@ -178,9 +178,9 @@ export function CustomShellButton() {
onKeyDown={(event) => { onKeyDown={(event) => {
event.stopPropagation(); event.stopPropagation();
if (event.key === 'Enter') { if (event.key === "Enter") {
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
} else if (event.key === 'Escape') { } else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber = (event.target as HTMLInputElement).valueAsNumber =
customShell.penetration.near; customShell.penetration.near;
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
@@ -188,7 +188,7 @@ export function CustomShellButton() {
}} }}
> >
<TextField.Slot side="right"> <TextField.Slot side="right">
{literals(strings.common.units.mm, [''])} {literals(strings.common.units.mm, [""])}
</TextField.Slot> </TextField.Slot>
</TextField.Root> </TextField.Root>
</Flex> </Flex>
@@ -223,9 +223,9 @@ export function CustomShellButton() {
step={Number.EPSILON} step={Number.EPSILON}
defaultValue={[customShell.normalization ?? 0]} defaultValue={[customShell.normalization ?? 0]}
onValueChange={([value]) => { onValueChange={([value]) => {
console.log('asd'); console.log("asd");
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell!.normalization = value; draft.customShell!.normalization = value;
}); });
}} }}
@@ -254,7 +254,7 @@ export function CustomShellButton() {
step={Number.EPSILON} step={Number.EPSILON}
defaultValue={[customShell.ricochet ?? 90]} defaultValue={[customShell.ricochet ?? 90]}
onValueChange={throttle(([value]) => { onValueChange={throttle(([value]) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell!.ricochet = value; draft.customShell!.ricochet = value;
}); });
}, 1000)} }, 1000)}
@@ -269,11 +269,11 @@ export function CustomShellButton() {
<Text>Explosion radius</Text> <Text>Explosion radius</Text>
<TextField.Root <TextField.Root
variant="classic" variant="classic"
style={{ maxWidth: '6rem' }} style={{ maxWidth: "6rem" }}
defaultValue={customShell.explosion_radius ?? 0} defaultValue={customShell.explosion_radius ?? 0}
type="number" type="number"
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={(event: ChangeEvent<HTMLInputElement>) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell!.explosion_radius = draft.customShell!.explosion_radius =
event.target.valueAsNumber; event.target.valueAsNumber;
}); });
@@ -282,9 +282,9 @@ export function CustomShellButton() {
onKeyDown={(event) => { onKeyDown={(event) => {
event.stopPropagation(); event.stopPropagation();
if (event.key === 'Enter') { if (event.key === "Enter") {
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
} else if (event.key === 'Escape') { } else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber = (event.target as HTMLInputElement).valueAsNumber =
customShell.explosion_radius ?? 1; customShell.explosion_radius ?? 1;
(event.target as HTMLInputElement).blur(); (event.target as HTMLInputElement).blur();
@@ -309,7 +309,7 @@ export function CustomShellButton() {
<Button <Button
color="red" color="red"
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell; draft.customShell = antagonistShell;
}); });
}} }}
@@ -1,14 +1,14 @@
import { asset } from '@blitzkit/core'; import { asset } from "@blitzkit/core";
import { IconButton } from '@radix-ui/themes'; import { IconButton } from "@radix-ui/themes";
import { useArmor } from '../../../../../../hooks/useArmor'; import { useArmor } from "../../../../../../hooks/useArmor";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
export function DynamicArmorSwitcher() { export function DynamicArmorSwitcher() {
const tank = Duel.use((state) => state.protagonist.tank); const tank = Duel.use((state) => state.protagonist.tank);
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const isDynamicArmorActive = Duel.use((state) => const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73), state.protagonist.consumables.includes(73)
); );
const { hasDynamicArmor } = useArmor(tank.id); const { hasDynamicArmor } = useArmor(tank.id);
@@ -16,9 +16,9 @@ export function DynamicArmorSwitcher() {
return ( return (
<IconButton <IconButton
color={isDynamicArmorActive ? undefined : 'gray'} color={isDynamicArmorActive ? undefined : "gray"}
variant="soft" variant="soft"
size={{ initial: '2', sm: '3' }} size={{ initial: "2", sm: "3" }}
onClick={() => { onClick={() => {
mutateDuel((draft) => { mutateDuel((draft) => {
if (draft.protagonist.consumables.includes(73)) { if (draft.protagonist.consumables.includes(73)) {
@@ -32,17 +32,17 @@ export function DynamicArmorSwitcher() {
} }
} }
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
}); });
}} }}
> >
<img <img
alt="Dynamic Armor" alt="Dynamic Armor"
src={asset('icons/consumables/73.webp')} src={asset("icons/consumables/73.webp")}
style={{ style={{
width: '50%', width: "50%",
height: '50%', height: "50%",
}} }}
/> />
</IconButton> </IconButton>
@@ -1,44 +1,44 @@
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons'; import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from "@radix-ui/react-icons";
import { Button, Checkbox, Flex, Text } from '@radix-ui/themes'; import { Button, Checkbox, Flex, Text } from "@radix-ui/themes";
import { Suspense } from 'react'; import { Suspense } from "react";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor'; import type { ThicknessRange } from "../../../../../Armor/components/StaticArmor";
import { DynamicArmorSwitcher } from './DynamicArmorSwitcher'; import { DynamicArmorSwitcher } from "./DynamicArmorSwitcher";
type ThicknessesProps = MaybeSkeletonComponentProps & { type ThicknessesProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
}; };
export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) { export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
const display = TankopediaEphemeral.use((state) => state.display); const display = Tankopedia.use((state) => state.display);
const showExternalModules = TankopediaPersistent.use( const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules, (state) => state.showExternalModules
); );
const showSpacedArmor = TankopediaPersistent.use( const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor, (state) => state.showSpacedArmor
); );
const showPrimaryArmor = TankopediaPersistent.use( const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor, (state) => state.showPrimaryArmor
); );
const mutateTankopediaPersistent = TankopediaPersistent.useMutation(); const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const editStatic = TankopediaEphemeral.use((state) => state.editStatic); const editStatic = Tankopedia.use((state) => state.editStatic);
const { strings } = useLocale(); const { strings } = useLocale();
return ( return (
<Flex <Flex
position="absolute" position="absolute"
right={display === TankopediaDisplay.StaticArmor ? '0' : '-6rem'} right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"}
top="50%" top="50%"
mr="4" mr="4"
style={{ style={{
transform: 'translateY(-50%)', transform: "translateY(-50%)",
userSelect: 'none', userSelect: "none",
transitionDuration: '200ms', transitionDuration: "200ms",
}} }}
direction="column" direction="column"
gap="5" gap="5"
@@ -48,12 +48,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
<Flex <Flex
height="64px" height="64px"
gap="2" gap="2"
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }} style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.showPrimaryArmor = !draft.showPrimaryArmor; draft.showPrimaryArmor = !draft.showPrimaryArmor;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
}} }}
@@ -68,7 +68,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
{strings.website.tools.tankopedia.sandbox.static.primary} {strings.website.tools.tankopedia.sandbox.static.primary}
</Text> </Text>
<Text color="gray" size="1"> <Text color="gray" size="1">
{literals(strings.common.units.mm, ['0'])} {literals(strings.common.units.mm, ["0"])}
</Text> </Text>
</Flex> </Flex>
@@ -76,10 +76,10 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
align="center" align="center"
width="1rem" width="1rem"
style={{ style={{
borderRadius: 'var(--radius-full)', borderRadius: "var(--radius-full)",
overflow: 'hidden', overflow: "hidden",
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`, background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
color: 'black', color: "black",
}} }}
> >
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />} {showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
@@ -89,12 +89,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
<Flex <Flex
height="64px" height="64px"
gap="2" gap="2"
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }} style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.showSpacedArmor = !draft.showSpacedArmor; draft.showSpacedArmor = !draft.showSpacedArmor;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
}} }}
@@ -109,7 +109,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
{strings.website.tools.tankopedia.sandbox.static.spaced} {strings.website.tools.tankopedia.sandbox.static.spaced}
</Text> </Text>
<Text color="gray" size="1"> <Text color="gray" size="1">
{literals(strings.common.units.mm, ['0'])} {literals(strings.common.units.mm, ["0"])}
</Text> </Text>
</Flex> </Flex>
@@ -117,8 +117,8 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
align="center" align="center"
width="1rem" width="1rem"
style={{ style={{
borderRadius: 'var(--radius-full)', borderRadius: "var(--radius-full)",
overflow: 'hidden', overflow: "hidden",
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`, background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
}} }}
> >
@@ -128,12 +128,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
<Flex <Flex
gap="2" gap="2"
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }} style={{ opacity: showExternalModules ? 1 : 0.5, cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.showExternalModules = !draft.showExternalModules; draft.showExternalModules = !draft.showExternalModules;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
}} }}
@@ -149,10 +149,10 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
width="1rem" width="1rem"
height="1rem" height="1rem"
style={{ style={{
borderRadius: 'var(--radius-full)', borderRadius: "var(--radius-full)",
overflow: 'hidden', overflow: "hidden",
backgroundColor: '#c0c0c0', backgroundColor: "#c0c0c0",
color: 'black', color: "black",
}} }}
> >
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />} {showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
@@ -164,9 +164,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
<Flex align="center" gap="2"> <Flex align="center" gap="2">
<Flex <Flex
gap="2" gap="2"
style={{ cursor: 'pointer' }} style={{ cursor: "pointer" }}
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.editStatic = !draft.editStatic; draft.editStatic = !draft.editStatic;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
@@ -184,9 +184,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
size="1" size="1"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.editStatic = false; draft.editStatic = false;
draft.model = TankopediaEphemeral.initial.model; draft.model = Tankopedia.initial.model;
}); });
}} }}
> >
@@ -3,8 +3,8 @@ import {
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
TIER_ROMAN_NUMERALS, TIER_ROMAN_NUMERALS,
uniqueGuns, uniqueGuns,
} from '@blitzkit/core'; } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { import {
CameraIcon, CameraIcon,
CopyIcon, CopyIcon,
@@ -12,7 +12,7 @@ import {
EnterFullScreenIcon, EnterFullScreenIcon,
ExitFullScreenIcon, ExitFullScreenIcon,
EyeOpenIcon, EyeOpenIcon,
} from '@radix-ui/react-icons'; } from "@radix-ui/react-icons";
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -25,27 +25,27 @@ import {
Tabs, Tabs,
Text, Text,
Tooltip, Tooltip,
} from '@radix-ui/themes'; } from "@radix-ui/themes";
import { Suspense, useState, type RefObject } from 'react'; import { Suspense, useState, type RefObject } from "react";
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose'; import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
import { useEquipment } from '../../../../../hooks/useEquipment'; import { useEquipment } from "../../../../../hooks/useEquipment";
import { useFullScreen } from '../../../../../hooks/useFullScreen'; import { useFullScreen } from "../../../../../hooks/useFullScreen";
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability'; import { useFullscreenAvailability } from "../../../../../hooks/useFullscreenAvailability";
import { useLocale } from '../../../../../hooks/useLocale'; import { useLocale } from "../../../../../hooks/useLocale";
import { Duel } from '../../../../../stores/duel'; import { Duel } from "../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../stores/tankopedia";
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor'; import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton'; import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
import { SmallTankIcon } from '../../../../SmallTankIcon'; import { SmallTankIcon } from "../../../../SmallTankIcon";
import { TankSearch } from '../../../../TankSearch'; import { TankSearch } from "../../../../TankSearch";
import { screenshotReadyEvent } from '../TankSandbox/components/SceneProps'; import { screenshotReadyEvent } from "../TankSandbox/components/SceneProps";
import { CustomShellButton } from './components/CustomShellButton'; import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher'; import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from './components/QuickInputs'; import { QuickInputs } from "./components/QuickInputs";
import { Thicknesses } from './components/Thicknesses'; import { Thicknesses } from "./components/Thicknesses";
type OptionsProps = MaybeSkeletonComponentProps & { type OptionsProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -53,13 +53,13 @@ type OptionsProps = MaybeSkeletonComponentProps & {
}; };
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) { export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = TankopediaEphemeral.use( const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined, (state) => state.customShell !== undefined
); );
const display = TankopediaEphemeral.use((state) => state.display); const display = Tankopedia.use((state) => state.display);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use( const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting, (state) => state.advancedHighlighting
); );
const fullScreenAvailable = useFullscreenAvailability(true); const fullScreenAvailable = useFullscreenAvailability(true);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
@@ -68,14 +68,14 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false); const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
const antagonistTank = Duel.use((state) => state.antagonist.tank); const antagonistTank = Duel.use((state) => state.antagonist.tank);
const hasCalibratedShells = useEquipment(103, true); const hasCalibratedShells = useEquipment(103, true);
const [tab, setTab] = useState('search'); const [tab, setTab] = useState("search");
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const hasEnhancedArmor = useEquipment(110); const hasEnhancedArmor = useEquipment(110);
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets); const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
const mutateTankopediaPersistent = TankopediaPersistent.useMutation(); const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const revealed = TankopediaEphemeral.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = TankopediaEphemeral.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
return ( return (
<> <>
@@ -87,21 +87,21 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
gap="2" gap="2"
direction="column" direction="column"
top="50%" top="50%"
right={display === TankopediaDisplay.DynamicArmor ? '3' : '-4rem'} right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"}
style={{ style={{
position: 'absolute', position: "absolute",
transform: 'translateY(-50%)', transform: "translateY(-50%)",
transitionDuration: '200ms', transitionDuration: "200ms",
}} }}
align="end" align="end"
> >
{!hasCustomShell && ( {!hasCustomShell && (
<Text color="gray" size={{ initial: '1', sm: '2' }}> <Text color="gray" size={{ initial: "1", sm: "2" }}>
{literals(strings.common.units.mm, [ {literals(strings.common.units.mm, [
( (
resolvePenetrationCoefficient( resolvePenetrationCoefficient(
hasCalibratedShells, hasCalibratedShells,
antagonistShell.type, antagonistShell.type
) * antagonistShell.penetration.near ) * antagonistShell.penetration.near
).toFixed(0), ).toFixed(0),
])} ])}
@@ -115,14 +115,14 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
radius="full" radius="full"
variant="soft" variant="soft"
color="gray" color="gray"
size={{ initial: '2', sm: '3' }} size={{ initial: "2", sm: "3" }}
style={{ position: 'relative' }} style={{ position: "relative" }}
> >
<Text <Text
size="1" size="1"
color="gray" color="gray"
style={{ style={{
position: 'absolute', position: "absolute",
right: 0, right: 0,
bottom: 0, bottom: 0,
}} }}
@@ -134,11 +134,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<img <img
alt="Antagonist Gun" alt="Antagonist Gun"
src={asset('icons/modules/gun.webp')} src={asset("icons/modules/gun.webp")}
style={{ style={{
width: '65%', width: "65%",
height: '65%', height: "65%",
objectFit: 'contain', objectFit: "contain",
}} }}
/> />
</IconButton> </IconButton>
@@ -160,7 +160,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
selected={gun.id === antagonistGun.id} selected={gun.id === antagonistGun.id}
discriminator={TIER_ROMAN_NUMERALS[gun.tier]} discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
secondaryDiscriminator={ secondaryDiscriminator={
<Text style={{ fontSize: '0.75em' }}> <Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, [ {literals(strings.common.units.mm, [
gun.shells[0].caliber.toFixed(0), gun.shells[0].caliber.toFixed(0),
])} ])}
@@ -176,7 +176,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex <Flex
direction="column" direction="column"
style={{ style={{
borderRadius: 'var(--radius-full)', borderRadius: "var(--radius-full)",
}} }}
overflow="hidden" overflow="hidden"
> >
@@ -185,17 +185,17 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
color={ color={
thisShell.id === antagonistShell.id && !hasCustomShell thisShell.id === antagonistShell.id && !hasCustomShell
? undefined ? undefined
: 'gray' : "gray"
} }
variant="soft" variant="soft"
key={thisShell.id} key={thisShell.id}
size={{ initial: '2', sm: '3' }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
onClick={() => { onClick={() => {
mutateDuel((draft) => { mutateDuel((draft) => {
draft.antagonist.shell = thisShell; draft.antagonist.shell = thisShell;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.customShell = undefined; draft.customShell = undefined;
}); });
@@ -205,8 +205,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
alt={unwrap(thisShell.name)} alt={unwrap(thisShell.name)}
src={asset(`icons/shells/${thisShell.icon}.webp`)} src={asset(`icons/shells/${thisShell.icon}.webp`)}
style={{ style={{
width: '50%', width: "50%",
height: '50%', height: "50%",
}} }}
/> />
</IconButton> </IconButton>
@@ -218,15 +218,15 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex <Flex
direction="column" direction="column"
style={{ style={{
pointerEvents: 'auto', pointerEvents: "auto",
borderRadius: 'var(--radius-full)', borderRadius: "var(--radius-full)",
}} }}
overflow="hidden" overflow="hidden"
> >
<IconButton <IconButton
color={hasCalibratedShells ? undefined : 'gray'} color={hasCalibratedShells ? undefined : "gray"}
variant="soft" variant="soft"
size={{ initial: '2', sm: '3' }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
onClick={() => { onClick={() => {
mutateDuel((draft) => { mutateDuel((draft) => {
@@ -234,24 +234,24 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
? 0 ? 0
: 1; : 1;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
}); });
}} }}
> >
<img <img
alt="Calibrated Shells" alt="Calibrated Shells"
src={asset('icons/equipment/103.webp')} src={asset("icons/equipment/103.webp")}
style={{ style={{
width: '50%', width: "50%",
height: '50%', height: "50%",
}} }}
/> />
</IconButton> </IconButton>
<IconButton <IconButton
color={hasEnhancedArmor ? undefined : 'gray'} color={hasEnhancedArmor ? undefined : "gray"}
variant="soft" variant="soft"
size={{ initial: '2', sm: '3' }} size={{ initial: "2", sm: "3" }}
radius="none" radius="none"
onClick={() => { onClick={() => {
mutateDuel((draft) => { mutateDuel((draft) => {
@@ -259,17 +259,17 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
? 0 ? 0
: -1; : -1;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
}); });
}} }}
> >
<img <img
alt="Enhanced Armor" alt="Enhanced Armor"
src={asset('icons/equipment/110.webp')} src={asset("icons/equipment/110.webp")}
style={{ style={{
width: '50%', width: "50%",
height: '50%', height: "50%",
}} }}
/> />
</IconButton> </IconButton>
@@ -286,22 +286,22 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
direction="column" direction="column"
align="center" align="center"
position="absolute" position="absolute"
bottom={revealed ? '4' : '-100%'} bottom={revealed ? "4" : "-100%"}
left="50%" left="50%"
style={{ transform: 'translateX(-50%)', transitionDuration: '200ms' }} style={{ transform: "translateX(-50%)", transitionDuration: "200ms" }}
> >
<Flex <Flex
direction="column" direction="column"
align="center" align="center"
style={{ transitionDuration: '200ms' }} style={{ transitionDuration: "200ms" }}
position="relative" position="relative"
bottom={display === TankopediaDisplay.DynamicArmor ? '0' : '-7rem'} bottom={display === TankopediaDisplay.DynamicArmor ? "0" : "-7rem"}
> >
<Flex <Flex
align="center" align="center"
gap="2" gap="2"
mb="1" mb="1"
style={{ cursor: 'pointer' }} style={{ cursor: "pointer" }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.advancedHighlighting = !draft.advancedHighlighting; draft.advancedHighlighting = !draft.advancedHighlighting;
@@ -338,7 +338,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Tabs.Root <Tabs.Root
value={tab} value={tab}
onValueChange={setTab} onValueChange={setTab}
style={{ position: 'relative' }} style={{ position: "relative" }}
> >
<TankSearch <TankSearch
compact compact
@@ -366,7 +366,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
variant="classic" variant="classic"
value={`${disturbed ? display : -1}`} value={`${disturbed ? display : -1}`}
onValueChange={(value) => { onValueChange={(value) => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.disturbed = true; draft.disturbed = true;
draft.display = Number(value); draft.display = Number(value);
}); });
@@ -379,7 +379,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex height="100%" align="center"> <Flex height="100%" align="center">
<img <img
src="/assets/images/icons/tankopedia-model.png" src="/assets/images/icons/tankopedia-model.png"
style={{ height: '1.25em' }} style={{ height: "1.25em" }}
/> />
</Flex> </Flex>
</Tooltip> </Tooltip>
@@ -391,7 +391,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex height="100%" align="center"> <Flex height="100%" align="center">
<img <img
src="/assets/images/icons/tankopedia-dynamic-armor.png" src="/assets/images/icons/tankopedia-dynamic-armor.png"
style={{ height: '1.25em' }} style={{ height: "1.25em" }}
/> />
</Flex> </Flex>
</Tooltip> </Tooltip>
@@ -403,7 +403,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex height="100%" align="center"> <Flex height="100%" align="center">
<img <img
src="/assets/images/icons/tankopedia-static-armor.png" src="/assets/images/icons/tankopedia-static-armor.png"
style={{ height: '1.25em' }} style={{ height: "1.25em" }}
/> />
</Flex> </Flex>
</Tooltip> </Tooltip>
@@ -457,15 +457,15 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!canvas.current) return; if (!canvas.current) return;
const anchor = document.createElement('a'); const anchor = document.createElement("a");
anchor.setAttribute( anchor.setAttribute(
'download', "download",
`${protagonistTank.name}.png`, `${protagonistTank.name}.png`
); );
anchor.setAttribute( anchor.setAttribute(
'href', "href",
canvas.current.toDataURL('image/png'), canvas.current.toDataURL("image/png")
); );
anchor.click(); anchor.click();
@@ -493,7 +493,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
if (!blob) return; if (!blob) return;
navigator.clipboard.write([ navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob }), new ClipboardItem({ "image/png": blob }),
]); ]);
}); });
@@ -1,10 +1,10 @@
import { Box } from '@radix-ui/themes'; import { Box } from "@radix-ui/themes";
import { Var } from '../../../../../core/radix/var'; import { Var } from "../../../../../core/radix/var";
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../stores/tankopedia";
import './index.css'; import "./index.css";
export function ScrollHint() { export function ScrollHint() {
const disturbed = TankopediaEphemeral.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
if (disturbed) return null; if (disturbed) return null;
@@ -17,9 +17,9 @@ export function ScrollHint() {
height="2rem" height="2rem"
width="1rem" width="1rem"
style={{ style={{
borderRadius: '0.5rem', borderRadius: "0.5rem",
outline: `2px solid ${Var('gray-11')}`, outline: `2px solid ${Var("gray-11")}`,
transform: 'translateX(-50%)', transform: "translateX(-50%)",
}} }}
> >
<Box <Box
@@ -28,8 +28,8 @@ export function ScrollHint() {
height="0.75rem" height="0.75rem"
m="0.125rem" m="0.125rem"
style={{ style={{
borderRadius: '50%', borderRadius: "50%",
backgroundColor: Var('gray-12'), backgroundColor: Var("gray-12"),
}} }}
/> />
</Box> </Box>
@@ -1,17 +1,17 @@
import { Flex, Heading } from '@radix-ui/themes'; import { Flex, Heading } from "@radix-ui/themes";
import { useLocale } from '../../../../../../hooks/useLocale'; import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { Tracker } from './components/Tracker'; import { Tracker } from "./components/Tracker";
export function Title() { export function Title() {
const { unwrap } = useLocale(); const { unwrap } = useLocale();
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = TankopediaEphemeral.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const name = unwrap(protagonist.name); const name = unwrap(protagonist.name);
const fontSize = revealed const fontSize = revealed
? revealed ? revealed
? '2rem' ? "2rem"
: `min(16vh, ${125 / name.length}vw)` : `min(16vh, ${125 / name.length}vw)`
: `min(12vh, ${75 / name.length}vw)`; : `min(12vh, ${75 / name.length}vw)`;
@@ -20,7 +20,7 @@ export function Title() {
onPointerDown={(event) => { onPointerDown={(event) => {
event.preventDefault(); event.preventDefault();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.revealed = true; draft.revealed = true;
}); });
}} }}
@@ -28,27 +28,27 @@ export function Title() {
position="absolute" position="absolute"
align="center" align="center"
justify="center" justify="center"
top={revealed ? '6rem' : '50%'} top={revealed ? "6rem" : "50%"}
left="50%" left="50%"
width="100%" width="100%"
height={revealed ? fontSize : '100%'} height={revealed ? fontSize : "100%"}
style={{ style={{
pointerEvents: revealed ? 'none' : undefined, pointerEvents: revealed ? "none" : undefined,
transitionDuration: '1s', transitionDuration: "1s",
transform: 'translate(-50%, -50%)', transform: "translate(-50%, -50%)",
}} }}
> >
<Heading <Heading
style={{ style={{
fontWeight: 900, fontWeight: 900,
userSelect: 'none', userSelect: "none",
pointerEvents: 'none', pointerEvents: "none",
fontSize, fontSize,
whiteSpace: 'nowrap', whiteSpace: "nowrap",
opacity: revealed ? 1 : 0.5, opacity: revealed ? 1 : 0.5,
letterSpacing: revealed || !revealed ? 0 : '-0.03em', letterSpacing: revealed || !revealed ? 0 : "-0.03em",
transition: ` transition: `
letter-spacing 1.5s ${revealed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'}, letter-spacing 1.5s ${revealed ? "" : "cubic-bezier(0.81, -2, 0.68, 1)"},
font-size 1s, font-size 1s,
-webkit-text-stroke 2s, -webkit-text-stroke 2s,
opacity 1s opacity 1s
@@ -1,15 +1,15 @@
import { I_HAT, J_HAT } from '@blitzkit/core'; import { I_HAT, J_HAT } from "@blitzkit/core";
import { OrbitControls } from '@react-three/drei'; import { OrbitControls } from "@react-three/drei";
import { invalidate, useFrame, useThree } from '@react-three/fiber'; import { invalidate, useFrame, useThree } from "@react-three/fiber";
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
import { PerspectiveCamera, Vector3 } from 'three'; import { PerspectiveCamera, Vector3 } from "three";
import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; import { OrbitControls as OrbitControlsClass } from "three-stdlib";
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose'; import { Pose, poseEvent } from "../../../../../../core/blitzkit/pose";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
const poseDistances: Record<Pose, number> = { const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15, [Pose.HullDown]: 15,
@@ -56,23 +56,23 @@ export function Controls({
const protagonistHullOrigin = new Vector3( const protagonistHullOrigin = new Vector3(
protagonistTrackModelDefinition.origin.x, protagonistTrackModelDefinition.origin.x,
protagonistTrackModelDefinition.origin.y, protagonistTrackModelDefinition.origin.y,
-protagonistTrackModelDefinition.origin.z, -protagonistTrackModelDefinition.origin.z
); );
const protagonistTurretOrigin = new Vector3( const protagonistTurretOrigin = new Vector3(
protagonistModelDefinition.turret_origin.x, protagonistModelDefinition.turret_origin.x,
protagonistModelDefinition.turret_origin.y, protagonistModelDefinition.turret_origin.y,
-protagonistModelDefinition.turret_origin.z, -protagonistModelDefinition.turret_origin.z
); );
const protagonistGunOrigin = new Vector3( const protagonistGunOrigin = new Vector3(
protagonistTurretModelDefinition.gun_origin.x, protagonistTurretModelDefinition.gun_origin.x,
protagonistTurretModelDefinition.gun_origin.y, protagonistTurretModelDefinition.gun_origin.y,
-protagonistTurretModelDefinition.gun_origin.z, -protagonistTurretModelDefinition.gun_origin.z
); );
const antagonistGunHeight = const antagonistGunHeight =
protagonistTrackModelDefinition.origin.y + protagonistTrackModelDefinition.origin.y +
antagonistModelDefinition.turret_origin.y + antagonistModelDefinition.turret_origin.y +
antagonistTurretModelDefinition.gun_origin.y; antagonistTurretModelDefinition.gun_origin.y;
const disturbed = TankopediaEphemeral.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const doAutoRotate = autoRotate && !disturbed; const doAutoRotate = autoRotate && !disturbed;
const gunHeight = const gunHeight =
protagonistHullOrigin.y + protagonistHullOrigin.y +
@@ -81,7 +81,7 @@ export function Controls({
const inspectModeInitialPosition = new Vector3( const inspectModeInitialPosition = new Vector3(
-8, -8,
naked ? gunHeight + 10 : 2, naked ? gunHeight + 10 : 2,
-13, -13
); );
const t0 = useRef(Date.now() / 1000); const t0 = useRef(Date.now() / 1000);
@@ -100,11 +100,11 @@ export function Controls({
}); });
useEffect(() => { useEffect(() => {
const unsubscribeTankopediaEphemeral = TankopediaEphemeral.on( const unsubscribeTankopediaEphemeral = Tankopedia.on(
(state) => state.controlsEnabled, (state) => state.controlsEnabled,
(enabled) => { (enabled) => {
if (orbitControls.current) orbitControls.current.enabled = enabled; if (orbitControls.current) orbitControls.current.enabled = enabled;
}, }
); );
function handlePoseEvent(event: Pose) { function handlePoseEvent(event: Pose) {
@@ -112,12 +112,12 @@ export function Controls({
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
switch (event) { switch (event) {
@@ -128,7 +128,7 @@ export function Controls({
protagonistGunModelDefinition.pitch, protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw, protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
camera.position camera.position
@@ -140,14 +140,14 @@ export function Controls({
new Vector3( new Vector3(
0, 0,
poseDistances[event] * Math.sin(pitch), poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch), poseDistances[event] * -Math.cos(pitch)
), )
); );
camera.lookAt( camera.lookAt(
protagonistHullOrigin protagonistHullOrigin
.clone() .clone()
.add(protagonistTurretOrigin) .add(protagonistTurretOrigin)
.add(protagonistGunOrigin), .add(protagonistGunOrigin)
); );
orbitControls.current?.target.set(0, antagonistGunHeight, 0); orbitControls.current?.target.set(0, antagonistGunHeight, 0);
@@ -161,7 +161,7 @@ export function Controls({
protagonistGunModelDefinition.pitch, protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw, protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
camera.position camera.position
@@ -173,8 +173,8 @@ export function Controls({
new Vector3( new Vector3(
0, 0,
poseDistances[event] * Math.sin(pitch), poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch), poseDistances[event] * -Math.cos(pitch)
), )
); );
orbitControls.current?.target orbitControls.current?.target
.set(0, 0, 0) .set(0, 0, 0)
@@ -185,8 +185,8 @@ export function Controls({
new Vector3( new Vector3(
0, 0,
0.5 * poseDistances[event] * Math.sin(pitch), 0.5 * poseDistances[event] * Math.sin(pitch),
0.5 * poseDistances[event] * -Math.cos(pitch), 0.5 * poseDistances[event] * -Math.cos(pitch)
), )
); );
break; break;
@@ -220,15 +220,15 @@ export function Controls({
} }
function disturb() { function disturb() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.disturbed = true; draft.disturbed = true;
}); });
} }
poseEvent.on(disturb); poseEvent.on(disturb);
canvas.addEventListener('pointerdown', disturb); canvas.addEventListener("pointerdown", disturb);
canvas.addEventListener('wheel', handleWheel); canvas.addEventListener("wheel", handleWheel);
document.body.addEventListener('scroll', handleScroll); document.body.addEventListener("scroll", handleScroll);
function updateCamera() { function updateCamera() {
if (!orbitControls.current) return; if (!orbitControls.current) return;
@@ -239,7 +239,7 @@ export function Controls({
orbitControls.current.target.set( orbitControls.current.target.set(
0, 0,
gunHeight * (naked ? 1 / 4 : 3 / 4), gunHeight * (naked ? 1 / 4 : 3 / 4),
0, 0
); );
orbitControls.current.enablePan = true; orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true; orbitControls.current.enableZoom = true;
@@ -251,10 +251,10 @@ export function Controls({
updateCamera(); updateCamera();
return () => { return () => {
canvas.removeEventListener('pointerdown', disturb); canvas.removeEventListener("pointerdown", disturb);
poseEvent.off(disturb); poseEvent.off(disturb);
canvas.removeEventListener('wheel', handleWheel); canvas.removeEventListener("wheel", handleWheel);
document.body.removeEventListener('scroll', handleScroll); document.body.removeEventListener("scroll", handleScroll);
}; };
}, []); }, []);
@@ -266,7 +266,7 @@ export function Controls({
enableZoom={zoomable} enableZoom={zoomable}
zoomSpeed={zoomable ? undefined : 0} zoomSpeed={zoomable ? undefined : 0}
ref={orbitControls} ref={orbitControls}
enabled={TankopediaEphemeral.state.controlsEnabled} enabled={Tankopedia.state.controlsEnabled}
enableDamping={false} enableDamping={false}
autoRotateSpeed={1 / 4} autoRotateSpeed={1 / 4}
/> />
@@ -1,14 +1,14 @@
import { invalidate, useThree } from '@react-three/fiber'; import { invalidate, useThree } from "@react-three/fiber";
import { useEffect } from 'react'; import { useEffect } from "react";
import type { Fog } from 'three'; import type { Fog } from "three";
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from '..'; import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
export function InitialFogReveal() { export function InitialFogReveal() {
const fog = useThree((state) => state.scene.fog as Fog | null); const fog = useThree((state) => state.scene.fog as Fog | null);
useEffect(() => { useEffect(() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.revealed = true; draft.revealed = true;
}); });
@@ -1,17 +1,17 @@
import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber'; import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
import { useRef } from 'react'; import { useRef } from "react";
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three'; import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform'; import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
import { useModel } from '../../../../../../hooks/useModel'; import { useModel } from "../../../../../../hooks/useModel";
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
import { useTankTransform } from '../../../../../../hooks/useTankTransform'; import { useTankTransform } from "../../../../../../hooks/useTankTransform";
import { Duel } from '../../../../../../stores/duel'; import { Duel } from "../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper'; import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
export function TankModel() { export function TankModel() {
const duelStore = Duel.useStore(); const duelStore = Duel.useStore();
@@ -34,9 +34,9 @@ export function TankModel() {
return ( return (
<ModelTankWrapper ref={hullContainer}> <ModelTankWrapper ref={hullContainer}>
{nodes.map((node) => { {nodes.map((node) => {
const isHull = node.name === 'hull'; const isHull = node.name === "hull";
const isWheel = node.name.startsWith('chassis_wheel_'); const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith('chassis_track_'); const isTrack = node.name.startsWith("chassis_track_");
const isVisible = isHull || isWheel || isTrack; const isVisible = isHull || isWheel || isTrack;
const position = new Vector2(); const position = new Vector2();
const delta = new Vector2(); const delta = new Vector2();
@@ -59,19 +59,16 @@ export function TankModel() {
} }
function onPointerDown(event: ThreeEvent<PointerEvent>) { function onPointerDown(event: ThreeEvent<PointerEvent>) {
if ( if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) {
isTrack &&
TankopediaEphemeral.state.display === TankopediaDisplay.Model
) {
position.set(event.clientX, event.clientY); position.set(event.clientX, event.clientY);
event.stopPropagation(); event.stopPropagation();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = false; draft.controlsEnabled = false;
}); });
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} else { } else {
event.stopPropagation(); event.stopPropagation();
} }
@@ -85,12 +82,12 @@ export function TankModel() {
translateTexture(deltaX + deltaY); translateTexture(deltaX + deltaY);
} }
function handlePointerUp() { function handlePointerUp() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
return jsxTree(node, { return jsxTree(node, {
@@ -110,10 +107,10 @@ export function TankModel() {
<group ref={turretContainer}> <group ref={turretContainer}>
{nodes.map((node) => { {nodes.map((node) => {
const isTurret = node.name.startsWith('turret_'); const isTurret = node.name.startsWith("turret_");
const isCurrentTurret = const isCurrentTurret =
node.name === node.name ===
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`; `turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const position = new Vector2(); const position = new Vector2();
const delta = new Vector2(); const delta = new Vector2();
@@ -127,27 +124,27 @@ export function TankModel() {
position.set(event.clientX, event.clientY); position.set(event.clientX, event.clientY);
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = false; draft.controlsEnabled = false;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
@@ -161,16 +158,16 @@ export function TankModel() {
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
return jsxTree(node, { return jsxTree(node, {
@@ -194,10 +191,10 @@ export function TankModel() {
node.name === node.name ===
`gun_${gunModelDefinition.model_id `gun_${gunModelDefinition.model_id
.toString() .toString()
.padStart(2, '0')}_mask`; .padStart(2, "0")}_mask`;
const isCurrentGun = const isCurrentGun =
node.name === node.name ===
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`; `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentGun || isCurrentMantlet; const isVisible = isCurrentGun || isCurrentMantlet;
const position = new Vector2(); const position = new Vector2();
const delta = new Vector2(); const delta = new Vector2();
@@ -210,10 +207,10 @@ export function TankModel() {
function onPointerDown(event: ThreeEvent<PointerEvent>) { function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation(); event.stopPropagation();
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = false; draft.controlsEnabled = false;
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
@@ -222,20 +219,20 @@ export function TankModel() {
pitch = modelTransformEvent.last!.pitch; pitch = modelTransformEvent.last!.pitch;
yaw = modelTransformEvent.last!.yaw; yaw = modelTransformEvent.last!.yaw;
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = hasEquipment( const hasImprovedVerticalStabilizer = hasEquipment(
122, 122,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const hasDownImprovedVerticalStabilizer = hasEquipment( const hasDownImprovedVerticalStabilizer = hasEquipment(
124, 124,
duel.protagonist.tank.equipment_preset, duel.protagonist.tank.equipment_preset,
duel.protagonist.equipmentMatrix, duel.protagonist.equipmentMatrix
); );
const boundingRect = canvas.getBoundingClientRect(); const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position); delta.set(event.clientX, event.clientY).sub(position);
@@ -247,16 +244,16 @@ export function TankModel() {
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
} }
function handlePointerUp() { function handlePointerUp() {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.controlsEnabled = true; draft.controlsEnabled = true;
}); });
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
return jsxTree(node, { return jsxTree(node, {
@@ -1,14 +1,14 @@
import { useRef } from 'react'; import { useRef } from "react";
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from 'three'; import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from "three";
import { jsxTree } from '../../../../../../../core/blitzkit/jsxTree'; import { jsxTree } from "../../../../../../../core/blitzkit/jsxTree";
import { useModel } from '../../../../../../../hooks/useModel'; import { useModel } from "../../../../../../../hooks/useModel";
import { useTankModelDefinition } from '../../../../../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from "../../../../../../../hooks/useTankModelDefinition";
import { useTankTransform } from '../../../../../../../hooks/useTankTransform'; import { useTankTransform } from "../../../../../../../hooks/useTankTransform";
import { Duel } from '../../../../../../../stores/duel'; import { Duel } from "../../../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../../../stores/tankopedia";
import { ModelTankWrapper } from '../../../../../../Armor/components/ModelTankWrapper'; import { ModelTankWrapper } from "../../../../../../Armor/components/ModelTankWrapper";
import fragmentShader from './shaders/fragment.glsl?raw'; import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from './shaders/vertex.glsl?raw'; import vertexShader from "./shaders/vertex.glsl?raw";
const skeletonMaterial = new ShaderMaterial({ const skeletonMaterial = new ShaderMaterial({
vertexShader, vertexShader,
@@ -33,16 +33,16 @@ export function TransitionSkeleton() {
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const { gltf } = useModel(protagonist.tank.id); const { gltf } = useModel(protagonist.tank.id);
const nodes = Object.values(gltf.nodes); const nodes = Object.values(gltf.nodes);
const revealed = TankopediaEphemeral.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
useTankTransform(track, turret, turretContainer, gunContainer); useTankTransform(track, turret, turretContainer, gunContainer);
return ( return (
<ModelTankWrapper ref={hullContainer} visible={revealed}> <ModelTankWrapper ref={hullContainer} visible={revealed}>
{nodes.map((node) => { {nodes.map((node) => {
const isHull = node.name === 'hull'; const isHull = node.name === "hull";
const isWheel = node.name.startsWith('chassis_wheel_'); const isWheel = node.name.startsWith("chassis_wheel_");
const isTrack = node.name.startsWith('chassis_track_'); const isTrack = node.name.startsWith("chassis_track_");
const isVisible = isHull || isWheel || isTrack; const isVisible = isHull || isWheel || isTrack;
if (!isVisible) return null; if (!isVisible) return null;
@@ -58,7 +58,7 @@ export function TransitionSkeleton() {
{nodes.map((node) => { {nodes.map((node) => {
const isCurrentTurret = const isCurrentTurret =
node.name === node.name ===
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`; `turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
if (!isVisible) return null; if (!isVisible) return null;
@@ -76,10 +76,10 @@ export function TransitionSkeleton() {
node.name === node.name ===
`gun_${gunModelDefinition.model_id `gun_${gunModelDefinition.model_id
.toString() .toString()
.padStart(2, '0')}_mask`; .padStart(2, "0")}_mask`;
const isCurrentGun = const isCurrentGun =
node.name === node.name ===
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`; `gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
const isVisible = isCurrentGun || isCurrentMantlet; const isVisible = isCurrentGun || isCurrentMantlet;
if (!isVisible) return null; if (!isVisible) return null;
@@ -1,5 +1,5 @@
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core'; import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
import { invalidate } from '@react-three/fiber'; import { invalidate } from "@react-three/fiber";
import { import {
forwardRef, forwardRef,
Suspense, Suspense,
@@ -7,33 +7,33 @@ import {
useImperativeHandle, useImperativeHandle,
useRef, useRef,
useState, useState,
} from 'react'; } from "react";
import { Fog } from 'three'; import { Fog } from "three";
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform'; import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose'; import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
import { useEquipment } from '../../../../../hooks/useEquipment'; import { useEquipment } from "../../../../../hooks/useEquipment";
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition'; import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
import { Duel } from '../../../../../stores/duel'; import { Duel } from "../../../../../stores/duel";
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../../stores/tankopedia";
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
import { Armor } from '../../../../Armor'; import { Armor } from "../../../../Armor";
import { ArmorPlateDisplay } from '../../../../Armor/components/ArmorPlateDisplay'; import { ArmorPlateDisplay } from "../../../../Armor/components/ArmorPlateDisplay";
import { ShotDisplay } from '../../../../Armor/components/ShotDisplay'; import { ShotDisplay } from "../../../../Armor/components/ShotDisplay";
import { import {
StaticArmor, StaticArmor,
type ThicknessRange, type ThicknessRange,
} from '../../../../Armor/components/StaticArmor'; } from "../../../../Armor/components/StaticArmor";
import { SmartCanvas } from '../../../../SmartCanvas'; import { SmartCanvas } from "../../../../SmartCanvas";
import { AutoClear } from './components/AutoClear'; import { AutoClear } from "./components/AutoClear";
import { Controls } from './components/Control'; import { Controls } from "./components/Control";
import { InitialAligner } from './components/InitialAligner'; import { InitialAligner } from "./components/InitialAligner";
import { InitialFogReveal } from './components/InitialFogReveal'; import { InitialFogReveal } from "./components/InitialFogReveal";
import { Lighting } from './components/Lighting'; import { Lighting } from "./components/Lighting";
import { SceneProps } from './components/SceneProps'; import { SceneProps } from "./components/SceneProps";
import { TankModel } from './components/TankModel'; import { TankModel } from "./components/TankModel";
import { TransitionSkeleton } from './components/TransitionSkeleton'; import { TransitionSkeleton } from "./components/TransitionSkeleton";
interface TankSandboxProps { interface TankSandboxProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -48,7 +48,7 @@ export const fogAnimationTime = 2.5;
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>( export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange, naked }, ref) => { ({ thicknessRange, naked }, ref) => {
const fog = useRef(new Fog('black', forNear1, fogFar1)); const fog = useRef(new Fog("black", forNear1, fogFar1));
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124); const hasDownImprovedVerticalStabilizer = useEquipment(124);
@@ -58,10 +58,10 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[protagonistTurret.id]; tankModelDefinition.turrets[protagonistTurret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id]; const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
const rawDisplay = TankopediaEphemeral.use((state) => state.display); const rawDisplay = Tankopedia.use((state) => state.display);
const [display, setDisplay] = useState(rawDisplay); const [display, setDisplay] = useState(rawDisplay);
const hideTankModelUnderArmor = TankopediaPersistent.use( const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor, (state) => state.hideTankModelUnderArmor
); );
useEffect(() => { useEffect(() => {
@@ -95,8 +95,8 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
useImperativeHandle(ref, () => canvas.current!, []); useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() { function handlePointerDown() {
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
} }
function handlePointerMove(event: PointerEvent) { function handlePointerMove(event: PointerEvent) {
event.preventDefault(); event.preventDefault();
@@ -104,8 +104,8 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
function handlePointerUp(event: PointerEvent) { function handlePointerUp(event: PointerEvent) {
event.preventDefault(); event.preventDefault();
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
} }
useEffect(() => { useEffect(() => {
@@ -118,7 +118,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer, hasDownImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
@@ -132,7 +132,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
@@ -146,7 +146,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
0, 0,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
@@ -168,7 +168,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
modelTransformEvent.last!.yaw, modelTransformEvent.last!.yaw,
gunModelDefinition.pitch, gunModelDefinition.pitch,
turretModelDefinition.yaw, turretModelDefinition.yaw,
hasImprovedVerticalStabilizer, hasImprovedVerticalStabilizer
); );
modelTransformEvent.dispatch({ pitch, yaw }); modelTransformEvent.dispatch({ pitch, yaw });
@@ -176,13 +176,13 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
useEffect(() => { useEffect(() => {
if (display !== TankopediaDisplay.DynamicArmor) { if (display !== TankopediaDisplay.DynamicArmor) {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
}); });
} }
if (display !== TankopediaDisplay.StaticArmor) { if (display !== TankopediaDisplay.StaticArmor) {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
} }
@@ -200,16 +200,16 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
shadows="soft" shadows="soft"
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerMissed={() => { onPointerMissed={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.shot = undefined; draft.shot = undefined;
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
}); });
}} }}
style={{ style={{
userSelect: 'none', userSelect: "none",
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : '100%', width: naked ? BLITZKIT_TANK_ICON_SIZE.width : "100%",
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : '100%', height: naked ? BLITZKIT_TANK_ICON_SIZE.height : "100%",
outline: naked ? '1rem red solid' : undefined, outline: naked ? "1rem red solid" : undefined,
}} }}
> >
{!naked && <SceneProps />} {!naked && <SceneProps />}
@@ -236,5 +236,5 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
<InitialAligner /> <InitialAligner />
</SmartCanvas> </SmartCanvas>
); );
}, }
); );
@@ -1,28 +1,28 @@
import { Box, Flex } from '@radix-ui/themes'; import { Box, Flex } from "@radix-ui/themes";
import { times } from 'lodash-es'; import { times } from "lodash-es";
import { Suspense, useEffect, useMemo, useRef } from 'react'; import { Suspense, useEffect, useMemo, useRef } from "react";
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { useFullScreen } from '../../../hooks/useFullScreen'; import { useFullScreen } from "../../../hooks/useFullScreen";
import { Duel } from '../../../stores/duel'; import { Duel } from "../../../stores/duel";
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from '../../Armor/components/StaticArmor'; import type { ThicknessRange } from "../../Armor/components/StaticArmor";
import { Options } from './components/Options'; import { Options } from "./components/Options";
import { TankSandbox } from './components/TankSandbox'; import { TankSandbox } from "./components/TankSandbox";
import { Title } from './components/TankSandbox/Title'; import { Title } from "./components/TankSandbox/Title";
const tankDefinitions = await awaitableTankDefinitions; const tankDefinitions = await awaitableTankDefinitions;
export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) { export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const revealed = TankopediaEphemeral.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = TankopediaEphemeral.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const protagonist = Duel.use((state) => state.protagonist.tank); const protagonist = Duel.use((state) => state.protagonist.tank);
const thicknessRange = useMemo(() => { const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks); const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter( const filtered = entries.filter(
(thisTank) => thisTank.tier === protagonist.tier, (thisTank) => thisTank.tier === protagonist.tier
); );
const value = const value =
(filtered.reduce((accumulator, thisTank) => { (filtered.reduce((accumulator, thisTank) => {
@@ -41,7 +41,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
useEffect(() => { useEffect(() => {
if (disturbed) { if (disturbed) {
document.body.classList.remove('no-navbar'); document.body.classList.remove("no-navbar");
} }
}, [disturbed]); }, [disturbed]);
@@ -54,15 +54,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
mutateDuel((draft) => { mutateDuel((draft) => {
draft.antagonist.shell = shells[index]; draft.antagonist.shell = shells[index];
}); });
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.customShell = undefined; draft.customShell = undefined;
}); });
} }
}); });
} }
window.addEventListener('keydown', handleKeyDown); window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown);
}, []); }, []);
return ( return (
@@ -70,7 +70,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
overflow="hidden" overflow="hidden"
justify="center" justify="center"
style={{ style={{
backgroundColor: 'black', backgroundColor: "black",
}} }}
position="relative" position="relative"
> >
@@ -82,36 +82,36 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
left="0" left="0"
style={{ style={{
opacity: disturbed ? 0 : 1, opacity: disturbed ? 0 : 1,
background: 'black', background: "black",
transitionDuration: '1s', transitionDuration: "1s",
}} }}
/> />
{/* <ScrollHint /> */} {/* <ScrollHint /> */}
<Flex <Flex
direction={{ initial: 'column-reverse', md: 'row' }} direction={{ initial: "column-reverse", md: "row" }}
style={{ style={{
zIndex: isFullScreen ? 2 : undefined, zIndex: isFullScreen ? 2 : undefined,
transitionDuration: '1s', transitionDuration: "1s",
background: isFullScreen ? 'black' : undefined, background: isFullScreen ? "black" : undefined,
}} }}
height={ height={
isFullScreen isFullScreen
? '100vh' ? "100vh"
: disturbed : disturbed
? 'calc(100svh - 6rem)' ? "calc(100svh - 6rem)"
: { : {
initial: '28rem', initial: "28rem",
md: 'calc(100svh - 12rem)', md: "calc(100svh - 12rem)",
} }
} }
maxWidth={isFullScreen ? undefined : '120rem'} maxWidth={isFullScreen ? undefined : "120rem"}
flexGrow="1" flexGrow="1"
width={isFullScreen ? '100vw' : undefined} width={isFullScreen ? "100vw" : undefined}
position={isFullScreen ? 'fixed' : 'relative'} position={isFullScreen ? "fixed" : "relative"}
top={isFullScreen ? '0' : undefined} top={isFullScreen ? "0" : undefined}
left={isFullScreen ? '0' : undefined} left={isFullScreen ? "0" : undefined}
> >
<Box <Box
className="tank-sandbox-container" className="tank-sandbox-container"
@@ -126,14 +126,14 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
height="100%" height="100%"
position="relative" position="relative"
style={{ style={{
transitionDuration: '1s', transitionDuration: "1s",
}} }}
> >
<Box <Box
style={{ style={{
width: '100%', width: "100%",
height: '100%', height: "100%",
transitionDuration: '2s', transitionDuration: "2s",
opacity: revealed ? 1 : 0, opacity: revealed ? 1 : 0,
filter: disturbed filter: disturbed
? undefined ? undefined
@@ -1,12 +1,12 @@
import { asset, formatCompact, TIER_ROMAN_NUMERALS } from '@blitzkit/core'; import { asset, formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { Box, Flex, Skeleton, Text } from '@radix-ui/themes'; import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions'; import { awaitableAverageDefinitions } from "../../../../core/awaitables/averageDefinitions";
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
import { useLocale } from '../../../../hooks/useLocale'; import { useLocale } from "../../../../hooks/useLocale";
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
import { LinkI18n } from '../../../LinkI18n'; import { LinkI18n } from "../../../LinkI18n";
type NodeProps = MaybeSkeletonComponentProps & { type NodeProps = MaybeSkeletonComponentProps & {
id: number; id: number;
@@ -21,7 +21,7 @@ const [tankDefinitions, averageDefinitions] = await Promise.all([
export function Node({ id, highlight, nextIds, skeleton }: NodeProps) { export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
const { locale, strings, unwrap } = useLocale(); const { locale, strings, unwrap } = useLocale();
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier); const xpMultiplier = Tankopedia.use((state) => state.xpMultiplier);
const tank = tankDefinitions.tanks[id]; const tank = tankDefinitions.tanks[id];
const nextTanks = nextIds?.map((id) => tankDefinitions.tanks[id]); const nextTanks = nextIds?.map((id) => tankDefinitions.tanks[id]);
const thisTankXp = const thisTankXp =
@@ -36,7 +36,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
? 0 ? 0
: (tank.research_cost.research_cost_type!.value as number) / : (tank.research_cost.research_cost_type!.value as number) /
xpMultiplier), xpMultiplier),
0, 0
); );
const averages = averageDefinitions.averages[tank.id]?.mu; const averages = averageDefinitions.averages[tank.id]?.mu;
const averageXp = averages ? averages.xp / averages.battles : undefined; const averageXp = averages ? averages.xp / averages.battles : undefined;
@@ -49,32 +49,32 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
href={`/tanks/${tankDefinitions.tanks[id].slug}`} href={`/tanks/${tankDefinitions.tanks[id].slug}`}
key={id} key={id}
style={{ style={{
textDecoration: 'none', textDecoration: "none",
color: 'inherit', color: "inherit",
position: 'relative', position: "relative",
}} }}
> >
<Box <Box
mx={highlight ? '2' : '0'} mx={highlight ? "2" : "0"}
style={{ style={{
background: highlight background: highlight
? `url(${asset(`flags/fade_small/${tank.nation}.webp`)})` ? `url(${asset(`flags/fade_small/${tank.nation}.webp`)})`
: undefined, : undefined,
backgroundPosition: 'center', backgroundPosition: "center",
backgroundSize: 'cover', backgroundSize: "cover",
boxShadow: highlight ? 'var(--shadow-4)' : undefined, boxShadow: highlight ? "var(--shadow-4)" : undefined,
borderRadius: 'var(--radius-4)', borderRadius: "var(--radius-4)",
overflow: 'hidden', overflow: "hidden",
}} }}
> >
<Flex <Flex
direction="column" direction="column"
align="center" align="center"
py="4" py="4"
px={highlight ? '4' : '2'} px={highlight ? "4" : "2"}
style={{ style={{
backdropFilter: highlight ? 'brightness(0.25)' : undefined, backdropFilter: highlight ? "brightness(0.25)" : undefined,
WebkitBackdropFilter: highlight ? 'brightness(0.25)' : undefined, WebkitBackdropFilter: highlight ? "brightness(0.25)" : undefined,
}} }}
> >
<img <img
@@ -83,7 +83,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
width={64} width={64}
height={64} height={64}
style={{ style={{
objectFit: 'contain', objectFit: "contain",
}} }}
/> />
@@ -99,12 +99,12 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex gap="1" align="center"> <Flex gap="1" align="center">
<img <img
alt="XP" alt="XP"
src={asset('icons/currencies/xp.webp')} src={asset("icons/currencies/xp.webp")}
style={{ style={{
width: '1em', width: "1em",
height: '1em', height: "1em",
objectFit: 'contain', objectFit: "contain",
objectPosition: 'center', objectPosition: "center",
}} }}
/> />
{formatCompact(locale, thisTankXp)} {formatCompact(locale, thisTankXp)}
@@ -114,12 +114,12 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
<Flex gap="1" align="center"> <Flex gap="1" align="center">
<img <img
alt="Silver" alt="Silver"
src={asset('icons/currencies/silver.webp')} src={asset("icons/currencies/silver.webp")}
style={{ style={{
width: '1em', width: "1em",
height: '1em', height: "1em",
objectFit: 'contain', objectFit: "contain",
objectPosition: 'center', objectPosition: "center",
}} }}
/> />
{formatCompact(locale, tank.price.value)} {formatCompact(locale, tank.price.value)}
@@ -135,7 +135,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
? strings.website.tools.tankopedia.tech_tree.battle ? strings.website.tools.tankopedia.tech_tree.battle
: literals( : literals(
strings.website.tools.tankopedia.tech_tree.battles, strings.website.tools.tankopedia.tech_tree.battles,
[`${games}`], [`${games}`]
))} ))}
</Text> </Text>
)} )}
@@ -1,15 +1,15 @@
import { asset, TankType } from '@blitzkit/core'; import { asset, TankType } from "@blitzkit/core";
import { literals } from '@blitzkit/i18n/src/literals'; import { literals } from "@blitzkit/i18n/src/literals";
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons'; import { CaretLeftIcon, CaretRightIcon, PlusIcon } from "@radix-ui/react-icons";
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes'; import { Flex, Heading, IconButton, ScrollArea, Text } from "@radix-ui/themes";
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions'; import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { useLocale } from '../../../hooks/useLocale'; import { useLocale } from "../../../hooks/useLocale";
import { Duel } from '../../../stores/duel'; import { Duel } from "../../../stores/duel";
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { Tankopedia } from "../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps'; import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import { Arrow } from './components/Arrow'; import { Arrow } from "./components/Arrow";
import { Node } from './components/Node'; import { Node } from "./components/Node";
type Line = number[]; type Line = number[];
@@ -20,7 +20,7 @@ export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) { export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
const { locale, strings } = useLocale(); const { locale, strings } = useLocale();
const master = Duel.use((state) => state.protagonist.tank); const master = Duel.use((state) => state.protagonist.tank);
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier); const xpMultiplier = Tankopedia.use((state) => state.xpMultiplier);
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const lines = useMemo(() => { const lines = useMemo(() => {
function extend(line: Line): Line[] { function extend(line: Line): Line[] {
@@ -32,8 +32,8 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
if (root.ancestors.length === 1 || root.tier === 2) { if (root.ancestors.length === 1 || root.tier === 2) {
line.push( line.push(
root.ancestors.find( root.ancestors.find(
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated, (ancestor) => !tankDefinitions.tanks[ancestor].deprecated
) ?? root.ancestors[0], ) ?? root.ancestors[0]
); );
return extend(line); return extend(line);
} else { } else {
@@ -52,7 +52,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
const [lineIndex, setLineIndex] = useState(0); const [lineIndex, setLineIndex] = useState(0);
const line = useMemo( const line = useMemo(
() => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()), () => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()),
[master, lineIndex], [master, lineIndex]
); );
const totalXp = line.reduce( const totalXp = line.reduce(
(xp, id) => (xp, id) =>
@@ -62,11 +62,11 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
? 0 ? 0
: (tankDefinitions.tanks[id].research_cost.research_cost_type! : (tankDefinitions.tanks[id].research_cost.research_cost_type!
.value as number)), .value as number)),
0, 0
); );
const totalCredits = line.reduce( const totalCredits = line.reduce(
(credits, id) => credits + tankDefinitions.tanks[id].price.value, (credits, id) => credits + tankDefinitions.tanks[id].price.value,
0, 0
); );
useEffect(() => { useEffect(() => {
@@ -87,7 +87,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
direction="column" direction="column"
align="center" align="center"
gap="0" gap="0"
style={{ backgroundColor: 'var(--color-surface)' }} style={{ backgroundColor: "var(--color-surface)" }}
py="6" py="6"
> >
<Flex direction="column" align="center"> <Flex direction="column" align="center">
@@ -101,12 +101,12 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex gap="1" align="center"> <Flex gap="1" align="center">
<img <img
alt="XP" alt="XP"
src={asset('icons/currencies/xp.webp')} src={asset("icons/currencies/xp.webp")}
style={{ style={{
width: '1em', width: "1em",
height: '1em', height: "1em",
objectFit: 'contain', objectFit: "contain",
objectPosition: 'center', objectPosition: "center",
}} }}
/> />
{Math.round(totalXp / xpMultiplier).toLocaleString(locale)} {Math.round(totalXp / xpMultiplier).toLocaleString(locale)}
@@ -117,12 +117,12 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex gap="1" align="center"> <Flex gap="1" align="center">
<img <img
alt="Silver" alt="Silver"
src={asset('icons/currencies/silver.webp')} src={asset("icons/currencies/silver.webp")}
style={{ style={{
width: '1em', width: "1em",
height: '1em', height: "1em",
objectFit: 'contain', objectFit: "contain",
objectPosition: 'center', objectPosition: "center",
}} }}
/> />
{totalCredits.toLocaleString(locale)} {totalCredits.toLocaleString(locale)}
@@ -139,15 +139,15 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
<Flex <Flex
key={multiplier} key={multiplier}
style={{ style={{
background: 'url(/assets/images/icons/xp-multiplier.png)', background: "url(/assets/images/icons/xp-multiplier.png)",
backgroundSize: 'contain', backgroundSize: "contain",
backgroundRepeat: 'no-repeat', backgroundRepeat: "no-repeat",
backgroundPosition: 'center', backgroundPosition: "center",
opacity: selected ? 1 : 0.5, opacity: selected ? 1 : 0.5,
cursor: 'pointer', cursor: "pointer",
}} }}
onClick={() => { onClick={() => {
TankopediaEphemeral.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.xpMultiplier = multiplier; draft.xpMultiplier = multiplier;
}); });
}} }}
@@ -1,5 +1,5 @@
import { TankopediaEphemeral } from '../stores/tankopediaEphemeral'; import { Tankopedia } from "../stores/tankopedia";
export function useTankModelDefinition() { export function useTankModelDefinition() {
return TankopediaEphemeral.use((state) => state.model); return Tankopedia.use((state) => state.model);
} }
@@ -13,8 +13,8 @@ import {
useLocale, useLocale,
} from "../../../hooks/useLocale"; } from "../../../hooks/useLocale";
import { Duel } from "../../../stores/duel"; import { Duel } from "../../../stores/duel";
import { Guess, GuessState } from "../../../stores/guessEphemeral"; import { Guess, GuessState } from "../../../stores/guess";
import { TankopediaEphemeral } from "../../../stores/tankopediaEphemeral"; import { Tankopedia } from "../../../stores/tankopedia";
const [tankDefinitions, modelDefinitions, provisionDefinitions] = const [tankDefinitions, modelDefinitions, provisionDefinitions] =
await Promise.all([ await Promise.all([
@@ -42,7 +42,7 @@ function Container() {
const tank = Guess.use((state) => state.tank); const tank = Guess.use((state) => state.tank);
const model = modelDefinitions.models[tank.id]; const model = modelDefinitions.models[tank.id];
TankopediaEphemeral.useInitialization(model); Tankopedia.useInitialization(model);
return ( return (
<Duel.Provider data={{ tank, model, provisionDefinitions }}> <Duel.Provider data={{ tank, model, provisionDefinitions }}>
@@ -17,7 +17,7 @@ import {
} from "../../../../hooks/useLocale"; } from "../../../../hooks/useLocale";
import { App } from "../../../../stores/app"; import { App } from "../../../../stores/app";
import { Duel } from "../../../../stores/duel"; import { Duel } from "../../../../stores/duel";
import { TankopediaEphemeral } from "../../../../stores/tankopediaEphemeral"; import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
import type { TankGuide } from "./index.astro"; import type { TankGuide } from "./index.astro";
@@ -39,7 +39,7 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
const tank = tankDefinitions.tanks[id]; const tank = tankDefinitions.tanks[id];
const model = modelDefinitions.models[id]; const model = modelDefinitions.models[id];
TankopediaEphemeral.useInitialization(model); Tankopedia.useInitialization(model);
return ( return (
<LocaleProvider locale={locale}> <LocaleProvider locale={locale}>
@@ -5,7 +5,7 @@ import { awaitableTankDefinitions } from "../../../../../core/awaitables/tankDef
import { LocaleProvider } from "../../../../../hooks/useLocale"; import { LocaleProvider } from "../../../../../hooks/useLocale";
import { App } from "../../../../../stores/app"; import { App } from "../../../../../stores/app";
import { Duel } from "../../../../../stores/duel"; import { Duel } from "../../../../../stores/duel";
import { TankopediaEphemeral } from "../../../../../stores/tankopediaEphemeral"; import { Tankopedia } from "../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
const [provisionDefinitions, tankDefinitions, modelDefinitions] = const [provisionDefinitions, tankDefinitions, modelDefinitions] =
@@ -23,7 +23,7 @@ export function Page({ id }: PageProps) {
const tank = tankDefinitions.tanks[id]; const tank = tankDefinitions.tanks[id];
const model = modelDefinitions.models[id]; const model = modelDefinitions.models[id];
TankopediaEphemeral.useInitialization(model); Tankopedia.useInitialization(model);
return ( return (
<LocaleProvider locale="en"> <LocaleProvider locale="en">
@@ -2,21 +2,21 @@ import {
createDefaultSkills, createDefaultSkills,
ShellDefinition, ShellDefinition,
type ModelDefinition, type ModelDefinition,
} from '@blitzkit/core'; } from "@blitzkit/core";
import type { Vector3 } from 'three'; import type { Vector3 } from "three";
import { Varuna } from 'varuna'; import { Varuna } from "varuna";
import type { ArmorType } from '../components/Armor/components/SpacedArmorScene'; import type { ArmorType } from "../components/Armor/components/SpacedArmorScene";
import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent'; import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
import type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection'; import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions'; import { awaitableSkillDefinitions } from "../core/awaitables/skillDefinitions";
import { TankopediaDisplay } from './tankopediaPersistent/constants'; import { TankopediaDisplay } from "./tankopediaPersistent/constants";
export interface ShotLayerBase { export interface ShotLayerBase {
index: number; index: number;
thickness: number; thickness: number;
point: Vector3; point: Vector3;
surfaceNormal: Vector3; surfaceNormal: Vector3;
status: 'blocked' | 'penetration' | 'ricochet'; status: "blocked" | "penetration" | "ricochet";
} }
interface ShotLayerExternal extends ShotLayerBase { interface ShotLayerExternal extends ShotLayerBase {
@@ -32,7 +32,7 @@ export interface ShotLayerNonExternal extends ShotLayerBase {
export interface ShotLayerGap { export interface ShotLayerGap {
type: null; type: null;
status: 'penetration' | 'blocked'; status: "penetration" | "blocked";
distance: number; distance: number;
} }
@@ -55,7 +55,7 @@ export type Shot = {
}; };
}; };
export type ShotStatus = 'penetration' | 'blocked' | 'ricochet' | 'splash'; export type ShotStatus = "penetration" | "blocked" | "ricochet" | "splash";
export type ArmorPiercingLayer = export type ArmorPiercingLayer =
| { | {
@@ -63,10 +63,10 @@ export type ArmorPiercingLayer =
nominal: number; nominal: number;
angled: number; angled: number;
ricochet: boolean; ricochet: boolean;
type: 'core' | 'spaced' | 'external'; type: "core" | "spaced" | "external";
} }
| { | {
type: 'gap'; type: "gap";
gap: number; gap: number;
}; };
@@ -76,7 +76,7 @@ export enum TankopediaRelativeAgainst {
All, All,
} }
interface TankopediaEphemeral { interface Tankopedia {
disturbed: boolean; disturbed: boolean;
revealed: boolean; revealed: boolean;
shot?: Shot; shot?: Shot;
@@ -107,10 +107,7 @@ interface TankopediaEphemeral {
const skillDefinitions = await awaitableSkillDefinitions; const skillDefinitions = await awaitableSkillDefinitions;
export const TankopediaEphemeral = new Varuna< export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
TankopediaEphemeral,
ModelDefinition
>((model) => ({
disturbed: false, disturbed: false,
revealed: false, revealed: false,
relativeAgainst: TankopediaRelativeAgainst.Class, relativeAgainst: TankopediaRelativeAgainst.Class,