feat: armor modes

This commit is contained in:
TrèsAbhi committed 2024-09-22 21:12:18 -05:00
1 parent ffa1395b50
commit f1410cf321
42 files changed
+290 -261

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
export * from './assertSecret'; export * from './assertSecret';
export * from './asset'; export * from './asset';
// export * from './availableProvisions'; export * from './availableProvisions';
// export * from './averageDefinitions'; // export * from './averageDefinitions';
// export * from './camouflageDefinitions'; // export * from './camouflageDefinitions';
// export * from './checkConsumableProvisionInclusivity'; // export * from './checkConsumableProvisionInclusivity';
@@ -11,7 +11,7 @@ export * from './asset';
// export * from './degressiveStat'; // export * from './degressiveStat';
// export * from './discoveredIdDefinitions'; // export * from './discoveredIdDefinitions';
// export * from './emblemURL'; // export * from './emblemURL';
// export * from './equipmentDefinitions'; export * from './equipmentDefinitions';
export * from './eventManager'; export * from './eventManager';
// export * from './fetchCdonLz4'; // export * from './fetchCdonLz4';
// export * from './flagUserActivity'; // export * from './flagUserActivity';
@@ -21,7 +21,7 @@ export * from './gameDefinitions';
// export * from './getArchivedRatingLeaderboard'; // export * from './getArchivedRatingLeaderboard';
// export * from './getArchivedRatingMidnightLeaderboard'; // export * from './getArchivedRatingMidnightLeaderboard';
// export * from './getTimeDaysAgo'; // export * from './getTimeDaysAgo';
// export * from './isExplosive'; export * from './isExplosive';
// export * from './linkBlitzAndDiscord'; // export * from './linkBlitzAndDiscord';
// export * from './mapDefinitions'; // export * from './mapDefinitions';
export * from './modelDefinitions'; export * from './modelDefinitions';
@@ -42,7 +42,7 @@ export * from './tankDefinitions';
export * from './tankIcon'; export * from './tankIcon';
export * from './tankNames'; export * from './tankNames';
export * from './unionBoundingBox'; export * from './unionBoundingBox';
// export * from './uniqueGuns'; export * from './uniqueGuns';
// export * from './unwrapBlitzkitResponse'; // export * from './unwrapBlitzkitResponse';
// export * from './votes'; // export * from './votes';
// export * from './withCORSProxy'; // export * from './withCORSProxy';
+2 -6
View File
@@ -13,7 +13,6 @@
"@blitzkit/core": "workspace:*", "@blitzkit/core": "workspace:*",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"@nivo/bar": "^0.87.0", "@nivo/bar": "^0.87.0",
"three": "0.159.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next": "^14.2.7", "next": "^14.2.7",
"three-stdlib": "^2.32.2", "three-stdlib": "^2.32.2",
@@ -26,13 +25,10 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-fuzzysort": "^1.0.1", "react-fuzzysort": "^1.0.1",
"immer": "^10.1.1", "immer": "^10.1.1",
"@vanilla-extract/dynamic": "^2.1.2", "@vanilla-extract/dynamic": "^2.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react-dom": "^18",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@types/three": "^0.160.0", "@vanilla-extract/next-plugin": "^2.4.5"
"@vanilla-extract/next-plugin": "^2.4.5",
"@types/react": "^18.3.5"
} }
} }
+5 -1
View File
@@ -18,6 +18,7 @@
"@radix-ui/themes": "^3.1.3", "@radix-ui/themes": "^3.1.3",
"@vanilla-extract/css": "^1.15.5", "@vanilla-extract/css": "^1.15.5",
"astro": "^4.15.6", "astro": "^4.15.6",
"three": "0.159.0",
"@react-three/drei": "^9.111.5", "@react-three/drei": "^9.111.5",
"@react-three/fiber": "^8.17.6", "@react-three/fiber": "^8.17.6",
"astro-purgecss": "^4.6.0", "astro-purgecss": "^4.6.0",
@@ -29,6 +30,9 @@
"react-promise-suspense": "^0.3.4" "react-promise-suspense": "^0.3.4"
}, },
"devDependencies": { "devDependencies": {
"@vanilla-extract/vite-plugin": "^4.0.15" "@types/three": "^0.160.0",
"@vanilla-extract/vite-plugin": "^4.0.15",
"@types/react-dom": "^18",
"@types/react": "^18.3.5"
} }
} }
@@ -9,6 +9,10 @@ import {
} 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 { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
import { App } from '../../../stores/app';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { layerTypeNames } from './ShotDisplayCard'; import { layerTypeNames } from './ShotDisplayCard';
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from './SpacedArmorScene';
@@ -6,6 +6,7 @@ import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { useArmor } from '../../../hooks/useArmor'; import { useArmor } from '../../../hooks/useArmor';
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 { ModelTankWrapper } from './ModelTankWrapper'; import { ModelTankWrapper } from './ModelTankWrapper';
import { PrimaryArmorSceneComponent } from './PrimaryArmorSceneComponent'; import { PrimaryArmorSceneComponent } from './PrimaryArmorSceneComponent';
@@ -1,5 +1,5 @@
import { import {
ShellDefinition, type ShellDefinition,
canSplash, canSplash,
isExplosive, isExplosive,
resolveNearPenetration, resolveNearPenetration,
@@ -8,13 +8,13 @@ import {
import { useFrame } from '@react-three/fiber'; import { useFrame } from '@react-three/fiber';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three'; import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils.js';
import { hasEquipment } from '../../../../../../website-legacy/src/core/blitzkit/hasEquipment'; import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../website-legacy/src/core/blitzkit/jsxTree'; import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import * as Duel from '../../../../../../website-legacy/src/stores/duel'; import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
import * as TankopediaPersistent from '../../../../../../website-legacy/src/stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
import fragmentShader from './shaders/fragment.glsl'; import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl'; import vertexShader from './shaders/vertex.glsl?raw';
import { spacedArmorRenderTarget } from './target'; import { spacedArmorRenderTarget } from './target';
interface PrimaryArmorSceneComponentProps { interface PrimaryArmorSceneComponentProps {
@@ -76,15 +76,17 @@ export function PrimaryArmorSceneComponent({
await handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix); await handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix);
await handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix); await handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix);
} }
function handleVisualChange( function handleGreenPenetrationChange(greenPenetration: boolean) {
visual: TankopediaPersistent.TankopediaPersistent['model']['visual'], material.uniforms.greenPenetration.value = greenPenetration;
) { }
material.uniforms.greenPenetration.value = visual.greenPenetration; function handleOpaqueChange(opaque: boolean) {
material.uniforms.opaque.value = visual.opaque || visual.wireframe; material.uniforms.opaque.value = opaque;
material.wireframe = visual.wireframe; }
function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe;
} }
async function handleProtagonistEquipmentChange( async function handleProtagonistEquipmentChange(
equipment: Duel.EquipmentMatrix, equipment: EquipmentMatrix,
) { ) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = await hasEquipment(
@@ -96,9 +98,7 @@ export function PrimaryArmorSceneComponent({
? thickness * 1.03 ? thickness * 1.03
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange( async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
equipment: Duel.EquipmentMatrix,
) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const shell = duel.antagonist.shell; const shell = duel.antagonist.shell;
const penetration = resolveNearPenetration(shell.penetration); const penetration = resolveNearPenetration(shell.penetration);
@@ -114,7 +114,11 @@ export function PrimaryArmorSceneComponent({
} }
handleShellChange(duelStore.getState().antagonist.shell); handleShellChange(duelStore.getState().antagonist.shell);
handleVisualChange(tankopediaPersistentStore.getState().model.visual); handleGreenPenetrationChange(
tankopediaPersistentStore.getState().greenPenetration,
);
handleOpaqueChange(tankopediaPersistentStore.getState().opaque);
handleWireframeChange(tankopediaPersistentStore.getState().wireframe);
handleProtagonistEquipmentChange( handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix, duelStore.getState().protagonist.equipmentMatrix,
); );
@@ -125,9 +129,10 @@ export function PrimaryArmorSceneComponent({
const unsubscribes = [ const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
tankopediaPersistentStore.subscribe( tankopediaPersistentStore.subscribe(
(state) => state.model.visual, (state) => state.greenPenetration,
handleVisualChange, handleGreenPenetrationChange,
), ),
duelStore.subscribe( duelStore.subscribe(
(state) => state.protagonist.equipmentMatrix, (state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange, handleProtagonistEquipmentChange,
@@ -1,9 +1,9 @@
import { J_HAT, K_HAT } from '@blitzkit/core'; import { K_HAT } from '@blitzkit/core';
import { Box, Flex, Text } from '@radix-ui/themes'; import { Box, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei'; import { Html } from '@react-three/drei';
import { useFrame } from '@react-three/fiber'; import { useFrame } from '@react-three/fiber';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { type ComponentProps, useEffect, useRef } from 'react'; import { useEffect, useRef, type ComponentProps } from 'react';
import { import {
BufferGeometry, BufferGeometry,
DoubleSide, DoubleSide,
@@ -16,12 +16,16 @@ import {
Path, Path,
Quaternion, Quaternion,
} from 'three'; } from 'three';
import { useFullScreen } from '../../../../../website-legacy/src/hooks/useFullScreen'; import { useFullScreen } from '../../../hooks/useFullScreen';
import * as TankopediaEphemeral from '../../../../../website-legacy/src/stores/tankopediaEphemeral'; import {
TankopediaEphemeral,
type ShotLayerNonExternal,
type ShotStatus,
} from '../../../stores/tankopediaEphemeral';
import { ShotDisplayCard } from './ShotDisplayCard'; import { ShotDisplayCard } from './ShotDisplayCard';
export const shotStatusColors: Record< export const shotStatusColors: Record<
TankopediaEphemeral.ShotStatus, ShotStatus,
ComponentProps<typeof Text>['color'] ComponentProps<typeof Text>['color']
> = { > = {
blocked: 'red', blocked: 'red',
@@ -127,16 +131,13 @@ export function ShotDisplay() {
}, 0) + SHOT_DISPLAY_LENGTH_INFINITY; }, 0) + SHOT_DISPLAY_LENGTH_INFINITY;
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 TankopediaEphemeral.ShotLayerNonExternal (shot.out.layers.at(-1) as ShotLayerNonExternal).point,
).point.distanceTo(
(shot.out.layers.at(-1) as TankopediaEphemeral.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 TankopediaEphemeral.ShotLayerNonExternal; ) as ShotLayerNonExternal;
const outTitleColor = shot.out const outTitleColor = shot.out
? shotStatusColors[shot.out.status] ? shotStatusColors[shot.out.status]
: undefined; : undefined;
@@ -182,7 +183,7 @@ export function ShotDisplay() {
</Box> </Box>
</Html> </Html>
{[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => { {/* {[...shot.in.layers, ...(shot.out?.layers ?? [])].map((layer, index) => {
if (layer.type === null) return null; if (layer.type === null) return null;
const shotStatusColor = const shotStatusColor =
@@ -275,7 +276,7 @@ export function ShotDisplay() {
/> />
</mesh> </mesh>
</group> </group>
)} )} */}
</group> </group>
); );
} }
@@ -1,17 +1,11 @@
import { import { Flex, Text } from '@radix-ui/themes';
Card,
CardProps,
Flex,
Inset,
Separator,
Text,
} from '@radix-ui/themes';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import { radToDeg } from 'three/src/math/MathUtils'; import type {
import * as TankopediaEphemeral from '../../../../../website-legacy/src/stores/tankopediaEphemeral'; ShotLayer,
import { shotStatusColors } from './ShotDisplay'; ShotStatus,
} from '../../../stores/tankopediaEphemeral';
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from './SpacedArmorScene';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent'; import type { ExternalModuleVariant } from './SpacedArmorSceneComponent';
export const layerTypeNames: Record<ArmorType | 'null', string> = { export const layerTypeNames: Record<ArmorType | 'null', string> = {
[ArmorType.Primary]: 'Primary', [ArmorType.Primary]: 'Primary',
@@ -25,7 +19,7 @@ const externalLayerNames: Record<ExternalModuleVariant, string> = {
track: 'Tracks', track: 'Tracks',
}; };
const shotStatusNames: Record<TankopediaEphemeral.ShotStatus, string> = { const shotStatusNames: Record<ShotStatus, string> = {
blocked: 'Blocked', blocked: 'Blocked',
ricochet: 'Ricochet', ricochet: 'Ricochet',
penetration: 'penetration', penetration: 'penetration',
@@ -42,7 +36,7 @@ function LayerEntry({
layerIndex?: string; layerIndex?: string;
shotStatusColor: ComponentProps<typeof Text>['color']; shotStatusColor: ComponentProps<typeof Text>['color'];
layerName: string; layerName: string;
layer: TankopediaEphemeral.ShotLayer; layer: ShotLayer;
angle?: number; angle?: number;
}) { }) {
return ( return (
@@ -95,7 +89,7 @@ function LayerEntry({
} }
interface ShotDisplayCardProps extends CardProps { interface ShotDisplayCardProps extends CardProps {
shot: TankopediaEphemeral.Shot; shot: Shot;
} }
export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) { export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
@@ -7,6 +7,7 @@ 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 { ModelTankWrapper } from './ModelTankWrapper'; import { ModelTankWrapper } from './ModelTankWrapper';
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent'; import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
@@ -16,8 +16,8 @@ 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 { ArmorType } from '../../../SpacedArmorScene'; import { ArmorType } from '../../../SpacedArmorScene';
import fragmentShader from './shaders/fragment.glsl'; import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl'; import vertexShader from './shaders/vertex.glsl?raw';
interface SpacedArmorSubExternalProps { interface SpacedArmorSubExternalProps {
node: Object3D; node: Object3D;
@@ -1,24 +1,24 @@
import { import {
ShellDefinition,
isExplosive, isExplosive,
resolveNearPenetration, resolveNearPenetration,
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
type ShellDefinition,
} from '@blitzkit/core'; } from '@blitzkit/core';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { import {
AdditiveBlending, AdditiveBlending,
MeshBasicMaterial, MeshBasicMaterial,
Object3D,
ShaderMaterial, ShaderMaterial,
type Object3D,
} from 'three'; } from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils.js';
import { ArmorUserData } from '../..'; import type { ArmorUserData } from '../..';
import { hasEquipment } from '../../../../../../../../website-legacy/src/core/blitzkit/hasEquipment'; import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../../../website-legacy/src/core/blitzkit/jsxTree'; import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
import * as Duel from '../../../../../../../../website-legacy/src/stores/duel'; import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
import { ArmorType } from '../../../SpacedArmorScene'; import { ArmorType } from '../../../SpacedArmorScene';
import fragmentShader from './shaders/fragment.glsl'; import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl'; import vertexShader from './shaders/vertex.glsl?raw';
interface SpacedArmorSubSpacedProps { interface SpacedArmorSubSpacedProps {
node: Object3D; node: Object3D;
@@ -66,7 +66,7 @@ export function SpacedArmorSubSpaced({
); );
} }
async function handleProtagonistEquipmentChange( async function handleProtagonistEquipmentChange(
equipment: Duel.EquipmentMatrix, equipment: EquipmentMatrix,
) { ) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment( const hasEnhancedArmor = await hasEquipment(
@@ -78,9 +78,7 @@ export function SpacedArmorSubSpaced({
? thickness * 1.03 ? thickness * 1.03
: thickness; : thickness;
} }
async function handleAntagonistEquipmentChange( async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
equipment: Duel.EquipmentMatrix,
) {
const duel = duelStore.getState(); const duel = duelStore.getState();
const shell = duelStore.getState().antagonist.shell; const shell = duelStore.getState().antagonist.shell;
const penetration = resolveNearPenetration(shell.penetration); const penetration = resolveNearPenetration(shell.penetration);
@@ -1,5 +1,5 @@
import { import {
type ShellType, ShellType,
isExplosive, isExplosive,
resolveNearPenetration, resolveNearPenetration,
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
@@ -16,9 +16,17 @@ import {
Scene, Scene,
Vector3, Vector3,
} from 'three'; } from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils.js';
import * as Duel from '../../../../stores/duel'; import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral'; import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
import { Duel } from '../../../../stores/duel';
import {
type Shot,
type ShotLayerBase,
type ShotLayerNonExternal,
TankopediaEphemeral,
} from '../../../../stores/tankopediaEphemeral';
import { ArmorType } from '../SpacedArmorScene'; import { ArmorType } from '../SpacedArmorScene';
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
@@ -83,7 +91,7 @@ export function SpacedArmorSceneComponent({
const { antagonist, protagonist } = duelStore.getState(); const { antagonist, protagonist } = duelStore.getState();
const shell = antagonist.shell; const shell = antagonist.shell;
const cameraNormal = camera.position.clone().sub(point).normalize(); const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: TankopediaEphemeral.Shot = { const shot: Shot = {
splashRadius: shell.explosionRadius, splashRadius: shell.explosionRadius,
damage: -1, damage: -1,
containsGaps: shell.type === ShellType.HEAT, containsGaps: shell.type === ShellType.HEAT,
@@ -157,9 +165,7 @@ export function SpacedArmorSceneComponent({
); );
if (shell.type === ShellType.HEAT && index > 0) { if (shell.type === ShellType.HEAT && index > 0) {
const lastLayer = shot.in.layers.at( const lastLayer = shot.in.layers.at(-1) as ShotLayerBase;
-1,
) as TankopediaEphemeral.ShotLayerBase;
const distance = lastLayer.point.distanceTo(intersection.point); const distance = lastLayer.point.distanceTo(intersection.point);
remainingPenetration -= 0.5 * remainingPenetration * distance; remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration <= 0; const blocked = remainingPenetration <= 0;
@@ -255,11 +261,9 @@ export function SpacedArmorSceneComponent({
} }
const lastLayer = shot.in.layers.at(-1) as const lastLayer = shot.in.layers.at(-1) as
| TankopediaEphemeral.ShotLayerNonExternal | ShotLayerNonExternal
| undefined;
const firstLayer = shot.in.layers[0] as
| TankopediaEphemeral.ShotLayerBase
| undefined; | undefined;
const firstLayer = shot.in.layers[0] as ShotLayerBase | undefined;
if (!lastLayer || !firstLayer) { if (!lastLayer || !firstLayer) {
return null; return null;
@@ -12,6 +12,7 @@ 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 { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
import { ModelTankWrapper } from './ModelTankWrapper'; import { ModelTankWrapper } from './ModelTankWrapper';
@@ -1,5 +1,5 @@
import { I_HAT, J_HAT } from '@blitzkit/core'; import { I_HAT, J_HAT } from '@blitzkit/core';
import { 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 { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { import {
@@ -15,20 +15,20 @@ import {
Quaternion, Quaternion,
Vector3, Vector3,
} from 'three'; } from 'three';
import { unrotateDavaVector } from '../../../../../website-legacy/src/core/blitz/unrotateDavaVector'; import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
import { jsxTree } from '../../../../../website-legacy/src/core/blitzkit/jsxTree'; import { jsxTree } from '../../../core/blitzkit/jsxTree';
import { import {
ModelTransformEventData, type ModelTransformEventData,
modelTransformEvent, modelTransformEvent,
} from '../../../../../website-legacy/src/core/blitzkit/modelTransform'; } from '../../../core/blitzkit/modelTransform';
import { discardClippingPlane } from '../../../../../website-legacy/src/core/three/discardClippingPlane'; import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
import * as TankopediaEphemeral from '../../../../../website-legacy/src/stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { ArmorType } from './SpacedArmorScene'; import { ArmorType } from './SpacedArmorScene';
import { import type {
ArmorUserData, ArmorUserData,
ExternalModuleVariant, ExternalModuleVariant,
} from './SpacedArmorSceneComponent'; } from './SpacedArmorSceneComponent';
import { ThicknessRange } from './StaticArmor'; import type { ThicknessRange } from './StaticArmor';
type StaticArmorSceneComponentProps = { type StaticArmorSceneComponentProps = {
name: string; name: string;
@@ -1,7 +1,7 @@
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { import {
TankComponentButton, TankComponentButton,
TankComponentButtonProps, type TankComponentButtonProps,
} from './TankComponentButton'; } from './TankComponentButton';
interface GenericTankComponentButtonProps extends TankComponentButtonProps { interface GenericTankComponentButtonProps extends TankComponentButtonProps {
@@ -1,6 +1,6 @@
import { asset } from '@blitzkit/core'; import { asset } from '@blitzkit/core';
import { GenericTankComponentButton } from './GenericTankComponentButton'; import { GenericTankComponentButton } from './GenericTankComponentButton';
import { TankComponentButtonProps } from './TankComponentButton'; import type { TankComponentButtonProps } from './TankComponentButton';
interface ModuleProps extends TankComponentButtonProps { interface ModuleProps extends TankComponentButtonProps {
module: string; module: string;
@@ -2,6 +2,7 @@ import { asset } from '@blitzkit/core';
import { IconButton } from '@radix-ui/themes'; import { IconButton } from '@radix-ui/themes';
import { invalidate } from '@react-three/fiber'; import { invalidate } from '@react-three/fiber';
import { useArmor } from '../../../../../../hooks/useArmor'; import { useArmor } from '../../../../../../hooks/useArmor';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
export function DynamicArmorSwitcher() { export function DynamicArmorSwitcher() {
@@ -1,12 +1,15 @@
import { HeightIcon, WidthIcon } from '@radix-ui/react-icons'; import { HeightIcon, WidthIcon } from '@radix-ui/react-icons';
import { Flex, TextField } from '@radix-ui/themes'; import { Flex, TextField } from '@radix-ui/themes';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { degToRad, radToDeg } from 'three/src/math/MathUtils'; import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent, ModelTransformEventData } from '../../../../../../core/blitzkit/modelTransform'; import {
import { useEquipment } from '../../../../../../hooks/useEquipment'; type ModelTransformEventData,
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition'; modelTransformEvent,
import * as Duel from '../../../../../../stores/duel'; } from '../../../../../../../core/blitzkit/modelTransform';
import { useEquipment } from '../../../../../../../hooks/useEquipment';
import { useTankModelDefinition } from '../../../../../../../hooks/useTankModelDefinition';
import { Duel } from '../../../../../../../stores/duel';
import * as styles from './index.css'; import * as styles from './index.css';
export function QuickInputs() { export function QuickInputs() {
@@ -22,13 +25,19 @@ export function QuickInputs() {
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
useEffect(() => { useEffect(() => {
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1); if (!yawInput.current) return;
yawInput.current.value = radToDeg(protagonist.yaw).toFixed(1);
}, [protagonist.yaw]); }, [protagonist.yaw]);
useEffect(() => { useEffect(() => {
pitchInput.current!.value = ( if (!pitchInput.current) return;
pitchInput.current.value = (
-radToDeg(protagonist.pitch) + initialGunPitch -radToDeg(protagonist.pitch) + initialGunPitch
).toFixed(1); ).toFixed(1);
}, [protagonist.pitch]); }, [protagonist.pitch]);
useEffect(() => { useEffect(() => {
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) { function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
if (!pitchInput.current || !yawInput.current) return; if (!pitchInput.current || !yawInput.current) return;
@@ -66,10 +75,8 @@ export function QuickInputs() {
<TextField.Root <TextField.Root
size="1" size="1"
radius="full" radius="full"
style={{ style={{ flex: 1, textAlign: 'right' }}
flex: 1, defaultValue="0.0"
textAlign: 'right',
}}
onBlur={() => { onBlur={() => {
const value = Number(yawInput.current!.value); const value = Number(yawInput.current!.value);
if (isNaN(value)) { if (isNaN(value)) {
@@ -98,23 +105,17 @@ export function QuickInputs() {
onFocus={() => yawInput.current?.focus()} onFocus={() => yawInput.current?.focus()}
ref={yawInput} ref={yawInput}
> >
<TextField.Slot <TextField.Slot style={{ userSelect: 'none' }}>
style={{
userSelect: 'none',
}}
>
<WidthIcon /> <WidthIcon />
</TextField.Slot> </TextField.Slot>
<TextField.Slot style={{ userSelect: 'none' }}>°</TextField.Slot> <TextField.Slot />
</TextField.Root> </TextField.Root>
<TextField.Root <TextField.Root
size="1" size="1"
radius="full" radius="full"
style={{ style={{ flex: 1, textAlign: 'right' }}
flex: 1, defaultValue="0.0"
textAlign: 'right',
}}
onBlur={() => { onBlur={() => {
const value = Number(pitchInput.current!.value); const value = Number(pitchInput.current!.value);
if (isNaN(value)) { if (isNaN(value)) {
@@ -154,13 +155,7 @@ export function QuickInputs() {
> >
<HeightIcon /> <HeightIcon />
</TextField.Slot> </TextField.Slot>
<TextField.Slot <TextField.Slot />
style={{
userSelect: 'none',
}}
>
°
</TextField.Slot>
</TextField.Root> </TextField.Root>
</Flex> </Flex>
); );
@@ -1,11 +1,11 @@
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons'; import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
import { Box, Button, Checkbox, Flex, Text } from '@radix-ui/themes'; import { Box, Button, Checkbox, Flex, Text } from '@radix-ui/themes';
import { ThicknessRange } from '../../../../../components/Armor/components/StaticArmor'; import { useArmor } from '../../../../../../hooks/useArmor';
import { useArmor } from '../../../../../hooks/useArmor'; import { Duel } from '../../../../../../stores/duel';
import * as Duel from '../../../../../stores/duel'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import * as TankopediaEphemeral from '../../../../../stores/tankopediaEphemeral'; import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import * as TankopediaPersistent from '../../../../../stores/tankopediaPersistent'; import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
import { DynamicArmorSwitcher } from './Model/components/Options/components/DynamicArmorSwitcher'; import { DynamicArmorSwitcher } from './DynamicArmorSwitcher';
interface ThicknessesProps { interface ThicknessesProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -13,13 +13,13 @@ interface ThicknessesProps {
export function Thicknesses({ thicknessRange }: ThicknessesProps) { export function Thicknesses({ thicknessRange }: ThicknessesProps) {
const showExternalModules = TankopediaPersistent.use( const showExternalModules = TankopediaPersistent.use(
(state) => state.model.visual.showExternalModules, (state) => state.showExternalModules,
); );
const showSpacedArmor = TankopediaPersistent.use( const showSpacedArmor = TankopediaPersistent.use(
(state) => state.model.visual.showSpacedArmor, (state) => state.showSpacedArmor,
); );
const showPrimaryArmor = TankopediaPersistent.use( const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.model.visual.showPrimaryArmor, (state) => state.showPrimaryArmor,
); );
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
const mutateTankopediaPersistent = TankopediaPersistent.useMutation(); const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
@@ -46,8 +46,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }} style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showPrimaryArmor = draft.showPrimaryArmor = !draft.showPrimaryArmor;
!draft.model.visual.showPrimaryArmor;
}); });
mutateTankopediaEphemeral((draft) => { mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
@@ -83,8 +82,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }} style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showSpacedArmor = draft.showSpacedArmor = !draft.showSpacedArmor;
!draft.model.visual.showSpacedArmor;
}); });
mutateTankopediaEphemeral((draft) => { mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
@@ -119,8 +117,7 @@ export function Thicknesses({ thicknessRange }: ThicknessesProps) {
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }} style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showExternalModules = draft.showExternalModules = !draft.showExternalModules;
!draft.model.visual.showExternalModules;
}); });
mutateTankopediaEphemeral((draft) => { mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined; draft.highlightArmor = undefined;
@@ -28,22 +28,24 @@ import {
Tooltip, Tooltip,
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import { invalidate } from '@react-three/fiber'; import { invalidate } from '@react-three/fiber';
import { type RefObject, useState } from 'react'; import { useState, type RefObject } from 'react';
import { ENVIRONMENTS } from '../../../../../constants/lightingEnvironments'; import { ENVIRONMENTS } from '../../../../../constants/lightingEnvironments';
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 { useStoreBoolean } from '../../../../../hooks/useStoreBoolean'; import { App } from '../../../../../stores/app';
import { useStoreKey } from '../../../../../hooks/useStoreKey'; import { Duel } from '../../../../../stores/duel';
import { $app } from '../../../../../stores/app'; import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { $tankopediaPersistent } from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants'; import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
import type { 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 { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher'; import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
import { QuickInputs } from './components/QuickInputs';
import { Thicknesses } from './components/Thicknesses';
interface OptionsProps { interface OptionsProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -51,23 +53,20 @@ interface OptionsProps {
} }
export function Options({ thicknessRange, canvas }: OptionsProps) { export function Options({ thicknessRange, canvas }: OptionsProps) {
// const { display } = useStore($tankopediaPersistent, { keys: ['display'] }); const display = TankopediaPersistent.use((state) => state.display);
const display = useStoreKey($tankopediaPersistent, 'display');
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const showGrid = useStoreBoolean($tankopediaPersistent, 'showGrid'); const showGrid = TankopediaPersistent.use((state) => state.showGrid);
const showEnvironment = useStoreBoolean( const showEnvironment = TankopediaPersistent.use(
$tankopediaPersistent, (state) => state.showEnvironment,
'showEnvironment',
); );
const greenPenetration = useStoreBoolean( const greenPenetration = TankopediaPersistent.use(
$tankopediaPersistent, (state) => state.greenPenetration,
'greenPenetration',
); );
const wireframe = useStoreBoolean($tankopediaPersistent, 'wireframe'); const wireframe = TankopediaPersistent.use((state) => state.wireframe);
const opaque = useStoreBoolean($tankopediaPersistent, 'opaque'); const opaque = TankopediaPersistent.use((state) => state.opaque);
const fullScreenAvailable = useFullscreenAvailability(); const fullScreenAvailable = useFullscreenAvailability();
const environment = useStoreKey($tankopediaPersistent, 'environment'); const environment = TankopediaPersistent.use((state) => state.environment);
const developerMode = useStoreBoolean($app, 'developerMode'); const developerMode = App.use((state) => state.developerMode);
const protagonistTank = Duel.use((state) => state.protagonist.tank); const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistGun = Duel.use((state) => state.antagonist.gun); const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell); const antagonistShell = Duel.use((state) => state.antagonist.shell);
@@ -78,6 +77,8 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
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 mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
return ( return (
<> <>
@@ -395,7 +396,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={greenPenetration} checked={greenPenetration}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.greenPenetration = checked; draft.greenPenetration = checked;
}); });
}} }}
> >
@@ -406,7 +407,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={opaque} checked={opaque}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.opaque = checked; draft.opaque = checked;
}); });
}} }}
> >
@@ -418,7 +419,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={wireframe} checked={wireframe}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.wireframe = checked; draft.wireframe = checked;
}); });
}} }}
> >
@@ -434,7 +435,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={showGrid} checked={showGrid}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showGrid = checked; draft.showGrid = checked;
}); });
}} }}
> >
@@ -445,7 +446,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={showEnvironment} checked={showEnvironment}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.showEnvironment = checked; draft.showEnvironment = checked;
}); });
}} }}
> >
@@ -463,7 +464,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
value={environment} value={environment}
onClick={() => { onClick={() => {
mutateTankopediaPersistent((draft) => { mutateTankopediaPersistent((draft) => {
draft.model.visual.environment = environment; draft.environment = environment;
}); });
}} }}
> >
@@ -4,12 +4,12 @@ import { useThree } from '@react-three/fiber';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Vector3 } from 'three'; import { Vector3 } from 'three';
import { OrbitControls as OrbitControlsClass } from 'three-stdlib'; import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
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 { useAwait } from '../../../../../hooks/useAwait'; import { useAwait } from '../../../../../../hooks/useAwait';
import * as Duel from '../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
const initialPosition = [0, 4, -18] as const; const initialPosition = [0, 4, -18] as const;
const poseDistances: Record<Pose, number> = { const poseDistances: Record<Pose, number> = {
@@ -1,17 +1,15 @@
import { Environment } from '@react-three/drei'; import { Environment } from '@react-three/drei';
import { useModel } from '../../../../../hooks/useModel'; import { useModel } from '../../../../../../hooks/useModel';
import * as Duel from '../../../../../stores/duel'; import { Duel } from '../../../../../../stores/duel';
import * as TankopediaPersistent from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
export function Lighting() { export function Lighting() {
const protagonist = Duel.use((state) => state.protagonist); const protagonist = Duel.use((state) => state.protagonist);
const { hasPbr } = useModel(protagonist.tank.id); const { hasPbr } = useModel(protagonist.tank.id);
const showEnvironment = TankopediaPersistent.use( const showEnvironment = TankopediaPersistent.use(
(state) => state.model.visual.showEnvironment, (state) => state.showEnvironment,
);
const environment = TankopediaPersistent.use(
(state) => state.model.visual.environment,
); );
const environment = TankopediaPersistent.use((state) => state.environment);
return ( return (
<> <>
@@ -2,12 +2,11 @@ import { useFrame, useLoader } from '@react-three/fiber';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { useRef } from 'react'; import { useRef } from 'react';
import { MeshStandardMaterial, TextureLoader } from 'three'; import { MeshStandardMaterial, TextureLoader } from 'three';
import * as TankopediaPersistent from '../../../../../stores/tankopediaPersistent'; import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
export function SceneProps() { export function SceneProps() {
const show = TankopediaPersistent.use( const show = TankopediaPersistent.use(
(state) => (state) => state.showGrid && !state.showEnvironment,
state.model.visual.showGrid && !state.model.visual.showEnvironment,
); );
const texture = useLoader(TextureLoader, 'https://i.imgur.com/C28Z8nU.png'); const texture = useLoader(TextureLoader, 'https://i.imgur.com/C28Z8nU.png');
const material = useRef<MeshStandardMaterial>(null); const material = useRef<MeshStandardMaterial>(null);
@@ -1,19 +1,19 @@
import { normalizeAngleRad } from '@blitzkit/core'; import { normalizeAngleRad } from '@blitzkit/core';
import { invalidate, ThreeEvent, useThree } from '@react-three/fiber'; import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber';
import { memo, useRef } from 'react'; import { memo, useRef } from 'react';
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three'; import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
import { ModelTankWrapper } from '../../../../../../../components/Armor/components/ModelTankWrapper'; 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 * as Duel from '../../../../../../../stores/duel'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import * as TankopediaEphemeral from '../../../../../../../stores/tankopediaEphemeral'; import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import * as TankopediaPersistent from '../../../../../../../stores/tankopediaPersistent'; import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants'; import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
export const TankModel = memo(() => { export const TankModel = memo(() => {
const mutateDuel = Duel.useMutation(); const mutateDuel = Duel.useMutation();
@@ -6,29 +6,29 @@ import {
useImperativeHandle, useImperativeHandle,
useRef, useRef,
} from 'react'; } from 'react';
import { Armor } from '../../../../../../components/Armor'; import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { ArmorPlateDisplay } from '../../../../../../components/Armor/components/ArmorPlateDisplay'; import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay'; import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
import { useEquipment } from '../../../../../hooks/useEquipment';
import { useOnScreen } from '../../../../../hooks/useOnScreen';
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
import { Armor } from '../../../../Armor';
import { ArmorPlateDisplay } from '../../../../Armor/components/ArmorPlateDisplay';
import { ShotDisplay } from '../../../../Armor/components/ShotDisplay';
import { import {
StaticArmor, StaticArmor,
ThicknessRange, type ThicknessRange,
} from '../../../../../../components/Armor/components/StaticArmor'; } from '../../../../Armor/components/StaticArmor';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits'; import { AutoClear } from './components/AutoClear';
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform'; import { Controls } from './components/Control';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose'; import { Lighting } from './components/Lighting';
import { useEquipment } from '../../../../../../hooks/useEquipment'; import { ModelLoader } from './components/ModelLoader';
import { useOnScreen } from '../../../../../../hooks/useOnScreen'; import { SceneProps } from './components/SceneProps';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition'; import { TankModel } from './components/TankModel';
import * as Duel from '../../../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../../../stores/tankopediaEphemeral';
import * as TankopediaPersistent from '../../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
import { Controls } from '../../../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Control';
import { Lighting } from '../../../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Lighting';
import { SceneProps } from '../../../../../../website-legacy/src/app/tools/tankopedia/[id]/components/SceneProps';
import { AutoClear } from '../../../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Model/AutoClear';
import { ModelLoader } from '../../../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Model/components/ModelLoader';
import { TankModel } from '../../../../../../website-legacy/src/app/tools/tankopedia/[id]/components/Model/components/TankModel';
interface TankSandboxProps { interface TankSandboxProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
@@ -178,7 +178,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
<Controls /> <Controls />
<SceneProps /> <SceneProps />
{display !== TankopediaDisplay.StaticArmor && <TankModel />} {display !== TankopediaDisplay.StaticArmor && <TankModel />}
<ShotDisplay /> <ShotDisplay />
<ArmorPlateDisplay /> <ArmorPlateDisplay />
<AutoClear /> <AutoClear />
@@ -1,5 +1,5 @@
import { tankIcon } from '@blitzkit/core'; import { tankIcon } from '@blitzkit/core';
import { Flex, FlexProps, Progress } from '@radix-ui/themes'; import { Flex, type FlexProps, Progress } from '@radix-ui/themes';
import { useProgress } from '@react-three/drei'; import { useProgress } from '@react-three/drei';
type TankSandboxLoaderProps = Omit<FlexProps, 'id'> & { type TankSandboxLoaderProps = Omit<FlexProps, 'id'> & {
@@ -4,6 +4,7 @@ import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { NAVBAR_HEIGHT } from '../../../constants/navbar'; import { NAVBAR_HEIGHT } from '../../../constants/navbar';
import { Var } from '../../../core/radix/var'; import { Var } from '../../../core/radix/var';
import { useFullScreen } from '../../../hooks/useFullScreen'; import { useFullScreen } from '../../../hooks/useFullScreen';
import { Duel } from '../../../stores/duel';
import type { ThicknessRange } from '../../Armor/components/StaticArmor'; import type { ThicknessRange } from '../../Armor/components/StaticArmor';
import { classIcons } from '../../ClassIcon'; import { classIcons } from '../../ClassIcon';
import { Options } from './components/Options'; import { Options } from './components/Options';
@@ -12,14 +13,10 @@ import { TankSandboxLoader } from './components/TankSandboxLoader';
const awaitedTankDefinitions = await tankDefinitions; const awaitedTankDefinitions = await tankDefinitions;
interface HeroSectionProps { export function HeroSection() {
id: number;
}
export function HeroSection({ id }: HeroSectionProps) {
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const protagonist = awaitedTankDefinitions[id]; const protagonist = Duel.use((state) => state.protagonist.tank);
const Icon = classIcons[protagonist.class]; const Icon = classIcons[protagonist.class];
const [dummyLoader, setDummyLoader] = useState(true); const [dummyLoader, setDummyLoader] = useState(true);
const treeColor = const treeColor =
@@ -1,6 +1,10 @@
import { normalizeAngleRad, PitchLimits, YawLimits } from '@blitzkit/core'; import {
normalizeAngleRad,
type PitchLimits,
type YawLimits,
} from '@blitzkit/core';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils.js';
export function applyPitchYawLimits( export function applyPitchYawLimits(
pitch: number, // rad pitch: number, // rad
@@ -1,5 +1,5 @@
import { equipmentDefinitions } from '@blitzkit/core'; import { equipmentDefinitions } from '@blitzkit/core';
import { EquipmentMatrix } from '../../stores/duel'; import type { EquipmentMatrix } from '../../stores/duel';
export async function hasEquipment( export async function hasEquipment(
id: number, id: number,
@@ -1,4 +1,4 @@
import { ModelArmor } from '@blitzkit/core'; import type { ModelArmor } from '@blitzkit/core';
export function resolveArmor(armor: ModelArmor, index: number) { export function resolveArmor(armor: ModelArmor, index: number) {
const spaced = armor.spaced?.includes(index) ?? false; const spaced = armor.spaced?.includes(index) ?? false;
@@ -1,9 +1,9 @@
import { import {
availableProvisions, availableProvisions,
ProvisionDefinitions, type ProvisionDefinitions,
TankDefinition, type TankDefinition,
} from '@blitzkit/core'; } from '@blitzkit/core';
import { DuelMember } from '../../stores/duel'; import type { DuelMember } from '../../stores/duel';
import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants'; import { genericDefaultEquipmentMatrix } from '../../stores/duel/constants';
const PROVISION_PREFERENCES = [ const PROVISION_PREFERENCES = [
+1 -1
View File
@@ -1,6 +1,6 @@
import { equipmentDefinitions } from '@blitzkit/core'; import { equipmentDefinitions } from '@blitzkit/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import * as Duel from '../stores/duel'; import { Duel } from '../stores/duel';
const awaitedEquipmentDefinitions = await equipmentDefinitions; const awaitedEquipmentDefinitions = await equipmentDefinitions;
@@ -1,4 +1,4 @@
import * as TankopediaEphemeral from '../stores/tankopediaEphemeral'; import { TankopediaEphemeral } from '../stores/tankopediaEphemeral';
export function useTankModelDefinition() { export function useTankModelDefinition() {
return TankopediaEphemeral.use((state) => state.model); return TankopediaEphemeral.use((state) => state.model);
@@ -1,13 +1,13 @@
import { I_HAT, J_HAT, K_HAT } from '@blitzkit/core'; import { I_HAT, J_HAT, K_HAT } from '@blitzkit/core';
import { type RefObject, useEffect } from 'react'; import { type RefObject, useEffect } from 'react';
import { Euler, Group, Vector3 } from 'three'; import { Euler, Group, Vector3 } from 'three';
import { degToRad } from 'three/src/math/MathUtils'; import { degToRad } from 'three/src/math/MathUtils.js';
import { correctZYTuple } from '../core/blitz/correctZYTuple'; import { correctZYTuple } from '../core/blitz/correctZYTuple';
import { import {
ModelTransformEventData, type ModelTransformEventData,
modelTransformEvent, modelTransformEvent,
} from '../core/blitzkit/modelTransform'; } from '../core/blitzkit/modelTransform';
import { DuelMember } from '../stores/duel'; import type { DuelMember } from '../stores/duel';
import { useTankModelDefinition } from './useTankModelDefinition'; import { useTankModelDefinition } from './useTankModelDefinition';
export function useTankTransform( export function useTankTransform(
@@ -1,6 +1,7 @@
--- ---
import { tankDefinitions } from '@blitzkit/core'; import { tankDefinitions } from '@blitzkit/core';
import type { GetStaticPaths } from 'astro'; import type { GetStaticPaths } from 'astro';
import ToolLayout from '../../../../layouts/ToolLayout.astro';
import { Page } from './index'; import { Page } from './index';
export const getStaticPaths = (async () => { export const getStaticPaths = (async () => {
@@ -14,4 +15,6 @@ export const getStaticPaths = (async () => {
const { id } = Astro.params; const { id } = Astro.params;
--- ---
<Page id={id} client:load /> <ToolLayout>
<Page id={id} client:load />
</ToolLayout>
@@ -1,14 +1,36 @@
import {
modelDefinitions,
provisionDefinitions,
tankDefinitions,
} from '@blitzkit/core';
import { PageWrapper } from '../../../../components/PageWrapper'; import { PageWrapper } from '../../../../components/PageWrapper';
import { HeroSection } from '../../../../components/Tankopedia/HeroSection'; import { HeroSection } from '../../../../components/Tankopedia/HeroSection';
import { App } from '../../../../stores/app';
import { Duel } from '../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
interface PageProps { interface PageProps {
id: number; id: number;
} }
const awaitedTankDefinitions = await tankDefinitions;
const awaitedProvisionDefinitions = await provisionDefinitions;
const awaitedModelDefinitions = await modelDefinitions;
export function Page({ id }: PageProps) { export function Page({ id }: PageProps) {
const tank = awaitedTankDefinitions[id];
const model = awaitedModelDefinitions[id];
return ( return (
<TankopediaEphemeral.Provider data={model}>
<App.Provider>
<TankopediaPersistent.Provider>
<Duel.Provider
data={{ tank, provisionDefinitions: awaitedProvisionDefinitions }}
>
<PageWrapper p="0" noMaxWidth color="purple" size={1600} gap="9"> <PageWrapper p="0" noMaxWidth color="purple" size={1600} gap="9">
<HeroSection id={id} /> <HeroSection />
{/* <ShotDisplaySection /> {/* <ShotDisplaySection />
<MetaSection /> <MetaSection />
<TechTreeSection /> <TechTreeSection />
@@ -21,5 +43,9 @@ export function Page({ id }: PageProps) {
<HistorySection /> <HistorySection />
{!exempt && <AdMidSectionResponsive mb="6" />} */} {!exempt && <AdMidSectionResponsive mb="6" />} */}
</PageWrapper> </PageWrapper>
</Duel.Provider>
</TankopediaPersistent.Provider>
</App.Provider>
</TankopediaEphemeral.Provider>
); );
} }
@@ -1,4 +1,4 @@
import { EquipmentMatrix } from '.'; import type { EquipmentMatrix } from '.';
export const genericDefaultEquipmentMatrix: EquipmentMatrix = [ export const genericDefaultEquipmentMatrix: EquipmentMatrix = [
[-1, 1, -1], [-1, 1, -1],
@@ -1,4 +1,4 @@
import lodash from 'lodash-es'; import * as lodash from 'lodash-es';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, subscribeWithSelector } from 'zustand/middleware'; import { persist, subscribeWithSelector } from 'zustand/middleware';
import { createContextualStore } from '../../core/zustand/createContextualStore'; import { createContextualStore } from '../../core/zustand/createContextualStore';
@@ -1,8 +1,8 @@
import type { ModelDefinition } from '@blitzkit/core'; import type { ModelDefinition } from '@blitzkit/core';
import { Vector3 } from 'three'; import type { Vector3 } from 'three';
import { create } from 'zustand'; import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware'; import { subscribeWithSelector } from 'zustand/middleware';
import { 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 { createContextualStore } from '../core/zustand/createContextualStore'; import { createContextualStore } from '../core/zustand/createContextualStore';
@@ -66,19 +66,6 @@ export type ArmorPiercingLayer =
gap: number; gap: number;
}; };
export const TankopediaEphemeral = createContextualStore(
(model: ModelDefinition) =>
create<TankopediaEphemeral>()(
subscribeWithSelector<TankopediaEphemeral>(() => ({
editStatic: false,
skills: {},
model,
controlsEnabled: true,
xpMultiplier: 1,
})),
),
);
interface TankopediaEphemeral { interface TankopediaEphemeral {
shot?: Shot; shot?: Shot;
skills: Record<string, number>; skills: Record<string, number>;
@@ -101,3 +88,16 @@ interface TankopediaEphemeral {
); );
xpMultiplier: (typeof XP_MULTIPLIERS)[number]; xpMultiplier: (typeof XP_MULTIPLIERS)[number];
} }
export const TankopediaEphemeral = createContextualStore(
(model: ModelDefinition) =>
create<TankopediaEphemeral>()(
subscribeWithSelector<TankopediaEphemeral>(() => ({
editStatic: false,
skills: {},
model,
controlsEnabled: true,
xpMultiplier: 1,
})),
),
);
@@ -1,6 +1,6 @@
import * as lodash from 'lodash-es'; import * as lodash from 'lodash-es';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist, subscribeWithSelector } from 'zustand/middleware';
import { ENVIRONMENTS } from '../../constants/lightingEnvironments'; import { ENVIRONMENTS } from '../../constants/lightingEnvironments';
import { createContextualStore } from '../../core/zustand/createContextualStore'; import { createContextualStore } from '../../core/zustand/createContextualStore';
import { SORT_NAMES, TankopediaDisplay } from './constants'; import { SORT_NAMES, TankopediaDisplay } from './constants';
@@ -31,13 +31,13 @@ export interface TankopediaPersistentStore {
export const TankopediaPersistent = createContextualStore(() => export const TankopediaPersistent = createContextualStore(() =>
create<TankopediaPersistentStore>()( create<TankopediaPersistentStore>()(
persist( persist(
(set) => ({ subscribeWithSelector<TankopediaPersistentStore>(() => ({
wireframe: false, wireframe: false,
opaque: false, opaque: false,
environment: ENVIRONMENTS[0], environment: ENVIRONMENTS[0],
showGrid: true, showGrid: true,
greenPenetration: true, greenPenetration: false,
showEnvironment: true, showEnvironment: false,
showSpacedArmor: true, showSpacedArmor: true,
showExternalModules: true, showExternalModules: true,
showPrimaryArmor: true, showPrimaryArmor: true,
@@ -46,7 +46,7 @@ export const TankopediaPersistent = createContextualStore(() =>
direction: 'ascending', direction: 'ascending',
}, },
display: TankopediaDisplay.Model, display: TankopediaDisplay.Model,
}), })),
{ name: 'tankopedia', merge: (a, b) => lodash.merge(b, a) }, { name: 'tankopedia', merge: (a, b) => lodash.merge(b, a) },
), ),
), ),