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