mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-13 20:56:11 +01:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cde6904513 | ||
|
|
997d1df8f9 | ||
|
|
9f15ded0c9 | ||
|
|
b2ee97b3d4 | ||
|
|
03d819a6c1 | ||
|
|
8ff26d57f6 | ||
|
|
c243ebd19a | ||
|
|
40f5042896 | ||
|
|
21130b29f8 | ||
|
|
d50b054243 | ||
|
|
6b7f8049fe | ||
|
|
e6029157aa | ||
|
|
ba656eb603 | ||
|
|
e8dd26b2ba | ||
|
|
897da9479d | ||
|
|
291395718f | ||
|
|
42e860b171 | ||
|
|
3b6d339310 | ||
|
|
728fd5b824 | ||
|
|
34217b2754 | ||
|
|
0b5c88531f | ||
|
|
e145d7c6e8 | ||
|
|
86f4712b87 | ||
|
|
ea90ca21b1 | ||
|
|
b3046d8655 |
No files matched your search
Vendored
+1
@@ -222,6 +222,7 @@
|
||||
"yolo",
|
||||
"youtuber",
|
||||
"youtubers",
|
||||
"zoomable",
|
||||
"zustand"
|
||||
],
|
||||
"deno.enable": false,
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.1.2",
|
||||
"googleapis": "^144.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"protobufjs": "^7.5.2",
|
||||
|
||||
@@ -913,6 +913,13 @@
|
||||
"percentage": "%s1%",
|
||||
"ghost_factory": "Ghost Factory",
|
||||
"dead_rail": "Dead Rail"
|
||||
},
|
||||
|
||||
"flexibility": {
|
||||
"yaw": "Yaw",
|
||||
"pitch": "Pitch",
|
||||
"depression": "Depression",
|
||||
"elevation": "Elevation"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1026,7 +1033,6 @@
|
||||
|
||||
"review": {
|
||||
"title": "Review videos",
|
||||
"disclaimer": "BlitzKit does not endorse any review videos.",
|
||||
"join": "Apply to %s1 YouTuber program"
|
||||
},
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export async function tankIcons() {
|
||||
|
||||
const nationVehicleId = tank.id;
|
||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toUniqueId } from '@blitzkit/core';
|
||||
import { NodeIO } from '@gltf-transform/core';
|
||||
import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
|
||||
import { readdir, writeFile } from 'fs/promises';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { extractModel } from '../core/blitz/extractModel';
|
||||
import { readXMLDVPL } from '../core/blitz/readXMLDVPL';
|
||||
import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL';
|
||||
@@ -32,6 +32,7 @@ export async function tankModels() {
|
||||
if (tankKey.includes('tutorial_bot')) continue;
|
||||
|
||||
const id = toUniqueId(nation, tank.id);
|
||||
|
||||
const parameters = await readYAMLDVPL<TankParameters>(
|
||||
`${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
||||
);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -7,8 +7,10 @@ import { invalidate, useFrame } from '@react-three/fiber';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
MeshBasicMaterial,
|
||||
NormalBlending,
|
||||
Object3D,
|
||||
ShaderMaterial,
|
||||
SubtractiveBlending,
|
||||
UniformsLib,
|
||||
UniformsUtils,
|
||||
Vector2,
|
||||
@@ -32,6 +34,9 @@ const excludeMaterial = new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
});
|
||||
|
||||
const date = new Date();
|
||||
const isHalloween = date.getMonth() === 9 && date.getDate() === 31;
|
||||
|
||||
export function PrimaryArmorSceneComponent({
|
||||
node,
|
||||
thickness,
|
||||
@@ -46,6 +51,8 @@ export function PrimaryArmorSceneComponent({
|
||||
fog: true,
|
||||
transparent: true,
|
||||
|
||||
blending: isHalloween ? SubtractiveBlending : NormalBlending,
|
||||
|
||||
uniforms: UniformsUtils.merge([
|
||||
UniformsLib.common,
|
||||
UniformsLib.fog,
|
||||
|
||||
+6
-7
@@ -25,7 +25,6 @@ uniform mat4 inverseProjectionMatrix;
|
||||
uniform highp sampler2D spacedArmorBuffer;
|
||||
uniform highp sampler2D spacedArmorDepth;
|
||||
|
||||
const float HALF = 0.5;
|
||||
const float EXPLOSION_DAMAGE_FACTOR = 1.1;
|
||||
const float RANDOMIZATION_FACTOR = 0.05;
|
||||
const float GREEN_VALUE = 0.392;
|
||||
@@ -38,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
|
||||
|
||||
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
|
||||
if (advancedHighlighting && couldHaveRicochet) {
|
||||
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
|
||||
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0);
|
||||
}
|
||||
|
||||
return vec3(0.0, 1.0, 0.0);
|
||||
@@ -81,12 +80,12 @@ void main() {
|
||||
float distFromArmor = primaryDist - spacedDist;
|
||||
|
||||
if (canSplash) {
|
||||
float finalDamage = HALF * damage * (1.0 - distFromArmor / explosionRadius) - EXPLOSION_DAMAGE_FACTOR * (finalThickness + spacedArmorThickness);
|
||||
float finalDamage = 0.5 * damage * (1.0 - distFromArmor / explosionRadius) - EXPLOSION_DAMAGE_FACTOR * (finalThickness + spacedArmorThickness);
|
||||
splashChance = step(0.0, finalDamage);
|
||||
penetrationChance = 0.0;
|
||||
|
||||
} else {
|
||||
remainingPen -= HALF * remainingPen * distFromArmor;
|
||||
remainingPen -= 0.5 * remainingPen * distFromArmor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,17 +97,17 @@ void main() {
|
||||
penetrationChance = clamp(1.0 - (delta + randomization) / (2.0 * randomization), 0.0, 1.0);
|
||||
|
||||
if (canSplash) {
|
||||
float splashDamage = HALF * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
|
||||
float splashDamage = 0.5 * damage - EXPLOSION_DAMAGE_FACTOR * finalThickness;
|
||||
splashChance = step(0.0, splashDamage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float alpha = opaque ? 1.0 : HALF;
|
||||
float alpha = opaque ? 1.0 : 0.5;
|
||||
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
|
||||
|
||||
if (advancedHighlighting && didRicochet) {
|
||||
baseColor = vec3(1.0, baseColor.g, 1.0);
|
||||
baseColor = vec3(1.0, baseColor.g, 0.5);
|
||||
}
|
||||
|
||||
if (greenPenetration || advancedHighlighting) {
|
||||
|
||||
@@ -198,7 +198,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateDuel((draft) => {
|
||||
@@ -298,7 +298,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
@@ -386,7 +386,7 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { GuessEphemeral } from '../stores/guessEphemeral';
|
||||
import { TankopediaDisplay } from '../stores/tankopediaPersistent/constants';
|
||||
import { SmartCanvas } from './SmartCanvas';
|
||||
import { Controls } from './Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
||||
import { Lighting } from './Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
||||
import { TankModel } from './Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
||||
@@ -13,7 +13,7 @@ export function GuessRenderer() {
|
||||
const isRevealed = guessState !== null;
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
<SmartCanvas
|
||||
style={{
|
||||
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
||||
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
||||
@@ -25,6 +25,6 @@ export function GuessRenderer() {
|
||||
<Controls distanceScale={2} zoomable={false} />
|
||||
<Lighting display={TankopediaDisplay.Model} />
|
||||
<TankModel />
|
||||
</Canvas>
|
||||
</SmartCanvas>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Canvas, type CanvasProps } from '@react-three/fiber';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import { useOnScreen } from '../hooks/useOnScreen';
|
||||
|
||||
export const SmartCanvas = forwardRef<HTMLCanvasElement, CanvasProps>(
|
||||
(props, ref) => {
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const onScreen = useOnScreen(canvas);
|
||||
|
||||
useImperativeHandle(ref, () => canvas.current!, []);
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
frameloop={onScreen ? 'demand' : 'never'}
|
||||
ref={canvas}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
import type { AssaultRanges } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { Box, Card, Flex, Text } from '@radix-ui/themes';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import { clamp } from 'three/src/math/MathUtils.js';
|
||||
import {
|
||||
fakeLogSquishyHigh,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { Var } from '../../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||
import { VisualizerCard } from '../VisualizerCard';
|
||||
import { AssaultCursor } from './components/Cursor';
|
||||
|
||||
interface Props {
|
||||
@@ -29,7 +30,7 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Card mb="6" variant="classic" style={{ aspectRatio: '1 / 1' }}>
|
||||
<VisualizerCard>
|
||||
<Flex
|
||||
position="absolute"
|
||||
top="0"
|
||||
@@ -152,6 +153,6 @@ export function AssaultRangesVisualizer({ ranges: _ranges, stats }: Props) {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</VisualizerCard>
|
||||
);
|
||||
}
|
||||
+10
-3
@@ -21,14 +21,20 @@ import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps';
|
||||
import { AssaultRangesVisualizer } from './AssaultRangesVisualizer';
|
||||
import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer';
|
||||
import { Info } from './Info';
|
||||
import { InfoWithDelta } from './InfoWithDelta';
|
||||
import { ReloadVisualizer } from './ReloadVisualizer';
|
||||
import { RicochetVisualizer } from './RicochetVisualizer';
|
||||
import { StatsTableWrapper } from './StatsTableWrapper';
|
||||
import type { StatsAcceptorProps } from './TraverseVisualizer';
|
||||
|
||||
export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
export function Firepower({
|
||||
stats,
|
||||
skeleton,
|
||||
}: StatsAcceptorProps & MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const { strings, unwrap } = useLocale();
|
||||
const [penetrationDistance, setPenetrationDistance] = useState(250);
|
||||
@@ -185,7 +191,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
value="intraClip"
|
||||
/>
|
||||
)}
|
||||
{/* <ReloadVisualizer stats={stats} /> */}
|
||||
<ReloadVisualizer stats={stats} />
|
||||
|
||||
{typeof shell.penetration !== 'number' && (
|
||||
<>
|
||||
@@ -342,7 +348,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={1}
|
||||
prefix="x "
|
||||
prefix="x"
|
||||
indent
|
||||
deltaType="lowerIsBetter"
|
||||
value="dispersionGunDamaged"
|
||||
@@ -403,6 +409,7 @@ export function Firepower({ stats }: StatsAcceptorProps) {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<GunFlexibilityVisualizer skeleton={skeleton} />
|
||||
</StatsTableWrapper>
|
||||
);
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
|
||||
export const zoom = 60;
|
||||
|
||||
export function CameraPositioner() {
|
||||
useFrame(({ camera, gl: { domElement } }) => {
|
||||
const { width } = domElement.getBoundingClientRect();
|
||||
const physicalWidth = width / zoom;
|
||||
|
||||
camera.position.set(10, 0, -physicalWidth / 6);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import type { QuicklimeEvent } from 'quicklime';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { Group } from 'three';
|
||||
import {
|
||||
modelTransformEvent,
|
||||
type ModelTransformEventData,
|
||||
} from '../../../../../../../../core/blitzkit/modelTransform';
|
||||
import { ModelChunk } from './ModelChunk';
|
||||
|
||||
export function CanvasContent() {
|
||||
const hull = useRef<Group>(null);
|
||||
const gun = useRef<Group>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleModelTransformEvent(
|
||||
event: QuicklimeEvent<ModelTransformEventData>,
|
||||
) {
|
||||
if (!gun.current) return;
|
||||
|
||||
gun.current.rotation.x = event.data.pitch;
|
||||
}
|
||||
|
||||
modelTransformEvent.on(handleModelTransformEvent);
|
||||
|
||||
return () => {
|
||||
modelTransformEvent.off(handleModelTransformEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<group ref={hull}>
|
||||
<ModelChunk />
|
||||
</group>
|
||||
|
||||
<group ref={gun}>
|
||||
<ModelChunk onlyGun />
|
||||
</group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { SmartCanvas } from '../../../../../../../SmartCanvas';
|
||||
import { CameraPositioner, zoom } from './CameraPositioner';
|
||||
import { CanvasContent } from './CanvasContent';
|
||||
|
||||
export function FlexibilityCanvas() {
|
||||
return (
|
||||
<SmartCanvas orthographic camera={{ zoom, rotation: [0, Math.PI / 2, 0] }}>
|
||||
<CanvasContent />
|
||||
<CameraPositioner />
|
||||
</SmartCanvas>
|
||||
);
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { Box } from '@radix-ui/themes';
|
||||
import { useRef, useState } from 'react';
|
||||
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||
import {
|
||||
DEFAULT_PITCH_TRANSITION,
|
||||
applyPitchYawLimits,
|
||||
} from '../../../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { hasEquipment } from '../../../../../../../../core/blitzkit/hasEquipment';
|
||||
import { modelTransformEvent } from '../../../../../../../../core/blitzkit/modelTransform';
|
||||
import { Var } from '../../../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../../../stores/duel';
|
||||
import { VisualizerCard } from '../../VisualizerCard';
|
||||
import { VisualizerCornerStat } from '../../VisualizerCornerStat';
|
||||
|
||||
const ANGLE_COEFFICIENT = 1 / 10;
|
||||
|
||||
const modelDefinition = await awaitableModelDefinitions;
|
||||
|
||||
function mag(x: number) {
|
||||
return (1 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1 / 2;
|
||||
}
|
||||
|
||||
function magInverse(y: number): number {
|
||||
return (1 / ANGLE_COEFFICIENT) * Math.tan(Math.PI * y - Math.PI / 2);
|
||||
}
|
||||
|
||||
function c(thetaDeg: number, m = 1) {
|
||||
const theta = thetaDeg * (Math.PI / 180);
|
||||
return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`;
|
||||
}
|
||||
|
||||
export function FlexibilityCard() {
|
||||
const { strings } = useLocale();
|
||||
|
||||
const [yaw, setYaw] = useState(0);
|
||||
const [pitch, setPitch] = useState(0);
|
||||
const [minPitch, setMinPitch] = useState(0);
|
||||
const [maxPitch, setMaxPitch] = useState(0);
|
||||
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const highlighter = useRef<HTMLDivElement>(null);
|
||||
const pointer = useRef<HTMLDivElement>(null);
|
||||
|
||||
const duelStore = Duel.useStore();
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
|
||||
const tankModel = modelDefinition.models[tank.id];
|
||||
const turretModel = tankModel.turrets[turret.id];
|
||||
const gunModel = turretModel.guns[gun.gun_type!.value.base.id];
|
||||
|
||||
const p = gunModel.pitch;
|
||||
const y = turretModel.yaw;
|
||||
const f = p.front;
|
||||
const b = p.back;
|
||||
const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION;
|
||||
|
||||
let d = '';
|
||||
|
||||
// right
|
||||
const mo = mag(p.max);
|
||||
if (y) {
|
||||
d += `M 0 0`;
|
||||
d += `L ${c(90 - y.max, mo)}`;
|
||||
} else if (b) {
|
||||
d += `M ${c(-90 + b.range / 2 + t, mo)}`;
|
||||
} else {
|
||||
d += `M ${c(-90, mo)}`;
|
||||
}
|
||||
if (f) {
|
||||
d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`;
|
||||
} else {
|
||||
d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`;
|
||||
}
|
||||
|
||||
// front
|
||||
if (f) {
|
||||
const m = mag(f.max);
|
||||
d += `L ${c(90 - f.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||
}
|
||||
|
||||
// left
|
||||
if (f) {
|
||||
d += `L ${c(90 + f.range / 2 + t, mo)}`;
|
||||
} else {
|
||||
d += `L ${c(90, mo)}`;
|
||||
}
|
||||
if (y) {
|
||||
d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`;
|
||||
} else if (b) {
|
||||
d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`;
|
||||
} else {
|
||||
d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`;
|
||||
}
|
||||
|
||||
// back
|
||||
if (!y && b) {
|
||||
const m = mag(b.max);
|
||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||
}
|
||||
|
||||
d += `Z`;
|
||||
|
||||
// right
|
||||
const mi = mag(p.min);
|
||||
if (y) {
|
||||
d += `M 0 0`;
|
||||
d += `L ${c(90 - y.max, mi)}`;
|
||||
} else if (b) {
|
||||
d += `M ${c(-90 + b.range / 2 + t, mi)}`;
|
||||
} else {
|
||||
d += `M ${c(-90, mi)}`;
|
||||
}
|
||||
if (f) {
|
||||
d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`;
|
||||
} else {
|
||||
d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`;
|
||||
}
|
||||
|
||||
// front
|
||||
if (f) {
|
||||
const m = mag(f.min);
|
||||
d += `L ${c(90 - f.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||
}
|
||||
|
||||
// left
|
||||
if (f) {
|
||||
d += `L ${c(90 + f.range / 2 + t, mi)}`;
|
||||
} else {
|
||||
d += `L ${c(90, mi)}`;
|
||||
}
|
||||
if (y) {
|
||||
d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`;
|
||||
} else if (b) {
|
||||
d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`;
|
||||
} else {
|
||||
d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`;
|
||||
}
|
||||
|
||||
// back
|
||||
if (!y && b) {
|
||||
const m = mag(b.min);
|
||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||
}
|
||||
|
||||
d += `Z`;
|
||||
|
||||
const r = mag(radToDeg(-pitch));
|
||||
const dotX = r * Math.sin(yaw);
|
||||
const dotY = r * Math.cos(yaw);
|
||||
|
||||
return (
|
||||
<VisualizerCard
|
||||
ref={container}
|
||||
onPointerMove={(event) => {
|
||||
if (!container.current || !highlighter.current || !pointer.current)
|
||||
return;
|
||||
|
||||
const rect = container.current.getBoundingClientRect();
|
||||
const u = event.clientX - rect.left - rect.width / 2;
|
||||
const v = event.clientY - rect.top - rect.height / 2;
|
||||
let yaw = Math.atan2(u, -v);
|
||||
|
||||
const {
|
||||
equipmentMatrix,
|
||||
tank: { equipment_preset },
|
||||
} = duelStore.getState().protagonist;
|
||||
const hasImprovedVerticalStabilizer = hasEquipment(
|
||||
122,
|
||||
equipment_preset,
|
||||
equipmentMatrix,
|
||||
);
|
||||
const hasDownImprovedVerticalStabilizer = hasEquipment(
|
||||
124,
|
||||
equipment_preset,
|
||||
equipmentMatrix,
|
||||
);
|
||||
|
||||
const min = applyPitchYawLimits(
|
||||
-Math.PI,
|
||||
yaw,
|
||||
p,
|
||||
y,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
const max = applyPitchYawLimits(
|
||||
Math.PI,
|
||||
yaw,
|
||||
p,
|
||||
y,
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
yaw = min[1];
|
||||
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
||||
t = clamp(t, 0, 1);
|
||||
let pitch = degToRad(magInverse(-t));
|
||||
pitch = clamp(pitch, min[0], max[0]);
|
||||
|
||||
setPitch(pitch);
|
||||
setYaw(yaw);
|
||||
setMinPitch(min[0]);
|
||||
setMaxPitch(max[0]);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}}
|
||||
mb="0"
|
||||
>
|
||||
<Box
|
||||
ref={highlighter}
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
borderRadius: Var('radius-2'),
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
opacity: 2 ** -3,
|
||||
backgroundImage: `url(/assets/images/tankopedia/visualizers/ricochet/armor-hash.png)`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<svg
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 'calc(100%)',
|
||||
height: 'calc(100%)',
|
||||
}}
|
||||
viewBox="-1 -1 2 2"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
fill="var(--gray-5)"
|
||||
stroke="var(--gray-8)"
|
||||
strokeWidth="1px"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
d={d}
|
||||
/>
|
||||
|
||||
<path
|
||||
stroke="var(--gray-11)"
|
||||
fill="transparent"
|
||||
strokeWidth="1px"
|
||||
strokeDasharray="0.5rem 0.5rem"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
d={`
|
||||
M 0.5 0
|
||||
A 0.5 0.5 0 1 1 -0.5 0
|
||||
A 0.5 0.5 0 1 1 0.5 0
|
||||
Z
|
||||
`}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Box
|
||||
ref={pointer}
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
width="0"
|
||||
height="0"
|
||||
style={{
|
||||
transform: `translate(-50%, -50%) rotate(${yaw + Math.PI}rad)`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width="1pt"
|
||||
height="14rem"
|
||||
style={{
|
||||
borderRadius: Var('radius-1'),
|
||||
background: `linear-gradient(${Var('gray-a11')}, ${Var('gray-a6')})`,
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
left={`${50 * (dotX + 1)}%`}
|
||||
bottom={`${50 * (dotY + 1)}%`}
|
||||
width="0.25rem"
|
||||
height="0.25rem"
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
backgroundColor: Var('gray-12'),
|
||||
transform: 'translate(-50%, 50%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<VisualizerCornerStat
|
||||
label={
|
||||
strings.website.tools.tankopedia.visualizers.flexibility.elevation
|
||||
}
|
||||
value={literals(strings.common.units.deg, [
|
||||
radToDeg(maxPitch).toFixed(0),
|
||||
])}
|
||||
side="top-left"
|
||||
/>
|
||||
|
||||
<VisualizerCornerStat
|
||||
label={
|
||||
strings.website.tools.tankopedia.visualizers.flexibility.depression
|
||||
}
|
||||
value={literals(strings.common.units.deg, [
|
||||
radToDeg(-minPitch).toFixed(0),
|
||||
])}
|
||||
side="top-right"
|
||||
/>
|
||||
|
||||
<VisualizerCornerStat
|
||||
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
|
||||
value={literals(strings.common.units.deg, [radToDeg(pitch).toFixed(0)])}
|
||||
side="bottom-left"
|
||||
/>
|
||||
|
||||
<VisualizerCornerStat
|
||||
label={strings.website.tools.tankopedia.visualizers.flexibility.yaw}
|
||||
value={literals(strings.common.units.deg, [radToDeg(yaw).toFixed(0)])}
|
||||
side="bottom-right"
|
||||
/>
|
||||
</VisualizerCard>
|
||||
);
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { mauveDark } from '@radix-ui/colors';
|
||||
import { LineBasicMaterial, MeshBasicMaterial } from 'three';
|
||||
import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions';
|
||||
import { jsxTree } from '../../../../../../../../core/blitzkit/jsxTree';
|
||||
import { useModel } from '../../../../../../../../hooks/useModel';
|
||||
import { Duel } from '../../../../../../../../stores/duel';
|
||||
|
||||
const modelDefinitions = await awaitableModelDefinitions;
|
||||
|
||||
const surfaceMaterial = new MeshBasicMaterial({
|
||||
color: mauveDark.mauve6,
|
||||
toneMapped: false,
|
||||
});
|
||||
const outlineMaterial = new LineBasicMaterial({
|
||||
color: mauveDark.mauve8,
|
||||
toneMapped: false,
|
||||
});
|
||||
|
||||
interface Props {
|
||||
onlyGun?: boolean;
|
||||
}
|
||||
|
||||
export function ModelChunk({ onlyGun = false }: Props) {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
|
||||
const tankModel = modelDefinitions.models[tank.id];
|
||||
const turretModel = tankModel.turrets[turret.id];
|
||||
const gunModel = turretModel.guns[gun.gun_type!.value.base.id];
|
||||
|
||||
const originZ = tankModel.turret_origin.z + turretModel.gun_origin.z;
|
||||
const originY = tankModel.turret_origin.y + turretModel.gun_origin.y;
|
||||
|
||||
const { gltf } = useModel(tank.id);
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
|
||||
return (
|
||||
<group position={[0, -originY, originZ]} rotation={[-Math.PI / 2, 0, 0]}>
|
||||
{nodes.map((node) => {
|
||||
const isCurrentMantlet =
|
||||
node.name ===
|
||||
`gun_${gunModel.model_id.toString().padStart(2, '0')}_mask`;
|
||||
const isCurrentGun =
|
||||
node.name === `gun_${gunModel.model_id.toString().padStart(2, '0')}`;
|
||||
const isVisible = isCurrentGun || isCurrentMantlet;
|
||||
const isHull = node.name === 'hull';
|
||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
||||
const isTrack = node.name.startsWith('chassis_track_');
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModel.model_id.toString().padStart(2, '0')}`;
|
||||
const isAnythingElse = isHull || isWheel || isTrack || isCurrentTurret;
|
||||
|
||||
if ((onlyGun && isVisible) || (!onlyGun && isAnythingElse)) {
|
||||
return jsxTree(node, {
|
||||
mesh(mesh, props, key) {
|
||||
return (
|
||||
<>
|
||||
<mesh key={key} {...props} material={surfaceMaterial} />
|
||||
<lineSegments material={outlineMaterial}>
|
||||
<edgesGeometry args={[mesh.geometry, 29]} />
|
||||
</lineSegments>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
})}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Box, Card, Flex } from '@radix-ui/themes';
|
||||
import { Var } from '../../../../../../../core/radix/var';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps';
|
||||
import { FlexibilityCanvas } from './components/FlexibilityCanvas';
|
||||
import { FlexibilityCard } from './components/FlexibilityCard';
|
||||
|
||||
export function GunFlexibilityVisualizer({
|
||||
skeleton,
|
||||
}: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<Flex direction="column-reverse" gap="0">
|
||||
<Box mt="-4" px="2">
|
||||
<Card variant="classic" style={{ height: '10rem' }}>
|
||||
<Box
|
||||
style={{ backgroundColor: Var('gray-1') }}
|
||||
position="absolute"
|
||||
width="100%"
|
||||
height="100%"
|
||||
top="0"
|
||||
left="0"
|
||||
>
|
||||
{!skeleton && <FlexibilityCanvas />}
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<FlexibilityCard />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -162,7 +162,6 @@ export function InfoWithDelta({
|
||||
<Info
|
||||
deltaType={deltaType}
|
||||
indent={indent}
|
||||
{...props}
|
||||
name={
|
||||
'name' in props
|
||||
? props.name
|
||||
@@ -171,12 +170,13 @@ export function InfoWithDelta({
|
||||
]
|
||||
}
|
||||
delta={delta}
|
||||
{...props}
|
||||
>
|
||||
{uhWhatDoICallThisVariable}
|
||||
</Info>
|
||||
|
||||
{!noRanking && (
|
||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2" mb="2">
|
||||
<Flex pl={indent ? '2' : '0'} align="center" gap="2">
|
||||
<Progress
|
||||
variant="soft"
|
||||
size="1"
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ export function ShellBorder(props: ComponentProps<'svg'>) {
|
||||
<path
|
||||
d="M149.917 7.5L151.5 9.91406V50.085L149.917 52.5H134.674C133.504 51.6657 132.116 51.1813 130.673 51.1133L130.319 51.1045H93.085C91.8919 51.1046 90.7173 51.3895 89.6582 51.9336L89.4482 52.0459L88.6494 52.4883C48.806 52.0806 22.9344 39.046 11.1416 30C22.9344 20.954 48.806 7.91846 88.6494 7.51074L89.4482 7.9541C90.5611 8.57122 91.8125 8.89543 93.085 8.89551H130.319C131.888 8.89551 133.409 8.40216 134.674 7.5H149.917Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="15"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="15"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
+4
-11
@@ -1,10 +1,11 @@
|
||||
import { type Datum, type Serie } from '@nivo/line';
|
||||
import { Box, Card, Code } from '@radix-ui/themes';
|
||||
import { Box } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Quicklime, type QuicklimeCallback } from 'quicklime';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Var } from '../../../../../../../core/radix/var';
|
||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||
import { VisualizerCard } from '../VisualizerCard';
|
||||
import { Shell } from './components/Shell';
|
||||
import { Target } from './components/Target';
|
||||
|
||||
@@ -21,7 +22,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
const reloadCircle = useRef<HTMLDivElement>(null);
|
||||
const reloadCore = useRef<HTMLDivElement>(null);
|
||||
const reloadGlow = useRef<HTMLDivElement>(null);
|
||||
const dpmDisplay = useRef<HTMLSpanElement>(null);
|
||||
|
||||
/**
|
||||
* 3-shell gun example:
|
||||
@@ -52,8 +52,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
}
|
||||
|
||||
function frame() {
|
||||
if (!dpmDisplay.current) return;
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
const dt = now - lastT;
|
||||
let reload;
|
||||
@@ -64,7 +62,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
});
|
||||
|
||||
dpm.current = shotPool.current.size * stats.damage;
|
||||
dpmDisplay.current.innerText = `${Math.round(dpm.current)}`;
|
||||
|
||||
if (state.current < 1) {
|
||||
state.current = Math.min(1, state.current + dt / stats.shellReload!);
|
||||
@@ -133,7 +130,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card variant="classic" mb="6" style={{ aspectRatio: '1 / 1' }}>
|
||||
<VisualizerCard>
|
||||
<Box position="absolute" top="0" left="0" width="100%" height="100%">
|
||||
<Target />
|
||||
|
||||
@@ -223,10 +220,6 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
times(stats.shells, (index) => (
|
||||
<Shell stats={stats} index={index} key={index} />
|
||||
))}
|
||||
|
||||
<Box position="absolute" right="3" bottom="3">
|
||||
<Code ref={dpmDisplay}>0 dpm</Code>
|
||||
</Box>
|
||||
</Card>
|
||||
</VisualizerCard>
|
||||
);
|
||||
}
|
||||
+64
-100
@@ -1,12 +1,14 @@
|
||||
import { asset, isExplosive } from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { Box, Card, Code, Flex, Text } from '@radix-ui/themes';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import { useRef, useState } from 'react';
|
||||
import { clamp, degToRad, radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import { Var } from '../../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../../stores/duel';
|
||||
import type { StatsAcceptorProps } from '../TraverseVisualizer';
|
||||
import { VisualizerCard } from '../VisualizerCard';
|
||||
import { VisualizerCornerStat } from '../VisualizerCornerStat';
|
||||
import { ShellSwitcher } from './components/ShellSwitcher';
|
||||
|
||||
export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
@@ -26,13 +28,9 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
|
||||
return (
|
||||
<Flex direction="column" mb="6" align="center">
|
||||
<Card
|
||||
variant="classic"
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1 / 1',
|
||||
touchAction: 'none',
|
||||
}}
|
||||
<VisualizerCard
|
||||
mb="0"
|
||||
style={{ width: '100%' }}
|
||||
ref={container}
|
||||
onPointerMove={(event) => {
|
||||
if (!container.current || explodes) return;
|
||||
@@ -73,23 +71,23 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
position="absolute"
|
||||
style={{
|
||||
background: `conic-gradient(
|
||||
at 0 50%,
|
||||
at 0 50%,
|
||||
|
||||
${Var('ruby-a2')},
|
||||
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
|
||||
${Var('ruby-a2')},
|
||||
${Var('ruby-a1')} ${Math.PI / 2 - ricochet}rad,
|
||||
|
||||
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
|
||||
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
|
||||
${Var('jade-a2')} ${Math.PI / 2 - ricochet}rad,
|
||||
${Var('jade-a4')} ${Math.PI / 2 - normalization}rad,
|
||||
|
||||
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
|
||||
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
|
||||
${Var('cyan-a5')} ${Math.PI / 2 - normalization}rad,
|
||||
${Var('cyan-a3')} ${Math.PI / 2 + normalization}rad,
|
||||
|
||||
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
|
||||
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
|
||||
${Var('jade-a2')} ${Math.PI / 2 + normalization}rad,
|
||||
${Var('jade-a4')} ${Math.PI / 2 + ricochet}rad,
|
||||
|
||||
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
|
||||
${Var('ruby-a2')} 180deg
|
||||
`,
|
||||
${Var('ruby-a1')} ${Math.PI / 2 + ricochet}rad,
|
||||
${Var('ruby-a2')} 180deg
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -160,85 +158,51 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Flex
|
||||
style={{ userSelect: 'none' }}
|
||||
gap="4"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
p="3"
|
||||
width="100%"
|
||||
justify="between"
|
||||
>
|
||||
<Flex gap="2">
|
||||
<Text color="gray" size="1">
|
||||
{strings.website.tools.tankopedia.visualizers.ricochet.angle}
|
||||
</Text>
|
||||
<Code size="1" variant="ghost">
|
||||
{literals(strings.common.units.deg, [
|
||||
radToDeg(Math.abs(angle)).toFixed(0),
|
||||
])}
|
||||
</Code>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2">
|
||||
<Text color="gray" size="1">
|
||||
{
|
||||
strings.website.tools.tankopedia.visualizers.ricochet
|
||||
.normalization
|
||||
}
|
||||
</Text>
|
||||
<Code size="1" variant="ghost">
|
||||
{doesRicochet
|
||||
? '-'
|
||||
: literals(strings.common.units.deg, [
|
||||
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(
|
||||
0,
|
||||
),
|
||||
])}
|
||||
</Code>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
style={{ userSelect: 'none' }}
|
||||
gap="4"
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
left="0"
|
||||
p="3"
|
||||
width="100%"
|
||||
justify="between"
|
||||
>
|
||||
<Flex gap="2">
|
||||
<Text color="gray" size="1">
|
||||
{
|
||||
strings.website.tools.tankopedia.visualizers.ricochet
|
||||
.effective_angle
|
||||
}
|
||||
</Text>
|
||||
<Code size="1" variant="ghost">
|
||||
{doesRicochet
|
||||
? '-'
|
||||
: literals(strings.common.units.deg, [
|
||||
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
||||
])}
|
||||
</Code>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2">
|
||||
<Text color="gray" size="1">
|
||||
{strings.website.tools.tankopedia.visualizers.ricochet.armor}
|
||||
</Text>
|
||||
<Code size="1" variant="ghost">
|
||||
{doesRicochet
|
||||
? '-'
|
||||
: literals(strings.common.units.percentage, [
|
||||
armorPercentage.toFixed(0),
|
||||
])}
|
||||
</Code>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<VisualizerCornerStat
|
||||
label={strings.website.tools.tankopedia.visualizers.ricochet.angle}
|
||||
value={literals(strings.common.units.deg, [
|
||||
radToDeg(Math.abs(angle)).toFixed(0),
|
||||
])}
|
||||
side="top-left"
|
||||
/>
|
||||
<VisualizerCornerStat
|
||||
label={
|
||||
strings.website.tools.tankopedia.visualizers.ricochet.normalization
|
||||
}
|
||||
value={
|
||||
doesRicochet
|
||||
? '-'
|
||||
: literals(strings.common.units.deg, [
|
||||
radToDeg(Math.min(normalization, Math.abs(angle))).toFixed(0),
|
||||
])
|
||||
}
|
||||
side="top-right"
|
||||
/>
|
||||
<VisualizerCornerStat
|
||||
label={
|
||||
strings.website.tools.tankopedia.visualizers.ricochet
|
||||
.effective_angle
|
||||
}
|
||||
value={
|
||||
doesRicochet
|
||||
? '-'
|
||||
: literals(strings.common.units.deg, [
|
||||
radToDeg(Math.abs(effectiveAngle)).toFixed(0),
|
||||
])
|
||||
}
|
||||
side="bottom-left"
|
||||
/>
|
||||
<VisualizerCornerStat
|
||||
label={strings.website.tools.tankopedia.visualizers.ricochet.armor}
|
||||
value={
|
||||
doesRicochet
|
||||
? '-'
|
||||
: literals(strings.common.units.percentage, [
|
||||
armorPercentage.toFixed(0),
|
||||
])
|
||||
}
|
||||
side="bottom-right"
|
||||
/>
|
||||
|
||||
{explodes && (
|
||||
<Flex
|
||||
@@ -262,7 +226,7 @@ export function RicochetVisualizer({ stats }: StatsAcceptorProps) {
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Card>
|
||||
</VisualizerCard>
|
||||
|
||||
<ShellSwitcher />
|
||||
</Flex>
|
||||
|
||||
+4
-3
@@ -1,8 +1,9 @@
|
||||
import { literals } from '@blitzkit/i18n';
|
||||
import { Card, Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
|
||||
import { Checkbox, Code, Flex, Tabs, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { TankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { useLocale } from '../../../../../../../hooks/useLocale';
|
||||
import { VisualizerCard } from '../VisualizerCard';
|
||||
|
||||
export interface StatsAcceptorProps {
|
||||
stats: TankCharacteristics;
|
||||
@@ -58,7 +59,7 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
|
||||
}, [terrain, turretSpeedDeg, hullSpeedDeg]);
|
||||
|
||||
return (
|
||||
<Card variant="classic" style={{ aspectRatio: '1 / 1' }}>
|
||||
<VisualizerCard>
|
||||
<div
|
||||
ref={hull}
|
||||
style={{
|
||||
@@ -188,6 +189,6 @@ export function TraverseVisualizer({ stats }: StatsAcceptorProps) {
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Card>
|
||||
</VisualizerCard>
|
||||
);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { Card, type CardProps } from '@radix-ui/themes';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export const VisualizerCard = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ style, ...props }, ref) => {
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
variant="classic"
|
||||
mt="4"
|
||||
mb="6"
|
||||
style={{ aspectRatio: '1 / 1', ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Code, Flex, Text } from '@radix-ui/themes';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: string;
|
||||
side: `${'top' | 'bottom'}-${'left' | 'right'}`;
|
||||
}
|
||||
|
||||
export function VisualizerCornerStat({ label, value, side }: Props) {
|
||||
return (
|
||||
<Flex
|
||||
gap="2"
|
||||
position="absolute"
|
||||
top={side === 'top-left' || side === 'top-right' ? '3' : undefined}
|
||||
bottom={
|
||||
side === 'bottom-left' || side === 'bottom-right' ? '3' : undefined
|
||||
}
|
||||
left={side === 'top-left' || side === 'bottom-left' ? '3' : undefined}
|
||||
right={side === 'top-right' || side === 'bottom-right' ? '3' : undefined}
|
||||
>
|
||||
<Text color="gray" size="1">
|
||||
{label}
|
||||
</Text>
|
||||
<Code size="1" variant="ghost">
|
||||
{value}
|
||||
</Code>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+3
-2
@@ -8,6 +8,7 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
|
||||
import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||
import { Crew } from './components/Crew';
|
||||
import { Firepower } from './components/Firepower';
|
||||
import { Maneuverability } from './components/Maneuverability';
|
||||
@@ -18,7 +19,7 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([
|
||||
awaitableProvisionDefinitions,
|
||||
]);
|
||||
|
||||
export function Characteristics() {
|
||||
export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const mutateDuel = Duel.useMutation();
|
||||
const crewSkills = TankopediaEphemeral.use((state) => state.skills);
|
||||
const provisions = Duel.use((state) => state.protagonist.provisions);
|
||||
@@ -93,7 +94,7 @@ export function Characteristics() {
|
||||
align={{ initial: 'center', sm: 'start' }}
|
||||
width={{ initial: '100%', sm: 'auto' }}
|
||||
>
|
||||
<Firepower stats={stats} />
|
||||
<Firepower skeleton={skeleton} stats={stats} />
|
||||
|
||||
<Flex
|
||||
gap="8"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
import { Characteristics } from './components/Characteristics';
|
||||
import { CompareOptions } from './components/Characteristics/components/CompareOptions';
|
||||
import { StatSearch } from './components/Characteristics/components/StatSearch';
|
||||
@@ -9,7 +10,9 @@ import { Modules } from './components/Modules';
|
||||
import { Provisions } from './components/Provisions';
|
||||
import { Skills } from './components/Skills';
|
||||
|
||||
export function CharacteristicsSection() {
|
||||
export function CharacteristicsSection({
|
||||
skeleton,
|
||||
}: MaybeSkeletonComponentProps) {
|
||||
return (
|
||||
<Flex direction="column" gap="3">
|
||||
<Flex
|
||||
@@ -64,7 +67,7 @@ export function CharacteristicsSection() {
|
||||
<CompareOptions />
|
||||
</Flex>
|
||||
|
||||
<Characteristics />
|
||||
<Characteristics skeleton={skeleton} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Flex, Link, Text } from '@radix-ui/themes';
|
||||
import { Var } from '../../../../core/radix/var';
|
||||
import type { TankGuide } from '../../../../pages/[...locale]/tanks/[slug]/index.astro';
|
||||
|
||||
interface Props {
|
||||
guide: TankGuide;
|
||||
}
|
||||
|
||||
export function GuidesBlitz({ guide }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Flex direction="column" gap="4" position="relative">
|
||||
<Text size="4" weight="medium">
|
||||
<Flex width="100%" justify="start" align="center" gap="2">
|
||||
<img
|
||||
style={{
|
||||
width: '1.5em',
|
||||
height: '1.5em',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
src="https://guidesblitz.com/wp-content/uploads/2023/11/cropped-logo-1.png"
|
||||
/>
|
||||
GuidesBlitz
|
||||
</Flex>
|
||||
</Text>
|
||||
|
||||
<Text size="2">{guide.description}</Text>
|
||||
<img style={{ width: '100%' }} src={guide.image} />
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
left="0"
|
||||
style={{
|
||||
background: `linear-gradient(${Var('gray-a1')}, ${Var('gray-1')})`,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Link underline="always" href={guide.url} target="_blank">
|
||||
<Flex width="100%" justify="center" align="center" gap="1">
|
||||
Read the full guide
|
||||
<CaretRightIcon width="1em" height="1em" />
|
||||
</Flex>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import type { TankGuide } from '../../../pages/[...locale]/tanks/[slug]/index.astro';
|
||||
import { GuidesBlitz } from './components/GuidesBlitz';
|
||||
|
||||
interface Props {
|
||||
guide: TankGuide;
|
||||
}
|
||||
|
||||
export function GuideSection({ guide }: Props) {
|
||||
return (
|
||||
<Flex justify="center">
|
||||
<Flex maxWidth="35rem" direction="column" gap="4">
|
||||
<GuidesBlitz guide={guide} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -97,7 +97,7 @@ export function QuickInputs() {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
mutateDuel((state) => {
|
||||
state.protagonist.pitch = pitch;
|
||||
state.protagonist.yaw = yaw;
|
||||
@@ -140,7 +140,7 @@ export function QuickInputs() {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
mutateDuel((state) => {
|
||||
state.protagonist.pitch = pitch;
|
||||
state.protagonist.yaw = yaw;
|
||||
|
||||
+1
-30
@@ -15,7 +15,6 @@ import {
|
||||
GearIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
@@ -36,10 +35,7 @@ import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAva
|
||||
import { useLocale } from '../../../../../hooks/useLocale';
|
||||
import { App } from '../../../../../stores/app';
|
||||
import { Duel } from '../../../../../stores/duel';
|
||||
import {
|
||||
ShootingRangeZoom,
|
||||
TankopediaEphemeral,
|
||||
} from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
|
||||
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps';
|
||||
@@ -97,23 +93,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<>
|
||||
{disturbed && <QuickInputs />}
|
||||
|
||||
{display === TankopediaDisplay.ShootingRange && (
|
||||
<Box
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
style={{
|
||||
zIndex: 2,
|
||||
backgroundImage: 'url(/assets/images/icons/aim-caret.png)',
|
||||
backgroundSize: 'contain',
|
||||
transform: 'translateX(-50%)',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||
|
||||
<Flex
|
||||
@@ -408,7 +387,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.disturbed = true;
|
||||
draft.display = Number(value);
|
||||
draft.shootingRangeZoom = ShootingRangeZoom.Arcade;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -448,13 +426,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</SegmentedControl.Item>
|
||||
{/* <SegmentedControl.Item value={`${TankopediaDisplay.ShootingRange}`}>
|
||||
<Tooltip content="Shooting range">
|
||||
<Flex height="100%" align="center">
|
||||
<img src="/assets/images/icons/tankopedia-shooting-range.png" style={{ height: '1.25em' }} />
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
</SegmentedControl.Item> */}
|
||||
</SegmentedControl.Root>
|
||||
|
||||
{disturbed && (
|
||||
|
||||
+18
-4
@@ -1,5 +1,5 @@
|
||||
import { TankType } from '@blitzkit/core';
|
||||
import { Box, Heading } from '@radix-ui/themes';
|
||||
import { Flex, Heading } from '@radix-ui/themes';
|
||||
import { Var } from '../../../../../../core/radix/var';
|
||||
import { useLocale } from '../../../../../../hooks/useLocale';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
@@ -87,13 +87,27 @@ export function Title({ foreground }: TitleProps) {
|
||||
? '2rem'
|
||||
: `min(65vh, ${125 / name.length}vw)`
|
||||
: `min(48vh, ${75 / name.length}vw)`;
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Flex
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.disturbed = true;
|
||||
});
|
||||
}}
|
||||
direction="column"
|
||||
position="absolute"
|
||||
align="center"
|
||||
justify="center"
|
||||
top={disturbed ? '6rem' : '50%'}
|
||||
left="50%"
|
||||
width="100%"
|
||||
height={disturbed ? fontSize : '100%'}
|
||||
style={{
|
||||
pointerEvents: disturbed ? 'none' : undefined,
|
||||
transitionDuration: '1s',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
@@ -109,7 +123,7 @@ export function Title({ foreground }: TitleProps) {
|
||||
opacity: foreground && disturbed ? 0 : 1,
|
||||
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
||||
transition: `
|
||||
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(.81,-2,.68,1)'},
|
||||
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(0.81, -2, 0.68, 1)'},
|
||||
font-size 1s,
|
||||
-webkit-text-stroke 2s,
|
||||
opacity 1s
|
||||
@@ -121,6 +135,6 @@ export function Title({ foreground }: TitleProps) {
|
||||
</Heading>
|
||||
|
||||
{!revealed && <Tracker fontSize={fontSize} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+18
-139
@@ -1,23 +1,15 @@
|
||||
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { invalidate, useThree } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { invalidate, useFrame, useThree } from '@react-three/fiber';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { PerspectiveCamera, Vector3 } from 'three';
|
||||
import { OrbitControls as OrbitControlsClass } from 'three-stdlib';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import {
|
||||
SHOOTING_RANGE_ZOOM_COEFFICIENTS,
|
||||
ShootingRangeZoom,
|
||||
TankopediaEphemeral,
|
||||
} from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||
import { aimTarget } from './SceneProps/constants';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
|
||||
const poseDistances: Record<Pose, number> = {
|
||||
[Pose.HullDown]: 15,
|
||||
@@ -27,11 +19,6 @@ const poseDistances: Record<Pose, number> = {
|
||||
|
||||
const modelDefinitions = await awaitableModelDefinitions;
|
||||
|
||||
const ARCADE_MODE_DISTANCE = 19;
|
||||
// const ARCADE_MODE_ANGLE = Math.PI / 8;
|
||||
const ARCADE_MODE_ANGLE = degToRad(10);
|
||||
const ARCADE_MODE_FOV = 54;
|
||||
|
||||
interface ControlsProps {
|
||||
naked?: boolean;
|
||||
autoRotate?: boolean;
|
||||
@@ -48,7 +35,6 @@ export function Controls({
|
||||
distanceScale = 1,
|
||||
}: ControlsProps) {
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const display = TankopediaEphemeral.use((state) => state.display);
|
||||
const duelStore = Duel.useStore();
|
||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||
const camera = useThree((state) => state.camera as PerspectiveCamera);
|
||||
@@ -103,42 +89,19 @@ export function Controls({
|
||||
gunHeight + (naked ? 10 : 2),
|
||||
-13,
|
||||
).multiplyScalar(distanceScale);
|
||||
const protagonistGunOriginOnlyY = new Vector3(
|
||||
0,
|
||||
protagonistTurretModelDefinition.gun_origin.y,
|
||||
0,
|
||||
);
|
||||
const shellOrigin = protagonistHullOrigin
|
||||
.clone()
|
||||
.add(protagonistTurretOrigin)
|
||||
.add(protagonistGunOriginOnlyY);
|
||||
|
||||
useEffect(() => {
|
||||
if (!orbitControls.current || !doAutoRotate) return;
|
||||
const t0 = useRef(Date.now() / 1000);
|
||||
useFrame(() => {
|
||||
if (!doAutoRotate) return;
|
||||
|
||||
const t0 = Date.now() / 1000;
|
||||
let cancel = false;
|
||||
const now = Date.now() / 1000;
|
||||
const t = now - t0.current;
|
||||
|
||||
function frame() {
|
||||
if (!cancel) requestAnimationFrame(frame);
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
const t = now - t0;
|
||||
|
||||
camera.position
|
||||
.copy(inspectModeInitialPosition)
|
||||
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
frame();
|
||||
|
||||
return () => {
|
||||
cancel = true;
|
||||
};
|
||||
}, [doAutoRotate, camera]);
|
||||
camera.position
|
||||
.copy(inspectModeInitialPosition)
|
||||
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
||||
@@ -252,38 +215,11 @@ export function Controls({
|
||||
}, [camera, protagonistTank.id, antagonistTank.id]);
|
||||
|
||||
useEffect(() => {
|
||||
let isInitialIteration = true;
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (display !== TankopediaDisplay.ShootingRange || event.repeat) return;
|
||||
|
||||
if (event.key === 'Shift') {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.shootingRangeZoom =
|
||||
draft.shootingRangeZoom === ShootingRangeZoom.Arcade
|
||||
? ShootingRangeZoom.Zoom0
|
||||
: ShootingRangeZoom.Arcade;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(event: WheelEvent) {
|
||||
if (display !== TankopediaDisplay.ShootingRange) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.shootingRangeZoom = clamp(
|
||||
draft.shootingRangeZoom - Math.sign(event.deltaY),
|
||||
ShootingRangeZoom.Arcade,
|
||||
ShootingRangeZoom.Zoom2,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
if (display !== TankopediaDisplay.ShootingRange) return;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -295,7 +231,6 @@ export function Controls({
|
||||
|
||||
poseEvent.on(disturb);
|
||||
canvas.addEventListener('pointerdown', disturb);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
canvas.addEventListener('wheel', handleWheel);
|
||||
document.body.addEventListener('scroll', handleScroll);
|
||||
|
||||
@@ -304,80 +239,24 @@ export function Controls({
|
||||
|
||||
orbitControls.current.rotateSpeed = 0.25;
|
||||
|
||||
if (display === TankopediaDisplay.ShootingRange) {
|
||||
const { shootingRangeZoom } = tankopediaEphemeralStore.getState();
|
||||
|
||||
orbitControls.current.enablePan = false;
|
||||
orbitControls.current.enableZoom = false;
|
||||
orbitControls.current.rotateSpeed *=
|
||||
SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
|
||||
camera.fov =
|
||||
ARCADE_MODE_FOV * SHOOTING_RANGE_ZOOM_COEFFICIENTS[shootingRangeZoom];
|
||||
|
||||
if (shootingRangeZoom === ShootingRangeZoom.Arcade) {
|
||||
orbitControls.current.target.set(0, gunHeight + 3, 0);
|
||||
|
||||
if (isInitialIteration) {
|
||||
isInitialIteration = false;
|
||||
camera.position.set(
|
||||
0,
|
||||
gunHeight + ARCADE_MODE_DISTANCE * Math.sin(ARCADE_MODE_ANGLE),
|
||||
ARCADE_MODE_DISTANCE * Math.cos(ARCADE_MODE_ANGLE),
|
||||
);
|
||||
} else {
|
||||
camera.position
|
||||
.copy(aimTarget)
|
||||
.sub(orbitControls.current.target)
|
||||
.normalize()
|
||||
.multiplyScalar(-ARCADE_MODE_DISTANCE)
|
||||
.add(orbitControls.current.target);
|
||||
}
|
||||
} else {
|
||||
orbitControls.current.target.copy(shellOrigin);
|
||||
|
||||
camera.position
|
||||
.copy(aimTarget)
|
||||
.sub(shellOrigin)
|
||||
.normalize()
|
||||
.multiplyScalar(-Number.EPSILON)
|
||||
.add(shellOrigin);
|
||||
}
|
||||
} else {
|
||||
camera.position.copy(inspectModeInitialPosition);
|
||||
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
||||
orbitControls.current.enablePan = true;
|
||||
orbitControls.current.enableZoom = true;
|
||||
camera.fov = naked ? 20 : 25;
|
||||
}
|
||||
camera.position.copy(inspectModeInitialPosition);
|
||||
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
||||
orbitControls.current.enablePan = true;
|
||||
orbitControls.current.enableZoom = true;
|
||||
camera.fov = naked ? 20 : 25;
|
||||
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
|
||||
updateCamera();
|
||||
|
||||
const unsubscribeDisplay = tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.display === TankopediaDisplay.ShootingRange,
|
||||
() => {
|
||||
disturb();
|
||||
updateCamera();
|
||||
},
|
||||
);
|
||||
|
||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.shootingRangeZoom,
|
||||
updateCamera,
|
||||
);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('pointerdown', disturb);
|
||||
poseEvent.off(disturb);
|
||||
unsubscribeDisplay();
|
||||
unsubscribeShootingRangeZoom();
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
document.body.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [display === TankopediaDisplay.ShootingRange]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OrbitControls
|
||||
|
||||
-19
@@ -1,5 +1,4 @@
|
||||
import { Environment } from '@react-three/drei';
|
||||
import { times } from 'lodash-es';
|
||||
import { Euler } from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
import { useModel } from '../../../../../../hooks/useModel';
|
||||
@@ -30,24 +29,6 @@ export function Lighting({ display }: LightingProps) {
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
<Environment preset="lobby" />
|
||||
)}
|
||||
{display === TankopediaDisplay.ShootingRange && (
|
||||
<>
|
||||
<directionalLight position={[0, -1, 0]} />
|
||||
|
||||
{times(LIGHTS_COUNT, (index) => (
|
||||
<directionalLight
|
||||
key={index}
|
||||
castShadow
|
||||
intensity={3 / LIGHTS_COUNT}
|
||||
position={[
|
||||
Math.sin(2 * Math.PI * (index / LIGHTS_COUNT)),
|
||||
1,
|
||||
Math.cos(2 * Math.PI * (index / LIGHTS_COUNT)),
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
+5
-45
@@ -1,5 +1,4 @@
|
||||
import { asset, I_HAT, J_HAT } from '@blitzkit/core';
|
||||
import { Html } from '@react-three/drei';
|
||||
import { useFrame, useLoader } from '@react-three/fiber';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { useRef } from 'react';
|
||||
@@ -21,8 +20,6 @@ import { modelTransformEvent } from '../../../../../../../core/blitzkit/modelTra
|
||||
import { tankCharacteristics } from '../../../../../../../core/blitzkit/tankCharacteristics';
|
||||
import { Duel } from '../../../../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants';
|
||||
import { TargetCircle } from './components/TargetCircle';
|
||||
import { aimTarget } from './constants';
|
||||
|
||||
const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
|
||||
@@ -121,7 +118,6 @@ export function SceneProps() {
|
||||
|
||||
useFrame(({ raycaster, camera, clock, gl }) => {
|
||||
if (
|
||||
display !== TankopediaDisplay.ShootingRange ||
|
||||
!playground.current ||
|
||||
!targetCircleWrapper.current ||
|
||||
DEBUG_disable ||
|
||||
@@ -193,7 +189,7 @@ export function SceneProps() {
|
||||
dispersion = Math.max(characteristics.dispersion * penalty, dispersion);
|
||||
}
|
||||
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
lastPitch = pitch;
|
||||
lastYaw = yaw;
|
||||
|
||||
@@ -241,45 +237,9 @@ export function SceneProps() {
|
||||
if (!disturbed) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{display !== TankopediaDisplay.ShootingRange && (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial ref={material} map={texture} transparent />
|
||||
</mesh>
|
||||
)}
|
||||
|
||||
{display === TankopediaDisplay.ShootingRange && (
|
||||
<>
|
||||
<group ref={targetCircleWrapper}>
|
||||
<Html center occlude={false} zIndexRange={[0, 0]}>
|
||||
<TargetCircle variant="client" ref={clientTargetCircle} />
|
||||
<TargetCircle variant="server" ref={serverTargetCircle} />
|
||||
</Html>
|
||||
</group>
|
||||
|
||||
<group ref={playground}>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[100, 100]} />
|
||||
<meshStandardMaterial color={0x404040} />
|
||||
</mesh>
|
||||
|
||||
<mesh
|
||||
receiveShadow
|
||||
castShadow
|
||||
position={[10, 1.5, -10]}
|
||||
rotation={[0, Math.PI / 4, 0]}
|
||||
>
|
||||
<boxGeometry args={[1, 3, 1]} />
|
||||
<meshStandardMaterial color={0xff8040} />
|
||||
</mesh>
|
||||
|
||||
<group position={[0, 0, -25]} rotation={[-Math.PI / 2, 0, Math.PI]}>
|
||||
<primitive object={mockTank.scene} />
|
||||
</group>
|
||||
</group>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial ref={material} map={texture} transparent />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
+5
-31
@@ -1,6 +1,6 @@
|
||||
import { normalizeAngleRad } from '@blitzkit/core';
|
||||
import { invalidate, useThree, type ThreeEvent } from '@react-three/fiber';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from 'three';
|
||||
import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawLimits';
|
||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||
@@ -10,10 +10,7 @@ import { useModel } from '../../../../../../hooks/useModel';
|
||||
import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition';
|
||||
import { useTankTransform } from '../../../../../../hooks/useTankTransform';
|
||||
import { Duel } from '../../../../../../stores/duel';
|
||||
import {
|
||||
ShootingRangeZoom,
|
||||
TankopediaEphemeral,
|
||||
} from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaDisplay } from '../../../../../../stores/tankopediaPersistent/constants';
|
||||
import { ModelTankWrapper } from '../../../../../Armor/components/ModelTankWrapper';
|
||||
|
||||
@@ -37,18 +34,6 @@ export function TankModel() {
|
||||
const { gltf } = useModel(protagonist.tank.id);
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.shootingRangeZoom,
|
||||
(zoom) => {
|
||||
if (!hullContainer.current) return;
|
||||
hullContainer.current.visible = zoom === ShootingRangeZoom.Arcade;
|
||||
},
|
||||
);
|
||||
|
||||
return unsubscribeShootingRangeZoom;
|
||||
}, []);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
return (
|
||||
@@ -163,12 +148,7 @@ export function TankModel() {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (
|
||||
!isTurret ||
|
||||
tankopediaEphemeralStore.getState().display ===
|
||||
TankopediaDisplay.ShootingRange
|
||||
)
|
||||
return;
|
||||
if (!isTurret) return;
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
yaw = protagonist.yaw;
|
||||
@@ -209,7 +189,7 @@ export function TankModel() {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateDuel((draft) => {
|
||||
@@ -277,12 +257,6 @@ export function TankModel() {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (
|
||||
tankopediaEphemeralStore.getState().display ===
|
||||
TankopediaDisplay.ShootingRange
|
||||
)
|
||||
return;
|
||||
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
@@ -322,7 +296,7 @@ export function TankModel() {
|
||||
hasImprovedVerticalStabilizer,
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
|
||||
+2
-17
@@ -1,14 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { Group, ShaderMaterial, UniformsLib, UniformsUtils } from 'three';
|
||||
import { jsxTree } from '../../../../../../../core/blitzkit/jsxTree';
|
||||
import { useModel } from '../../../../../../../hooks/useModel';
|
||||
import { useTankModelDefinition } from '../../../../../../../hooks/useTankModelDefinition';
|
||||
import { useTankTransform } from '../../../../../../../hooks/useTankTransform';
|
||||
import { Duel } from '../../../../../../../stores/duel';
|
||||
import {
|
||||
ShootingRangeZoom,
|
||||
TankopediaEphemeral,
|
||||
} from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { ModelTankWrapper } from '../../../../../../Armor/components/ModelTankWrapper';
|
||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||
import vertexShader from './shaders/vertex.glsl?raw';
|
||||
@@ -40,18 +37,6 @@ export function TransitionSkeleton() {
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
const revealed = tankopediaEphemeralStore((state) => state.revealed);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeShootingRangeZoom = tankopediaEphemeralStore.subscribe(
|
||||
(state) => state.shootingRangeZoom,
|
||||
(zoom) => {
|
||||
if (!hullContainer.current) return;
|
||||
hullContainer.current.visible = zoom === ShootingRangeZoom.Arcade;
|
||||
},
|
||||
);
|
||||
|
||||
return unsubscribeShootingRangeZoom;
|
||||
}, []);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
return (
|
||||
|
||||
+7
-15
@@ -1,5 +1,5 @@
|
||||
import { BLITZKIT_TANK_ICON_SIZE } from '@blitzkit/core';
|
||||
import { Canvas, invalidate } from '@react-three/fiber';
|
||||
import { invalidate } from '@react-three/fiber';
|
||||
import {
|
||||
forwardRef,
|
||||
Suspense,
|
||||
@@ -13,7 +13,6 @@ import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimi
|
||||
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';
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
StaticArmor,
|
||||
type ThicknessRange,
|
||||
} from '../../../../Armor/components/StaticArmor';
|
||||
import { SmartCanvas } from '../../../../SmartCanvas';
|
||||
import { AutoClear } from './components/AutoClear';
|
||||
import { Controls } from './components/Control';
|
||||
import { InitialFogReveal } from './components/InitialFogReveal';
|
||||
@@ -65,7 +65,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||
(state) => state.hideTankModelUnderArmor,
|
||||
);
|
||||
const onScreen = useOnScreen(canvas);
|
||||
|
||||
useEffect(() => {
|
||||
if (rawDisplay === display) return;
|
||||
@@ -125,7 +124,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
hasDownImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
draft.protagonist.pitch = pitch;
|
||||
draft.protagonist.yaw = yaw;
|
||||
});
|
||||
@@ -143,7 +142,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
hasImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
draft.protagonist.pitch = pitch;
|
||||
draft.protagonist.yaw = yaw;
|
||||
});
|
||||
@@ -161,7 +160,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
hasImprovedVerticalStabilizer,
|
||||
);
|
||||
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
draft.protagonist.pitch = pitch;
|
||||
draft.protagonist.yaw = yaw;
|
||||
});
|
||||
@@ -204,16 +203,9 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
}, [display]);
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
<SmartCanvas
|
||||
ref={canvas}
|
||||
scene={{ fog: naked ? undefined : fog.current }}
|
||||
frameloop={
|
||||
onScreen
|
||||
? display === TankopediaDisplay.ShootingRange
|
||||
? 'always'
|
||||
: 'demand'
|
||||
: 'never'
|
||||
}
|
||||
gl={{
|
||||
clippingPlanes: [],
|
||||
localClippingEnabled: true,
|
||||
@@ -254,7 +246,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
)}
|
||||
<Lighting display={display} />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
</SmartCanvas>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -112,8 +112,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
top={isFullScreen ? '0' : undefined}
|
||||
left={isFullScreen ? '0' : undefined}
|
||||
>
|
||||
<Title />
|
||||
|
||||
<Box
|
||||
className="tank-sandbox-container"
|
||||
flexGrow="1"
|
||||
@@ -122,6 +120,8 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
position="relative"
|
||||
>
|
||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||
<Title />
|
||||
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
@@ -136,7 +136,9 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
height: '100%',
|
||||
transitionDuration: '2s',
|
||||
opacity: revealed ? 1 : 0,
|
||||
filter: disturbed ? undefined : `drop-shadow(0 0 1rem black)`,
|
||||
filter: disturbed
|
||||
? undefined
|
||||
: `drop-shadow(0 1rem 1rem black)`,
|
||||
}}
|
||||
>
|
||||
<Suspense>
|
||||
@@ -145,6 +147,8 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Title foreground />
|
||||
|
||||
<Options
|
||||
skeleton={skeleton}
|
||||
canvas={canvas}
|
||||
@@ -152,8 +156,6 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Title foreground />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -24,10 +24,6 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<Heading size="6">
|
||||
{strings.website.tools.tankopedia.review.title}
|
||||
</Heading>
|
||||
|
||||
<Text color="gray">
|
||||
{strings.website.tools.tankopedia.review.disclaimer}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" wrap="wrap" justify="center">
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
import { clamp } from 'lodash-es';
|
||||
import { degToRad } from 'three/src/math/MathUtils.js';
|
||||
|
||||
export const DEFAULT_PITCH_TRANSITION = 15;
|
||||
|
||||
export function applyPitchYawLimits(
|
||||
pitch: number, // rad
|
||||
yaw: number, // rad
|
||||
@@ -28,7 +30,9 @@ export function applyPitchYawLimits(
|
||||
const elevationBuff = verticalStabilizer ? degToRad(4) : 0;
|
||||
let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff;
|
||||
let upperPitch = -degToRad(pitchLimits.min) + elevationBuff;
|
||||
const transition = degToRad(pitchLimits.transition ?? 15);
|
||||
const transition = degToRad(
|
||||
pitchLimits.transition ?? DEFAULT_PITCH_TRANSITION,
|
||||
);
|
||||
|
||||
if (pitchLimits.back) {
|
||||
const range = degToRad(pitchLimits.back.range);
|
||||
@@ -82,5 +86,5 @@ export function applyPitchYawLimits(
|
||||
|
||||
newPitch = clamp(newPitch, lowerPitch, upperPitch);
|
||||
|
||||
return [newPitch, newYaw];
|
||||
return [newPitch, newYaw] as const;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { EquipmentMatrix } from '../../stores/duel';
|
||||
import { awaitableEquipmentDefinitions } from '../awaitables/equipmentDefinitions';
|
||||
|
||||
export async function hasEquipment(
|
||||
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
||||
|
||||
export function hasEquipment(
|
||||
id: number,
|
||||
equipmentPreset: string,
|
||||
equipmentMatrix: EquipmentMatrix,
|
||||
) {
|
||||
const equipmentDefinitions = await awaitableEquipmentDefinitions;
|
||||
const preset = equipmentDefinitions.presets[equipmentPreset];
|
||||
return preset.slots.some((slot, index) => {
|
||||
const row = Math.floor(index / 3);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EventManager } from '@blitzkit/core';
|
||||
import { Quicklime } from 'quicklime';
|
||||
|
||||
export interface ModelTransformEventData {
|
||||
yaw?: number;
|
||||
pitch: number;
|
||||
}
|
||||
|
||||
export const modelTransformEvent = new EventManager<ModelTransformEventData>();
|
||||
export const modelTransformEvent = new Quicklime<ModelTransformEventData>();
|
||||
@@ -25,5 +25,6 @@ export function useModel(id: number) {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return cache[id];
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { PageWrapper } from '../../../../components/PageWrapper';
|
||||
import { CalloutsSection } from '../../../../components/Tankopedia/CalloutsSection';
|
||||
import { CharacteristicsSection } from '../../../../components/Tankopedia/CharacteristicsSection';
|
||||
import { GameModeSection } from '../../../../components/Tankopedia/GameModeSection';
|
||||
import { GuideSection } from '../../../../components/Tankopedia/GuideSection';
|
||||
import { HeroSection } from '../../../../components/Tankopedia/HeroSection';
|
||||
import { MetaSection } from '../../../../components/Tankopedia/MetaSection';
|
||||
import { TechTreeSection } from '../../../../components/Tankopedia/TechTreeSection';
|
||||
@@ -19,10 +20,12 @@ import { Duel } from '../../../../stores/duel';
|
||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../../types/maybeSkeletonComponentProps';
|
||||
import type { TankGuide } from './index.astro';
|
||||
|
||||
type PageProps = MaybeSkeletonComponentProps &
|
||||
LocaleAcceptorProps & {
|
||||
id: number;
|
||||
guide?: TankGuide;
|
||||
};
|
||||
|
||||
const [tankDefinitions, provisionDefinitions, modelDefinitions] =
|
||||
@@ -32,7 +35,7 @@ const [tankDefinitions, provisionDefinitions, modelDefinitions] =
|
||||
awaitableModelDefinitions,
|
||||
]);
|
||||
|
||||
export function Page({ id, skeleton, locale }: PageProps) {
|
||||
export function Page({ id, skeleton, locale, guide }: PageProps) {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const model = modelDefinitions.models[id];
|
||||
|
||||
@@ -51,8 +54,9 @@ export function Page({ id, skeleton, locale }: PageProps) {
|
||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||
<TechTreeSection skeleton={skeleton} />
|
||||
)}
|
||||
<CharacteristicsSection />
|
||||
<CharacteristicsSection skeleton={skeleton} />
|
||||
<GameModeSection />
|
||||
{guide && <GuideSection guide={guide} />}
|
||||
<VideoSection skeleton={skeleton} />
|
||||
</PageWrapper>
|
||||
</Duel.Provider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { AstroSeo } from '@astrolib/seo';
|
||||
import {
|
||||
assertSecret,
|
||||
asset,
|
||||
createDefaultProvisions,
|
||||
createDefaultSkills,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
import { DEFAULT_LOCALE, unwrapper } from '@blitzkit/i18n';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import type { GetStaticPaths } from 'astro';
|
||||
import { load } from 'cheerio';
|
||||
import { tankCharacteristics } from '../../../../core/blitzkit/tankCharacteristics';
|
||||
import { getStrings } from '../../../../core/i18n/getStrings';
|
||||
import BaseLayout from '../../../../layouts/BaseLayout.astro';
|
||||
@@ -43,6 +45,7 @@ const skillDefinitions = await fetchSkillDefinitions();
|
||||
const provisionDefinitions = await fetchProvisionDefinitions();
|
||||
const equipmentDefinitions = await fetchEquipmentDefinitions();
|
||||
const tankModelDefinitions = await fetchModelDefinitions();
|
||||
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
const tankModelDefinition = tankModelDefinitions.models[tank.id];
|
||||
const unwrap = unwrapper(locale);
|
||||
@@ -57,6 +60,7 @@ const title = literals(strings.website.tools.tankopedia.tank_seo_title, [
|
||||
strings.common.nations[tank.nation as keyof typeof strings.common.nations],
|
||||
]);
|
||||
const url = import.meta.env.SITE + Astro.url.pathname;
|
||||
|
||||
const icon = asset(`icons/tanks/big/${id}.webp`);
|
||||
const turret = tank.turrets.at(-1)!;
|
||||
const gun = turret.guns.at(-1)!;
|
||||
@@ -96,6 +100,30 @@ const description = literals(
|
||||
Math.round(characteristics.health).toLocaleString(locale),
|
||||
],
|
||||
);
|
||||
|
||||
export interface TankGuide {
|
||||
url: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const guideUrl = `${assertSecret(import.meta.env.PUBLIC_GUIDE_WEBSITE)}/${tank.slug}`;
|
||||
let guide: TankGuide | undefined = undefined;
|
||||
const guideResponse = await fetch(guideUrl);
|
||||
|
||||
if (guideResponse.ok) {
|
||||
const $ = await guideResponse.text().then(load);
|
||||
const description = $('.entry-content p').first().text();
|
||||
const image = $('.entry-content img').first().attr('src')?.toString();
|
||||
|
||||
if (description !== undefined || image !== undefined) {
|
||||
guide = {
|
||||
url: guideUrl,
|
||||
description,
|
||||
image,
|
||||
};
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
<AstroSeo
|
||||
@@ -110,7 +138,7 @@ const description = literals(
|
||||
<meta name="twitter:image:src" content={icon} />
|
||||
|
||||
<BaseLayout integrated>
|
||||
<Page {locale} id={id} client:only="react">
|
||||
<Page {locale} id={id} skeleton slot="fallback" />
|
||||
<Page {locale} id={id} {guide} client:only="react">
|
||||
<Page {locale} id={id} {guide} skeleton slot="fallback" />
|
||||
</Page>
|
||||
</BaseLayout>
|
||||
@@ -78,28 +78,10 @@ export enum TankopediaRelativeAgainst {
|
||||
All,
|
||||
}
|
||||
|
||||
export enum ShootingRangeZoom {
|
||||
Arcade,
|
||||
Zoom0,
|
||||
Zoom1,
|
||||
Zoom2,
|
||||
}
|
||||
|
||||
export const SHOOTING_RANGE_ZOOM_COEFFICIENTS: Record<
|
||||
ShootingRangeZoom,
|
||||
number
|
||||
> = {
|
||||
[ShootingRangeZoom.Arcade]: 1,
|
||||
[ShootingRangeZoom.Zoom0]: 1 / 8,
|
||||
[ShootingRangeZoom.Zoom1]: 1 / 16,
|
||||
[ShootingRangeZoom.Zoom2]: 1 / 25,
|
||||
};
|
||||
|
||||
interface TankopediaEphemeral {
|
||||
disturbed: boolean;
|
||||
revealed: boolean;
|
||||
shot?: Shot;
|
||||
shootingRangeZoom: ShootingRangeZoom;
|
||||
skills: Record<string, number>;
|
||||
relativeAgainst: TankopediaRelativeAgainst;
|
||||
controlsEnabled: boolean;
|
||||
@@ -133,7 +115,6 @@ export const TankopediaEphemeral = createContextualStore(
|
||||
subscribeWithSelector<TankopediaEphemeral>(() => ({
|
||||
disturbed: false,
|
||||
revealed: false,
|
||||
shootingRangeZoom: ShootingRangeZoom.Arcade,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
editStatic: false,
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
|
||||
@@ -38,5 +38,4 @@ export enum TankopediaDisplay {
|
||||
Model,
|
||||
DynamicArmor,
|
||||
StaticArmor,
|
||||
ShootingRange,
|
||||
}
|
||||
Reference in new issue
Block a user