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
+293 -264

No files matched your search

@@ -9,6 +9,10 @@ import {
} from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useRef } from 'react';
import { radToDeg } from 'three/src/math/MathUtils.js';
import { resolveArmorIndex } from '../../../core/blitzkit/resolveArmorIndex';
import { App } from '../../../stores/app';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { layerTypeNames } from './ShotDisplayCard';
import { ArmorType } from './SpacedArmorScene';
@@ -6,6 +6,7 @@ import { resolveArmor } from '../../../core/blitzkit/resolveThickness';
import { useArmor } from '../../../hooks/useArmor';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel';
import { ModelTankWrapper } from './ModelTankWrapper';
import { PrimaryArmorSceneComponent } from './PrimaryArmorSceneComponent';
@@ -1,5 +1,5 @@
import {
ShellDefinition,
type ShellDefinition,
canSplash,
isExplosive,
resolveNearPenetration,
@@ -8,13 +8,13 @@ import {
import { useFrame } from '@react-three/fiber';
import { useEffect } from 'react';
import { MeshBasicMaterial, Object3D, ShaderMaterial, Vector2 } from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { hasEquipment } from '../../../../../../website-legacy/src/core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../website-legacy/src/core/blitzkit/jsxTree';
import * as Duel from '../../../../../../website-legacy/src/stores/duel';
import * as TankopediaPersistent from '../../../../../../website-legacy/src/stores/tankopediaPersistent';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
import { degToRad } from 'three/src/math/MathUtils.js';
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import { Duel, type EquipmentMatrix } from '../../../../stores/duel';
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl?raw';
import { spacedArmorRenderTarget } from './target';
interface PrimaryArmorSceneComponentProps {
@@ -76,15 +76,17 @@ export function PrimaryArmorSceneComponent({
await handleProtagonistEquipmentChange(duel.protagonist.equipmentMatrix);
await handleAntagonistEquipmentChange(duel.antagonist.equipmentMatrix);
}
function handleVisualChange(
visual: TankopediaPersistent.TankopediaPersistent['model']['visual'],
) {
material.uniforms.greenPenetration.value = visual.greenPenetration;
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
material.wireframe = visual.wireframe;
function handleGreenPenetrationChange(greenPenetration: boolean) {
material.uniforms.greenPenetration.value = greenPenetration;
}
function handleOpaqueChange(opaque: boolean) {
material.uniforms.opaque.value = opaque;
}
function handleWireframeChange(wireframe: boolean) {
material.wireframe = wireframe;
}
async function handleProtagonistEquipmentChange(
equipment: Duel.EquipmentMatrix,
equipment: EquipmentMatrix,
) {
const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment(
@@ -96,9 +98,7 @@ export function PrimaryArmorSceneComponent({
? thickness * 1.03
: thickness;
}
async function handleAntagonistEquipmentChange(
equipment: Duel.EquipmentMatrix,
) {
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const shell = duel.antagonist.shell;
const penetration = resolveNearPenetration(shell.penetration);
@@ -114,7 +114,11 @@ export function PrimaryArmorSceneComponent({
}
handleShellChange(duelStore.getState().antagonist.shell);
handleVisualChange(tankopediaPersistentStore.getState().model.visual);
handleGreenPenetrationChange(
tankopediaPersistentStore.getState().greenPenetration,
);
handleOpaqueChange(tankopediaPersistentStore.getState().opaque);
handleWireframeChange(tankopediaPersistentStore.getState().wireframe);
handleProtagonistEquipmentChange(
duelStore.getState().protagonist.equipmentMatrix,
);
@@ -125,9 +129,10 @@ export function PrimaryArmorSceneComponent({
const unsubscribes = [
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
tankopediaPersistentStore.subscribe(
(state) => state.model.visual,
handleVisualChange,
(state) => state.greenPenetration,
handleGreenPenetrationChange,
),
duelStore.subscribe(
(state) => state.protagonist.equipmentMatrix,
handleProtagonistEquipmentChange,
@@ -1,9 +1,9 @@
import { J_HAT, K_HAT } from '@blitzkit/core';
import { Box, Flex, Text } from '@radix-ui/themes';
import { K_HAT } from '@blitzkit/core';
import { Box, Text } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
import { useFrame } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import { type ComponentProps, useEffect, useRef } from 'react';
import { useEffect, useRef, type ComponentProps } from 'react';
import {
BufferGeometry,
DoubleSide,
@@ -16,12 +16,16 @@ import {
Path,
Quaternion,
} from 'three';
import { useFullScreen } from '../../../../../website-legacy/src/hooks/useFullScreen';
import * as TankopediaEphemeral from '../../../../../website-legacy/src/stores/tankopediaEphemeral';
import { useFullScreen } from '../../../hooks/useFullScreen';
import {
TankopediaEphemeral,
type ShotLayerNonExternal,
type ShotStatus,
} from '../../../stores/tankopediaEphemeral';
import { ShotDisplayCard } from './ShotDisplayCard';
export const shotStatusColors: Record<
TankopediaEphemeral.ShotStatus,
ShotStatus,
ComponentProps<typeof Text>['color']
> = {
blocked: 'red',
@@ -127,16 +131,13 @@ export function ShotDisplay() {
}, 0) + SHOT_DISPLAY_LENGTH_INFINITY;
const outLength =
shot.out && shot.out.layers.length > 0
? (
shot.in.layers.at(-1) as TankopediaEphemeral.ShotLayerNonExternal
).point.distanceTo(
(shot.out.layers.at(-1) as TankopediaEphemeral.ShotLayerNonExternal)
.point,
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
)
: SHOT_DISPLAY_LENGTH_INFINITY;
const inLast = shot.in.layers.findLast(
(layer) => layer.type !== null,
) as TankopediaEphemeral.ShotLayerNonExternal;
) as ShotLayerNonExternal;
const outTitleColor = shot.out
? shotStatusColors[shot.out.status]
: undefined;
@@ -182,7 +183,7 @@ export function ShotDisplay() {
</Box>
</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;
const shotStatusColor =
@@ -275,7 +276,7 @@ export function ShotDisplay() {
/>
</mesh>
</group>
)}
)} */}
</group>
);
}
@@ -1,17 +1,11 @@
import {
Card,
CardProps,
Flex,
Inset,
Separator,
Text,
} from '@radix-ui/themes';
import { Flex, Text } from '@radix-ui/themes';
import type { ComponentProps } from 'react';
import { radToDeg } from 'three/src/math/MathUtils';
import * as TankopediaEphemeral from '../../../../../website-legacy/src/stores/tankopediaEphemeral';
import { shotStatusColors } from './ShotDisplay';
import type {
ShotLayer,
ShotStatus,
} from '../../../stores/tankopediaEphemeral';
import { ArmorType } from './SpacedArmorScene';
import { ExternalModuleVariant } from './SpacedArmorSceneComponent';
import type { ExternalModuleVariant } from './SpacedArmorSceneComponent';
export const layerTypeNames: Record<ArmorType | 'null', string> = {
[ArmorType.Primary]: 'Primary',
@@ -25,7 +19,7 @@ const externalLayerNames: Record<ExternalModuleVariant, string> = {
track: 'Tracks',
};
const shotStatusNames: Record<TankopediaEphemeral.ShotStatus, string> = {
const shotStatusNames: Record<ShotStatus, string> = {
blocked: 'Blocked',
ricochet: 'Ricochet',
penetration: 'penetration',
@@ -42,7 +36,7 @@ function LayerEntry({
layerIndex?: string;
shotStatusColor: ComponentProps<typeof Text>['color'];
layerName: string;
layer: TankopediaEphemeral.ShotLayer;
layer: ShotLayer;
angle?: number;
}) {
return (
@@ -95,7 +89,7 @@ function LayerEntry({
}
interface ShotDisplayCardProps extends CardProps {
shot: TankopediaEphemeral.Shot;
shot: Shot;
}
export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
@@ -7,6 +7,7 @@ import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel';
import { ModelTankWrapper } from './ModelTankWrapper';
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
@@ -16,8 +16,8 @@ import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
import { ArmorType } from '../../../SpacedArmorScene';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl?raw';
interface SpacedArmorSubExternalProps {
node: Object3D;
@@ -1,24 +1,24 @@
import {
ShellDefinition,
isExplosive,
resolveNearPenetration,
resolvePenetrationCoefficient,
type ShellDefinition,
} from '@blitzkit/core';
import { useEffect } from 'react';
import {
AdditiveBlending,
MeshBasicMaterial,
Object3D,
ShaderMaterial,
type Object3D,
} from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import { ArmorUserData } from '../..';
import { hasEquipment } from '../../../../../../../../website-legacy/src/core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../../../website-legacy/src/core/blitzkit/jsxTree';
import * as Duel from '../../../../../../../../website-legacy/src/stores/duel';
import { degToRad } from 'three/src/math/MathUtils.js';
import type { ArmorUserData } from '../..';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
import { ArmorType } from '../../../SpacedArmorScene';
import fragmentShader from './shaders/fragment.glsl';
import vertexShader from './shaders/vertex.glsl';
import fragmentShader from './shaders/fragment.glsl?raw';
import vertexShader from './shaders/vertex.glsl?raw';
interface SpacedArmorSubSpacedProps {
node: Object3D;
@@ -66,7 +66,7 @@ export function SpacedArmorSubSpaced({
);
}
async function handleProtagonistEquipmentChange(
equipment: Duel.EquipmentMatrix,
equipment: EquipmentMatrix,
) {
const duel = duelStore.getState();
const hasEnhancedArmor = await hasEquipment(
@@ -78,9 +78,7 @@ export function SpacedArmorSubSpaced({
? thickness * 1.03
: thickness;
}
async function handleAntagonistEquipmentChange(
equipment: Duel.EquipmentMatrix,
) {
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
const duel = duelStore.getState();
const shell = duelStore.getState().antagonist.shell;
const penetration = resolveNearPenetration(shell.penetration);
@@ -1,5 +1,5 @@
import {
type ShellType,
ShellType,
isExplosive,
resolveNearPenetration,
resolvePenetrationCoefficient,
@@ -16,9 +16,17 @@ import {
Scene,
Vector3,
} from 'three';
import { degToRad } from 'three/src/math/MathUtils';
import * as Duel from '../../../../stores/duel';
import * as TankopediaEphemeral from '../../../../stores/tankopediaEphemeral';
import { degToRad } from 'three/src/math/MathUtils.js';
import { hasEquipment } from '../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
import { Duel } from '../../../../stores/duel';
import {
type Shot,
type ShotLayerBase,
type ShotLayerNonExternal,
TankopediaEphemeral,
} from '../../../../stores/tankopediaEphemeral';
import { ArmorType } from '../SpacedArmorScene';
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
@@ -83,7 +91,7 @@ export function SpacedArmorSceneComponent({
const { antagonist, protagonist } = duelStore.getState();
const shell = antagonist.shell;
const cameraNormal = camera.position.clone().sub(point).normalize();
const shot: TankopediaEphemeral.Shot = {
const shot: Shot = {
splashRadius: shell.explosionRadius,
damage: -1,
containsGaps: shell.type === ShellType.HEAT,
@@ -157,9 +165,7 @@ export function SpacedArmorSceneComponent({
);
if (shell.type === ShellType.HEAT && index > 0) {
const lastLayer = shot.in.layers.at(
-1,
) as TankopediaEphemeral.ShotLayerBase;
const lastLayer = shot.in.layers.at(-1) as ShotLayerBase;
const distance = lastLayer.point.distanceTo(intersection.point);
remainingPenetration -= 0.5 * remainingPenetration * distance;
const blocked = remainingPenetration <= 0;
@@ -255,11 +261,9 @@ export function SpacedArmorSceneComponent({
}
const lastLayer = shot.in.layers.at(-1) as
| TankopediaEphemeral.ShotLayerNonExternal
| undefined;
const firstLayer = shot.in.layers[0] as
| TankopediaEphemeral.ShotLayerBase
| ShotLayerNonExternal
| undefined;
const firstLayer = shot.in.layers[0] as ShotLayerBase | undefined;
if (!lastLayer || !firstLayer) {
return null;
@@ -12,6 +12,7 @@ import { useArmor } from '../../../hooks/useArmor';
import { useModel } from '../../../hooks/useModel';
import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../hooks/useTankTransform';
import { Duel } from '../../../stores/duel';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
import { ModelTankWrapper } from './ModelTankWrapper';
@@ -1,5 +1,5 @@
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 { useEffect, useMemo } from 'react';
import {
@@ -15,20 +15,20 @@ import {
Quaternion,
Vector3,
} from 'three';
import { unrotateDavaVector } from '../../../../../website-legacy/src/core/blitz/unrotateDavaVector';
import { jsxTree } from '../../../../../website-legacy/src/core/blitzkit/jsxTree';
import { unrotateDavaVector } from '../../../core/blitz/unrotateDavaVector';
import { jsxTree } from '../../../core/blitzkit/jsxTree';
import {
ModelTransformEventData,
type ModelTransformEventData,
modelTransformEvent,
} from '../../../../../website-legacy/src/core/blitzkit/modelTransform';
import { discardClippingPlane } from '../../../../../website-legacy/src/core/three/discardClippingPlane';
import * as TankopediaEphemeral from '../../../../../website-legacy/src/stores/tankopediaEphemeral';
} from '../../../core/blitzkit/modelTransform';
import { discardClippingPlane } from '../../../core/three/discardClippingPlane';
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
import { ArmorType } from './SpacedArmorScene';
import {
import type {
ArmorUserData,
ExternalModuleVariant,
} from './SpacedArmorSceneComponent';
import { ThicknessRange } from './StaticArmor';
import type { ThicknessRange } from './StaticArmor';
type StaticArmorSceneComponentProps = {
name: string;
@@ -1,7 +1,7 @@
import type { HTMLAttributes } from 'react';
import {
TankComponentButton,
TankComponentButtonProps,
type TankComponentButtonProps,
} from './TankComponentButton';
interface GenericTankComponentButtonProps extends TankComponentButtonProps {
@@ -1,6 +1,6 @@
import { asset } from '@blitzkit/core';
import { GenericTankComponentButton } from './GenericTankComponentButton';
import { TankComponentButtonProps } from './TankComponentButton';
import type { TankComponentButtonProps } from './TankComponentButton';
interface ModuleProps extends TankComponentButtonProps {
module: string;
@@ -2,6 +2,7 @@ import { asset } from '@blitzkit/core';
import { IconButton } from '@radix-ui/themes';
import { invalidate } from '@react-three/fiber';
import { useArmor } from '../../../../../../hooks/useArmor';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
export function DynamicArmorSwitcher() {
@@ -1,12 +1,15 @@
import { HeightIcon, WidthIcon } from '@radix-ui/react-icons';
import { Flex, TextField } from '@radix-ui/themes';
import { useEffect, useRef } from 'react';
import { degToRad, radToDeg } from 'three/src/math/MathUtils';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent, ModelTransformEventData } from '../../../../../../core/blitzkit/modelTransform';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
import * as Duel from '../../../../../../stores/duel';
import { degToRad, radToDeg } from 'three/src/math/MathUtils.js';
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
import {
type ModelTransformEventData,
modelTransformEvent,
} 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';
export function QuickInputs() {
@@ -22,13 +25,19 @@ export function QuickInputs() {
const hasImprovedVerticalStabilizer = useEquipment(122);
useEffect(() => {
yawInput.current!.value = radToDeg(protagonist.yaw).toFixed(1);
if (!yawInput.current) return;
yawInput.current.value = radToDeg(protagonist.yaw).toFixed(1);
}, [protagonist.yaw]);
useEffect(() => {
pitchInput.current!.value = (
if (!pitchInput.current) return;
pitchInput.current.value = (
-radToDeg(protagonist.pitch) + initialGunPitch
).toFixed(1);
}, [protagonist.pitch]);
useEffect(() => {
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
if (!pitchInput.current || !yawInput.current) return;
@@ -66,10 +75,8 @@ export function QuickInputs() {
<TextField.Root
size="1"
radius="full"
style={{
flex: 1,
textAlign: 'right',
}}
style={{ flex: 1, textAlign: 'right' }}
defaultValue="0.0"
onBlur={() => {
const value = Number(yawInput.current!.value);
if (isNaN(value)) {
@@ -98,23 +105,17 @@ export function QuickInputs() {
onFocus={() => yawInput.current?.focus()}
ref={yawInput}
>
<TextField.Slot
style={{
userSelect: 'none',
}}
>
<TextField.Slot style={{ userSelect: 'none' }}>
<WidthIcon />
</TextField.Slot>
<TextField.Slot style={{ userSelect: 'none' }}>°</TextField.Slot>
<TextField.Slot />
</TextField.Root>
<TextField.Root
size="1"
radius="full"
style={{
flex: 1,
textAlign: 'right',
}}
style={{ flex: 1, textAlign: 'right' }}
defaultValue="0.0"
onBlur={() => {
const value = Number(pitchInput.current!.value);
if (isNaN(value)) {
@@ -154,13 +155,7 @@ export function QuickInputs() {
>
<HeightIcon />
</TextField.Slot>
<TextField.Slot
style={{
userSelect: 'none',
}}
>
°
</TextField.Slot>
<TextField.Slot />
</TextField.Root>
</Flex>
);
@@ -0,0 +1,179 @@
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from '@radix-ui/react-icons';
import { Box, Button, Checkbox, Flex, Text } from '@radix-ui/themes';
import { useArmor } from '../../../../../../hooks/useArmor';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import type { ThicknessRange } from '../../../../../Armor/components/StaticArmor';
import { DynamicArmorSwitcher } from './DynamicArmorSwitcher';
interface ThicknessesProps {
thicknessRange: ThicknessRange;
}
export function Thicknesses({ thicknessRange }: ThicknessesProps) {
const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules,
);
const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor,
);
const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor,
);
const mutateDuel = Duel.useMutation();
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const editStatic = TankopediaEphemeral.use((state) => state.editStatic);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const { hasDynamicArmor } = useArmor(protagonistTank.id);
return (
<Flex
position="absolute"
right="0"
top="50%"
mr="4"
style={{ transform: 'translateY(-50%)', userSelect: 'none' }}
direction="column"
gap="2"
align="end"
>
<Flex
height="64px"
gap="2"
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: 'pointer' }}
onClick={() => {
mutateTankopediaPersistent((draft) => {
draft.showPrimaryArmor = !draft.showPrimaryArmor;
});
mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined;
});
}}
>
<Flex direction="column" align="end" justify="between">
<Text color="gray" size="1">
{(thicknessRange.value * 1.5).toFixed(0)}
</Text>
<Flex gap="1">
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
<Text size="1">Primary</Text>
</Flex>
<Text color="gray" size="1">
0
</Text>
</Flex>
<Box
width="16px"
style={{
borderRadius: 'var(--radius-full)',
overflow: 'hidden',
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
}}
/>
</Flex>
<Flex
height="64px"
gap="2"
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: 'pointer' }}
onClick={() => {
mutateTankopediaPersistent((draft) => {
draft.showSpacedArmor = !draft.showSpacedArmor;
});
mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined;
});
}}
>
<Flex direction="column" align="end" justify="between">
<Text color="gray" size="1">
{thicknessRange.value.toFixed(0)}
</Text>
<Flex gap="1">
{showSpacedArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
<Text size="1">Spaced</Text>
</Flex>
<Text color="gray" size="1">
0
</Text>
</Flex>
<Box
width="16px"
style={{
borderRadius: 'var(--radius-full)',
overflow: 'hidden',
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
}}
/>
</Flex>
<Flex
gap="2"
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: 'pointer' }}
onClick={() => {
mutateTankopediaPersistent((draft) => {
draft.showExternalModules = !draft.showExternalModules;
});
mutateTankopediaEphemeral((draft) => {
draft.highlightArmor = undefined;
});
}}
>
<Flex direction="column" align="end" justify="between">
<Flex gap="1">
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
<Text size="1">External</Text>
</Flex>
</Flex>
<Box
width="16px"
height="16px"
style={{
borderRadius: 'var(--radius-full)',
overflow: 'hidden',
backgroundColor: '#3eb0c6',
}}
/>
</Flex>
<Flex align="center" gap="2" mt="2">
<Button
color="red"
size="1"
variant="ghost"
onClick={() => {
mutateTankopediaEphemeral((draft) => {
draft.editStatic = false;
draft.model = tankopediaEphemeralStore.getInitialState().model;
});
}}
>
<ReloadIcon />
Reset
</Button>
<Flex
gap="2"
style={{ cursor: 'pointer' }}
onClick={() => {
mutateTankopediaEphemeral((draft) => {
draft.editStatic = !draft.editStatic;
draft.highlightArmor = undefined;
});
}}
>
<Text size="1">Edit</Text>
<Checkbox size="2" checked={editStatic} />
</Flex>
</Flex>
<DynamicArmorSwitcher />
</Flex>
);
}
@@ -28,22 +28,24 @@ import {
Tooltip,
} from '@radix-ui/themes';
import { invalidate } from '@react-three/fiber';
import { type RefObject, useState } from 'react';
import { useState, type RefObject } from 'react';
import { ENVIRONMENTS } from '../../../../../constants/lightingEnvironments';
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
import { useEquipment } from '../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../hooks/useFullScreen';
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
import { useStoreBoolean } from '../../../../../hooks/useStoreBoolean';
import { useStoreKey } from '../../../../../hooks/useStoreKey';
import { $app } from '../../../../../stores/app';
import { $tankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../SmallTankIcon';
import { TankSearch } from '../../../../TankSearch';
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
import { QuickInputs } from './components/QuickInputs';
import { Thicknesses } from './components/Thicknesses';
interface OptionsProps {
thicknessRange: ThicknessRange;
@@ -51,23 +53,20 @@ interface OptionsProps {
}
export function Options({ thicknessRange, canvas }: OptionsProps) {
// const { display } = useStore($tankopediaPersistent, { keys: ['display'] });
const display = useStoreKey($tankopediaPersistent, 'display');
const display = TankopediaPersistent.use((state) => state.display);
const isFullScreen = useFullScreen();
const showGrid = useStoreBoolean($tankopediaPersistent, 'showGrid');
const showEnvironment = useStoreBoolean(
$tankopediaPersistent,
'showEnvironment',
const showGrid = TankopediaPersistent.use((state) => state.showGrid);
const showEnvironment = TankopediaPersistent.use(
(state) => state.showEnvironment,
);
const greenPenetration = useStoreBoolean(
$tankopediaPersistent,
'greenPenetration',
const greenPenetration = TankopediaPersistent.use(
(state) => state.greenPenetration,
);
const wireframe = useStoreBoolean($tankopediaPersistent, 'wireframe');
const opaque = useStoreBoolean($tankopediaPersistent, 'opaque');
const wireframe = TankopediaPersistent.use((state) => state.wireframe);
const opaque = TankopediaPersistent.use((state) => state.opaque);
const fullScreenAvailable = useFullscreenAvailability();
const environment = useStoreKey($tankopediaPersistent, 'environment');
const developerMode = useStoreBoolean($app, 'developerMode');
const environment = TankopediaPersistent.use((state) => state.environment);
const developerMode = App.use((state) => state.developerMode);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell);
@@ -78,6 +77,8 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
const mutateDuel = Duel.useMutation();
const hasEnhancedArmor = useEquipment(110);
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
return (
<>
@@ -395,7 +396,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={greenPenetration}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.model.visual.greenPenetration = checked;
draft.greenPenetration = checked;
});
}}
>
@@ -406,7 +407,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={opaque}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.model.visual.opaque = checked;
draft.opaque = checked;
});
}}
>
@@ -418,7 +419,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={wireframe}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.model.visual.wireframe = checked;
draft.wireframe = checked;
});
}}
>
@@ -434,7 +435,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={showGrid}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.model.visual.showGrid = checked;
draft.showGrid = checked;
});
}}
>
@@ -445,7 +446,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
checked={showEnvironment}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.model.visual.showEnvironment = checked;
draft.showEnvironment = checked;
});
}}
>
@@ -463,7 +464,7 @@ export function Options({ thicknessRange, canvas }: OptionsProps) {
value={environment}
onClick={() => {
mutateTankopediaPersistent((draft) => {
draft.model.visual.environment = environment;
draft.environment = environment;
});
}}
>
@@ -0,0 +1,9 @@
import { useFrame } from '@react-three/fiber';
export function AutoClear() {
useFrame(({ gl }) => {
gl.clear();
}, 0);
return null;
}
@@ -0,0 +1,195 @@
import { modelDefinitions } from '@blitzkit/core';
import { OrbitControls } from '@react-three/drei';
import { useThree } from '@react-three/fiber';
import { useEffect, useRef, useState } from 'react';
import { Vector3 } from 'three';
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
import { useAwait } from '../../../../../../hooks/useAwait';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
const initialPosition = [0, 4, -18] as const;
const poseDistances: Record<Pose, number> = {
[Pose.HullDown]: 15,
[Pose.FaceHug]: 5,
[Pose.Default]: -1,
};
export function Controls() {
const duelStore = Duel.useStore();
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
const camera = useThree((state) => state.camera);
const canvas = useThree((state) => state.gl.domElement);
const orbitControls = useRef<OrbitControlsClass>(null);
const awaitedModelDefinitions = useAwait(modelDefinitions);
const protagonist = Duel.use((state) => state.protagonist);
const antagonist = Duel.use((state) => state.antagonist);
const protagonistModelDefinition =
awaitedModelDefinitions[protagonist.tank.id];
const protagonistTrackModelDefinition =
awaitedModelDefinitions[protagonist.tank.id].tracks[protagonist.track.id];
const antagonistModelDefinition = awaitedModelDefinitions[antagonist.tank.id];
const protagonistTurretModelDefinition =
protagonistModelDefinition.turrets[protagonist.turret.id];
const antagonistTurretModelDefinition =
antagonistModelDefinition.turrets[antagonist.turret.id];
const protagonistGunModelDefinition =
protagonistTurretModelDefinition.guns[protagonist.gun.id];
const protagonistHullOrigin = new Vector3(
protagonistTrackModelDefinition.origin[0],
protagonistTrackModelDefinition.origin[1],
-protagonistTrackModelDefinition.origin[2],
);
const protagonistTurretOrigin = new Vector3(
protagonistModelDefinition.turretOrigin[0],
protagonistModelDefinition.turretOrigin[1],
-protagonistModelDefinition.turretOrigin[2],
);
const protagonistGunOrigin = new Vector3(
protagonistTurretModelDefinition.gunOrigin[0],
protagonistTurretModelDefinition.gunOrigin[1],
-protagonistTurretModelDefinition.gunOrigin[2],
);
const antagonistGunHeight =
protagonistTrackModelDefinition.origin[1] +
antagonistModelDefinition.turretOrigin[1] +
antagonistTurretModelDefinition.gunOrigin[1];
const [autoRotate, setAutoRotate] = useState(true);
useEffect(() => {
camera.position.set(...initialPosition);
orbitControls.current?.target.set(0, 1.25, 0);
}, [camera]);
useEffect(() => {
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
(state) => state.controlsEnabled,
(enabled) => {
if (orbitControls.current) orbitControls.current.enabled = enabled;
},
);
async function handlePoseEvent(event: Pose) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist.tank.equipment,
duel.protagonist.equipmentMatrix,
);
switch (event) {
case Pose.HullDown: {
const [pitch] = applyPitchYawLimits(
-Infinity,
0,
protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
camera.position
.set(0, 0, 0)
.add(protagonistHullOrigin)
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
.add(
new Vector3(
0,
poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch),
),
);
camera.lookAt(
protagonistHullOrigin
.clone()
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin),
);
orbitControls.current?.target.set(0, antagonistGunHeight, 0);
break;
}
case Pose.FaceHug: {
const [pitch] = applyPitchYawLimits(
0,
0,
protagonistGunModelDefinition.pitch,
protagonistTurretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
camera.position
.set(0, 0, 0)
.add(protagonistHullOrigin)
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
.add(
new Vector3(
0,
poseDistances[event] * Math.sin(pitch),
poseDistances[event] * -Math.cos(pitch),
),
);
orbitControls.current?.target
.set(0, 0, 0)
.add(protagonistHullOrigin)
.add(protagonistTurretOrigin)
.add(protagonistGunOrigin)
.add(
new Vector3(
0,
0.5 * poseDistances[event] * Math.sin(pitch),
0.5 * poseDistances[event] * -Math.cos(pitch),
),
);
break;
}
case Pose.Default: {
camera.position.set(...initialPosition);
orbitControls.current?.target.set(0, 1.25, 0);
break;
}
}
}
poseEvent.on(handlePoseEvent);
return () => {
unsubscribeTankopediaEphemeral();
poseEvent.off(handlePoseEvent);
};
}, [camera, protagonist.tank.id, antagonist.tank.id]);
useEffect(() => {
function handleDisturbance() {
setAutoRotate(false);
}
poseEvent.on(handleDisturbance);
canvas.addEventListener('pointerdown', handleDisturbance);
return () => {
canvas.removeEventListener('pointerdown', handleDisturbance);
poseEvent.off(handleDisturbance);
};
}, []);
return (
<OrbitControls
maxDistance={20}
minDistance={5}
ref={orbitControls}
enabled={tankopediaEphemeralStore.getState().controlsEnabled}
rotateSpeed={0.25}
enableDamping={false}
autoRotate={autoRotate}
autoRotateSpeed={1 / 4}
/>
);
}
@@ -0,0 +1,32 @@
import { Environment } from '@react-three/drei';
import { useModel } from '../../../../../../hooks/useModel';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
export function Lighting() {
const protagonist = Duel.use((state) => state.protagonist);
const { hasPbr } = useModel(protagonist.tank.id);
const showEnvironment = TankopediaPersistent.use(
(state) => state.showEnvironment,
);
const environment = TankopediaPersistent.use((state) => state.environment);
return (
<>
<Environment preset={environment} background={showEnvironment} blur={0} />
<directionalLight
position={[1, 1, -1]}
intensity={hasPbr ? 1 : 5}
castShadow
color={'rgb(225, 225, 255)'}
/>
<directionalLight
position={[-1, 1, 1]}
intensity={hasPbr ? 1 : 4}
castShadow
color={'rgb(240, 255, 240)'}
/>
</>
);
}
@@ -0,0 +1,10 @@
import { Spinner } from '@radix-ui/themes';
import { Html } from '@react-three/drei';
export function ModelLoader() {
return (
<Html center position={[0, 1.5, 0]}>
<Spinner size="3" />
</Html>
);
}
@@ -0,0 +1,29 @@
import { useFrame, useLoader } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import { useRef } from 'react';
import { MeshStandardMaterial, TextureLoader } from 'three';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
export function SceneProps() {
const show = TankopediaPersistent.use(
(state) => state.showGrid && !state.showEnvironment,
);
const texture = useLoader(TextureLoader, 'https://i.imgur.com/C28Z8nU.png');
const material = useRef<MeshStandardMaterial>(null);
useFrame(({ camera }) => {
if (!material.current) return;
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
});
texture.anisotropy = 2;
if (!show) return null;
return (
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial ref={material} map={texture} transparent />
</mesh>
);
}
@@ -0,0 +1,332 @@
import { normalizeAngleRad } from '@blitzkit/core';
import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber';
import { memo, useRef } from 'react';
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
import { useModel } from '../../../../../../hooks/useModel';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
import { useTankTransform } from '../../../../../../hooks/useTankTransform';
import { Duel } from '../../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../../stores/tankopediaPersistent';
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
export const TankModel = memo(() => {
const mutateDuel = Duel.useMutation();
const duelStore = Duel.useStore();
const protagonist = Duel.use((draft) => draft.protagonist);
const tankopediaPersistentStore = TankopediaPersistent.useStore();
const canvas = useThree((state) => state.gl.domElement);
const hullContainer = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const { gltf } = useModel(protagonist.tank.id);
const nodes = Object.values(gltf.nodes);
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73),
);
useTankTransform(protagonist, turretContainer, gunContainer);
return (
<ModelTankWrapper ref={hullContainer}>
{nodes.map((node) => {
const isHull = node.name === 'hull';
const isWheel = node.name.startsWith('chassis_wheel_');
const isTrack = node.name.startsWith('chassis_track_');
const isVisible = isHull || isWheel || isTrack;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null;
function translateTexture(offset: number) {
const mesh = node.children[0] as Mesh;
const material = mesh?.material as MeshStandardMaterial;
if (!material) return;
invalidate();
if (material.map) material.map.offset.y += offset;
if (material.aoMap) material.aoMap.offset.y += offset;
if (material.normalMap) material.normalMap.offset.y += offset;
if (material.roughnessMap) material.roughnessMap.offset.y += offset;
if (material.metalnessMap) material.metalnessMap.offset.y += offset;
}
function onPointerDown(event: ThreeEvent<PointerEvent>) {
if (
isTrack &&
tankopediaPersistentStore.getState().display ===
TankopediaDisplay.Model
) {
position.set(event.clientX, event.clientY);
event.stopPropagation();
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
} else {
event.stopPropagation();
}
}
function handlePointerMove(event: PointerEvent) {
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
const deltaX = delta.x / window.innerWidth;
const deltaY = delta.y / window.innerHeight;
translateTexture(deltaX + deltaY);
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
return jsxTree(
node,
{
mesh: {
castShadow: true,
receiveShadow: true,
onPointerDown,
},
// group(group) {
// console.log(group.name);
// return {
// visible:
// group.name !== 'ST_base' &&
// group.name !== 'MR_Base2' &&
// (!group.name.includes('state_entity') ||
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive && group.name.endsWith('shields'))),
// };
// },
},
node.uuid,
);
})}
<group ref={turretContainer}>
{nodes.map((node) => {
const isTurret = node.name.startsWith('turret_');
const isCurrentTurret =
node.name ===
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentTurret;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null;
let pitch = 0;
let yaw = 0;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
if (!isTurret) return;
position.set(event.clientX, event.clientY);
yaw = protagonist.yaw;
pitch = protagonist.pitch;
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist.tank.equipment,
duel.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
[pitch, yaw] = applyPitchYawLimits(
pitch,
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
function handlePointerUp() {
mutateDuel((draft) => {
draft.protagonist.pitch = normalizeAngleRad(pitch);
draft.protagonist.yaw = normalizeAngleRad(yaw);
});
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = true;
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
return jsxTree(
node,
{
mesh: {
castShadow: true,
receiveShadow: true,
onPointerDown,
},
// group(group) {
// return {
// visible:
// group.name !== 'ST_base' &&
// group.name !== 'MR_Base2' &&
// (!group.name.includes('state_entity') ||
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive &&
// group.name.endsWith('shields'))),
// };
// },
},
node.uuid,
);
})}
<group ref={gunContainer}>
{nodes.map((node) => {
const isCurrentMantlet =
node.name ===
`gun_${gunModelDefinition.model
.toString()
.padStart(2, '0')}_mask`;
const isCurrentGun =
node.name ===
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
const isVisible = isCurrentGun || isCurrentMantlet;
const position = new Vector2();
const delta = new Vector2();
if (!isVisible) return null;
let pitch = 0;
let yaw = 0;
function onPointerDown(event: ThreeEvent<PointerEvent>) {
event.stopPropagation();
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = false;
});
mutateTankopediaEphemeral((draft) => {
draft.shot = undefined;
draft.highlightArmor = undefined;
});
position.set(event.clientX, event.clientY);
pitch = protagonist.pitch;
yaw = protagonist.yaw;
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}
async function handlePointerMove(event: PointerEvent) {
const duel = duelStore.getState();
const hasImprovedVerticalStabilizer = await hasEquipment(
122,
duel.protagonist.tank.equipment,
duel.protagonist.equipmentMatrix,
);
const boundingRect = canvas.getBoundingClientRect();
delta.set(event.clientX, event.clientY).sub(position);
position.set(event.clientX, event.clientY);
[pitch, yaw] = applyPitchYawLimits(
pitch - delta.y * (Math.PI / boundingRect.height),
yaw + delta.x * (Math.PI / boundingRect.width),
gunModelDefinition.pitch,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
);
modelTransformEvent.emit({ pitch, yaw });
}
function handlePointerUp() {
mutateTankopediaEphemeral((draft) => {
draft.controlsEnabled = true;
});
mutateDuel((draft) => {
draft.protagonist.pitch = normalizeAngleRad(pitch);
draft.protagonist.yaw = normalizeAngleRad(yaw);
});
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
}
return jsxTree(
node,
{
mesh: {
castShadow: true,
receiveShadow: true,
onPointerDown,
},
// group(group) {
// return {
// visible:
// group.name !== 'ST_base' &&
// group.name !== 'MR_Base2' &&
// (!group.name.includes('state_entity') ||
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive &&
// group.name.endsWith('shields'))),
// };
// },
},
node.uuid,
);
})}
</group>
</group>
</ModelTankWrapper>
);
});
@@ -6,29 +6,29 @@ import {
useImperativeHandle,
useRef,
} from 'react';
import { Armor } from '../../../../../../components/Armor';
import { ArmorPlateDisplay } from '../../../../../../components/Armor/components/ArmorPlateDisplay';
import { ShotDisplay } from '../../../../../../components/Armor/components/ShotDisplay';
import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../core/blitzkit/modelTransform';
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
import { useEquipment } from '../../../../../hooks/useEquipment';
import { 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 {
StaticArmor,
ThicknessRange,
} from '../../../../../../components/Armor/components/StaticArmor';
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
import { useEquipment } from '../../../../../../hooks/useEquipment';
import { useOnScreen } from '../../../../../../hooks/useOnScreen';
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
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';
type ThicknessRange,
} from '../../../../Armor/components/StaticArmor';
import { AutoClear } from './components/AutoClear';
import { Controls } from './components/Control';
import { Lighting } from './components/Lighting';
import { ModelLoader } from './components/ModelLoader';
import { SceneProps } from './components/SceneProps';
import { TankModel } from './components/TankModel';
interface TankSandboxProps {
thicknessRange: ThicknessRange;
@@ -178,7 +178,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
<Controls />
<SceneProps />
{display !== TankopediaDisplay.StaticArmor && <TankModel />}
<ShotDisplay />
<ArmorPlateDisplay />
<AutoClear />
@@ -1,5 +1,5 @@
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';
type TankSandboxLoaderProps = Omit<FlexProps, 'id'> & {
@@ -4,6 +4,7 @@ import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { NAVBAR_HEIGHT } from '../../../constants/navbar';
import { Var } from '../../../core/radix/var';
import { useFullScreen } from '../../../hooks/useFullScreen';
import { Duel } from '../../../stores/duel';
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
import { classIcons } from '../../ClassIcon';
import { Options } from './components/Options';
@@ -12,14 +13,10 @@ import { TankSandboxLoader } from './components/TankSandboxLoader';
const awaitedTankDefinitions = await tankDefinitions;
interface HeroSectionProps {
id: number;
}
export function HeroSection({ id }: HeroSectionProps) {
export function HeroSection() {
const canvas = useRef<HTMLCanvasElement>(null);
const isFullScreen = useFullScreen();
const protagonist = awaitedTankDefinitions[id];
const protagonist = Duel.use((state) => state.protagonist.tank);
const Icon = classIcons[protagonist.class];
const [dummyLoader, setDummyLoader] = useState(true);
const treeColor =