mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-11 11:46:10 +01:00
Compare commits
6
Commits
f56c31fb48
...
fe4afdcbf0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4afdcbf0 | ||
|
|
ef2a47e756 | ||
|
|
c2d625e1ed | ||
|
|
d33990bdb4 | ||
|
|
96482d4268 | ||
|
|
8b527f2333 |
No files matched your search
@@ -2,10 +2,6 @@ import { produce } from "immer";
|
||||
import { merge } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ProviderProps<Arguments> {
|
||||
args: Arguments;
|
||||
}
|
||||
|
||||
export class Varuna<Type, Arguments = void> {
|
||||
private listeners = new Set<(state: Type) => void>();
|
||||
|
||||
@@ -53,11 +49,15 @@ export class Varuna<Type, Arguments = void> {
|
||||
}
|
||||
|
||||
on<Slice>(slicer: (state: Type) => Slice, callback: (state: Slice) => void) {
|
||||
const last = slicer(this.state);
|
||||
let last = slicer(this.state);
|
||||
|
||||
function listener(state: Type) {
|
||||
const next = slicer(state);
|
||||
if (next !== last) callback(next);
|
||||
|
||||
if (next !== last) {
|
||||
callback(next);
|
||||
last = next;
|
||||
}
|
||||
}
|
||||
|
||||
this.listeners.add(listener);
|
||||
@@ -94,8 +94,11 @@ export class Varuna<Type, Arguments = void> {
|
||||
use<Slice = Type>(
|
||||
slicer: (state: Type) => Slice = (state) => state as unknown as Slice
|
||||
) {
|
||||
const instance = this;
|
||||
const [slice, setSlice] = useState(slicer(this.state));
|
||||
useEffect(() => this.on(slicer, setSlice), []);
|
||||
|
||||
useEffect(() => instance.on(slicer, setSlice), []);
|
||||
|
||||
return slice;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Card,
|
||||
Code,
|
||||
@@ -6,21 +6,19 @@ import {
|
||||
IconButton,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { useRef } from 'react';
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
|
||||
import { App } from '../../../stores/app';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import { layerTypeNames } from './ShotDisplayCard';
|
||||
import { ArmorType } from './SpacedArmorScene';
|
||||
} from "@radix-ui/themes";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { useRef } from "react";
|
||||
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||
import { resolveArmorIndex } from "../../../core/blitzkit/resolveArmorIndex";
|
||||
import { App } from "../../../stores/app";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import { layerTypeNames } from "./ShotDisplayCard";
|
||||
import { ArmorType } from "./SpacedArmorScene";
|
||||
|
||||
export function ArmorPlateDisplay() {
|
||||
const highlightArmor = TankopediaEphemeral.use(
|
||||
(state) => state.highlightArmor,
|
||||
);
|
||||
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
|
||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||
const duelStore = Duel.useStore();
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
@@ -31,20 +29,20 @@ export function ArmorPlateDisplay() {
|
||||
<group position={highlightArmor.point}>
|
||||
<Html
|
||||
center
|
||||
style={{ pointerEvents: highlightArmor.editingPlate ? 'auto' : 'none' }}
|
||||
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
|
||||
>
|
||||
<Card
|
||||
variant="classic"
|
||||
mb="4"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
whiteSpace: "nowrap",
|
||||
color: highlightArmor.color,
|
||||
transform: 'translateY(-50%)',
|
||||
transform: "translateY(-50%)",
|
||||
}}
|
||||
>
|
||||
<Flex direction="column">
|
||||
<Text weight="bold">
|
||||
{layerTypeNames[highlightArmor.type]}{' '}
|
||||
{layerTypeNames[highlightArmor.type]}{" "}
|
||||
{highlightArmor.thickness.toFixed(0)}
|
||||
<Text size="1" weight="regular">
|
||||
mm
|
||||
@@ -90,10 +88,10 @@ export function ArmorPlateDisplay() {
|
||||
|
||||
if (isNaN(thickness) || thickness < 0) return;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
input.current?.blur();
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
const { protagonist } = duelStore.getState();
|
||||
const tank = draft.model;
|
||||
@@ -101,23 +99,23 @@ export function ArmorPlateDisplay() {
|
||||
const gun = turret.guns[protagonist.gun.id];
|
||||
const track = tank.tracks[protagonist.track.id];
|
||||
|
||||
if (highlightArmor.name.startsWith('hull_')) {
|
||||
if (highlightArmor.name.startsWith("hull_")) {
|
||||
tank.armor.thickness[index] = thickness;
|
||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
||||
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||
turret.armor.thickness[index] = thickness;
|
||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
||||
if (highlightArmor.name.includes('_armor_')) {
|
||||
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||
if (highlightArmor.name.includes("_armor_")) {
|
||||
gun.armor.thickness[index] = thickness;
|
||||
} else {
|
||||
gun.thickness = thickness;
|
||||
}
|
||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
||||
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||
track.thickness = thickness;
|
||||
}
|
||||
});
|
||||
} else if (event.key === 'Escape') {
|
||||
} else if (event.key === "Escape") {
|
||||
input.current?.blur();
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}
|
||||
@@ -135,7 +133,7 @@ export function ArmorPlateDisplay() {
|
||||
mt="2"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
if (!highlightArmor.editingPlate) return;
|
||||
|
||||
const index = resolveArmorIndex(highlightArmor.name);
|
||||
@@ -143,7 +141,7 @@ export function ArmorPlateDisplay() {
|
||||
if (index === undefined) return;
|
||||
|
||||
draft.highlightArmor = undefined;
|
||||
const initialState = TankopediaEphemeral.initial;
|
||||
const initialState = Tankopedia.initial;
|
||||
const { protagonist } = duelStore.getState();
|
||||
const tank = draft.model;
|
||||
const initialTank = initialState.model;
|
||||
@@ -156,20 +154,20 @@ export function ArmorPlateDisplay() {
|
||||
const initialTrack =
|
||||
initialTank.tracks[protagonist.track.id];
|
||||
|
||||
if (highlightArmor.name.startsWith('hull_')) {
|
||||
if (highlightArmor.name.startsWith("hull_")) {
|
||||
tank.armor.thickness[index] =
|
||||
initialTank.armor.thickness[index];
|
||||
} else if (highlightArmor.name.startsWith('turret_')) {
|
||||
} else if (highlightArmor.name.startsWith("turret_")) {
|
||||
turret.armor.thickness[index] =
|
||||
initialTurret.armor.thickness[index];
|
||||
} else if (highlightArmor.name.startsWith('gun_')) {
|
||||
if (highlightArmor.name.includes('_armor_')) {
|
||||
} else if (highlightArmor.name.startsWith("gun_")) {
|
||||
if (highlightArmor.name.includes("_armor_")) {
|
||||
gun.armor.thickness[index] =
|
||||
initialGun.armor.thickness[index];
|
||||
} else {
|
||||
gun.thickness = initialGun.thickness;
|
||||
}
|
||||
} else if (highlightArmor.name.startsWith('chassis_')) {
|
||||
} else if (highlightArmor.name.startsWith("chassis_")) {
|
||||
track.thickness = initialTrack.thickness;
|
||||
}
|
||||
});
|
||||
|
||||
+30
-30
@@ -2,9 +2,9 @@ import {
|
||||
canSplash,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { invalidate, useFrame } from '@react-three/fiber';
|
||||
import { useEffect } from 'react';
|
||||
} from "@blitzkit/core";
|
||||
import { invalidate, useFrame } from "@react-three/fiber";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
MeshBasicMaterial,
|
||||
NormalBlending,
|
||||
@@ -14,16 +14,16 @@ import {
|
||||
UniformsLib,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
} from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
||||
import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||
import vertexShader from './shaders/vertex.glsl?raw';
|
||||
import { spacedArmorRenderTarget } from './target';
|
||||
} from "three";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||
import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||
import { spacedArmorRenderTarget } from "./target";
|
||||
|
||||
interface PrimaryArmorSceneComponentProps {
|
||||
thickness: number;
|
||||
@@ -81,15 +81,15 @@ export function PrimaryArmorSceneComponent({
|
||||
useEffect(() => {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const tankopediaEphemeral = Tankopedia.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
|
||||
material.uniforms.caliber.value = shell.caliber;
|
||||
material.uniforms.ricochet.value = degToRad(
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||
);
|
||||
material.uniforms.normalization.value = degToRad(
|
||||
shell.normalization ?? 0,
|
||||
shell.normalization ?? 0
|
||||
);
|
||||
material.uniforms.isExplosive.value = isExplosive(shell.type);
|
||||
material.uniforms.canSplash.value = canSplash(shell.type);
|
||||
@@ -116,13 +116,13 @@ export function PrimaryArmorSceneComponent({
|
||||
}
|
||||
function handleProtagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
noInvalidate = false,
|
||||
noInvalidate = false
|
||||
) {
|
||||
const duel = duelStore.getState();
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
equipment
|
||||
);
|
||||
material.uniforms.thickness.value = hasEnhancedArmor
|
||||
? thickness * 1.03
|
||||
@@ -132,16 +132,16 @@ export function PrimaryArmorSceneComponent({
|
||||
}
|
||||
function handleAntagonistEquipmentChange(
|
||||
equipment: EquipmentMatrix,
|
||||
noInvalidate = false,
|
||||
noInvalidate = false
|
||||
) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const tankopediaEphemeral = Tankopedia.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
equipment
|
||||
);
|
||||
|
||||
material.uniforms.penetration.value =
|
||||
@@ -159,34 +159,34 @@ export function PrimaryArmorSceneComponent({
|
||||
handleOpaqueChange(initialState.opaque);
|
||||
handleWireframeChange(initialState.wireframe);
|
||||
handleProtagonistEquipmentChange(
|
||||
duelStore.getState().protagonist.equipmentMatrix,
|
||||
duelStore.getState().protagonist.equipmentMatrix
|
||||
);
|
||||
handleAntagonistEquipmentChange(
|
||||
duelStore.getState().antagonist.equipmentMatrix,
|
||||
duelStore.getState().antagonist.equipmentMatrix
|
||||
);
|
||||
|
||||
const unsubscribes = [
|
||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||
tankopediaPersistentStore.subscribe(
|
||||
(state) => state.greenPenetration,
|
||||
handleGreenPenetrationChange,
|
||||
handleGreenPenetrationChange
|
||||
),
|
||||
tankopediaPersistentStore.subscribe(
|
||||
(state) => state.advancedHighlighting,
|
||||
handleAdvancedHighlightingChange,
|
||||
handleAdvancedHighlightingChange
|
||||
),
|
||||
tankopediaPersistentStore.subscribe(
|
||||
(state) => state.opaque,
|
||||
handleOpaqueChange,
|
||||
handleOpaqueChange
|
||||
),
|
||||
duelStore.subscribe(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
(equipment) => handleProtagonistEquipmentChange(equipment),
|
||||
(equipment) => handleProtagonistEquipmentChange(equipment)
|
||||
),
|
||||
duelStore.subscribe(
|
||||
(state) => state.antagonist.equipmentMatrix,
|
||||
(equipment) => handleAntagonistEquipmentChange(equipment),
|
||||
(equipment) => handleAntagonistEquipmentChange(equipment)
|
||||
),
|
||||
];
|
||||
|
||||
@@ -197,7 +197,7 @@ export function PrimaryArmorSceneComponent({
|
||||
|
||||
useFrame(({ gl, camera }) => {
|
||||
gl.getSize(material.uniforms.resolution.value).multiplyScalar(
|
||||
gl.getPixelRatio(),
|
||||
gl.getPixelRatio()
|
||||
);
|
||||
material.uniforms.spacedArmorBuffer.value = spacedArmorRenderTarget.texture;
|
||||
material.uniforms.spacedArmorDepth.value =
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { J_HAT, K_HAT } from '@blitzkit/core';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { invalidate, useFrame } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { useEffect, useRef, type ComponentProps } from 'react';
|
||||
import { J_HAT, K_HAT } from "@blitzkit/core";
|
||||
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { invalidate, useFrame } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import { useEffect, useRef, type ComponentProps } from "react";
|
||||
import {
|
||||
BufferGeometry,
|
||||
DoubleSide,
|
||||
@@ -15,24 +15,24 @@ import {
|
||||
MeshBasicMaterial,
|
||||
Path,
|
||||
Quaternion,
|
||||
} from 'three';
|
||||
import { isHalloween } from '../../../core/blitzkit/isHalloween';
|
||||
import { LocaleProvider, useLocale } from '../../../hooks/useLocale';
|
||||
} from "three";
|
||||
import { isHalloween } from "../../../core/blitzkit/isHalloween";
|
||||
import { LocaleProvider, useLocale } from "../../../hooks/useLocale";
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
Tankopedia,
|
||||
type ShotLayerNonExternal,
|
||||
type ShotStatus,
|
||||
} from '../../../stores/tankopediaEphemeral';
|
||||
import { ShotDisplayCard } from './ShotDisplayCard';
|
||||
} from "../../../stores/tankopedia";
|
||||
import { ShotDisplayCard } from "./ShotDisplayCard";
|
||||
|
||||
export const shotStatusColors: Record<
|
||||
ShotStatus,
|
||||
ComponentProps<typeof Text>['color']
|
||||
ComponentProps<typeof Text>["color"]
|
||||
> = {
|
||||
blocked: 'red',
|
||||
penetration: 'green',
|
||||
ricochet: 'yellow',
|
||||
splash: 'orange',
|
||||
blocked: "red",
|
||||
penetration: "green",
|
||||
ricochet: "yellow",
|
||||
splash: "orange",
|
||||
};
|
||||
|
||||
const SURFACE_POINT_SIZE = 12;
|
||||
@@ -41,7 +41,7 @@ const TRACER_THICK = 1 / 32;
|
||||
const TRACER_THIN = TRACER_THICK / 2;
|
||||
|
||||
export function ShotDisplay() {
|
||||
const shot = TankopediaEphemeral.use((state) => state.shot);
|
||||
const shot = Tankopedia.use((state) => state.shot);
|
||||
const inTracer = useRef<Mesh>(null);
|
||||
const outTracer = useRef<Mesh>(null);
|
||||
const splashRadiusWrapper = useRef<Group>(null);
|
||||
@@ -52,16 +52,16 @@ export function ShotDisplay() {
|
||||
useEffect(() => {
|
||||
if (shot?.splashRadius !== undefined) {
|
||||
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({
|
||||
color: 'orange',
|
||||
color: "orange",
|
||||
dashSize: 0.05,
|
||||
gapSize: 0.025,
|
||||
});
|
||||
const outline = new Line(
|
||||
outlineGeometry,
|
||||
outlineMaterial,
|
||||
outlineMaterial
|
||||
).computeLineDistances();
|
||||
|
||||
splashRadiusWrapper.current?.add(outline);
|
||||
@@ -76,9 +76,9 @@ export function ShotDisplay() {
|
||||
if (
|
||||
isHalloween() &&
|
||||
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.volume = 0.25;
|
||||
@@ -134,11 +134,11 @@ export function ShotDisplay() {
|
||||
const outLength =
|
||||
shot.out && shot.out.layers.length > 0
|
||||
? (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;
|
||||
const inLast = shot.in.layers.findLast(
|
||||
(layer) => layer.type !== null,
|
||||
(layer) => layer.type !== null
|
||||
) as ShotLayerNonExternal;
|
||||
const outTitleColor = shot.out
|
||||
? shotStatusColors[shot.out.status]
|
||||
@@ -154,7 +154,7 @@ export function ShotDisplay() {
|
||||
position={inLast.point}
|
||||
ref={splashRadiusWrapper}
|
||||
rotation={new Euler().setFromQuaternion(
|
||||
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
|
||||
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
|
||||
)}
|
||||
>
|
||||
<mesh>
|
||||
@@ -172,13 +172,13 @@ export function ShotDisplay() {
|
||||
<Html position={inLast.point} center>
|
||||
<Box
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
pointerEvents: "none",
|
||||
transform: `translateY(${tracerGoingUp ? 50 : -50}%)`,
|
||||
}}
|
||||
mb={tracerGoingUp ? '0' : '9'}
|
||||
pb={tracerGoingUp ? '0' : '4'}
|
||||
mt={tracerGoingUp ? '9' : '0'}
|
||||
pt={tracerGoingUp ? '4' : '0'}
|
||||
mb={tracerGoingUp ? "0" : "9"}
|
||||
pb={tracerGoingUp ? "0" : "4"}
|
||||
mt={tracerGoingUp ? "9" : "0"}
|
||||
pt={tracerGoingUp ? "4" : "0"}
|
||||
>
|
||||
<LocaleProvider locale={locale}>
|
||||
<ShotDisplayCard shot={shot} />
|
||||
@@ -190,7 +190,7 @@ export function ShotDisplay() {
|
||||
if (layer.type === null) return null;
|
||||
|
||||
const shotStatusColor =
|
||||
shot.in.status === 'splash'
|
||||
shot.in.status === "splash"
|
||||
? shotStatusColors.splash
|
||||
: shotStatusColors[layer.status];
|
||||
|
||||
@@ -199,8 +199,8 @@ export function ShotDisplay() {
|
||||
position={layer.point}
|
||||
key={index}
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
transform: 'translateY(-50%)',
|
||||
pointerEvents: "none",
|
||||
transform: "translateY(-50%)",
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
@@ -225,7 +225,7 @@ export function ShotDisplay() {
|
||||
<group
|
||||
position={inLast.point}
|
||||
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}>
|
||||
@@ -253,8 +253,8 @@ export function ShotDisplay() {
|
||||
rotation={new Euler().setFromQuaternion(
|
||||
new Quaternion().setFromUnitVectors(
|
||||
J_HAT,
|
||||
shot.out.surfaceNormal.clone(),
|
||||
),
|
||||
shot.out.surfaceNormal.clone()
|
||||
)
|
||||
)}
|
||||
>
|
||||
<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 {
|
||||
Card,
|
||||
Flex,
|
||||
@@ -6,19 +6,19 @@ import {
|
||||
Separator,
|
||||
Text,
|
||||
type CardProps,
|
||||
} from '@radix-ui/themes';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import type { Shot, ShotLayer } from '../../../stores/tankopediaEphemeral';
|
||||
import { shotStatusColors } from './ShotDisplay';
|
||||
import { ArmorType } from './SpacedArmorScene';
|
||||
} from "@radix-ui/themes";
|
||||
import type { ComponentProps } from "react";
|
||||
import { radToDeg } from "three/src/math/MathUtils.js";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import type { Shot, ShotLayer } from "../../../stores/tankopedia";
|
||||
import { shotStatusColors } from "./ShotDisplay";
|
||||
import { ArmorType } from "./SpacedArmorScene";
|
||||
|
||||
export const layerTypeNames = {
|
||||
[ArmorType.Primary]: 'primary',
|
||||
[ArmorType.Spaced]: 'spaced',
|
||||
[ArmorType.External]: 'external',
|
||||
null: 'gap',
|
||||
[ArmorType.Primary]: "primary",
|
||||
[ArmorType.Spaced]: "spaced",
|
||||
[ArmorType.External]: "external",
|
||||
null: "gap",
|
||||
} as const;
|
||||
|
||||
function LayerEntry({
|
||||
@@ -29,7 +29,7 @@ function LayerEntry({
|
||||
angle,
|
||||
}: {
|
||||
layerIndex?: string;
|
||||
shotStatusColor: ComponentProps<typeof Text>['color'];
|
||||
shotStatusColor: ComponentProps<typeof Text>["color"];
|
||||
layerName: string;
|
||||
layer: ShotLayer;
|
||||
angle?: number;
|
||||
@@ -77,7 +77,7 @@ function LayerEntry({
|
||||
[
|
||||
Math.round(layer.thicknessAngled).toLocaleString(locale),
|
||||
radToDeg(angle ?? 0).toFixed(0),
|
||||
],
|
||||
]
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
@@ -89,7 +89,7 @@ function LayerEntry({
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||
.ricochet_loss,
|
||||
[Math.max(-100, -50 * layer.distance).toFixed(0)],
|
||||
[Math.max(-100, -50 * layer.distance).toFixed(0)]
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
@@ -100,7 +100,7 @@ function LayerEntry({
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
|
||||
.nominal,
|
||||
[Math.round(layer.thickness).toLocaleString(locale)],
|
||||
[Math.round(layer.thickness).toLocaleString(locale)]
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
@@ -132,14 +132,14 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
||||
shot.in.status
|
||||
],
|
||||
[Math.round(shot.damage).toLocaleString(locale)],
|
||||
[Math.round(shot.damage).toLocaleString(locale)]
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<table
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
borderSpacing: 'var(--space-1) 0',
|
||||
whiteSpace: "nowrap",
|
||||
borderSpacing: "var(--space-1) 0",
|
||||
}}
|
||||
>
|
||||
{shot.in.layers.map((layer, index) => {
|
||||
@@ -155,7 +155,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
? undefined
|
||||
: `${(shot.containsGaps ? index / 2 : index) + 1}.`;
|
||||
const shotStatusColor =
|
||||
shot.in.status === 'splash'
|
||||
shot.in.status === "splash"
|
||||
? shotStatusColors.splash
|
||||
: shotStatusColors[layer.status];
|
||||
|
||||
@@ -202,7 +202,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.status[shot.out.status],
|
||||
[Math.round(shot.damage).toLocaleString(locale)],
|
||||
[Math.round(shot.damage).toLocaleString(locale)]
|
||||
)}
|
||||
</Text>
|
||||
|
||||
@@ -224,7 +224,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
1
|
||||
}.`;
|
||||
const shotStatusColor =
|
||||
shot.out!.status === 'splash'
|
||||
shot.out!.status === "splash"
|
||||
? shotStatusColors.splash
|
||||
: shotStatusColors[layer.status];
|
||||
|
||||
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
import { resolvePenetrationCoefficient } from '@blitzkit/core';
|
||||
import { useEffect } from 'react';
|
||||
import { resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
AdditiveBlending,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
Plane,
|
||||
ShaderMaterial,
|
||||
} from 'three';
|
||||
import type { ArmorUserData, ExternalModuleVariant } from '../..';
|
||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { ArmorType } from '../../../SpacedArmorScene';
|
||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||
import vertexShader from './shaders/vertex.glsl?raw';
|
||||
} from "three";
|
||||
import type { ArmorUserData, ExternalModuleVariant } from "../..";
|
||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { ArmorType } from "../../../SpacedArmorScene";
|
||||
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||
|
||||
interface SpacedArmorSubExternalProps {
|
||||
node: Object3D;
|
||||
@@ -49,7 +49,7 @@ export function SpacedArmorSubExternal({
|
||||
useEffect(() => {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const tankopediaEphemeral = Tankopedia.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
material.uniforms.penetration.value = shell.penetration.near;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export function SpacedArmorSubExternal({
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
equipment
|
||||
);
|
||||
material.uniforms.thickness.value = hasEnhancedArmor
|
||||
? thickness * 1.03
|
||||
@@ -66,13 +66,13 @@ export function SpacedArmorSubExternal({
|
||||
}
|
||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const tankopediaEphemeral = Tankopedia.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
equipment
|
||||
);
|
||||
|
||||
material.uniforms.penetration.value =
|
||||
@@ -82,22 +82,22 @@ export function SpacedArmorSubExternal({
|
||||
|
||||
handleShellChange();
|
||||
handleProtagonistEquipmentChange(
|
||||
duelStore.getState().protagonist.equipmentMatrix,
|
||||
duelStore.getState().protagonist.equipmentMatrix
|
||||
);
|
||||
handleAntagonistEquipmentChange(
|
||||
duelStore.getState().antagonist.equipmentMatrix,
|
||||
duelStore.getState().antagonist.equipmentMatrix
|
||||
);
|
||||
|
||||
const unsubscribes = [
|
||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||
duelStore.subscribe(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
handleProtagonistEquipmentChange,
|
||||
handleProtagonistEquipmentChange
|
||||
),
|
||||
duelStore.subscribe(
|
||||
(state) => state.antagonist.equipmentMatrix,
|
||||
handleAntagonistEquipmentChange,
|
||||
handleAntagonistEquipmentChange
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
+23
-23
@@ -1,20 +1,20 @@
|
||||
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core';
|
||||
import { useEffect } from 'react';
|
||||
import { isExplosive, resolvePenetrationCoefficient } from "@blitzkit/core";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
AdditiveBlending,
|
||||
MeshBasicMaterial,
|
||||
ShaderMaterial,
|
||||
type Object3D,
|
||||
} from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import type { ArmorUserData } from '../..';
|
||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { ArmorType } from '../../../SpacedArmorScene';
|
||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||
import vertexShader from './shaders/vertex.glsl?raw';
|
||||
} from "three";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import type { ArmorUserData } from "../..";
|
||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||
import { Duel, type EquipmentMatrix } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { ArmorType } from "../../../SpacedArmorScene";
|
||||
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||
|
||||
interface SpacedArmorSubSpacedProps {
|
||||
node: Object3D;
|
||||
@@ -51,16 +51,16 @@ export function SpacedArmorSubSpaced({
|
||||
useEffect(() => {
|
||||
function handleShellChange() {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const tankopediaEphemeral = Tankopedia.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
|
||||
material.uniforms.penetration.value = shell.penetration.near;
|
||||
material.uniforms.caliber.value = shell.caliber;
|
||||
material.uniforms.ricochet.value = degToRad(
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||
);
|
||||
material.uniforms.normalization.value = degToRad(
|
||||
shell.normalization ?? 0,
|
||||
shell.normalization ?? 0
|
||||
);
|
||||
}
|
||||
function handleProtagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
@@ -68,7 +68,7 @@ export function SpacedArmorSubSpaced({
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
equipment
|
||||
);
|
||||
material.uniforms.thickness.value = hasEnhancedArmor
|
||||
? thickness * 1.03
|
||||
@@ -76,13 +76,13 @@ export function SpacedArmorSubSpaced({
|
||||
}
|
||||
function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||
const duel = duelStore.getState();
|
||||
const tankopediaEphemeral = TankopediaEphemeral.state;
|
||||
const tankopediaEphemeral = Tankopedia.state;
|
||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||
const penetration = shell.penetration.near;
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
duel.antagonist.tank.equipment_preset,
|
||||
equipment,
|
||||
equipment
|
||||
);
|
||||
|
||||
material.uniforms.penetration.value =
|
||||
@@ -92,22 +92,22 @@ export function SpacedArmorSubSpaced({
|
||||
|
||||
handleShellChange();
|
||||
handleProtagonistEquipmentChange(
|
||||
duelStore.getState().protagonist.equipmentMatrix,
|
||||
duelStore.getState().protagonist.equipmentMatrix
|
||||
);
|
||||
handleAntagonistEquipmentChange(
|
||||
duelStore.getState().antagonist.equipmentMatrix,
|
||||
duelStore.getState().antagonist.equipmentMatrix
|
||||
);
|
||||
|
||||
const unsubscribes = [
|
||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||
TankopediaEphemeral.on((state) => state.customShell, handleShellChange),
|
||||
Tankopedia.on((state) => state.customShell, handleShellChange),
|
||||
duelStore.subscribe(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
handleProtagonistEquipmentChange,
|
||||
handleProtagonistEquipmentChange
|
||||
),
|
||||
duelStore.subscribe(
|
||||
(state) => state.antagonist.equipmentMatrix,
|
||||
handleAntagonistEquipmentChange,
|
||||
handleAntagonistEquipmentChange
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
+51
-51
@@ -2,9 +2,9 @@ import {
|
||||
ShellType,
|
||||
isExplosive,
|
||||
resolvePenetrationCoefficient,
|
||||
} from '@blitzkit/core';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { useCallback } from 'react';
|
||||
} from "@blitzkit/core";
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
type Intersection,
|
||||
MeshBasicMaterial,
|
||||
@@ -14,21 +14,21 @@ import {
|
||||
Raycaster,
|
||||
Scene,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
|
||||
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
||||
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
|
||||
import { Duel } from '../../../../stores/duel';
|
||||
} from "three";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
||||
import { Duel } from "../../../../stores/duel";
|
||||
import {
|
||||
type Shot,
|
||||
type ShotLayerBase,
|
||||
type ShotLayerNonExternal,
|
||||
TankopediaEphemeral,
|
||||
} from '../../../../stores/tankopediaEphemeral';
|
||||
import { ArmorType } from '../SpacedArmorScene';
|
||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
||||
Tankopedia,
|
||||
} from "../../../../stores/tankopedia";
|
||||
import { ArmorType } from "../SpacedArmorScene";
|
||||
import { SpacedArmorSubExternal } from "./components/SpacedArmorSubExternal";
|
||||
import { SpacedArmorSubSpaced } from "./components/SpacedArmorSubSpaced";
|
||||
|
||||
type SpacedArmorSceneComponentProps = {
|
||||
node: Object3D;
|
||||
@@ -45,7 +45,7 @@ type SpacedArmorSceneComponentProps = {
|
||||
}
|
||||
);
|
||||
|
||||
export type ExternalModuleVariant = 'gun' | 'track';
|
||||
export type ExternalModuleVariant = "gun" | "track";
|
||||
export type ArmorUserData = {
|
||||
thickness: number;
|
||||
} & (
|
||||
@@ -84,10 +84,10 @@ export function SpacedArmorSceneComponent({
|
||||
point: Vector3,
|
||||
intersections: Intersection[],
|
||||
allowRicochet: boolean,
|
||||
remainingPenetrationInput?: number,
|
||||
remainingPenetrationInput?: number
|
||||
) => {
|
||||
const { antagonist, protagonist } = duelStore.getState();
|
||||
const { customShell } = TankopediaEphemeral.state;
|
||||
const { customShell } = Tankopedia.state;
|
||||
const shell = customShell ?? antagonist.shell;
|
||||
const cameraNormal = camera.position.clone().sub(point).normalize();
|
||||
const shot: Shot = {
|
||||
@@ -98,19 +98,19 @@ export function SpacedArmorSceneComponent({
|
||||
in: {
|
||||
surfaceNormal: cameraNormal,
|
||||
layers: [],
|
||||
status: 'blocked',
|
||||
status: "blocked",
|
||||
},
|
||||
};
|
||||
|
||||
const hasCalibratedShells = hasEquipment(
|
||||
103,
|
||||
antagonist.tank.equipment_preset,
|
||||
antagonist.equipmentMatrix,
|
||||
antagonist.equipmentMatrix
|
||||
);
|
||||
const hasEnhancedArmor = hasEquipment(
|
||||
110,
|
||||
protagonist.tank.equipment_preset,
|
||||
protagonist.equipmentMatrix,
|
||||
protagonist.equipmentMatrix
|
||||
);
|
||||
const penetration =
|
||||
shell.penetration.near *
|
||||
@@ -118,8 +118,8 @@ export function SpacedArmorSceneComponent({
|
||||
const thicknessCoefficient = hasEnhancedArmor ? 1.03 : 1;
|
||||
const filteredIntersections = intersections.filter(
|
||||
(intersection) =>
|
||||
'type' in intersection.object.userData &&
|
||||
!discardClippingPlane(intersection.object, intersection.point),
|
||||
"type" in intersection.object.userData &&
|
||||
!discardClippingPlane(intersection.object, intersection.point)
|
||||
) as unknown as Intersection<Object3D & { userData: ArmorUserData }>[];
|
||||
const encounteredExternalModuleVariants: ExternalModuleVariant[] = [];
|
||||
const noDuplicateIntersections: typeof filteredIntersections = [];
|
||||
@@ -128,12 +128,12 @@ export function SpacedArmorSceneComponent({
|
||||
if (intersection.object.userData.type === ArmorType.External) {
|
||||
if (
|
||||
!encounteredExternalModuleVariants.includes(
|
||||
intersection.object.userData.variant,
|
||||
intersection.object.userData.variant
|
||||
)
|
||||
) {
|
||||
noDuplicateIntersections.push(intersection);
|
||||
encounteredExternalModuleVariants.push(
|
||||
intersection.object.userData.variant,
|
||||
intersection.object.userData.variant
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -161,7 +161,7 @@ export function SpacedArmorSceneComponent({
|
||||
const surfaceNormal = intersection
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
intersection.object.getWorldQuaternion(new Quaternion()),
|
||||
intersection.object.getWorldQuaternion(new Quaternion())
|
||||
);
|
||||
|
||||
if (shell.type === ShellType.HEAT && index > 0) {
|
||||
@@ -173,7 +173,7 @@ export function SpacedArmorSceneComponent({
|
||||
shot.in.layers.push({
|
||||
type: null,
|
||||
distance,
|
||||
status: blocked ? 'blocked' : 'penetration',
|
||||
status: blocked ? "blocked" : "penetration",
|
||||
});
|
||||
|
||||
if (blocked) break;
|
||||
@@ -193,16 +193,16 @@ export function SpacedArmorSceneComponent({
|
||||
surfaceNormal,
|
||||
status:
|
||||
shell.type === ShellType.HE
|
||||
? 'blocked'
|
||||
? "blocked"
|
||||
: blocked
|
||||
? 'blocked'
|
||||
: 'penetration',
|
||||
? "blocked"
|
||||
: "penetration",
|
||||
variant: layer.variant,
|
||||
});
|
||||
} else {
|
||||
const thickness = layer.thickness * thicknessCoefficient;
|
||||
const ricochet = degToRad(
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
||||
isExplosive(shell.type) ? 90 : shell.ricochet!
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
|
||||
@@ -226,7 +226,7 @@ export function SpacedArmorSceneComponent({
|
||||
thicknessAngled: finalThickness,
|
||||
point: intersection.point,
|
||||
surfaceNormal,
|
||||
status: 'ricochet',
|
||||
status: "ricochet",
|
||||
angle,
|
||||
});
|
||||
|
||||
@@ -244,10 +244,10 @@ export function SpacedArmorSceneComponent({
|
||||
surfaceNormal: intersection.face!.normal,
|
||||
status:
|
||||
shell.type === ShellType.HE && layer.type !== ArmorType.Primary
|
||||
? 'blocked'
|
||||
? "blocked"
|
||||
: blocked
|
||||
? 'blocked'
|
||||
: 'penetration',
|
||||
? "blocked"
|
||||
: "penetration",
|
||||
angle,
|
||||
});
|
||||
}
|
||||
@@ -281,10 +281,10 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
return acc + layer.thicknessAngled;
|
||||
},
|
||||
0,
|
||||
0
|
||||
);
|
||||
const distanceFromSpacedArmor = firstLayer.point.distanceTo(
|
||||
lastLayer.point,
|
||||
lastLayer.point
|
||||
);
|
||||
const finalDamage = Math.max(
|
||||
0,
|
||||
@@ -293,7 +293,7 @@ export function SpacedArmorSceneComponent({
|
||||
(1 - distanceFromSpacedArmor / shell.explosion_radius!) -
|
||||
1.1 *
|
||||
(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.
|
||||
*/
|
||||
|
||||
if (shot.in.layers.length > 1 || lastLayer.status === 'blocked') {
|
||||
shot.in.status = finalDamage > 0 ? 'splash' : 'blocked';
|
||||
if (shot.in.layers.length > 1 || lastLayer.status === "blocked") {
|
||||
shot.in.status = finalDamage > 0 ? "splash" : "blocked";
|
||||
shot.damage = finalDamage;
|
||||
} else {
|
||||
shot.in.status = 'penetration';
|
||||
shot.in.status = "penetration";
|
||||
shot.damage = shell.armor_damage;
|
||||
}
|
||||
} else {
|
||||
if (lastLayer.status === 'blocked') {
|
||||
shot.in.status = 'blocked';
|
||||
if (lastLayer.status === "blocked") {
|
||||
shot.in.status = "blocked";
|
||||
shot.damage = 0;
|
||||
} else if (lastLayer.status === 'penetration') {
|
||||
shot.in.status = 'penetration';
|
||||
} else if (lastLayer.status === "penetration") {
|
||||
shot.in.status = "penetration";
|
||||
shot.damage = shell.armor_damage;
|
||||
} else {
|
||||
const caster = new Raycaster();
|
||||
@@ -330,29 +330,29 @@ export function SpacedArmorSceneComponent({
|
||||
.sub(
|
||||
lastLayer.surfaceNormal
|
||||
.clone()
|
||||
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal)),
|
||||
.multiplyScalar(2 * shellNormal.dot(lastLayer.surfaceNormal))
|
||||
);
|
||||
|
||||
caster.set(lastLayer.point, ricochetNormal);
|
||||
|
||||
const ricochetIntersections = caster.intersectObjects(
|
||||
props.scene.children,
|
||||
true,
|
||||
true
|
||||
);
|
||||
const outShot = shoot(
|
||||
lastLayer.point,
|
||||
ricochetIntersections,
|
||||
false,
|
||||
remainingPenetration,
|
||||
remainingPenetration
|
||||
);
|
||||
|
||||
shot.in.status = 'ricochet';
|
||||
shot.in.status = "ricochet";
|
||||
if (outShot === null) {
|
||||
shot.damage = 0;
|
||||
shot.out = {
|
||||
surfaceNormal: ricochetNormal,
|
||||
layers: [],
|
||||
status: 'ricochet',
|
||||
status: "ricochet",
|
||||
};
|
||||
} else {
|
||||
shot.damage = outShot.damage;
|
||||
@@ -365,7 +365,7 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
return shot;
|
||||
},
|
||||
[camera],
|
||||
[camera]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -390,7 +390,7 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
const shot = shoot(event.point, event.intersections, true)!;
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = shot;
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useThree, type ThreeEvent } from '@react-three/fiber';
|
||||
import { memo, useRef } from 'react';
|
||||
import { Group, Plane, Vector2, Vector3 } from 'three';
|
||||
import { applyPitchYawLimits } from '../../../core/blitz/applyPitchYawLimits';
|
||||
import { correctZYTuple } from '../../../core/blitz/correctZYTuple';
|
||||
import { hasEquipment } from '../../../core/blitzkit/hasEquipment';
|
||||
import { modelTransformEvent } from '../../../core/blitzkit/modelTransform';
|
||||
import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId';
|
||||
import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
|
||||
import { useArmor } from '../../../hooks/useArmor';
|
||||
import { useModel } from '../../../hooks/useModel';
|
||||
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
|
||||
import { useTankTransform } from '../../../hooks/useTankTransform';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
||||
import { ArmorType } from './SpacedArmorScene';
|
||||
import { StaticArmorSceneComponent } from './StaticArmorSceneComponent';
|
||||
import { useThree, type ThreeEvent } from "@react-three/fiber";
|
||||
import { memo, useRef } from "react";
|
||||
import { Group, Plane, Vector2, Vector3 } from "three";
|
||||
import { applyPitchYawLimits } from "../../../core/blitz/applyPitchYawLimits";
|
||||
import { correctZYTuple } from "../../../core/blitz/correctZYTuple";
|
||||
import { hasEquipment } from "../../../core/blitzkit/hasEquipment";
|
||||
import { modelTransformEvent } from "../../../core/blitzkit/modelTransform";
|
||||
import { nameToArmorId } from "../../../core/blitzkit/nameToArmorId";
|
||||
import { resolveArmor } from "../../../core/blitzkit/resolveThickness";
|
||||
import { useArmor } from "../../../hooks/useArmor";
|
||||
import { useModel } from "../../../hooks/useModel";
|
||||
import { useTankModelDefinition } from "../../../hooks/useTankModelDefinition";
|
||||
import { useTankTransform } from "../../../hooks/useTankTransform";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||
import { ModelTankWrapper } from "./ModelTankWrapper";
|
||||
import { ArmorType } from "./SpacedArmorScene";
|
||||
import { StaticArmorSceneComponent } from "./StaticArmorSceneComponent";
|
||||
|
||||
interface ArmorSceneProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
@@ -52,16 +52,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
? undefined
|
||||
: gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y;
|
||||
const showPrimaryArmor = TankopediaPersistent.use(
|
||||
(state) => state.showPrimaryArmor,
|
||||
(state) => state.showPrimaryArmor
|
||||
);
|
||||
const showSpacedArmor = TankopediaPersistent.use(
|
||||
(state) => state.showSpacedArmor,
|
||||
(state) => state.showSpacedArmor
|
||||
);
|
||||
const showExternalModules = TankopediaPersistent.use(
|
||||
(state) => state.showExternalModules,
|
||||
(state) => state.showExternalModules
|
||||
);
|
||||
const isDynamicArmorActive = Duel.use((state) =>
|
||||
state.protagonist.consumables.includes(73),
|
||||
state.protagonist.consumables.includes(73)
|
||||
);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
@@ -70,12 +70,12 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
<ModelTankWrapper ref={wrapper}>
|
||||
<group position={hullOrigin}>
|
||||
{armorNodes.map((node) => {
|
||||
const isHull = node.name.startsWith('hull_');
|
||||
const isHull = node.name.startsWith("hull_");
|
||||
const isVisible = isHull;
|
||||
const armorId = nameToArmorId(node.name);
|
||||
const { spaced, thickness } = resolveArmor(
|
||||
tankModelDefinition.armor,
|
||||
armorId,
|
||||
armorId
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -83,8 +83,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
thickness === undefined ||
|
||||
(!showPrimaryArmor && !spaced) ||
|
||||
(!showSpacedArmor && spaced) ||
|
||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
||||
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -103,8 +103,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
</group>
|
||||
|
||||
{modelNodes.map((node) => {
|
||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||
const isTrack = node.name.startsWith('chassis_track_');
|
||||
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||
const isTrack = node.name.startsWith("chassis_track_");
|
||||
const isVisible = isWheel || isTrack;
|
||||
|
||||
if (!isVisible || !showExternalModules) return null;
|
||||
@@ -126,13 +126,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
<group ref={turretContainer}>
|
||||
{armorNodes.map((node) => {
|
||||
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 armorId = nameToArmorId(node.name);
|
||||
const { spaced, thickness } = resolveArmor(
|
||||
turretModelDefinition.armor,
|
||||
armorId,
|
||||
armorId
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -140,8 +140,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
thickness === undefined ||
|
||||
(!showPrimaryArmor && !spaced) ||
|
||||
(!showSpacedArmor && spaced) ||
|
||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
||||
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -154,27 +154,27 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
@@ -188,16 +188,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -219,13 +219,13 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
<group ref={gunContainer}>
|
||||
{armorNodes.map((node) => {
|
||||
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 armorId = nameToArmorId(node.name);
|
||||
const { spaced, thickness } = resolveArmor(
|
||||
gunModelDefinition.armor,
|
||||
armorId,
|
||||
armorId
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -233,8 +233,8 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
thickness === undefined ||
|
||||
(!showPrimaryArmor && !spaced) ||
|
||||
(!showSpacedArmor && spaced) ||
|
||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
||||
(isDynamicArmorActive && node.name.includes("state_00")) ||
|
||||
(!isDynamicArmorActive && node.name.includes("state_01"))
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -244,30 +244,30 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
@@ -281,16 +281,16 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -312,7 +312,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
{modelNodes.map((node) => {
|
||||
const gunString = `gun_${gunModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
.padStart(2, "0")}`;
|
||||
const isCurrentGun = gunModelDefinition.mask
|
||||
? node.name.startsWith(gunString)
|
||||
: node.name === gunString;
|
||||
@@ -326,29 +326,29 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
@@ -362,17 +362,17 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { type MeshProps, useThree } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import type { QuicklimeEvent } from 'quicklime';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { I_HAT, J_HAT } from "@blitzkit/core";
|
||||
import { type MeshProps, useThree } from "@react-three/fiber";
|
||||
import { clamp } from "lodash-es";
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import {
|
||||
Box3,
|
||||
Color,
|
||||
@@ -15,28 +15,28 @@ import {
|
||||
Plane,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
|
||||
import { jsxTree } from '../../../core/blitzkit/jsxTree';
|
||||
} from "three";
|
||||
import { unrotateDavaVector } from "../../../core/blitz/unrotateDavaVector";
|
||||
import { jsxTree } from "../../../core/blitzkit/jsxTree";
|
||||
import {
|
||||
type ModelTransformEventData,
|
||||
modelTransformEvent,
|
||||
} from '../../../core/blitzkit/modelTransform';
|
||||
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import { ArmorType } from './SpacedArmorScene';
|
||||
} from "../../../core/blitzkit/modelTransform";
|
||||
import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import { ArmorType } from "./SpacedArmorScene";
|
||||
import type {
|
||||
ArmorUserData,
|
||||
ExternalModuleVariant,
|
||||
} from './SpacedArmorSceneComponent';
|
||||
import type { ThicknessRange } from './StaticArmor';
|
||||
} from "./SpacedArmorSceneComponent";
|
||||
import type { ThicknessRange } from "./StaticArmor";
|
||||
|
||||
type StaticArmorSceneComponentProps = {
|
||||
name: string;
|
||||
thickness: number;
|
||||
thicknessRange: ThicknessRange;
|
||||
node: Object3D;
|
||||
onPointerDown?: MeshProps['onPointerDown'];
|
||||
onPointerDown?: MeshProps["onPointerDown"];
|
||||
} & (
|
||||
| {
|
||||
type: Exclude<ArmorType, ArmorType.External>;
|
||||
@@ -90,7 +90,7 @@ export function StaticArmorSceneComponent({
|
||||
color = new Color(
|
||||
clamp(1 - (7 / 8) * xClamped, 0, 1),
|
||||
0,
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1),
|
||||
clamp(1 - (1 / 8) * xClamped, 0, 1)
|
||||
);
|
||||
opacity = clamp(x + 1 / 2, 0, 1);
|
||||
break;
|
||||
@@ -113,7 +113,7 @@ export function StaticArmorSceneComponent({
|
||||
depthWrite,
|
||||
...(props.clip ? { clippingPlanes: [props.clip] } : {}),
|
||||
}),
|
||||
[thickness],
|
||||
[thickness]
|
||||
);
|
||||
const outlineMaterial = useMemo(
|
||||
() =>
|
||||
@@ -122,7 +122,7 @@ export function StaticArmorSceneComponent({
|
||||
.clone()
|
||||
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
|
||||
}),
|
||||
[thickness],
|
||||
[thickness]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -137,11 +137,11 @@ export function StaticArmorSceneComponent({
|
||||
outlineMaterial.visible = true;
|
||||
} else if (
|
||||
selectedName === name ||
|
||||
(name.startsWith('chassis_') && selectedName.startsWith('chassis_')) ||
|
||||
(name.startsWith('gun_') &&
|
||||
selectedName.startsWith('gun_') &&
|
||||
!name.includes('_armor_') &&
|
||||
!selectedName.includes('_armor_'))
|
||||
(name.startsWith("chassis_") && selectedName.startsWith("chassis_")) ||
|
||||
(name.startsWith("gun_") &&
|
||||
selectedName.startsWith("gun_") &&
|
||||
!name.includes("_armor_") &&
|
||||
!selectedName.includes("_armor_"))
|
||||
) {
|
||||
// this selected, stand out!
|
||||
surfaceMaterial.opacity = 1;
|
||||
@@ -164,9 +164,9 @@ export function StaticArmorSceneComponent({
|
||||
}
|
||||
|
||||
const unsubscribes = [
|
||||
TankopediaEphemeral.on(
|
||||
Tankopedia.on(
|
||||
(state) => state.highlightArmor?.name,
|
||||
handleHighlightArmor,
|
||||
handleHighlightArmor
|
||||
),
|
||||
];
|
||||
|
||||
@@ -179,14 +179,14 @@ export function StaticArmorSceneComponent({
|
||||
const { clip } = props;
|
||||
const gunOrigin = unrotateDavaVector(props.gunOrigin.clone());
|
||||
const neckOrigin = unrotateDavaVector(props.hullOrigin.clone()).add(
|
||||
unrotateDavaVector(props.turretOrigin.clone()),
|
||||
unrotateDavaVector(props.turretOrigin.clone())
|
||||
);
|
||||
const barrelOrigin = neckOrigin.clone().add(gunOrigin);
|
||||
const distanceToBarrel = clip.distanceToPoint(barrelOrigin);
|
||||
const point = new Vector3();
|
||||
|
||||
function handleModelTransform(
|
||||
event: QuicklimeEvent<ModelTransformEventData>,
|
||||
event: QuicklimeEvent<ModelTransformEventData>
|
||||
) {
|
||||
clip.normal
|
||||
.set(0, 0, -1)
|
||||
@@ -227,7 +227,7 @@ export function StaticArmorSceneComponent({
|
||||
{
|
||||
type: props.type,
|
||||
variant:
|
||||
props.type === ArmorType.External ? props.variant : 'gun',
|
||||
props.type === ArmorType.External ? props.variant : "gun",
|
||||
thickness,
|
||||
} satisfies ArmorUserData
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export function StaticArmorSceneComponent({
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const { editStatic } = TankopediaEphemeral.state;
|
||||
const { editStatic } = Tankopedia.state;
|
||||
|
||||
const bounds = new Box3().setFromObject(event.object);
|
||||
const point = bounds.min
|
||||
@@ -251,13 +251,13 @@ export function StaticArmorSceneComponent({
|
||||
const surfaceNormal = event
|
||||
.normal!.clone()
|
||||
.applyQuaternion(
|
||||
event.object.getWorldQuaternion(new Quaternion()),
|
||||
event.object.getWorldQuaternion(new Quaternion())
|
||||
);
|
||||
const angle = surfaceNormal.angleTo(cameraNormal);
|
||||
const thicknessAngled =
|
||||
thickness / Math.sin(Math.PI / 2 - angle);
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = {
|
||||
type: props.type,
|
||||
name,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { Flex, Heading, IconButton, Table } from '@radix-ui/themes';
|
||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
import { CompareRow } from './CompareRow';
|
||||
import { CompareSectionTitle } from './CompareSectionTitle';
|
||||
import { asset } from "@blitzkit/core";
|
||||
import { Flex, Heading, IconButton, Table } from "@radix-ui/themes";
|
||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||
import { CompareRow } from "./CompareRow";
|
||||
import { CompareSectionTitle } from "./CompareSectionTitle";
|
||||
|
||||
interface BodyProps {
|
||||
stats: TankCharacteristics[];
|
||||
@@ -14,10 +14,9 @@ interface BodyProps {
|
||||
export function Body({ stats }: BodyProps) {
|
||||
const members = CompareEphemeral.use((state) => state.members);
|
||||
const hasNonRegularGun = members.some(
|
||||
({ gun }) => gun.gun_type!.$case !== 'regular',
|
||||
({ gun }) => gun.gun_type!.$case !== "regular"
|
||||
);
|
||||
const { unwrap, strings } = useLocale();
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -35,7 +34,7 @@ export function Body({ stats }: BodyProps) {
|
||||
<Flex>
|
||||
{gun.shells.map((thisShell, shellIndex) => (
|
||||
<IconButton
|
||||
color={thisShell.id === shell.id ? undefined : 'gray'}
|
||||
color={thisShell.id === shell.id ? undefined : "gray"}
|
||||
variant="soft"
|
||||
key={thisShell.id}
|
||||
style={{
|
||||
@@ -49,7 +48,7 @@ export function Body({ stats }: BodyProps) {
|
||||
marginLeft: shellIndex === 0 ? 0 : -1,
|
||||
}}
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].shell = thisShell;
|
||||
});
|
||||
}}
|
||||
@@ -88,7 +87,7 @@ export function Body({ stats }: BodyProps) {
|
||||
}
|
||||
display={(stats) =>
|
||||
stats.shellReload?.toFixed(2) ??
|
||||
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(', ')
|
||||
stats.shellReloads!.map((reload) => reload.toFixed(2)).join(", ")
|
||||
}
|
||||
deltaNominalDisplay={(delta) => delta.toFixed(2)}
|
||||
/>
|
||||
|
||||
@@ -2,21 +2,21 @@ import {
|
||||
CaretLeftIcon,
|
||||
CaretRightIcon,
|
||||
CaretSortIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { Flex, IconButton, Table, Text } from '@radix-ui/themes';
|
||||
import { useRef } from 'react';
|
||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
||||
import { Var } from '../../core/radix/var';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { ComparePersistent } from '../../stores/comparePersistent';
|
||||
import { StickyRowHeaderCell } from '../StickyRowHeaderCell';
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Flex, IconButton, Table, Text } from "@radix-ui/themes";
|
||||
import { useRef } from "react";
|
||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||
import { Var } from "../../core/radix/var";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import { ComparePersistent } from "../../stores/comparePersistent";
|
||||
import { StickyRowHeaderCell } from "../StickyRowHeaderCell";
|
||||
|
||||
type CompareRowProps = {
|
||||
display?: (
|
||||
member: Awaited<TankCharacteristics>,
|
||||
member: Awaited<TankCharacteristics>
|
||||
) => number | string | undefined;
|
||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
||||
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||
decimals?: number;
|
||||
deltaNominalDisplay?: (delta: number) => string | number;
|
||||
indent?: boolean;
|
||||
@@ -33,7 +33,7 @@ type CompareRowProps = {
|
||||
|
||||
export function CompareRow({
|
||||
display,
|
||||
deltaType = 'higherIsBetter',
|
||||
deltaType = "higherIsBetter",
|
||||
decimals,
|
||||
deltaNominalDisplay,
|
||||
indent,
|
||||
@@ -41,17 +41,16 @@ export function CompareRow({
|
||||
...props
|
||||
}: CompareRowProps) {
|
||||
const { strings } = useLocale();
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const sorting = CompareEphemeral.use((state) => state.sorting);
|
||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||
const values = stats.map((stat) =>
|
||||
typeof props.value === 'function'
|
||||
typeof props.value === "function"
|
||||
? props.value(stat)!
|
||||
: (stat[props.value] as number),
|
||||
: (stat[props.value] as number)
|
||||
);
|
||||
const id = useRef(Math.random());
|
||||
const name =
|
||||
typeof props.value === 'string'
|
||||
typeof props.value === "string"
|
||||
? strings.website.tools.tankopedia.characteristics.values[props.value]
|
||||
: props.name;
|
||||
|
||||
@@ -60,21 +59,21 @@ export function CompareRow({
|
||||
<StickyRowHeaderCell>
|
||||
<Flex
|
||||
align="center"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
gap={indent ? { initial: '4', sm: '6' } : { initial: '1', sm: '2' }}
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
gap={indent ? { initial: "4", sm: "6" } : { initial: "1", sm: "2" }}
|
||||
>
|
||||
<IconButton
|
||||
color={sorting?.by === id.current ? undefined : 'gray'}
|
||||
color={sorting?.by === id.current ? undefined : "gray"}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members.sort((memberA, memberB) => {
|
||||
const indexA = draft.members.indexOf(memberA);
|
||||
const indexB = draft.members.indexOf(memberB);
|
||||
const valueA = values[indexA];
|
||||
const valueB = values[indexB];
|
||||
|
||||
return draft.sorting?.direction === 'ascending' &&
|
||||
return draft.sorting?.direction === "ascending" &&
|
||||
draft.sorting.by === id.current
|
||||
? valueA - valueB
|
||||
: valueB - valueA;
|
||||
@@ -83,26 +82,26 @@ export function CompareRow({
|
||||
draft.sorting = {
|
||||
by: id.current,
|
||||
direction:
|
||||
draft.sorting?.direction === 'ascending' &&
|
||||
draft.sorting?.direction === "ascending" &&
|
||||
draft.sorting.by === id.current
|
||||
? 'descending'
|
||||
: 'ascending',
|
||||
? "descending"
|
||||
: "ascending",
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
{(sorting === undefined || sorting.by !== id.current) && (
|
||||
<CaretSortIcon style={{ transform: 'rotate(90deg)' }} />
|
||||
<CaretSortIcon style={{ transform: "rotate(90deg)" }} />
|
||||
)}
|
||||
{sorting?.by === id.current && (
|
||||
<>
|
||||
{sorting.direction === 'ascending' && <CaretRightIcon />}
|
||||
{sorting.direction === 'descending' && <CaretLeftIcon />}
|
||||
{sorting.direction === "ascending" && <CaretRightIcon />}
|
||||
{sorting.direction === "descending" && <CaretLeftIcon />}
|
||||
</>
|
||||
)}
|
||||
</IconButton>
|
||||
|
||||
<Text size={{ initial: '1', sm: '2' }}>{name}</Text>
|
||||
<Text size={{ initial: "1", sm: "2" }}>{name}</Text>
|
||||
</Flex>
|
||||
</StickyRowHeaderCell>
|
||||
|
||||
@@ -110,7 +109,7 @@ export function CompareRow({
|
||||
const delta = value - values[0];
|
||||
const deltaPercentage = value / values[0] - 1;
|
||||
const normalizedDeltaPercentage = Math.round(
|
||||
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25),
|
||||
Math.min(100, Math.abs(deltaPercentage) * 2 * 100 + 25)
|
||||
);
|
||||
const resolvedDisplayValue = display
|
||||
? display(stats[index])
|
||||
@@ -129,12 +128,12 @@ export function CompareRow({
|
||||
value === values[0]
|
||||
? undefined
|
||||
: (
|
||||
deltaType === 'higherIsBetter'
|
||||
deltaType === "higherIsBetter"
|
||||
? value > values[0]
|
||||
: value < values[0]
|
||||
)
|
||||
? `color-mix(in srgb, ${Var('green-7')} ${normalizedDeltaPercentage}%, ${Var('green-3')})`
|
||||
: `color-mix(in srgb, ${Var('red-7')} ${normalizedDeltaPercentage}%, ${Var('red-3')})`,
|
||||
? `color-mix(in srgb, ${Var("green-7")} ${normalizedDeltaPercentage}%, ${Var("green-3")})`
|
||||
: `color-mix(in srgb, ${Var("red-7")} ${normalizedDeltaPercentage}%, ${Var("red-3")})`,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
@@ -142,11 +141,11 @@ export function CompareRow({
|
||||
justify="center"
|
||||
gap="2"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Text style={{ textAlign: 'center' }} wrap="nowrap">
|
||||
<Text style={{ textAlign: "center" }} wrap="nowrap">
|
||||
{resolvedDisplayValue}
|
||||
</Text>
|
||||
|
||||
@@ -154,10 +153,10 @@ export function CompareRow({
|
||||
resolvedDisplayValue !== undefined &&
|
||||
values[0] !== undefined && (
|
||||
<>
|
||||
{deltaMode === 'absolute' && (
|
||||
{deltaMode === "absolute" && (
|
||||
<Text color="gray">
|
||||
(
|
||||
{`${delta > 0 ? '+' : ''}${
|
||||
{`${delta > 0 ? "+" : ""}${
|
||||
deltaNominalDisplay
|
||||
? deltaNominalDisplay(delta)
|
||||
: decimals === undefined
|
||||
@@ -168,11 +167,11 @@ export function CompareRow({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{deltaMode === 'percentage' && (
|
||||
{deltaMode === "percentage" && (
|
||||
<Text color="gray">
|
||||
(
|
||||
{`${deltaPercentage > 0 ? '+' : ''}${Math.round(
|
||||
deltaPercentage * 100,
|
||||
{`${deltaPercentage > 0 ? "+" : ""}${Math.round(
|
||||
deltaPercentage * 100
|
||||
)}%`}
|
||||
)
|
||||
</Text>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { asset, availableProvisions, permanentSkills } from '@blitzkit/core';
|
||||
import { checkConsumableProvisionInclusivity } from '@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity';
|
||||
import { ComponentPlaceholderIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, IconButton, Popover, Table } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { awaitableConsumableDefinitions } from '../../core/awaitables/consumableDefinitions';
|
||||
import { awaitableEquipmentDefinitions } from '../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableSkillDefinitions } from '../../core/awaitables/skillDefinitions';
|
||||
import type { TankCharacteristics } from '../../core/blitzkit/tankCharacteristics';
|
||||
import { Var } from '../../core/radix/var';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import type { EquipmentMatrix } from '../../stores/duel';
|
||||
import { BlitzkitButtonGrayIcon } from '../BlitzkitButtonGrayIcon';
|
||||
import { ConsumablesManager } from '../ConsumablesManager';
|
||||
import { CrewSkillManager } from '../CrewSkillManager';
|
||||
import { EquipmentManager } from '../EquipmentManager';
|
||||
import { ModuleManager } from '../ModuleManager';
|
||||
import { ProvisionsManager } from '../ProvisionsManager';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
import { StickyTableRoot } from '../StickyTableRoot';
|
||||
import { Body } from './Body';
|
||||
import { InsertionMarker } from './IntersectionMarker';
|
||||
import { TankCard } from './TankCard';
|
||||
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
|
||||
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
|
||||
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
|
||||
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||
import { awaitableSkillDefinitions } from "../../core/awaitables/skillDefinitions";
|
||||
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
|
||||
import { Var } from "../../core/radix/var";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import type { EquipmentMatrix } from "../../stores/duel";
|
||||
import { BlitzkitButtonGrayIcon } from "../BlitzkitButtonGrayIcon";
|
||||
import { ConsumablesManager } from "../ConsumablesManager";
|
||||
import { CrewSkillManager } from "../CrewSkillManager";
|
||||
import { EquipmentManager } from "../EquipmentManager";
|
||||
import { ModuleManager } from "../ModuleManager";
|
||||
import { ProvisionsManager } from "../ProvisionsManager";
|
||||
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||
import { StickyTableRoot } from "../StickyTableRoot";
|
||||
import { Body } from "./Body";
|
||||
import { InsertionMarker } from "./IntersectionMarker";
|
||||
import { TankCard } from "./TankCard";
|
||||
|
||||
interface CompareTableProps {
|
||||
stats: TankCharacteristics[];
|
||||
@@ -43,7 +43,6 @@ const [
|
||||
export function CompareTable({ stats }: CompareTableProps) {
|
||||
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
|
||||
const members = CompareEphemeral.use((state) => state.members);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const { unwrap, strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -51,20 +50,20 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
size="1"
|
||||
variant="surface"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
height: '100%',
|
||||
position: "absolute",
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
height: "100%",
|
||||
// height: 'calc(100vh - 128px)'
|
||||
}}
|
||||
>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<StickyColumnHeaderCell width="0" style={{ height: '100%' }}>
|
||||
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
align="center"
|
||||
justify="center"
|
||||
@@ -82,11 +81,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell style={{ height: '100%' }}>
|
||||
<Table.Cell style={{ height: "100%" }}>
|
||||
<Flex
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
align="center"
|
||||
justify="center"
|
||||
@@ -107,15 +106,15 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
borderRadius: 2,
|
||||
backgroundColor:
|
||||
crewSkills[skill] === 0
|
||||
? Var('gray-11')
|
||||
? Var("gray-11")
|
||||
: permanentSkills.includes(skill)
|
||||
? Var('crimson-11')
|
||||
: Var('amber-11'),
|
||||
? Var("crimson-11")
|
||||
: Var("amber-11"),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
</Button>
|
||||
@@ -125,7 +124,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<CrewSkillManager
|
||||
skillLevels={crewSkills}
|
||||
onChange={(skills) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.crewSkills = skills;
|
||||
});
|
||||
}}
|
||||
@@ -135,7 +134,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||
draft.crewSkills[skill] = 0;
|
||||
});
|
||||
@@ -147,7 +146,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
Object.keys(draft.crewSkills).forEach((skill) => {
|
||||
draft.crewSkills[skill] = 7;
|
||||
});
|
||||
@@ -177,19 +176,19 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
consumables,
|
||||
camouflage,
|
||||
},
|
||||
index,
|
||||
index
|
||||
) => {
|
||||
const equipmentPreset =
|
||||
equipmentDefinitions.presets[tank.equipment_preset];
|
||||
const provisionsList = availableProvisions(
|
||||
tank,
|
||||
gun,
|
||||
provisionDefinitions,
|
||||
provisionDefinitions
|
||||
);
|
||||
const consumablesList = Object.values(
|
||||
consumableDefinitions.consumables,
|
||||
consumableDefinitions.consumables
|
||||
).filter((consumable) =>
|
||||
checkConsumableProvisionInclusivity(consumable, tank, gun),
|
||||
checkConsumableProvisionInclusivity(consumable, tank, gun)
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -205,7 +204,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button variant="ghost">
|
||||
<Flex align="center">
|
||||
{(
|
||||
['turret', 'gun', 'engine', 'chassis'] as const
|
||||
["turret", "gun", "engine", "chassis"] as const
|
||||
).map((module, index) => (
|
||||
<img
|
||||
key={module}
|
||||
@@ -215,7 +214,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
src={asset(`icons/modules/${module}.webp`)}
|
||||
style={{
|
||||
marginLeft: index > 0 ? -8 : 0,
|
||||
objectFit: 'contain',
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -232,7 +231,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
engine={engine}
|
||||
track={track}
|
||||
onChange={(modules) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index] = {
|
||||
...draft.members[index],
|
||||
...modules,
|
||||
@@ -245,7 +244,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
const member = draft.members[index];
|
||||
|
||||
member.turret = member.tank.turrets[0];
|
||||
@@ -261,7 +260,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
const member = draft.members[index];
|
||||
|
||||
member.turret = member.tank.turrets.at(-1)!;
|
||||
@@ -288,9 +287,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
variant="ghost"
|
||||
radius="large"
|
||||
style={{
|
||||
height: '100%',
|
||||
height: "100%",
|
||||
width: 12,
|
||||
position: 'relative',
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" justify="center">
|
||||
@@ -298,14 +297,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<img
|
||||
alt={unwrap(
|
||||
provisionDefinitions.provisions[provision]
|
||||
.name,
|
||||
.name
|
||||
)}
|
||||
key={provision}
|
||||
src={asset(
|
||||
`/icons/provisions/${provision}.webp`,
|
||||
`/icons/provisions/${provision}.webp`
|
||||
)}
|
||||
style={{
|
||||
left: '50%',
|
||||
left: "50%",
|
||||
/**
|
||||
* max = 100% - 16px - 4px
|
||||
* min = 4px
|
||||
@@ -320,11 +319,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
? 0.5
|
||||
: index / (provisions.length - 1)
|
||||
} * (100% - 24px))`,
|
||||
transform: 'translateX(-50%)',
|
||||
position: 'absolute',
|
||||
transform: "translateX(-50%)",
|
||||
position: "absolute",
|
||||
width: 16,
|
||||
height: 16,
|
||||
objectFit: 'contain',
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -339,12 +338,12 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Popover.Content>
|
||||
<ProvisionsManager
|
||||
provisions={provisionsList.map(
|
||||
(provision) => provision.id,
|
||||
(provision) => provision.id
|
||||
)}
|
||||
selected={provisions}
|
||||
disabled={tank.max_provisions === provisions.length}
|
||||
onChange={(provisions) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].provisions = provisions;
|
||||
});
|
||||
}}
|
||||
@@ -353,7 +352,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].provisions = [];
|
||||
});
|
||||
}}
|
||||
@@ -394,8 +393,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
height: 8,
|
||||
backgroundColor:
|
||||
equipment === -1
|
||||
? Var('crimson-11')
|
||||
: Var('gray-11'),
|
||||
? Var("crimson-11")
|
||||
: Var("gray-11"),
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
@@ -405,8 +404,8 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
height: 8,
|
||||
backgroundColor:
|
||||
equipment === 1
|
||||
? Var('crimson-11')
|
||||
: Var('gray-11'),
|
||||
? Var("crimson-11")
|
||||
: Var("gray-11"),
|
||||
borderRadius: 2,
|
||||
}}
|
||||
/>
|
||||
@@ -424,7 +423,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
matrix={equipmentMatrix}
|
||||
preset={equipmentPreset}
|
||||
onChange={(matrix) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].equipmentMatrix = matrix;
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -436,10 +435,10 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
variant="ghost"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].equipmentMatrix = times(
|
||||
3,
|
||||
() => times(3, () => 0),
|
||||
() => times(3, () => 0)
|
||||
) as EquipmentMatrix;
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -453,7 +452,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members.forEach((member) => {
|
||||
member.equipmentMatrix =
|
||||
draft.members[index].equipmentMatrix;
|
||||
@@ -477,9 +476,9 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
variant="ghost"
|
||||
radius="large"
|
||||
style={{
|
||||
height: '100%',
|
||||
height: "100%",
|
||||
width: 12,
|
||||
position: 'relative',
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Flex direction="column" justify="center">
|
||||
@@ -488,14 +487,14 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
alt={unwrap(
|
||||
consumableDefinitions.consumables[
|
||||
consumable
|
||||
].name,
|
||||
].name
|
||||
)}
|
||||
key={consumable}
|
||||
src={asset(
|
||||
`/icons/consumables/${consumable}.webp`,
|
||||
`/icons/consumables/${consumable}.webp`
|
||||
)}
|
||||
style={{
|
||||
left: '50%',
|
||||
left: "50%",
|
||||
/**
|
||||
* max = 100% - 16px - 4px
|
||||
* min = 4px
|
||||
@@ -510,11 +509,11 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
? 0.5
|
||||
: index / (consumables.length - 1)
|
||||
} * (100% - 24px))`,
|
||||
transform: 'translateX(-50%)',
|
||||
position: 'absolute',
|
||||
transform: "translateX(-50%)",
|
||||
position: "absolute",
|
||||
width: 16,
|
||||
height: 16,
|
||||
objectFit: 'contain',
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -531,7 +530,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
consumables={consumablesList}
|
||||
selected={consumables}
|
||||
onConsumablesChange={(consumables) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].consumables = consumables;
|
||||
});
|
||||
}}
|
||||
@@ -544,7 +543,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].consumables = [];
|
||||
});
|
||||
}}
|
||||
@@ -562,18 +561,18 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
{!tank.fixed_camouflage && (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index].camouflage = !camouflage;
|
||||
});
|
||||
}}
|
||||
variant={camouflage ? 'solid' : 'outline'}
|
||||
variant={camouflage ? "solid" : "outline"}
|
||||
>
|
||||
<img
|
||||
src={asset('icons/camo.webp')}
|
||||
src={asset("icons/camo.webp")}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
transform: 'scale(2) translate(15%, 15%)',
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
transform: "scale(2) translate(15%, 15%)",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -581,7 +580,7 @@ export function CompareTable({ stats }: CompareTableProps) {
|
||||
</Flex>
|
||||
</Table.Cell>
|
||||
);
|
||||
},
|
||||
}
|
||||
)}
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { PlusIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Dialog, Flex, SegmentedControl } from "@radix-ui/themes";
|
||||
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import {
|
||||
ComparePersistent,
|
||||
type DeltaMode,
|
||||
} from '../../stores/comparePersistent';
|
||||
import { TankSearch } from '../TankSearch';
|
||||
} from "../../stores/comparePersistent";
|
||||
import { TankSearch } from "../TankSearch";
|
||||
|
||||
interface ControlsProps {
|
||||
addTankDialogOpen: boolean;
|
||||
@@ -22,8 +22,6 @@ export function Controls({
|
||||
onAddTankDialogOpenChange,
|
||||
}: ControlsProps) {
|
||||
const deltaMode = ComparePersistent.use((state) => state.deltaMode);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const mutateComparePersistent = ComparePersistent.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -56,20 +54,20 @@ export function Controls({
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members.push(
|
||||
tankToCompareMember(tank, provisionDefinitions),
|
||||
tankToCompareMember(tank, provisionDefinitions)
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
onAddTankDialogOpenChange(false);
|
||||
}}
|
||||
onSelectAll={(tanks) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members.push(
|
||||
...tanks.map((tank) => {
|
||||
return tankToCompareMember(tank, provisionDefinitions);
|
||||
}),
|
||||
})
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -85,7 +83,7 @@ export function Controls({
|
||||
variant="soft"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members = [];
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -98,18 +96,18 @@ export function Controls({
|
||||
variant="classic"
|
||||
value={deltaMode}
|
||||
onValueChange={(value) => {
|
||||
mutateComparePersistent((draft) => {
|
||||
ComparePersistent.mutate((draft) => {
|
||||
draft.deltaMode = value as DeltaMode;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SegmentedControl.Item value={'none' satisfies DeltaMode}>
|
||||
<SegmentedControl.Item value={"none" satisfies DeltaMode}>
|
||||
{strings.website.tools.compare.actions.deltas.none}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={'percentage' satisfies DeltaMode}>
|
||||
<SegmentedControl.Item value={"percentage" satisfies DeltaMode}>
|
||||
{strings.website.tools.compare.actions.deltas.percentage}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={'absolute' satisfies DeltaMode}>
|
||||
<SegmentedControl.Item value={"absolute" satisfies DeltaMode}>
|
||||
{strings.website.tools.compare.actions.deltas.absolute}
|
||||
</SegmentedControl.Item>
|
||||
</SegmentedControl.Root>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { type TankDefinition, tankIcon } from '@blitzkit/core';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Vector2 } from 'three';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { StickyColumnHeaderCell } from '../StickyColumnHeaderCell';
|
||||
import { InsertionMarker } from './IntersectionMarker';
|
||||
import { insertionMarkers } from './IntersectionMarker/constants';
|
||||
import { TankControl } from './TankControl';
|
||||
import { type TankDefinition, tankIcon } from "@blitzkit/core";
|
||||
import { Flex, Text } from "@radix-ui/themes";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Vector2 } from "three";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
|
||||
import { InsertionMarker } from "./IntersectionMarker";
|
||||
import { insertionMarkers } from "./IntersectionMarker/constants";
|
||||
import { TankControl } from "./TankControl";
|
||||
|
||||
interface TankCardProps {
|
||||
index: number;
|
||||
@@ -16,7 +16,6 @@ interface TankCardProps {
|
||||
|
||||
export function TankCard({ index, tank }: TankCardProps) {
|
||||
const draggable = useRef<HTMLDivElement>(null);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
const { unwrap } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,8 +33,8 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
|
||||
delta.set(0, 0);
|
||||
current.copy(initial.set(event.clientX, event.clientY));
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
@@ -43,7 +42,7 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
current.set(event.clientX, event.clientY);
|
||||
delta.copy(current).sub(initial);
|
||||
insertionMarkers.forEach(({ element }) => {
|
||||
element.style.opacity = '0';
|
||||
element.style.opacity = "0";
|
||||
});
|
||||
|
||||
const distances = insertionMarkers.map((insertionMarker) => {
|
||||
@@ -56,34 +55,34 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
};
|
||||
});
|
||||
const closest = distances.reduce((a, b) =>
|
||||
a.distance < b.distance ? a : b,
|
||||
a.distance < b.distance ? a : b
|
||||
);
|
||||
|
||||
dropIndex = closest.index;
|
||||
closest.element.style.opacity = '1';
|
||||
draggable.current!.style.position = 'fixed';
|
||||
draggable.current!.style.cursor = 'grabbing';
|
||||
draggable.current!.style.zIndex = '1';
|
||||
closest.element.style.opacity = "1";
|
||||
draggable.current!.style.position = "fixed";
|
||||
draggable.current!.style.cursor = "grabbing";
|
||||
draggable.current!.style.zIndex = "1";
|
||||
draggable.current!.style.left = `${initialRect.left + delta.x}px`;
|
||||
draggable.current!.style.top = `${initialRect.top + delta.y}px`;
|
||||
}
|
||||
function handlePointerUp(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
draggable.current!.style.position = 'static';
|
||||
draggable.current!.style.zIndex = 'unset';
|
||||
draggable.current!.style.cursor = 'grab';
|
||||
draggable.current!.style.position = "static";
|
||||
draggable.current!.style.zIndex = "unset";
|
||||
draggable.current!.style.cursor = "grab";
|
||||
|
||||
insertionMarkers.forEach(({ element }) => {
|
||||
element.style.opacity = '0';
|
||||
element.style.opacity = "0";
|
||||
});
|
||||
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
|
||||
if (dropIndex === -1) return;
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
const fromIndex = index;
|
||||
const toIndex = dropIndex > index ? dropIndex - 1 : dropIndex;
|
||||
|
||||
@@ -95,10 +94,10 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
dropIndex = -1;
|
||||
}
|
||||
|
||||
draggable.current?.addEventListener('pointerdown', handlePointerDown);
|
||||
draggable.current?.addEventListener("pointerdown", handlePointerDown);
|
||||
|
||||
return () => {
|
||||
draggable.current?.removeEventListener('pointerdown', handlePointerDown);
|
||||
draggable.current?.removeEventListener("pointerdown", handlePointerDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -114,9 +113,9 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
justify="between"
|
||||
gap="2"
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
cursor: 'grab',
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
cursor: "grab",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
@@ -126,16 +125,16 @@ export function TankCard({ index, tank }: TankCardProps) {
|
||||
height={64}
|
||||
draggable={false}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 128,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{unwrap(tank.name)}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
|
||||
import { useState } from 'react';
|
||||
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
|
||||
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { CompareEphemeral } from '../../stores/compareEphemeral';
|
||||
import { LinkI18n } from '../LinkI18n';
|
||||
import { TankSearch } from '../TankSearch';
|
||||
import { ExternalLinkIcon, LoopIcon, TrashIcon } from "@radix-ui/react-icons";
|
||||
import { Dialog, Flex, IconButton } from "@radix-ui/themes";
|
||||
import { useState } from "react";
|
||||
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
|
||||
import { tankToCompareMember } from "../../core/blitzkit/tankToCompareMember";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { CompareEphemeral } from "../../stores/compareEphemeral";
|
||||
import { LinkI18n } from "../LinkI18n";
|
||||
import { TankSearch } from "../TankSearch";
|
||||
|
||||
interface TankControlProps {
|
||||
index: number;
|
||||
@@ -18,14 +18,13 @@ const provisionDefinitions = await awaitableProvisionDefinitions;
|
||||
export function TankControl({ index, slug }: TankControlProps) {
|
||||
const { locale } = useLocale();
|
||||
const [switchTankDialogOpen, setSwitchTankDialogOpen] = useState(false);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
|
||||
return (
|
||||
<Flex gap="3" justify="center" style={{ width: '100%' }}>
|
||||
<Flex gap="3" justify="center" style={{ width: "100%" }}>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members.splice(index, 1);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
@@ -57,10 +56,10 @@ export function TankControl({ index, slug }: TankControlProps) {
|
||||
<TankSearch
|
||||
compact
|
||||
onSelect={(tank) => {
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members[index] = tankToCompareMember(
|
||||
tank,
|
||||
provisionDefinitions,
|
||||
provisionDefinitions
|
||||
);
|
||||
draft.sorting = undefined;
|
||||
});
|
||||
|
||||
@@ -7,17 +7,17 @@ import {
|
||||
idToRegion,
|
||||
sumCompositeStats,
|
||||
type IndividualTankStats,
|
||||
} from '@blitzkit/core';
|
||||
import { STRINGS } from '@blitzkit/i18n';
|
||||
import { ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { ContextMenu } from '@radix-ui/themes';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { breakdownConfig } from '../../constants/embeds';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { EmbedBreakdownPersistent } from '../../stores/embedBreakdownPersistent';
|
||||
import { useEmbedStateCurry } from '../../stores/embedState/utilities';
|
||||
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from '../TanksEmbed';
|
||||
} from "@blitzkit/core";
|
||||
import { STRINGS } from "@blitzkit/i18n";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { ContextMenu } from "@radix-ui/themes";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { breakdownConfig } from "../../constants/embeds";
|
||||
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { EmbedBreakdown } from "../../stores/embedBreakdown";
|
||||
import { useEmbedStateCurry } from "../../stores/embedState/utilities";
|
||||
import { BreakdownEmbedCard, BreakdownEmbedWrapper } from "../TanksEmbed";
|
||||
|
||||
export const compositeStatsKeysOptions = compositeStatsKeys.map((value) => ({
|
||||
value,
|
||||
@@ -55,7 +55,7 @@ export const fakeCompositeStats = compositeStats(
|
||||
wins: 30,
|
||||
xp: 1230 * 50,
|
||||
},
|
||||
averageDefinitions.averages[7297].mu,
|
||||
averageDefinitions.averages[7297].mu
|
||||
);
|
||||
|
||||
export function BreakdownPreview() {
|
||||
@@ -64,11 +64,11 @@ export function BreakdownPreview() {
|
||||
|
||||
return (
|
||||
<BreakdownEmbedWrapper>
|
||||
{useEmbedState('showTotal') && (
|
||||
{useEmbedState("showTotal") && (
|
||||
<BreakdownEmbedCard composite={fakeCompositeStats} tank={null} />
|
||||
)}
|
||||
|
||||
{tanks.slice(0, useEmbedState('listMaxTanks')).map((tank) => (
|
||||
{tanks.slice(0, useEmbedState("listMaxTanks")).map((tank) => (
|
||||
<BreakdownEmbedCard
|
||||
composite={fakeCompositeStats}
|
||||
key={tank.id}
|
||||
@@ -80,26 +80,17 @@ export function BreakdownPreview() {
|
||||
}
|
||||
|
||||
export function BreakdownRenderer() {
|
||||
return (
|
||||
<EmbedBreakdownPersistent.Provider>
|
||||
<BreakdownRendererContent />
|
||||
</EmbedBreakdownPersistent.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function BreakdownRendererContent() {
|
||||
const { useEmbedState } = useEmbedStateCurry<typeof breakdownConfig>();
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const mutateEmbedBreakdownPersistent = EmbedBreakdownPersistent.useMutation();
|
||||
const id = Number(params.get('id'));
|
||||
const tankStatsA = EmbedBreakdownPersistent.use((state) => state[id] ?? []);
|
||||
const id = Number(params.get("id"));
|
||||
const tankStatsA = EmbedBreakdown.use((state) => state[id] ?? []);
|
||||
const [tankStatsB, setTankStatsB] = useState<IndividualTankStats[]>([]);
|
||||
const diff = useMemo(
|
||||
() =>
|
||||
deltaTankStats(tankStatsA, tankStatsB).sort(
|
||||
(a, b) => b.last_battle_time - a.last_battle_time,
|
||||
(a, b) => b.last_battle_time - a.last_battle_time
|
||||
),
|
||||
[tankStatsB],
|
||||
[tankStatsB]
|
||||
);
|
||||
const compositeStats = diff
|
||||
.map((diff) => {
|
||||
@@ -109,14 +100,14 @@ function BreakdownRendererContent() {
|
||||
|
||||
const compositeStats = calculateCompositeStats(
|
||||
{ battle_life_time: diff.battle_life_time, ...diff.all },
|
||||
average.mu,
|
||||
average.mu
|
||||
);
|
||||
|
||||
return { id: diff.tank_id, compositeStats };
|
||||
})
|
||||
.filter((stats) => stats !== null);
|
||||
const sum = sumCompositeStats(
|
||||
compositeStats.map(({ compositeStats }) => compositeStats),
|
||||
compositeStats.map(({ compositeStats }) => compositeStats)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -135,12 +126,12 @@ function BreakdownRendererContent() {
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<BreakdownEmbedWrapper>
|
||||
{useEmbedState('showTotal') && (
|
||||
{useEmbedState("showTotal") && (
|
||||
<BreakdownEmbedCard composite={sum} tank={null} />
|
||||
)}
|
||||
|
||||
{compositeStats
|
||||
.slice(0, useEmbedState('listMaxTanks'))
|
||||
.slice(0, useEmbedState("listMaxTanks"))
|
||||
.map(({ id, compositeStats }) => {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
@@ -163,7 +154,7 @@ function BreakdownRendererContent() {
|
||||
|
||||
if (newA === null) return;
|
||||
|
||||
mutateEmbedBreakdownPersistent((draft) => {
|
||||
EmbedBreakdown.mutate((draft) => {
|
||||
draft[id] = newA;
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import locales from '@blitzkit/i18n/locales.json';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Flex, Text } from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { times } from 'lodash-es';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { awaitableGallery } from '../../core/awaitables/gallery';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { InlineSkeleton } from '../InlineSkeleton';
|
||||
import { GalleryCard } from './Card';
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Flex, Text } from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { times } from "lodash-es";
|
||||
import { useMemo, useState } from "react";
|
||||
import { awaitableGallery } from "../../core/awaitables/gallery";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { Gallery } from "../../stores/gallery";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { InlineSkeleton } from "../InlineSkeleton";
|
||||
import { GalleryCard } from "./Card";
|
||||
|
||||
export interface Avatar {
|
||||
name: string;
|
||||
@@ -22,7 +22,7 @@ const DEFAULT_LOADED = 42;
|
||||
const PREVIEW_COUNT = 28;
|
||||
|
||||
export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const search = GalleryEphemeral.use((state) => state.search);
|
||||
const search = Gallery.use((state) => state.search);
|
||||
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED);
|
||||
const { locale, strings } = useLocale();
|
||||
const filtered = useMemo(() => {
|
||||
@@ -35,9 +35,9 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
.go(search, gallery.avatars, {
|
||||
keys: [
|
||||
...locales.supported.map(
|
||||
(supported) => `name.locales.${supported.locale}`,
|
||||
(supported) => `name.locales.${supported.locale}`
|
||||
),
|
||||
'id',
|
||||
"id",
|
||||
],
|
||||
})
|
||||
.map((result) => result.obj);
|
||||
@@ -72,7 +72,7 @@ export function GalleryList({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
skeleton
|
||||
onIntersection={() => setLoadedCards((state) => state + 2)}
|
||||
/>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { TextField } from '@radix-ui/themes';
|
||||
import { useRef } from 'react';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { GalleryEphemeral } from '../../stores/galleryEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { TextField } from "@radix-ui/themes";
|
||||
import { useRef } from "react";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { Gallery } from "../../stores/gallery";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
|
||||
export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const mutateGalleryEphemeral = GalleryEphemeral.useMutation();
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -17,7 +16,7 @@ export function GallerySearch({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
placeholder={strings.website.tools.gallery.search.hint}
|
||||
ref={input}
|
||||
onChange={(event) => {
|
||||
mutateGalleryEphemeral((draft) => {
|
||||
Gallery.mutate((draft) => {
|
||||
const trimmed = event.target.value.trim();
|
||||
draft.search = trimmed.length === 0 ? undefined : trimmed;
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDe
|
||||
import { tankToDuelMember } from "../../core/blitzkit/tankToDuelMember";
|
||||
import { Var } from "../../core/radix/var";
|
||||
import { Duel } from "../../stores/duel";
|
||||
import { Guess } from "../../stores/guessEphemeral";
|
||||
import { TankopediaEphemeral } from "../../stores/tankopediaEphemeral";
|
||||
import { Guess, GuessState } from "../../stores/guess";
|
||||
import { Tankopedia } from "../../stores/tankopedia";
|
||||
import "./index.css";
|
||||
|
||||
const [modelDefinitions, provisionDefinitions] = await Promise.all([
|
||||
@@ -23,7 +23,7 @@ export function GuessBackground() {
|
||||
mutateDuel((state) => {
|
||||
state.protagonist = tankToDuelMember(tank, provisionDefinitions);
|
||||
});
|
||||
TankopediaEphemeral.mutate((state) => {
|
||||
Tankopedia.mutate((state) => {
|
||||
state.model = modelDefinitions.models[tank.id];
|
||||
});
|
||||
}, [tank]);
|
||||
@@ -39,9 +39,9 @@ export function GuessBackground() {
|
||||
style={{
|
||||
transitionDuration: "2s",
|
||||
backgroundColor: Var(
|
||||
guessState === null
|
||||
guessState === GuessState.NotGuessed
|
||||
? "accent-2"
|
||||
: guessState
|
||||
: guessState === GuessState.Correct
|
||||
? "jade-3"
|
||||
: "tomato-3"
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Guess } from "../stores/guessEphemeral";
|
||||
import { Guess, GuessState } from "../stores/guess";
|
||||
import { SmartCanvas } from "./SmartCanvas";
|
||||
import { Controls } from "./Tankopedia/HeroSection/components/TankSandbox/components/Control";
|
||||
import { Lighting } from "./Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
|
||||
@@ -9,7 +9,7 @@ const CONTRAST = 2 ** 2;
|
||||
|
||||
export function GuessRenderer() {
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const isRevealed = guessState !== null;
|
||||
const isRevealed = guessState !== GuessState.NotGuessed;
|
||||
|
||||
return (
|
||||
<SmartCanvas
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { Guess } from "../stores/guessEphemeral";
|
||||
import { Guess, GuessState } from "../stores/guess";
|
||||
import { classIcons } from "./ClassIcon";
|
||||
import { SearchResults } from "./SearchResults";
|
||||
|
||||
@@ -78,7 +78,7 @@ export function Guesser() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (guessState !== null) setSelected(null);
|
||||
if (guessState !== GuessState.NotGuessed) setSelected(null);
|
||||
}, [guessState]);
|
||||
|
||||
return (
|
||||
@@ -148,7 +148,7 @@ export function Guesser() {
|
||||
|
||||
<Flex gap="3">
|
||||
<TextField.Root
|
||||
disabled={guessState !== null}
|
||||
disabled={guessState !== GuessState.NotGuessed}
|
||||
ref={input}
|
||||
onChange={requestSearch}
|
||||
style={{ flex: 1 }}
|
||||
@@ -163,13 +163,19 @@ export function Guesser() {
|
||||
|
||||
<Button
|
||||
size="3"
|
||||
color={guessState === null && selected === null ? "red" : undefined}
|
||||
color={
|
||||
guessState === GuessState.NotGuessed && selected === null
|
||||
? "red"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
if (guessState === null) {
|
||||
if (guessState === GuessState.NotGuessed) {
|
||||
const correct = selected !== null && selected.id === tank.id;
|
||||
|
||||
Guess.mutate((draft) => {
|
||||
draft.guessState = correct;
|
||||
draft.guessState = correct
|
||||
? GuessState.Correct
|
||||
: GuessState.Incorrect;
|
||||
draft.totalGuesses++;
|
||||
draft.correctGuesses += correct ? 1 : 0;
|
||||
});
|
||||
@@ -181,13 +187,14 @@ export function Guesser() {
|
||||
if (!input.current) return;
|
||||
|
||||
draft.tank = tank;
|
||||
draft.guessState = null;
|
||||
draft.guessState = GuessState.NotGuessed;
|
||||
|
||||
input.current.value = "";
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{guessState === null ? (
|
||||
{guessState === GuessState.NotGuessed ? (
|
||||
<>
|
||||
{
|
||||
strings.website.tools.guess.search[
|
||||
|
||||
+12
-14
@@ -1,17 +1,15 @@
|
||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Flex, SegmentedControl, Text, type FlexProps } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Flex, SegmentedControl, Text, type FlexProps } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
Tankopedia,
|
||||
TankopediaRelativeAgainst,
|
||||
} from '../../../../../../stores/tankopediaEphemeral';
|
||||
} from "../../../../../../stores/tankopedia";
|
||||
|
||||
export function CompareOptions(props: FlexProps) {
|
||||
const relativeAgainst = TankopediaEphemeral.use(
|
||||
(state) => state.relativeAgainst,
|
||||
);
|
||||
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const { strings } = useLocale();
|
||||
|
||||
@@ -20,7 +18,7 @@ export function CompareOptions(props: FlexProps) {
|
||||
align="center"
|
||||
gap="2"
|
||||
justify="center"
|
||||
direction={{ initial: 'column', sm: 'row' }}
|
||||
direction={{ initial: "column", sm: "row" }}
|
||||
{...props}
|
||||
>
|
||||
<Text color="gray">
|
||||
@@ -32,7 +30,7 @@ export function CompareOptions(props: FlexProps) {
|
||||
size="1"
|
||||
value={`${relativeAgainst}`}
|
||||
onValueChange={(value) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.relativeAgainst = Number(value) as TankopediaRelativeAgainst;
|
||||
});
|
||||
}}
|
||||
@@ -43,13 +41,13 @@ export function CompareOptions(props: FlexProps) {
|
||||
[
|
||||
TIER_ROMAN_NUMERALS[tank.tier],
|
||||
strings.common.tank_class_short_plural_lowercase[tank.class],
|
||||
],
|
||||
]
|
||||
)}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.Tier}`}>
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.characteristics.compare.tier,
|
||||
[TIER_ROMAN_NUMERALS[tank.tier]],
|
||||
[TIER_ROMAN_NUMERALS[tank.tier]]
|
||||
)}
|
||||
</SegmentedControl.Item>
|
||||
<SegmentedControl.Item value={`${TankopediaRelativeAgainst.All}`}>
|
||||
|
||||
+19
-19
@@ -1,10 +1,10 @@
|
||||
import { CaretDownIcon, CaretUpIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { Quicklime } from 'quicklime';
|
||||
import { useEffect, useRef, type ReactNode } from 'react';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { Mark } from '../../../../../Mark';
|
||||
import { CaretDownIcon, CaretUpIcon } from "@radix-ui/react-icons";
|
||||
import { Box, Flex, Text } from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Quicklime } from "quicklime";
|
||||
import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { Mark } from "../../../../../Mark";
|
||||
|
||||
export const highlightedRows = new Set<HTMLDivElement>();
|
||||
export const highlightedRowsUpdate = new Quicklime();
|
||||
@@ -16,7 +16,7 @@ export interface InfoProps {
|
||||
delta?: number;
|
||||
decimals?: number;
|
||||
prefix?: string;
|
||||
deltaType?: 'higherIsBetter' | 'lowerIsBetter';
|
||||
deltaType?: "higherIsBetter" | "lowerIsBetter";
|
||||
}
|
||||
|
||||
export function Info({
|
||||
@@ -26,15 +26,15 @@ export function Info({
|
||||
delta,
|
||||
decimals,
|
||||
prefix,
|
||||
deltaType = 'higherIsBetter',
|
||||
deltaType = "higherIsBetter",
|
||||
}: InfoProps) {
|
||||
const statSearch = TankopediaEphemeral.use((state) => state.statSearch);
|
||||
const statSearch = Tankopedia.use((state) => state.statSearch);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
let label = name;
|
||||
let highlighted = false;
|
||||
|
||||
if (statSearch !== undefined && typeof name === 'string') {
|
||||
if (statSearch !== undefined && typeof name === "string") {
|
||||
const result = fuzzysort.single(statSearch, name);
|
||||
|
||||
if (result) {
|
||||
@@ -62,7 +62,7 @@ export function Info({
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
pl={indent ? '2' : '0'}
|
||||
pl={indent ? "2" : "0"}
|
||||
width="100%"
|
||||
gap="4"
|
||||
ref={container}
|
||||
@@ -77,9 +77,9 @@ export function Info({
|
||||
<>
|
||||
<Text
|
||||
color={
|
||||
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
|
||||
? 'green'
|
||||
: 'tomato'
|
||||
delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
|
||||
? "green"
|
||||
: "tomato"
|
||||
}
|
||||
>
|
||||
{decimals !== undefined
|
||||
@@ -88,9 +88,9 @@ export function Info({
|
||||
</Text>
|
||||
<Text
|
||||
color={
|
||||
delta * (deltaType === 'lowerIsBetter' ? -1 : 1) > 0
|
||||
? 'green'
|
||||
: 'tomato'
|
||||
delta * (deltaType === "lowerIsBetter" ? -1 : 1) > 0
|
||||
? "green"
|
||||
: "tomato"
|
||||
}
|
||||
>
|
||||
{delta > 0 ? <CaretUpIcon /> : <CaretDownIcon />}
|
||||
@@ -105,7 +105,7 @@ export function Info({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{decimals !== undefined && typeof children === 'number'
|
||||
{decimals !== undefined && typeof children === "number"
|
||||
? children.toFixed(decimals)
|
||||
: children}
|
||||
</Text>
|
||||
|
||||
+38
-40
@@ -1,27 +1,27 @@
|
||||
import { createDefaultSkills } from '@blitzkit/core';
|
||||
import { Flex, Progress, Text } from '@radix-ui/themes';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { memo, useMemo, type ComponentProps, type ReactNode } from 'react';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableSkillDefinitions } from '../../../../../../core/awaitables/skillDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../../../../core/awaitables/tankDefinitions';
|
||||
import { createDefaultSkills } from "@blitzkit/core";
|
||||
import { Flex, Progress, Text } from "@radix-ui/themes";
|
||||
import { clamp } from "lodash-es";
|
||||
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
||||
import { awaitableEquipmentDefinitions } from "../../../../../../core/awaitables/equipmentDefinitions";
|
||||
import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../../../../../core/awaitables/provisionDefinitions";
|
||||
import { awaitableSkillDefinitions } from "../../../../../../core/awaitables/skillDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../../../../core/awaitables/tankDefinitions";
|
||||
import {
|
||||
tankCharacteristics,
|
||||
type TankCharacteristics,
|
||||
} from '../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { tankToDuelMember } from '../../../../../../core/blitzkit/tankToDuelMember';
|
||||
import { useDelta } from '../../../../../../hooks/useDelta';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
} from "../../../../../../core/blitzkit/tankCharacteristics";
|
||||
import { tankToDuelMember } from "../../../../../../core/blitzkit/tankToDuelMember";
|
||||
import { useDelta } from "../../../../../../hooks/useDelta";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import {
|
||||
TankopediaEphemeral,
|
||||
Tankopedia,
|
||||
TankopediaRelativeAgainst,
|
||||
} from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { Info, type InfoProps } from './Info';
|
||||
} from "../../../../../../stores/tankopedia";
|
||||
import { Info, type InfoProps } from "./Info";
|
||||
|
||||
type InfoWithDeltaProps = Omit<InfoProps, 'name'> & {
|
||||
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
||||
stats: TankCharacteristics;
|
||||
noRanking?: boolean;
|
||||
} & (
|
||||
@@ -51,20 +51,18 @@ const [
|
||||
export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
({ stats, indent, noRanking, deltaType, ...props }) => {
|
||||
const { strings } = useLocale();
|
||||
const relativeAgainst = TankopediaEphemeral.use(
|
||||
(state) => state.relativeAgainst,
|
||||
);
|
||||
const relativeAgainst = Tankopedia.use((state) => state.relativeAgainst);
|
||||
const uhWhatDoICallThisVariable =
|
||||
typeof props.value === 'function'
|
||||
typeof props.value === "function"
|
||||
? props.value(stats)!
|
||||
: (stats[props.value] as number);
|
||||
const delta = useDelta(uhWhatDoICallThisVariable);
|
||||
const protagonistTank = Duel.use((state) => state.protagonist.tank);
|
||||
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(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
(state) => state.protagonist.equipmentMatrix
|
||||
);
|
||||
const others = useMemo(() => {
|
||||
const defaultSkills = createDefaultSkills(skillDefinitions);
|
||||
@@ -77,7 +75,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
tank.class === protagonistTank.class) ||
|
||||
(relativeAgainst === TankopediaRelativeAgainst.Tier &&
|
||||
tank.tier === protagonistTank.tier) ||
|
||||
relativeAgainst === TankopediaRelativeAgainst.All,
|
||||
relativeAgainst === TankopediaRelativeAgainst.All
|
||||
)
|
||||
.map((tank) => {
|
||||
const member = tankToDuelMember(tank, provisionDefinitions);
|
||||
@@ -112,14 +110,14 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
equipmentDefinitions,
|
||||
provisionDefinitions,
|
||||
tankModelDefinition: modelDefinitions.models[tank.id],
|
||||
},
|
||||
}
|
||||
);
|
||||
})
|
||||
.filter((tank) => {
|
||||
if (tank === undefined) return false;
|
||||
|
||||
const othersValue =
|
||||
typeof props.value === 'function'
|
||||
typeof props.value === "function"
|
||||
? props.value(tank)!
|
||||
: (tank[props.value] as number);
|
||||
|
||||
@@ -128,22 +126,22 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
}, [relativeAgainst, shellIndex, equipmentMatrix]);
|
||||
const betterTanks = others.filter((tank) => {
|
||||
const othersValue =
|
||||
typeof props.value === 'function'
|
||||
typeof props.value === "function"
|
||||
? props.value(tank)!
|
||||
: (tank[props.value] as number);
|
||||
|
||||
if (othersValue === undefined) return false;
|
||||
return deltaType === 'lowerIsBetter'
|
||||
return deltaType === "lowerIsBetter"
|
||||
? othersValue < uhWhatDoICallThisVariable
|
||||
: othersValue > uhWhatDoICallThisVariable;
|
||||
});
|
||||
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';
|
||||
else if (goodness <= 0.5) color = 'orange';
|
||||
else if (goodness <= 0.75) color = 'yellow';
|
||||
else color = 'green';
|
||||
if (goodness <= 0.25) color = "red";
|
||||
else if (goodness <= 0.5) color = "orange";
|
||||
else if (goodness <= 0.75) color = "yellow";
|
||||
else color = "green";
|
||||
|
||||
return (
|
||||
<Flex direction="column">
|
||||
@@ -151,7 +149,7 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
deltaType={deltaType}
|
||||
indent={indent}
|
||||
name={
|
||||
'name' in props
|
||||
"name" in props
|
||||
? props.name
|
||||
: strings.website.tools.tankopedia.characteristics.values[
|
||||
props.value
|
||||
@@ -164,17 +162,17 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
</Info>
|
||||
|
||||
{!noRanking && (
|
||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2">
|
||||
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
||||
<Progress
|
||||
variant="soft"
|
||||
size="1"
|
||||
value={goodness * 100}
|
||||
color={color}
|
||||
style={{ height: '0.125rem', opacity: 0.5 }}
|
||||
style={{ height: "0.125rem", opacity: 0.5 }}
|
||||
/>
|
||||
|
||||
<Text color="gray" size="1">
|
||||
{clamp(betterTanks.length + 1, 1, others.length)} /{' '}
|
||||
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
||||
{others.length}
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -184,6 +182,6 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
},
|
||||
|
||||
(a, b) =>
|
||||
(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 a.value === "function" ? a.value(a.stats) : a.stats[a.value]) ===
|
||||
(typeof b.value === "function" ? b.value(b.stats) : b.stats[b.value])
|
||||
);
|
||||
+19
-19
@@ -1,10 +1,10 @@
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Spinner, TextField, type FlexProps } from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { highlightedRows, highlightedRowsUpdate } from './Info';
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { Flex, Spinner, TextField, type FlexProps } from "@radix-ui/themes";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { highlightedRows, highlightedRowsUpdate } from "./Info";
|
||||
|
||||
export function StatSearch(props: FlexProps) {
|
||||
const { strings } = useLocale();
|
||||
@@ -19,7 +19,7 @@ export function StatSearch(props: FlexProps) {
|
||||
setSearching(true);
|
||||
} else {
|
||||
setSearching(false);
|
||||
count.current.textContent = '';
|
||||
count.current.textContent = "";
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
@@ -30,21 +30,21 @@ export function StatSearch(props: FlexProps) {
|
||||
if (!input.current || !count.current) return;
|
||||
|
||||
if (input.current.value.length === 0) {
|
||||
count.current.textContent = '';
|
||||
count.current.textContent = "";
|
||||
|
||||
setSearching(false);
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.statSearch = undefined;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSearching(false);
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.statSearch = input.current!.value;
|
||||
});
|
||||
}, 500),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,7 +52,7 @@ export function StatSearch(props: FlexProps) {
|
||||
if (!count.current || !input.current) return;
|
||||
|
||||
count.current.textContent =
|
||||
input.current.value.length === 0 ? '' : `0 / ${highlightedRows.size}`;
|
||||
input.current.value.length === 0 ? "" : `0 / ${highlightedRows.size}`;
|
||||
};
|
||||
|
||||
highlightedRowsUpdate.on(handleHighlightedRowsUpdate);
|
||||
@@ -68,23 +68,23 @@ export function StatSearch(props: FlexProps) {
|
||||
ref={input}
|
||||
placeholder={strings.website.tools.tankopedia.search}
|
||||
variant="classic"
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
onChange={search}
|
||||
onKeyDown={(event) => {
|
||||
if (!count.current) return;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
const newIndex = (index + 1) % highlightedRows.size;
|
||||
count.current.textContent = `${newIndex + 1} / ${highlightedRows.size}`;
|
||||
const array = [...highlightedRows];
|
||||
|
||||
array.forEach((element) => element.classList.remove('focused'));
|
||||
array.forEach((element) => element.classList.remove("focused"));
|
||||
|
||||
if (array[newIndex]) {
|
||||
array[newIndex].classList.add('focused');
|
||||
array[newIndex].classList.add("focused");
|
||||
array[newIndex].scrollIntoView({
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
block: "center",
|
||||
inline: "center",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+30
-30
@@ -1,19 +1,19 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { useEffect } from 'react';
|
||||
import { awaitableEquipmentDefinitions } from '../../../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../../../core/awaitables/provisionDefinitions';
|
||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
||||
import { tankCharacteristics } from '../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||
import { Crew } from './components/Crew';
|
||||
import { Firepower } from './components/Firepower';
|
||||
import { Maneuverability } from './components/Maneuverability';
|
||||
import { Survivability } from './components/Survivability';
|
||||
import { Flex } from "@radix-ui/themes";
|
||||
import { useEffect } from "react";
|
||||
import { awaitableEquipmentDefinitions } from "../../../../../core/awaitables/equipmentDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../../../../core/awaitables/provisionDefinitions";
|
||||
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
||||
import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
|
||||
import { tankCharacteristics } from "../../../../../core/blitzkit/tankCharacteristics";
|
||||
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||
import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
|
||||
import { Duel } from "../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
||||
import { Crew } from "./components/Crew";
|
||||
import { Firepower } from "./components/Firepower";
|
||||
import { Maneuverability } from "./components/Maneuverability";
|
||||
import { Survivability } from "./components/Survivability";
|
||||
|
||||
const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
awaitableEquipmentDefinitions,
|
||||
@@ -21,18 +21,18 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
]);
|
||||
|
||||
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 consumables = Duel.use((state) => state.protagonist.consumables);
|
||||
const camouflage = Duel.use((state) => state.protagonist.camouflage);
|
||||
const assaultDistance = Duel.use(
|
||||
(state) => state.protagonist.assaultDistance,
|
||||
(state) => state.protagonist.assaultDistance
|
||||
);
|
||||
const equipmentMatrix = Duel.use(
|
||||
(state) => state.protagonist.equipmentMatrix,
|
||||
(state) => state.protagonist.equipmentMatrix
|
||||
);
|
||||
const { tank, turret, gun, engine, track, shell } = Duel.use(
|
||||
(state) => state.protagonist,
|
||||
(state) => state.protagonist
|
||||
);
|
||||
const stockEngine = tank.engines[0];
|
||||
const stockTrack = tank.tracks[0];
|
||||
@@ -67,7 +67,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
tankModelDefinition,
|
||||
equipmentDefinitions: equipmentDefinitions,
|
||||
provisionDefinitions: provisionDefinitions,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
@@ -80,7 +80,7 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -89,25 +89,25 @@ export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<Flex
|
||||
gap="8"
|
||||
direction={{ initial: 'column', md: 'row' }}
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
width={{ initial: '100%', sm: 'auto' }}
|
||||
direction={{ initial: "column", md: "row" }}
|
||||
align={{ initial: "center", sm: "start" }}
|
||||
width={{ initial: "100%", sm: "auto" }}
|
||||
>
|
||||
<Firepower skeleton={skeleton} stats={stats} />
|
||||
|
||||
<Flex
|
||||
gap="8"
|
||||
direction={{ initial: 'column', lg: 'row' }}
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
width={{ initial: '100%', sm: 'auto' }}
|
||||
direction={{ initial: "column", lg: "row" }}
|
||||
align={{ initial: "center", sm: "start" }}
|
||||
width={{ initial: "100%", sm: "auto" }}
|
||||
>
|
||||
<Maneuverability stats={stats} />
|
||||
|
||||
<Flex
|
||||
gap="8"
|
||||
direction="column"
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
width={{ initial: '100%', sm: 'auto' }}
|
||||
align={{ initial: "center", sm: "start" }}
|
||||
width={{ initial: "100%", sm: "auto" }}
|
||||
>
|
||||
<Survivability stats={stats} />
|
||||
<Crew stats={stats} />
|
||||
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
import { InfoCircledIcon } from '@radix-ui/react-icons';
|
||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
IconButton,
|
||||
Popover,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import { CrewSkillManager } from '../../../CrewSkillManager';
|
||||
import { ConfigurationChildWrapper } from './ConfigurationChildWrapper';
|
||||
} from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../../hooks/useLocale";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import { CrewSkillManager } from "../../../CrewSkillManager";
|
||||
import { ConfigurationChildWrapper } from "./ConfigurationChildWrapper";
|
||||
|
||||
export function Skills() {
|
||||
const skillLevels = TankopediaEphemeral.use((state) => state.skills);
|
||||
const skillLevels = Tankopedia.use((state) => state.skills);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -45,7 +45,7 @@ export function Skills() {
|
||||
variant="ghost"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
Object.keys(draft.skills).forEach((skill) => {
|
||||
draft.skills[skill] = 0;
|
||||
});
|
||||
@@ -57,7 +57,7 @@ export function Skills() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
Object.keys(draft.skills).forEach((skill) => {
|
||||
draft.skills[skill] = 7;
|
||||
});
|
||||
@@ -71,7 +71,7 @@ export function Skills() {
|
||||
<CrewSkillManager
|
||||
skillLevels={skillLevels}
|
||||
onChange={(skills) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.skills = skills;
|
||||
});
|
||||
}}
|
||||
|
||||
+34
-34
@@ -1,5 +1,5 @@
|
||||
import { ShellType } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { ShellType } from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
Slider,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { type ChangeEvent } from 'react';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
} from "@radix-ui/themes";
|
||||
import { throttle } from "lodash-es";
|
||||
import { type ChangeEvent } from "react";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
|
||||
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 antagonistShell = Duel.use((state) => state.antagonist.shell);
|
||||
const { strings } = useLocale();
|
||||
@@ -27,24 +27,24 @@ export function CustomShellButton() {
|
||||
onOpenChange={(open) => {
|
||||
if (!open || customShell) return;
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell = antagonistShell;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Popover.Trigger>
|
||||
<IconButton
|
||||
color={customShell ? undefined : 'gray'}
|
||||
color={customShell ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
>
|
||||
<img
|
||||
alt="custom shell"
|
||||
src="/assets/images/icons/custom-shell.png"
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -90,13 +90,13 @@ export function CustomShellButton() {
|
||||
modified.ricochet = undefined;
|
||||
modified.explosion_radius =
|
||||
gun.shells.find(
|
||||
(shell) => shell.type === ShellType.HE,
|
||||
(shell) => shell.type === ShellType.HE
|
||||
)?.explosion_radius ?? 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell = modified;
|
||||
});
|
||||
}}
|
||||
@@ -129,11 +129,11 @@ export function CustomShellButton() {
|
||||
</Text>
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
style={{ maxWidth: '6rem' }}
|
||||
style={{ maxWidth: "6rem" }}
|
||||
defaultValue={customShell.caliber}
|
||||
type="number"
|
||||
onChange={(event) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell!.caliber = event.target.valueAsNumber;
|
||||
});
|
||||
}}
|
||||
@@ -141,9 +141,9 @@ export function CustomShellButton() {
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
} else if (event.key === 'Escape') {
|
||||
} else if (event.key === "Escape") {
|
||||
(event.target as HTMLInputElement).valueAsNumber =
|
||||
customShell.caliber;
|
||||
(event.target as HTMLInputElement).blur();
|
||||
@@ -151,7 +151,7 @@ export function CustomShellButton() {
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="right">
|
||||
{literals(strings.common.units.mm, [''])}
|
||||
{literals(strings.common.units.mm, [""])}
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
@@ -165,11 +165,11 @@ export function CustomShellButton() {
|
||||
</Text>
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
style={{ maxWidth: '6rem' }}
|
||||
style={{ maxWidth: "6rem" }}
|
||||
defaultValue={customShell.penetration.near}
|
||||
type="number"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell!.penetration.near =
|
||||
event.target.valueAsNumber;
|
||||
});
|
||||
@@ -178,9 +178,9 @@ export function CustomShellButton() {
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
} else if (event.key === 'Escape') {
|
||||
} else if (event.key === "Escape") {
|
||||
(event.target as HTMLInputElement).valueAsNumber =
|
||||
customShell.penetration.near;
|
||||
(event.target as HTMLInputElement).blur();
|
||||
@@ -188,7 +188,7 @@ export function CustomShellButton() {
|
||||
}}
|
||||
>
|
||||
<TextField.Slot side="right">
|
||||
{literals(strings.common.units.mm, [''])}
|
||||
{literals(strings.common.units.mm, [""])}
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</Flex>
|
||||
@@ -223,9 +223,9 @@ export function CustomShellButton() {
|
||||
step={Number.EPSILON}
|
||||
defaultValue={[customShell.normalization ?? 0]}
|
||||
onValueChange={([value]) => {
|
||||
console.log('asd');
|
||||
console.log("asd");
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell!.normalization = value;
|
||||
});
|
||||
}}
|
||||
@@ -254,7 +254,7 @@ export function CustomShellButton() {
|
||||
step={Number.EPSILON}
|
||||
defaultValue={[customShell.ricochet ?? 90]}
|
||||
onValueChange={throttle(([value]) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell!.ricochet = value;
|
||||
});
|
||||
}, 1000)}
|
||||
@@ -269,11 +269,11 @@ export function CustomShellButton() {
|
||||
<Text>Explosion radius</Text>
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
style={{ maxWidth: '6rem' }}
|
||||
style={{ maxWidth: "6rem" }}
|
||||
defaultValue={customShell.explosion_radius ?? 0}
|
||||
type="number"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell!.explosion_radius =
|
||||
event.target.valueAsNumber;
|
||||
});
|
||||
@@ -282,9 +282,9 @@ export function CustomShellButton() {
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
} else if (event.key === 'Escape') {
|
||||
} else if (event.key === "Escape") {
|
||||
(event.target as HTMLInputElement).valueAsNumber =
|
||||
customShell.explosion_radius ?? 1;
|
||||
(event.target as HTMLInputElement).blur();
|
||||
@@ -309,7 +309,7 @@ export function CustomShellButton() {
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell = antagonistShell;
|
||||
});
|
||||
}}
|
||||
|
||||
+12
-12
@@ -1,14 +1,14 @@
|
||||
import { asset } from '@blitzkit/core';
|
||||
import { IconButton } from '@radix-ui/themes';
|
||||
import { useArmor } from '../../../../../../hooks/useArmor';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { asset } from "@blitzkit/core";
|
||||
import { IconButton } from "@radix-ui/themes";
|
||||
import { useArmor } from "../../../../../../hooks/useArmor";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
|
||||
export function DynamicArmorSwitcher() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const isDynamicArmorActive = Duel.use((state) =>
|
||||
state.protagonist.consumables.includes(73),
|
||||
state.protagonist.consumables.includes(73)
|
||||
);
|
||||
const { hasDynamicArmor } = useArmor(tank.id);
|
||||
|
||||
@@ -16,9 +16,9 @@ export function DynamicArmorSwitcher() {
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
color={isDynamicArmorActive ? undefined : 'gray'}
|
||||
color={isDynamicArmorActive ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
onClick={() => {
|
||||
mutateDuel((draft) => {
|
||||
if (draft.protagonist.consumables.includes(73)) {
|
||||
@@ -32,17 +32,17 @@ export function DynamicArmorSwitcher() {
|
||||
}
|
||||
}
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="Dynamic Armor"
|
||||
src={asset('icons/consumables/73.webp')}
|
||||
src={asset("icons/consumables/73.webp")}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
+41
-41
@@ -1,44 +1,44 @@
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Checkbox, Flex, Text } from '@radix-ui/themes';
|
||||
import { Suspense } from 'react';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
|
||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
||||
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
|
||||
import { DynamicArmorSwitcher } from './DynamicArmorSwitcher';
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Checkbox, Flex, Text } from "@radix-ui/themes";
|
||||
import { Suspense } from "react";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
|
||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||
import type { ThicknessRange } from "../../../../../Armor/components/StaticArmor";
|
||||
import { DynamicArmorSwitcher } from "./DynamicArmorSwitcher";
|
||||
|
||||
type ThicknessesProps = MaybeSkeletonComponentProps & {
|
||||
thicknessRange: ThicknessRange;
|
||||
};
|
||||
|
||||
export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
const display = TankopediaEphemeral.use((state) => state.display);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const showExternalModules = TankopediaPersistent.use(
|
||||
(state) => state.showExternalModules,
|
||||
(state) => state.showExternalModules
|
||||
);
|
||||
const showSpacedArmor = TankopediaPersistent.use(
|
||||
(state) => state.showSpacedArmor,
|
||||
(state) => state.showSpacedArmor
|
||||
);
|
||||
const showPrimaryArmor = TankopediaPersistent.use(
|
||||
(state) => state.showPrimaryArmor,
|
||||
(state) => state.showPrimaryArmor
|
||||
);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
|
||||
const editStatic = Tankopedia.use((state) => state.editStatic);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position="absolute"
|
||||
right={display === TankopediaDisplay.StaticArmor ? '0' : '-6rem'}
|
||||
right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"}
|
||||
top="50%"
|
||||
mr="4"
|
||||
style={{
|
||||
transform: 'translateY(-50%)',
|
||||
userSelect: 'none',
|
||||
transitionDuration: '200ms',
|
||||
transform: "translateY(-50%)",
|
||||
userSelect: "none",
|
||||
transitionDuration: "200ms",
|
||||
}}
|
||||
direction="column"
|
||||
gap="5"
|
||||
@@ -48,12 +48,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showPrimaryArmor = !draft.showPrimaryArmor;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -68,7 +68,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
{strings.website.tools.tankopedia.sandbox.static.primary}
|
||||
</Text>
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, ['0'])}
|
||||
{literals(strings.common.units.mm, ["0"])}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -76,10 +76,10 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
align="center"
|
||||
width="1rem"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
borderRadius: "var(--radius-full)",
|
||||
overflow: "hidden",
|
||||
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 />}
|
||||
@@ -89,12 +89,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
<Flex
|
||||
height="64px"
|
||||
gap="2"
|
||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
|
||||
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showSpacedArmor = !draft.showSpacedArmor;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -109,7 +109,7 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
{strings.website.tools.tankopedia.sandbox.static.spaced}
|
||||
</Text>
|
||||
<Text color="gray" size="1">
|
||||
{literals(strings.common.units.mm, ['0'])}
|
||||
{literals(strings.common.units.mm, ["0"])}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -117,8 +117,8 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
align="center"
|
||||
width="1rem"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
borderRadius: "var(--radius-full)",
|
||||
overflow: "hidden",
|
||||
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
|
||||
}}
|
||||
>
|
||||
@@ -128,12 +128,12 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
|
||||
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.showExternalModules = !draft.showExternalModules;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
@@ -149,10 +149,10 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#c0c0c0',
|
||||
color: 'black',
|
||||
borderRadius: "var(--radius-full)",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#c0c0c0",
|
||||
color: "black",
|
||||
}}
|
||||
>
|
||||
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
|
||||
@@ -164,9 +164,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
<Flex align="center" gap="2">
|
||||
<Flex
|
||||
gap="2"
|
||||
style={{ cursor: 'pointer' }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.editStatic = !draft.editStatic;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -184,9 +184,9 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
size="1"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.editStatic = false;
|
||||
draft.model = TankopediaEphemeral.initial.model;
|
||||
draft.model = Tankopedia.initial.model;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+81
-81
@@ -3,8 +3,8 @@ import {
|
||||
resolvePenetrationCoefficient,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
uniqueGuns,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import {
|
||||
CameraIcon,
|
||||
CopyIcon,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
EnterFullScreenIcon,
|
||||
ExitFullScreenIcon,
|
||||
EyeOpenIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
} from "@radix-ui/react-icons";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -25,27 +25,27 @@ import {
|
||||
Tabs,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { Suspense, useState, type RefObject } from 'react';
|
||||
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useFullScreen } from '../../../../../hooks/useFullScreen';
|
||||
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
|
||||
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
|
||||
import { SmallTankIcon } from '../../../../SmallTankIcon';
|
||||
import { TankSearch } from '../../../../TankSearch';
|
||||
import { screenshotReadyEvent } from '../TankSandbox/components/SceneProps';
|
||||
import { CustomShellButton } from './components/CustomShellButton';
|
||||
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
|
||||
import { QuickInputs } from './components/QuickInputs';
|
||||
import { Thicknesses } from './components/Thicknesses';
|
||||
} from "@radix-ui/themes";
|
||||
import { Suspense, useState, type RefObject } from "react";
|
||||
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
|
||||
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||
import { useFullScreen } from "../../../../../hooks/useFullScreen";
|
||||
import { useFullscreenAvailability } from "../../../../../hooks/useFullscreenAvailability";
|
||||
import { useLocale } from "../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
|
||||
import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
|
||||
import type { ThicknessRange } from "../../../../Armor/components/StaticArmor";
|
||||
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
|
||||
import { SmallTankIcon } from "../../../../SmallTankIcon";
|
||||
import { TankSearch } from "../../../../TankSearch";
|
||||
import { screenshotReadyEvent } from "../TankSandbox/components/SceneProps";
|
||||
import { CustomShellButton } from "./components/CustomShellButton";
|
||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||
import { QuickInputs } from "./components/QuickInputs";
|
||||
import { Thicknesses } from "./components/Thicknesses";
|
||||
|
||||
type OptionsProps = MaybeSkeletonComponentProps & {
|
||||
thicknessRange: ThicknessRange;
|
||||
@@ -53,13 +53,13 @@ type OptionsProps = MaybeSkeletonComponentProps & {
|
||||
};
|
||||
|
||||
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
const hasCustomShell = TankopediaEphemeral.use(
|
||||
(state) => state.customShell !== undefined,
|
||||
const hasCustomShell = Tankopedia.use(
|
||||
(state) => state.customShell !== undefined
|
||||
);
|
||||
const display = TankopediaEphemeral.use((state) => state.display);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const isFullScreen = useFullScreen();
|
||||
const advancedHighlighting = TankopediaPersistent.use(
|
||||
(state) => state.advancedHighlighting,
|
||||
(state) => state.advancedHighlighting
|
||||
);
|
||||
const fullScreenAvailable = useFullscreenAvailability(true);
|
||||
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 antagonistTank = Duel.use((state) => state.antagonist.tank);
|
||||
const hasCalibratedShells = useEquipment(103, true);
|
||||
const [tab, setTab] = useState('search');
|
||||
const [tab, setTab] = useState("search");
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const hasEnhancedArmor = useEquipment(110);
|
||||
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -87,21 +87,21 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
gap="2"
|
||||
direction="column"
|
||||
top="50%"
|
||||
right={display === TankopediaDisplay.DynamicArmor ? '3' : '-4rem'}
|
||||
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: 'translateY(-50%)',
|
||||
transitionDuration: '200ms',
|
||||
position: "absolute",
|
||||
transform: "translateY(-50%)",
|
||||
transitionDuration: "200ms",
|
||||
}}
|
||||
align="end"
|
||||
>
|
||||
{!hasCustomShell && (
|
||||
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
||||
<Text color="gray" size={{ initial: "1", sm: "2" }}>
|
||||
{literals(strings.common.units.mm, [
|
||||
(
|
||||
resolvePenetrationCoefficient(
|
||||
hasCalibratedShells,
|
||||
antagonistShell.type,
|
||||
antagonistShell.type
|
||||
) * antagonistShell.penetration.near
|
||||
).toFixed(0),
|
||||
])}
|
||||
@@ -115,14 +115,14 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
radius="full"
|
||||
variant="soft"
|
||||
color="gray"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
style={{ position: 'relative' }}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<Text
|
||||
size="1"
|
||||
color="gray"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
@@ -134,11 +134,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
|
||||
<img
|
||||
alt="Antagonist Gun"
|
||||
src={asset('icons/modules/gun.webp')}
|
||||
src={asset("icons/modules/gun.webp")}
|
||||
style={{
|
||||
width: '65%',
|
||||
height: '65%',
|
||||
objectFit: 'contain',
|
||||
width: "65%",
|
||||
height: "65%",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -160,7 +160,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
selected={gun.id === antagonistGun.id}
|
||||
discriminator={TIER_ROMAN_NUMERALS[gun.tier]}
|
||||
secondaryDiscriminator={
|
||||
<Text style={{ fontSize: '0.75em' }}>
|
||||
<Text style={{ fontSize: "0.75em" }}>
|
||||
{literals(strings.common.units.mm, [
|
||||
gun.shells[0].caliber.toFixed(0),
|
||||
])}
|
||||
@@ -176,7 +176,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Flex
|
||||
direction="column"
|
||||
style={{
|
||||
borderRadius: 'var(--radius-full)',
|
||||
borderRadius: "var(--radius-full)",
|
||||
}}
|
||||
overflow="hidden"
|
||||
>
|
||||
@@ -185,17 +185,17 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
color={
|
||||
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
variant="soft"
|
||||
key={thisShell.id}
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
mutateDuel((draft) => {
|
||||
draft.antagonist.shell = thisShell;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
@@ -205,8 +205,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
alt={unwrap(thisShell.name)}
|
||||
src={asset(`icons/shells/${thisShell.icon}.webp`)}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -218,15 +218,15 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Flex
|
||||
direction="column"
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
borderRadius: 'var(--radius-full)',
|
||||
pointerEvents: "auto",
|
||||
borderRadius: "var(--radius-full)",
|
||||
}}
|
||||
overflow="hidden"
|
||||
>
|
||||
<IconButton
|
||||
color={hasCalibratedShells ? undefined : 'gray'}
|
||||
color={hasCalibratedShells ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
mutateDuel((draft) => {
|
||||
@@ -234,24 +234,24 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
? 0
|
||||
: 1;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="Calibrated Shells"
|
||||
src={asset('icons/equipment/103.webp')}
|
||||
src={asset("icons/equipment/103.webp")}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color={hasEnhancedArmor ? undefined : 'gray'}
|
||||
color={hasEnhancedArmor ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: '2', sm: '3' }}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
mutateDuel((draft) => {
|
||||
@@ -259,17 +259,17 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
? 0
|
||||
: -1;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="Enhanced Armor"
|
||||
src={asset('icons/equipment/110.webp')}
|
||||
src={asset("icons/equipment/110.webp")}
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '50%',
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -286,22 +286,22 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
direction="column"
|
||||
align="center"
|
||||
position="absolute"
|
||||
bottom={revealed ? '4' : '-100%'}
|
||||
bottom={revealed ? "4" : "-100%"}
|
||||
left="50%"
|
||||
style={{ transform: 'translateX(-50%)', transitionDuration: '200ms' }}
|
||||
style={{ transform: "translateX(-50%)", transitionDuration: "200ms" }}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
style={{ transitionDuration: '200ms' }}
|
||||
style={{ transitionDuration: "200ms" }}
|
||||
position="relative"
|
||||
bottom={display === TankopediaDisplay.DynamicArmor ? '0' : '-7rem'}
|
||||
bottom={display === TankopediaDisplay.DynamicArmor ? "0" : "-7rem"}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
gap="2"
|
||||
mb="1"
|
||||
style={{ cursor: 'pointer' }}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.advancedHighlighting = !draft.advancedHighlighting;
|
||||
@@ -338,7 +338,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Tabs.Root
|
||||
value={tab}
|
||||
onValueChange={setTab}
|
||||
style={{ position: 'relative' }}
|
||||
style={{ position: "relative" }}
|
||||
>
|
||||
<TankSearch
|
||||
compact
|
||||
@@ -366,7 +366,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
variant="classic"
|
||||
value={`${disturbed ? display : -1}`}
|
||||
onValueChange={(value) => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.disturbed = true;
|
||||
draft.display = Number(value);
|
||||
});
|
||||
@@ -379,7 +379,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Flex height="100%" align="center">
|
||||
<img
|
||||
src="/assets/images/icons/tankopedia-model.png"
|
||||
style={{ height: '1.25em' }}
|
||||
style={{ height: "1.25em" }}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
@@ -391,7 +391,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Flex height="100%" align="center">
|
||||
<img
|
||||
src="/assets/images/icons/tankopedia-dynamic-armor.png"
|
||||
style={{ height: '1.25em' }}
|
||||
style={{ height: "1.25em" }}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
@@ -403,7 +403,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<Flex height="100%" align="center">
|
||||
<img
|
||||
src="/assets/images/icons/tankopedia-static-armor.png"
|
||||
style={{ height: '1.25em' }}
|
||||
style={{ height: "1.25em" }}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
@@ -457,15 +457,15 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
requestAnimationFrame(() => {
|
||||
if (!canvas.current) return;
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
const anchor = document.createElement("a");
|
||||
|
||||
anchor.setAttribute(
|
||||
'download',
|
||||
`${protagonistTank.name}.png`,
|
||||
"download",
|
||||
`${protagonistTank.name}.png`
|
||||
);
|
||||
anchor.setAttribute(
|
||||
'href',
|
||||
canvas.current.toDataURL('image/png'),
|
||||
"href",
|
||||
canvas.current.toDataURL("image/png")
|
||||
);
|
||||
anchor.click();
|
||||
|
||||
@@ -493,7 +493,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
if (!blob) return;
|
||||
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({ 'image/png': blob }),
|
||||
new ClipboardItem({ "image/png": blob }),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
+10
-10
@@ -1,10 +1,10 @@
|
||||
import { Box } from '@radix-ui/themes';
|
||||
import { Var } from '../../../../../core/radix/var';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import './index.css';
|
||||
import { Box } from "@radix-ui/themes";
|
||||
import { Var } from "../../../../../core/radix/var";
|
||||
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||
import "./index.css";
|
||||
|
||||
export function ScrollHint() {
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
|
||||
if (disturbed) return null;
|
||||
|
||||
@@ -17,9 +17,9 @@ export function ScrollHint() {
|
||||
height="2rem"
|
||||
width="1rem"
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
outline: `2px solid ${Var('gray-11')}`,
|
||||
transform: 'translateX(-50%)',
|
||||
borderRadius: "0.5rem",
|
||||
outline: `2px solid ${Var("gray-11")}`,
|
||||
transform: "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -28,8 +28,8 @@ export function ScrollHint() {
|
||||
height="0.75rem"
|
||||
m="0.125rem"
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
backgroundColor: Var('gray-12'),
|
||||
borderRadius: "50%",
|
||||
backgroundColor: Var("gray-12"),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
+18
-18
@@ -1,17 +1,17 @@
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { Tracker } from './components/Tracker';
|
||||
import { Flex, Heading } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { Tracker } from "./components/Tracker";
|
||||
|
||||
export function Title() {
|
||||
const { unwrap } = useLocale();
|
||||
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 fontSize = revealed
|
||||
? revealed
|
||||
? '2rem'
|
||||
? "2rem"
|
||||
: `min(16vh, ${125 / name.length}vw)`
|
||||
: `min(12vh, ${75 / name.length}vw)`;
|
||||
|
||||
@@ -20,7 +20,7 @@ export function Title() {
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
}}
|
||||
@@ -28,27 +28,27 @@ export function Title() {
|
||||
position="absolute"
|
||||
align="center"
|
||||
justify="center"
|
||||
top={revealed ? '6rem' : '50%'}
|
||||
top={revealed ? "6rem" : "50%"}
|
||||
left="50%"
|
||||
width="100%"
|
||||
height={revealed ? fontSize : '100%'}
|
||||
height={revealed ? fontSize : "100%"}
|
||||
style={{
|
||||
pointerEvents: revealed ? 'none' : undefined,
|
||||
transitionDuration: '1s',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
pointerEvents: revealed ? "none" : undefined,
|
||||
transitionDuration: "1s",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
fontWeight: 900,
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
fontSize,
|
||||
whiteSpace: 'nowrap',
|
||||
whiteSpace: "nowrap",
|
||||
opacity: revealed ? 1 : 0.5,
|
||||
letterSpacing: revealed || !revealed ? 0 : '-0.03em',
|
||||
letterSpacing: revealed || !revealed ? 0 : "-0.03em",
|
||||
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,
|
||||
-webkit-text-stroke 2s,
|
||||
opacity 1s
|
||||
|
||||
+39
-39
@@ -1,15 +1,15 @@
|
||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { invalidate, useFrame, useThree } from '@react-three/fiber';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { PerspectiveCamera, Vector3 } from 'three';
|
||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { I_HAT, J_HAT } from "@blitzkit/core";
|
||||
import { OrbitControls } from "@react-three/drei";
|
||||
import { invalidate, useFrame, useThree } from "@react-three/fiber";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { PerspectiveCamera, Vector3 } from "three";
|
||||
import { OrbitControls as OrbitControlsClass } from "three-stdlib";
|
||||
import { awaitableModelDefinitions } from "../../../../../../core/awaitables/modelDefinitions";
|
||||
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
|
||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||
import { Pose, poseEvent } from "../../../../../../core/blitzkit/pose";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
|
||||
const poseDistances: Record<Pose, number> = {
|
||||
[Pose.HullDown]: 15,
|
||||
@@ -56,23 +56,23 @@ export function Controls({
|
||||
const protagonistHullOrigin = new Vector3(
|
||||
protagonistTrackModelDefinition.origin.x,
|
||||
protagonistTrackModelDefinition.origin.y,
|
||||
-protagonistTrackModelDefinition.origin.z,
|
||||
-protagonistTrackModelDefinition.origin.z
|
||||
);
|
||||
const protagonistTurretOrigin = new Vector3(
|
||||
protagonistModelDefinition.turret_origin.x,
|
||||
protagonistModelDefinition.turret_origin.y,
|
||||
-protagonistModelDefinition.turret_origin.z,
|
||||
-protagonistModelDefinition.turret_origin.z
|
||||
);
|
||||
const protagonistGunOrigin = new Vector3(
|
||||
protagonistTurretModelDefinition.gun_origin.x,
|
||||
protagonistTurretModelDefinition.gun_origin.y,
|
||||
-protagonistTurretModelDefinition.gun_origin.z,
|
||||
-protagonistTurretModelDefinition.gun_origin.z
|
||||
);
|
||||
const antagonistGunHeight =
|
||||
protagonistTrackModelDefinition.origin.y +
|
||||
antagonistModelDefinition.turret_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 gunHeight =
|
||||
protagonistHullOrigin.y +
|
||||
@@ -81,7 +81,7 @@ export function Controls({
|
||||
const inspectModeInitialPosition = new Vector3(
|
||||
-8,
|
||||
naked ? gunHeight + 10 : 2,
|
||||
-13,
|
||||
-13
|
||||
);
|
||||
|
||||
const t0 = useRef(Date.now() / 1000);
|
||||
@@ -100,11 +100,11 @@ export function Controls({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeTankopediaEphemeral = TankopediaEphemeral.on(
|
||||
const unsubscribeTankopediaEphemeral = Tankopedia.on(
|
||||
(state) => state.controlsEnabled,
|
||||
(enabled) => {
|
||||
if (orbitControls.current) orbitControls.current.enabled = enabled;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function handlePoseEvent(event: Pose) {
|
||||
@@ -112,12 +112,12 @@ export function Controls({
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
|
||||
switch (event) {
|
||||
@@ -128,7 +128,7 @@ export function Controls({
|
||||
protagonistGunModelDefinition.pitch,
|
||||
protagonistTurretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
camera.position
|
||||
@@ -140,14 +140,14 @@ export function Controls({
|
||||
new Vector3(
|
||||
0,
|
||||
poseDistances[event] * Math.sin(pitch),
|
||||
poseDistances[event] * -Math.cos(pitch),
|
||||
),
|
||||
poseDistances[event] * -Math.cos(pitch)
|
||||
)
|
||||
);
|
||||
camera.lookAt(
|
||||
protagonistHullOrigin
|
||||
.clone()
|
||||
.add(protagonistTurretOrigin)
|
||||
.add(protagonistGunOrigin),
|
||||
.add(protagonistGunOrigin)
|
||||
);
|
||||
orbitControls.current?.target.set(0, antagonistGunHeight, 0);
|
||||
|
||||
@@ -161,7 +161,7 @@ export function Controls({
|
||||
protagonistGunModelDefinition.pitch,
|
||||
protagonistTurretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
camera.position
|
||||
@@ -173,8 +173,8 @@ export function Controls({
|
||||
new Vector3(
|
||||
0,
|
||||
poseDistances[event] * Math.sin(pitch),
|
||||
poseDistances[event] * -Math.cos(pitch),
|
||||
),
|
||||
poseDistances[event] * -Math.cos(pitch)
|
||||
)
|
||||
);
|
||||
orbitControls.current?.target
|
||||
.set(0, 0, 0)
|
||||
@@ -185,8 +185,8 @@ export function Controls({
|
||||
new Vector3(
|
||||
0,
|
||||
0.5 * poseDistances[event] * Math.sin(pitch),
|
||||
0.5 * poseDistances[event] * -Math.cos(pitch),
|
||||
),
|
||||
0.5 * poseDistances[event] * -Math.cos(pitch)
|
||||
)
|
||||
);
|
||||
|
||||
break;
|
||||
@@ -220,15 +220,15 @@ export function Controls({
|
||||
}
|
||||
|
||||
function disturb() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.disturbed = true;
|
||||
});
|
||||
}
|
||||
|
||||
poseEvent.on(disturb);
|
||||
canvas.addEventListener('pointerdown', disturb);
|
||||
canvas.addEventListener('wheel', handleWheel);
|
||||
document.body.addEventListener('scroll', handleScroll);
|
||||
canvas.addEventListener("pointerdown", disturb);
|
||||
canvas.addEventListener("wheel", handleWheel);
|
||||
document.body.addEventListener("scroll", handleScroll);
|
||||
|
||||
function updateCamera() {
|
||||
if (!orbitControls.current) return;
|
||||
@@ -239,7 +239,7 @@ export function Controls({
|
||||
orbitControls.current.target.set(
|
||||
0,
|
||||
gunHeight * (naked ? 1 / 4 : 3 / 4),
|
||||
0,
|
||||
0
|
||||
);
|
||||
orbitControls.current.enablePan = true;
|
||||
orbitControls.current.enableZoom = true;
|
||||
@@ -251,10 +251,10 @@ export function Controls({
|
||||
updateCamera();
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('pointerdown', disturb);
|
||||
canvas.removeEventListener("pointerdown", disturb);
|
||||
poseEvent.off(disturb);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
document.body.removeEventListener('scroll', handleScroll);
|
||||
canvas.removeEventListener("wheel", handleWheel);
|
||||
document.body.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -266,7 +266,7 @@ export function Controls({
|
||||
enableZoom={zoomable}
|
||||
zoomSpeed={zoomable ? undefined : 0}
|
||||
ref={orbitControls}
|
||||
enabled={TankopediaEphemeral.state.controlsEnabled}
|
||||
enabled={Tankopedia.state.controlsEnabled}
|
||||
enableDamping={false}
|
||||
autoRotateSpeed={1 / 4}
|
||||
/>
|
||||
|
||||
+6
-6
@@ -1,14 +1,14 @@
|
||||
import { invalidate, useThree } from '@react-three/fiber';
|
||||
import { useEffect } from 'react';
|
||||
import type { Fog } from 'three';
|
||||
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from '..';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { invalidate, useThree } from "@react-three/fiber";
|
||||
import { useEffect } from "react";
|
||||
import type { Fog } from "three";
|
||||
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
|
||||
export function InitialFogReveal() {
|
||||
const fog = useThree((state) => state.scene.fog as Fog | null);
|
||||
|
||||
useEffect(() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
|
||||
|
||||
+48
-51
@@ -1,17 +1,17 @@
|
||||
import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber';
|
||||
import { useRef } from 'react';
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
||||
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||
import { useModel } from '../../../../../../hooks/useModel';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { useTankTransform } from '../../../../../../hooks/useTankTransform';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
|
||||
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
||||
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
|
||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
|
||||
import { useModel } from "../../../../../../hooks/useModel";
|
||||
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
|
||||
|
||||
export function TankModel() {
|
||||
const duelStore = Duel.useStore();
|
||||
@@ -34,9 +34,9 @@ export function TankModel() {
|
||||
return (
|
||||
<ModelTankWrapper ref={hullContainer}>
|
||||
{nodes.map((node) => {
|
||||
const isHull = node.name === 'hull';
|
||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||
const isTrack = node.name.startsWith('chassis_track_');
|
||||
const isHull = node.name === "hull";
|
||||
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||
const isTrack = node.name.startsWith("chassis_track_");
|
||||
const isVisible = isHull || isWheel || isTrack;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
@@ -59,19 +59,16 @@ export function TankModel() {
|
||||
}
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
if (
|
||||
isTrack &&
|
||||
TankopediaEphemeral.state.display === TankopediaDisplay.Model
|
||||
) {
|
||||
if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) {
|
||||
position.set(event.clientX, event.clientY);
|
||||
event.stopPropagation();
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
@@ -85,12 +82,12 @@ export function TankModel() {
|
||||
translateTexture(deltaX + deltaY);
|
||||
}
|
||||
function handlePointerUp() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
@@ -110,10 +107,10 @@ export function TankModel() {
|
||||
|
||||
<group ref={turretContainer}>
|
||||
{nodes.map((node) => {
|
||||
const isTurret = node.name.startsWith('turret_');
|
||||
const isTurret = node.name.startsWith("turret_");
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`;
|
||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
@@ -127,27 +124,27 @@ export function TankModel() {
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
|
||||
@@ -161,16 +158,16 @@ export function TankModel() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
@@ -194,10 +191,10 @@ export function TankModel() {
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, '0')}_mask`;
|
||||
.padStart(2, "0")}_mask`;
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`;
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
@@ -210,10 +207,10 @@ export function TankModel() {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
@@ -222,20 +219,20 @@ export function TankModel() {
|
||||
pitch = modelTransformEvent.last!.pitch;
|
||||
yaw = modelTransformEvent.last!.yaw;
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
const duel = duelStore.getState();
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
duel.protagonist.tank.equipment_preset,
|
||||
duel.protagonist.equipmentMatrix,
|
||||
duel.protagonist.equipmentMatrix
|
||||
);
|
||||
const boundingRect = canvas.getBoundingClientRect();
|
||||
delta.set(event.clientX, event.clientY).sub(position);
|
||||
@@ -247,16 +244,16 @@ export function TankModel() {
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
|
||||
+18
-18
@@ -1,14 +1,14 @@
|
||||
import { useRef } from 'react';
|
||||
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from 'three';
|
||||
import { jsxTree } from '../../../../../../../core/blitzkit/jsxTree';
|
||||
import { useModel } from '../../../../../../../hooks/useModel';
|
||||
import { useTankModelDefinition } from '../../../../../../../hooks/useTankModelDefinition';
|
||||
import { useTankTransform } from '../../../../../../../hooks/useTankTransform';
|
||||
import { Duel } from '../../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { ModelTankWrapper } from '../../../../../../Armor/components/ModelTankWrapper';
|
||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||
import vertexShader from './shaders/vertex.glsl?raw';
|
||||
import { useRef } from "react";
|
||||
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from "three";
|
||||
import { jsxTree } from "../../../../../../../core/blitzkit/jsxTree";
|
||||
import { useModel } from "../../../../../../../hooks/useModel";
|
||||
import { useTankModelDefinition } from "../../../../../../../hooks/useTankModelDefinition";
|
||||
import { useTankTransform } from "../../../../../../../hooks/useTankTransform";
|
||||
import { Duel } from "../../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../../stores/tankopedia";
|
||||
import { ModelTankWrapper } from "../../../../../../Armor/components/ModelTankWrapper";
|
||||
import fragmentShader from "./shaders/fragment.glsl?raw";
|
||||
import vertexShader from "./shaders/vertex.glsl?raw";
|
||||
|
||||
const skeletonMaterial = new ShaderMaterial({
|
||||
vertexShader,
|
||||
@@ -33,16 +33,16 @@ export function TransitionSkeleton() {
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
const { gltf } = useModel(protagonist.tank.id);
|
||||
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);
|
||||
|
||||
return (
|
||||
<ModelTankWrapper ref={hullContainer} visible={revealed}>
|
||||
{nodes.map((node) => {
|
||||
const isHull = node.name === 'hull';
|
||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||
const isTrack = node.name.startsWith('chassis_track_');
|
||||
const isHull = node.name === "hull";
|
||||
const isWheel = node.name.startsWith("chassis_wheel_");
|
||||
const isTrack = node.name.startsWith("chassis_track_");
|
||||
const isVisible = isHull || isWheel || isTrack;
|
||||
|
||||
if (!isVisible) return null;
|
||||
@@ -58,7 +58,7 @@ export function TransitionSkeleton() {
|
||||
{nodes.map((node) => {
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`;
|
||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
|
||||
if (!isVisible) return null;
|
||||
@@ -76,10 +76,10 @@ export function TransitionSkeleton() {
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, '0')}_mask`;
|
||||
.padStart(2, "0")}_mask`;
|
||||
const isCurrentGun =
|
||||
node.name ===
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`;
|
||||
`gun_${gunModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
+45
-45
@@ -1,5 +1,5 @@
|
||||
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core';
|
||||
import { invalidate } from '@react-three/fiber';
|
||||
import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
|
||||
import { invalidate } from "@react-three/fiber";
|
||||
import {
|
||||
forwardRef,
|
||||
Suspense,
|
||||
@@ -7,33 +7,33 @@ import {
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Fog } from 'three';
|
||||
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
|
||||
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
||||
import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||
import { Armor } from '../../../../Armor';
|
||||
import { ArmorPlateDisplay } from '../../../../Armor/components/ArmorPlateDisplay';
|
||||
import { ShotDisplay } from '../../../../Armor/components/ShotDisplay';
|
||||
} from "react";
|
||||
import { Fog } from "three";
|
||||
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
||||
import { modelTransformEvent } from "../../../../../core/blitzkit/modelTransform";
|
||||
import { Pose, poseEvent } from "../../../../../core/blitzkit/pose";
|
||||
import { useEquipment } from "../../../../../hooks/useEquipment";
|
||||
import { useTankModelDefinition } from "../../../../../hooks/useTankModelDefinition";
|
||||
import { Duel } from "../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
|
||||
import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
|
||||
import { Armor } from "../../../../Armor";
|
||||
import { ArmorPlateDisplay } from "../../../../Armor/components/ArmorPlateDisplay";
|
||||
import { ShotDisplay } from "../../../../Armor/components/ShotDisplay";
|
||||
import {
|
||||
StaticArmor,
|
||||
type ThicknessRange,
|
||||
} from '../../../../Armor/components/StaticArmor';
|
||||
import { SmartCanvas } from '../../../../SmartCanvas';
|
||||
import { AutoClear } from './components/AutoClear';
|
||||
import { Controls } from './components/Control';
|
||||
import { InitialAligner } from './components/InitialAligner';
|
||||
import { InitialFogReveal } from './components/InitialFogReveal';
|
||||
import { Lighting } from './components/Lighting';
|
||||
import { SceneProps } from './components/SceneProps';
|
||||
import { TankModel } from './components/TankModel';
|
||||
import { TransitionSkeleton } from './components/TransitionSkeleton';
|
||||
} from "../../../../Armor/components/StaticArmor";
|
||||
import { SmartCanvas } from "../../../../SmartCanvas";
|
||||
import { AutoClear } from "./components/AutoClear";
|
||||
import { Controls } from "./components/Control";
|
||||
import { InitialAligner } from "./components/InitialAligner";
|
||||
import { InitialFogReveal } from "./components/InitialFogReveal";
|
||||
import { Lighting } from "./components/Lighting";
|
||||
import { SceneProps } from "./components/SceneProps";
|
||||
import { TankModel } from "./components/TankModel";
|
||||
import { TransitionSkeleton } from "./components/TransitionSkeleton";
|
||||
|
||||
interface TankSandboxProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
@@ -48,7 +48,7 @@ export const fogAnimationTime = 2.5;
|
||||
|
||||
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
({ 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 hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
@@ -58,10 +58,10 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonistTurret.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 hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||
(state) => state.hideTankModelUnderArmor,
|
||||
(state) => state.hideTankModelUnderArmor
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -95,8 +95,8 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
useImperativeHandle(ref, () => canvas.current!, []);
|
||||
|
||||
function handlePointerDown() {
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
function handlePointerMove(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
@@ -104,8 +104,8 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
function handlePointerUp(event: PointerEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -118,7 +118,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -132,7 +132,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
0,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -146,7 +146,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
0,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -168,7 +168,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
modelTransformEvent.last!.yaw,
|
||||
gunModelDefinition.pitch,
|
||||
turretModelDefinition.yaw,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasImprovedVerticalStabilizer
|
||||
);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
@@ -176,13 +176,13 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
|
||||
useEffect(() => {
|
||||
if (display !== TankopediaDisplay.DynamicArmor) {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (display !== TankopediaDisplay.StaticArmor) {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}
|
||||
@@ -200,16 +200,16 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
shadows="soft"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMissed={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
draft.highlightArmor = undefined;
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : '100%',
|
||||
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : '100%',
|
||||
outline: naked ? '1rem red solid' : undefined,
|
||||
userSelect: "none",
|
||||
width: naked ? BLITZKIT_TANK_ICON_SIZE.width : "100%",
|
||||
height: naked ? BLITZKIT_TANK_ICON_SIZE.height : "100%",
|
||||
outline: naked ? "1rem red solid" : undefined,
|
||||
}}
|
||||
>
|
||||
{!naked && <SceneProps />}
|
||||
@@ -236,5 +236,5 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
<InitialAligner />
|
||||
</SmartCanvas>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,28 +1,28 @@
|
||||
import { Box, Flex } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Suspense, useEffect, useMemo, useRef } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { useFullScreen } from '../../../hooks/useFullScreen';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
||||
import { Options } from './components/Options';
|
||||
import { TankSandbox } from './components/TankSandbox';
|
||||
import { Title } from './components/TankSandbox/Title';
|
||||
import { Box, Flex } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { Suspense, useEffect, useMemo, useRef } from "react";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import { useFullScreen } from "../../../hooks/useFullScreen";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
import type { ThicknessRange } from "../../Armor/components/StaticArmor";
|
||||
import { Options } from "./components/Options";
|
||||
import { TankSandbox } from "./components/TankSandbox";
|
||||
import { Title } from "./components/TankSandbox/Title";
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const isFullScreen = useFullScreen();
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const thicknessRange = useMemo(() => {
|
||||
const entries = Object.values(tankDefinitions.tanks);
|
||||
const filtered = entries.filter(
|
||||
(thisTank) => thisTank.tier === protagonist.tier,
|
||||
(thisTank) => thisTank.tier === protagonist.tier
|
||||
);
|
||||
const value =
|
||||
(filtered.reduce((accumulator, thisTank) => {
|
||||
@@ -41,7 +41,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (disturbed) {
|
||||
document.body.classList.remove('no-navbar');
|
||||
document.body.classList.remove("no-navbar");
|
||||
}
|
||||
}, [disturbed]);
|
||||
|
||||
@@ -54,15 +54,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
mutateDuel((draft) => {
|
||||
draft.antagonist.shell = shells[index];
|
||||
});
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -70,7 +70,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
overflow="hidden"
|
||||
justify="center"
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
position="relative"
|
||||
>
|
||||
@@ -82,36 +82,36 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
left="0"
|
||||
style={{
|
||||
opacity: disturbed ? 0 : 1,
|
||||
background: 'black',
|
||||
transitionDuration: '1s',
|
||||
background: "black",
|
||||
transitionDuration: "1s",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <ScrollHint /> */}
|
||||
|
||||
<Flex
|
||||
direction={{ initial: 'column-reverse', md: 'row' }}
|
||||
direction={{ initial: "column-reverse", md: "row" }}
|
||||
style={{
|
||||
zIndex: isFullScreen ? 2 : undefined,
|
||||
transitionDuration: '1s',
|
||||
background: isFullScreen ? 'black' : undefined,
|
||||
transitionDuration: "1s",
|
||||
background: isFullScreen ? "black" : undefined,
|
||||
}}
|
||||
height={
|
||||
isFullScreen
|
||||
? '100vh'
|
||||
? "100vh"
|
||||
: disturbed
|
||||
? 'calc(100svh - 6rem)'
|
||||
? "calc(100svh - 6rem)"
|
||||
: {
|
||||
initial: '28rem',
|
||||
md: 'calc(100svh - 12rem)',
|
||||
initial: "28rem",
|
||||
md: "calc(100svh - 12rem)",
|
||||
}
|
||||
}
|
||||
maxWidth={isFullScreen ? undefined : '120rem'}
|
||||
maxWidth={isFullScreen ? undefined : "120rem"}
|
||||
flexGrow="1"
|
||||
width={isFullScreen ? '100vw' : undefined}
|
||||
position={isFullScreen ? 'fixed' : 'relative'}
|
||||
top={isFullScreen ? '0' : undefined}
|
||||
left={isFullScreen ? '0' : undefined}
|
||||
width={isFullScreen ? "100vw" : undefined}
|
||||
position={isFullScreen ? "fixed" : "relative"}
|
||||
top={isFullScreen ? "0" : undefined}
|
||||
left={isFullScreen ? "0" : undefined}
|
||||
>
|
||||
<Box
|
||||
className="tank-sandbox-container"
|
||||
@@ -126,14 +126,14 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
height="100%"
|
||||
position="relative"
|
||||
style={{
|
||||
transitionDuration: '1s',
|
||||
transitionDuration: "1s",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transitionDuration: '2s',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transitionDuration: "2s",
|
||||
opacity: revealed ? 1 : 0,
|
||||
filter: disturbed
|
||||
? undefined
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { asset, formatCompact, TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { Box, Flex, Skeleton, Text } from '@radix-ui/themes';
|
||||
import { awaitableAverageDefinitions } from '../../../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../../hooks/useLocale';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||
import { LinkI18n } from '../../../LinkI18n';
|
||||
import { asset, formatCompact, TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Box, Flex, Skeleton, Text } from "@radix-ui/themes";
|
||||
import { awaitableAverageDefinitions } from "../../../../core/awaitables/averageDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
|
||||
import { useLocale } from "../../../../hooks/useLocale";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
|
||||
import { LinkI18n } from "../../../LinkI18n";
|
||||
|
||||
type NodeProps = MaybeSkeletonComponentProps & {
|
||||
id: number;
|
||||
@@ -21,7 +21,7 @@ const [tankDefinitions, averageDefinitions] = await Promise.all([
|
||||
|
||||
export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
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 nextTanks = nextIds?.map((id) => tankDefinitions.tanks[id]);
|
||||
const thisTankXp =
|
||||
@@ -36,7 +36,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
? 0
|
||||
: (tank.research_cost.research_cost_type!.value as number) /
|
||||
xpMultiplier),
|
||||
0,
|
||||
0
|
||||
);
|
||||
const averages = averageDefinitions.averages[tank.id]?.mu;
|
||||
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}`}
|
||||
key={id}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
position: 'relative',
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
mx={highlight ? '2' : '0'}
|
||||
mx={highlight ? "2" : "0"}
|
||||
style={{
|
||||
background: highlight
|
||||
? `url(${asset(`flags/fade_small/${tank.nation}.webp`)})`
|
||||
: undefined,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
boxShadow: highlight ? 'var(--shadow-4)' : undefined,
|
||||
borderRadius: 'var(--radius-4)',
|
||||
overflow: 'hidden',
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
boxShadow: highlight ? "var(--shadow-4)" : undefined,
|
||||
borderRadius: "var(--radius-4)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
py="4"
|
||||
px={highlight ? '4' : '2'}
|
||||
px={highlight ? "4" : "2"}
|
||||
style={{
|
||||
backdropFilter: highlight ? 'brightness(0.25)' : undefined,
|
||||
WebkitBackdropFilter: highlight ? 'brightness(0.25)' : undefined,
|
||||
backdropFilter: highlight ? "brightness(0.25)" : undefined,
|
||||
WebkitBackdropFilter: highlight ? "brightness(0.25)" : undefined,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
@@ -83,7 +83,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
width={64}
|
||||
height={64}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -99,12 +99,12 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="XP"
|
||||
src={asset('icons/currencies/xp.webp')}
|
||||
src={asset("icons/currencies/xp.webp")}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
objectFit: "contain",
|
||||
objectPosition: "center",
|
||||
}}
|
||||
/>
|
||||
{formatCompact(locale, thisTankXp)}
|
||||
@@ -114,12 +114,12 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="Silver"
|
||||
src={asset('icons/currencies/silver.webp')}
|
||||
src={asset("icons/currencies/silver.webp")}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
objectFit: "contain",
|
||||
objectPosition: "center",
|
||||
}}
|
||||
/>
|
||||
{formatCompact(locale, tank.price.value)}
|
||||
@@ -135,7 +135,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
? strings.website.tools.tankopedia.tech_tree.battle
|
||||
: literals(
|
||||
strings.website.tools.tankopedia.tech_tree.battles,
|
||||
[`${games}`],
|
||||
[`${games}`]
|
||||
))}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { asset, TankType } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { Duel } from '../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
import { Arrow } from './components/Arrow';
|
||||
import { Node } from './components/Node';
|
||||
import { asset, TankType } from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import { Flex, Heading, IconButton, ScrollArea, Text } from "@radix-ui/themes";
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
import { Arrow } from "./components/Arrow";
|
||||
import { Node } from "./components/Node";
|
||||
|
||||
type Line = number[];
|
||||
|
||||
@@ -20,7 +20,7 @@ export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
|
||||
export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { locale, strings } = useLocale();
|
||||
const master = Duel.use((state) => state.protagonist.tank);
|
||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||
const xpMultiplier = Tankopedia.use((state) => state.xpMultiplier);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const lines = useMemo(() => {
|
||||
function extend(line: Line): Line[] {
|
||||
@@ -32,8 +32,8 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
if (root.ancestors.length === 1 || root.tier === 2) {
|
||||
line.push(
|
||||
root.ancestors.find(
|
||||
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated,
|
||||
) ?? root.ancestors[0],
|
||||
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated
|
||||
) ?? root.ancestors[0]
|
||||
);
|
||||
return extend(line);
|
||||
} else {
|
||||
@@ -52,7 +52,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const [lineIndex, setLineIndex] = useState(0);
|
||||
const line = useMemo(
|
||||
() => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()),
|
||||
[master, lineIndex],
|
||||
[master, lineIndex]
|
||||
);
|
||||
const totalXp = line.reduce(
|
||||
(xp, id) =>
|
||||
@@ -62,11 +62,11 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
? 0
|
||||
: (tankDefinitions.tanks[id].research_cost.research_cost_type!
|
||||
.value as number)),
|
||||
0,
|
||||
0
|
||||
);
|
||||
const totalCredits = line.reduce(
|
||||
(credits, id) => credits + tankDefinitions.tanks[id].price.value,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -87,7 +87,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
direction="column"
|
||||
align="center"
|
||||
gap="0"
|
||||
style={{ backgroundColor: 'var(--color-surface)' }}
|
||||
style={{ backgroundColor: "var(--color-surface)" }}
|
||||
py="6"
|
||||
>
|
||||
<Flex direction="column" align="center">
|
||||
@@ -101,12 +101,12 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="XP"
|
||||
src={asset('icons/currencies/xp.webp')}
|
||||
src={asset("icons/currencies/xp.webp")}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
objectFit: "contain",
|
||||
objectPosition: "center",
|
||||
}}
|
||||
/>
|
||||
{Math.round(totalXp / xpMultiplier).toLocaleString(locale)}
|
||||
@@ -117,12 +117,12 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="Silver"
|
||||
src={asset('icons/currencies/silver.webp')}
|
||||
src={asset("icons/currencies/silver.webp")}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
objectFit: "contain",
|
||||
objectPosition: "center",
|
||||
}}
|
||||
/>
|
||||
{totalCredits.toLocaleString(locale)}
|
||||
@@ -139,15 +139,15 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<Flex
|
||||
key={multiplier}
|
||||
style={{
|
||||
background: 'url(/assets/images/icons/xp-multiplier.png)',
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
background: "url(/assets/images/icons/xp-multiplier.png)",
|
||||
backgroundSize: "contain",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "center",
|
||||
opacity: selected ? 1 : 0.5,
|
||||
cursor: 'pointer',
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
TankopediaEphemeral.mutate((draft) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.xpMultiplier = multiplier;
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TankopediaEphemeral } from '../stores/tankopediaEphemeral';
|
||||
import { Tankopedia } from "../stores/tankopedia";
|
||||
|
||||
export function useTankModelDefinition() {
|
||||
return TankopediaEphemeral.use((state) => state.model);
|
||||
return Tankopedia.use((state) => state.model);
|
||||
}
|
||||
@@ -1,26 +1,25 @@
|
||||
import { createDefaultSkills } from '@blitzkit/core';
|
||||
import { Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CompareTable } from '../../../components/Compare/CompareTable';
|
||||
import { Controls } from '../../../components/Compare/Controls';
|
||||
import { DamageWarning } from '../../../components/DamageWarning';
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import { awaitableEquipmentDefinitions } from '../../../core/awaitables/equipmentDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions';
|
||||
import { awaitableProvisionDefinitions } from '../../../core/awaitables/provisionDefinitions';
|
||||
import { awaitableSkillDefinitions } from '../../../core/awaitables/skillDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { tankCharacteristics } from '../../../core/blitzkit/tankCharacteristics';
|
||||
import { tankToCompareMember } from '../../../core/blitzkit/tankToCompareMember';
|
||||
import { createDefaultSkills } from "@blitzkit/core";
|
||||
import { Flex, Heading, Text } from "@radix-ui/themes";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { CompareTable } from "../../../components/Compare/CompareTable";
|
||||
import { Controls } from "../../../components/Compare/Controls";
|
||||
import { DamageWarning } from "../../../components/DamageWarning";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import { awaitableEquipmentDefinitions } from "../../../core/awaitables/equipmentDefinitions";
|
||||
import { awaitableModelDefinitions } from "../../../core/awaitables/modelDefinitions";
|
||||
import { awaitableProvisionDefinitions } from "../../../core/awaitables/provisionDefinitions";
|
||||
import { awaitableSkillDefinitions } from "../../../core/awaitables/skillDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import { tankCharacteristics } from "../../../core/blitzkit/tankCharacteristics";
|
||||
import { tankToCompareMember } from "../../../core/blitzkit/tankToCompareMember";
|
||||
import {
|
||||
type LocaleAcceptorProps,
|
||||
LocaleProvider,
|
||||
useLocale,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { App } from '../../../stores/app';
|
||||
import { CompareEphemeral } from '../../../stores/compareEphemeral';
|
||||
import { ComparePersistent } from '../../../stores/comparePersistent';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
} from "../../../hooks/useLocale";
|
||||
import { App } from "../../../stores/app";
|
||||
import { CompareEphemeral } from "../../../stores/compareEphemeral";
|
||||
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||
|
||||
const [
|
||||
modelDefinitions,
|
||||
@@ -37,17 +36,13 @@ const [
|
||||
]);
|
||||
|
||||
export function Page({ locale }: LocaleAcceptorProps) {
|
||||
CompareEphemeral.useInitialization(createDefaultSkills(skillDefinitions));
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
<App.Provider>
|
||||
<TankopediaPersistent.Provider>
|
||||
<ComparePersistent.Provider>
|
||||
<CompareEphemeral.Provider
|
||||
data={createDefaultSkills(skillDefinitions)}
|
||||
>
|
||||
<Content />
|
||||
</CompareEphemeral.Provider>
|
||||
</ComparePersistent.Provider>
|
||||
<Content />
|
||||
</TankopediaPersistent.Provider>
|
||||
</App.Provider>
|
||||
</LocaleProvider>
|
||||
@@ -72,18 +67,15 @@ function Content() {
|
||||
stockTurret: thisMember.tank.turrets[0],
|
||||
applyReactiveArmor: members.some(
|
||||
(member) =>
|
||||
member.key !== thisMember.key &&
|
||||
member.consumables.includes(33),
|
||||
member.key !== thisMember.key && member.consumables.includes(33)
|
||||
),
|
||||
applyDynamicArmor: members.some(
|
||||
(member) =>
|
||||
member.key !== thisMember.key &&
|
||||
member.consumables.includes(73),
|
||||
member.key !== thisMember.key && member.consumables.includes(73)
|
||||
),
|
||||
applySpallLiner: members.some(
|
||||
(member) =>
|
||||
member.key !== thisMember.key &&
|
||||
member.provisions.includes(101),
|
||||
member.key !== thisMember.key && member.provisions.includes(101)
|
||||
),
|
||||
assaultDistance: thisMember.assaultDistance,
|
||||
},
|
||||
@@ -91,22 +83,21 @@ function Content() {
|
||||
equipmentDefinitions: equipmentDefinitions,
|
||||
provisionDefinitions: provisionDefinitions,
|
||||
tankModelDefinition: modelDefinitions.models[thisMember.tank.id],
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
[members, crewSkills],
|
||||
[members, crewSkills]
|
||||
);
|
||||
const mutateCompareEphemeral = CompareEphemeral.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
const tanksQuery = search.get('tanks');
|
||||
const tanksQuery = search.get("tanks");
|
||||
|
||||
if (tanksQuery === null) return;
|
||||
|
||||
const tanks = tanksQuery.split(',').map(Number);
|
||||
const tanks = tanksQuery.split(",").map(Number);
|
||||
|
||||
mutateCompareEphemeral((draft) => {
|
||||
CompareEphemeral.mutate((draft) => {
|
||||
draft.members = [];
|
||||
|
||||
tanks.forEach((id) => {
|
||||
@@ -122,12 +113,12 @@ function Content() {
|
||||
useEffect(() => {
|
||||
const search = new URLSearchParams(window.location.search);
|
||||
|
||||
search.set('tanks', members.map((member) => member.tank.id).join(','));
|
||||
search.set("tanks", members.map((member) => member.tank.id).join(","));
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`${window.location.pathname}?${search.toString()}`,
|
||||
"",
|
||||
`${window.location.pathname}?${search.toString()}`
|
||||
);
|
||||
}, [members]);
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { GalleryList } from '../../../components/Gallery/List';
|
||||
import { GallerySearch } from '../../../components/Gallery/Search';
|
||||
import { PageWrapper } from '../../../components/PageWrapper';
|
||||
import { GalleryList } from "../../../components/Gallery/List";
|
||||
import { GallerySearch } from "../../../components/Gallery/Search";
|
||||
import { PageWrapper } from "../../../components/PageWrapper";
|
||||
import {
|
||||
LocaleProvider,
|
||||
type LocaleAcceptorProps,
|
||||
} from '../../../hooks/useLocale';
|
||||
import { GalleryEphemeral } from '../../../stores/galleryEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
} from "../../../hooks/useLocale";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
|
||||
export function Page({
|
||||
locale,
|
||||
@@ -22,10 +21,8 @@ export function Page({
|
||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<PageWrapper maxWidth="80rem" color="gold">
|
||||
<GalleryEphemeral.Provider>
|
||||
<GallerySearch skeleton={skeleton} />
|
||||
<GalleryList skeleton={skeleton} />
|
||||
</GalleryEphemeral.Provider>
|
||||
<GallerySearch skeleton={skeleton} />
|
||||
<GalleryList skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
useLocale,
|
||||
} from "../../../hooks/useLocale";
|
||||
import { Duel } from "../../../stores/duel";
|
||||
import { Guess } from "../../../stores/guessEphemeral";
|
||||
import { TankopediaEphemeral } from "../../../stores/tankopediaEphemeral";
|
||||
import { Guess, GuessState } from "../../../stores/guess";
|
||||
import { Tankopedia } from "../../../stores/tankopedia";
|
||||
|
||||
const [tankDefinitions, modelDefinitions, provisionDefinitions] =
|
||||
await Promise.all([
|
||||
@@ -42,7 +42,7 @@ function Container() {
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const model = modelDefinitions.models[tank.id];
|
||||
|
||||
TankopediaEphemeral.useInitialization(model);
|
||||
Tankopedia.useInitialization(model);
|
||||
|
||||
return (
|
||||
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
|
||||
@@ -55,7 +55,7 @@ function Content() {
|
||||
const { unwrap } = useLocale();
|
||||
const tank = Guess.use((state) => state.tank);
|
||||
const guessState = Guess.use((state) => state.guessState);
|
||||
const isRevealed = guessState !== null;
|
||||
const isRevealed = guessState !== GuessState.NotGuessed;
|
||||
const name = unwrap(tank.name);
|
||||
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
||||
const transitionDuration = isRevealed ? "2s" : undefined;
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "../../../../hooks/useLocale";
|
||||
import { App } from "../../../../stores/app";
|
||||
import { Duel } from "../../../../stores/duel";
|
||||
import { TankopediaEphemeral } from "../../../../stores/tankopediaEphemeral";
|
||||
import { Tankopedia } from "../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../types/maybeSkeletonComponentProps";
|
||||
import type { TankGuide } from "./index.astro";
|
||||
@@ -39,7 +39,7 @@ export function Page({ id, skeleton, locale, guide }: PageProps) {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const model = modelDefinitions.models[id];
|
||||
|
||||
TankopediaEphemeral.useInitialization(model);
|
||||
Tankopedia.useInitialization(model);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { awaitableTankDefinitions } from "../../../../../core/awaitables/tankDef
|
||||
import { LocaleProvider } from "../../../../../hooks/useLocale";
|
||||
import { App } from "../../../../../stores/app";
|
||||
import { Duel } from "../../../../../stores/duel";
|
||||
import { TankopediaEphemeral } from "../../../../../stores/tankopediaEphemeral";
|
||||
import { Tankopedia } from "../../../../../stores/tankopedia";
|
||||
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
|
||||
|
||||
const [provisionDefinitions, tankDefinitions, modelDefinitions] =
|
||||
@@ -23,7 +23,7 @@ export function Page({ id }: PageProps) {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const model = modelDefinitions.models[id];
|
||||
|
||||
TankopediaEphemeral.useInitialization(model);
|
||||
Tankopedia.useInitialization(model);
|
||||
|
||||
return (
|
||||
<LocaleProvider locale="en">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
import type { DuelMember } from './duel';
|
||||
import { Varuna } from "varuna";
|
||||
import type { DuelMember } from "./duel";
|
||||
|
||||
export interface CompareMember extends DuelMember {
|
||||
key: string;
|
||||
@@ -10,15 +9,15 @@ export interface CompareEphemeral {
|
||||
crewSkills: Record<string, number>;
|
||||
members: CompareMember[];
|
||||
sorting?: {
|
||||
direction: 'ascending' | 'descending';
|
||||
direction: "ascending" | "descending";
|
||||
by: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const CompareEphemeral = createContextualStore(
|
||||
(crewSkills: Record<number, number>) =>
|
||||
create<CompareEphemeral>()(() => ({
|
||||
crewSkills,
|
||||
members: [],
|
||||
})),
|
||||
);
|
||||
export const CompareEphemeral = new Varuna<
|
||||
CompareEphemeral,
|
||||
Record<string, number>
|
||||
>((crewSkills) => ({
|
||||
crewSkills,
|
||||
members: [],
|
||||
}));
|
||||
@@ -1,23 +1,12 @@
|
||||
import * as lodash from 'lodash-es';
|
||||
import { create } from 'zustand';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export type DeltaMode = "none" | "percentage" | "absolute";
|
||||
|
||||
export type DeltaMode = 'none' | 'percentage' | 'absolute';
|
||||
export interface ComparePersistent {
|
||||
deltaMode: DeltaMode;
|
||||
}
|
||||
|
||||
export const ComparePersistent = createContextualStore(() =>
|
||||
create<ComparePersistent>()(
|
||||
persist(
|
||||
subscribeWithSelector<ComparePersistent>(() => ({
|
||||
deltaMode: 'none',
|
||||
})),
|
||||
{
|
||||
name: 'compare',
|
||||
merge: (a, b) => lodash.merge(b, a),
|
||||
},
|
||||
),
|
||||
),
|
||||
export const ComparePersistent = new Varuna<ComparePersistent>(
|
||||
{ deltaMode: "none" },
|
||||
"compare-2"
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type IndividualTankStats } from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
type EmbedBreakdown = Record<number, IndividualTankStats[]>;
|
||||
|
||||
export const EmbedBreakdown = new Varuna<EmbedBreakdown>(
|
||||
{},
|
||||
"embed-breakdown-2"
|
||||
);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { type IndividualTankStats } from '@blitzkit/core';
|
||||
import * as lodash from 'lodash-es';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
|
||||
type EmbedBreakdownPersistentStore = Record<number, IndividualTankStats[]>;
|
||||
|
||||
export const EmbedBreakdownPersistent = createContextualStore(() =>
|
||||
create<EmbedBreakdownPersistentStore>()(
|
||||
persist(() => ({}), {
|
||||
name: 'embed-breakdown',
|
||||
merge: (a, b) => lodash.merge(b, a),
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
interface Gallery {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const Gallery = new Varuna<Gallery>({});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||
|
||||
interface GalleryEphemeral {
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const GalleryEphemeral = createContextualStore(() => {
|
||||
return create<GalleryEphemeral>()(
|
||||
subscribeWithSelector<GalleryEphemeral>(() => ({})),
|
||||
);
|
||||
});
|
||||
+8
-5
@@ -1,19 +1,22 @@
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export enum GuessState {
|
||||
Correct,
|
||||
Incorrect,
|
||||
NotGuessed,
|
||||
}
|
||||
|
||||
export interface Guess {
|
||||
tank: TankDefinition;
|
||||
/**
|
||||
* true = correct, false = incorrect, null = not guessed
|
||||
*/
|
||||
guessState: boolean | null;
|
||||
guessState: GuessState;
|
||||
totalGuesses: number;
|
||||
correctGuesses: number;
|
||||
}
|
||||
|
||||
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
||||
tank,
|
||||
guessState: null,
|
||||
guessState: GuessState.NotGuessed,
|
||||
totalGuesses: 0,
|
||||
correctGuesses: 0,
|
||||
}));
|
||||
+15
-18
@@ -2,21 +2,21 @@ import {
|
||||
createDefaultSkills,
|
||||
ShellDefinition,
|
||||
type ModelDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import type { Vector3 } from 'three';
|
||||
import { Varuna } from 'varuna';
|
||||
import type { ArmorType } from '../components/Armor/components/SpacedArmorScene';
|
||||
import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent';
|
||||
import type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection';
|
||||
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions';
|
||||
import { TankopediaDisplay } from './tankopediaPersistent/constants';
|
||||
} from "@blitzkit/core";
|
||||
import type { Vector3 } from "three";
|
||||
import { Varuna } from "varuna";
|
||||
import type { ArmorType } from "../components/Armor/components/SpacedArmorScene";
|
||||
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
|
||||
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
|
||||
import { awaitableSkillDefinitions } from "../core/awaitables/skillDefinitions";
|
||||
import { TankopediaDisplay } from "./tankopediaPersistent/constants";
|
||||
|
||||
export interface ShotLayerBase {
|
||||
index: number;
|
||||
thickness: number;
|
||||
point: Vector3;
|
||||
surfaceNormal: Vector3;
|
||||
status: 'blocked' | 'penetration' | 'ricochet';
|
||||
status: "blocked" | "penetration" | "ricochet";
|
||||
}
|
||||
|
||||
interface ShotLayerExternal extends ShotLayerBase {
|
||||
@@ -32,7 +32,7 @@ export interface ShotLayerNonExternal extends ShotLayerBase {
|
||||
|
||||
export interface ShotLayerGap {
|
||||
type: null;
|
||||
status: 'penetration' | 'blocked';
|
||||
status: "penetration" | "blocked";
|
||||
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 =
|
||||
| {
|
||||
@@ -63,10 +63,10 @@ export type ArmorPiercingLayer =
|
||||
nominal: number;
|
||||
angled: number;
|
||||
ricochet: boolean;
|
||||
type: 'core' | 'spaced' | 'external';
|
||||
type: "core" | "spaced" | "external";
|
||||
}
|
||||
| {
|
||||
type: 'gap';
|
||||
type: "gap";
|
||||
gap: number;
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ export enum TankopediaRelativeAgainst {
|
||||
All,
|
||||
}
|
||||
|
||||
interface TankopediaEphemeral {
|
||||
interface Tankopedia {
|
||||
disturbed: boolean;
|
||||
revealed: boolean;
|
||||
shot?: Shot;
|
||||
@@ -107,10 +107,7 @@ interface TankopediaEphemeral {
|
||||
|
||||
const skillDefinitions = await awaitableSkillDefinitions;
|
||||
|
||||
export const TankopediaEphemeral = new Varuna<
|
||||
TankopediaEphemeral,
|
||||
ModelDefinition
|
||||
>((model) => ({
|
||||
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||
disturbed: false,
|
||||
revealed: false,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
Reference in new issue
Block a user