mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-14 13:16:11 +01:00
Merge branch 'main' into dev
This commit is contained in:
commit
d6ed395716
24 files changed
+654
-355
No files matched your search
@@ -139,7 +139,7 @@ export function Characteristics() {
|
||||
<Flex direction="column" gap="8" style={{ flex: 1 }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
<Heading size="5">Fire</Heading>
|
||||
<Heading size="5">Firepower</Heading>
|
||||
|
||||
<Flex>
|
||||
{gun.shells.map((thisShell, shellIndex) => (
|
||||
@@ -297,6 +297,11 @@ export function Characteristics() {
|
||||
<InfoWithDelta name="Damage" unit="hp" decimals={0}>
|
||||
{stats.damage}
|
||||
</InfoWithDelta>
|
||||
{gun.type !== 'regular' && (
|
||||
<InfoWithDelta name="Clipping potential" indent>
|
||||
{stats.clipDamage!}
|
||||
</InfoWithDelta>
|
||||
)}
|
||||
<InfoWithDelta name="Module damage" unit="hp" decimals={0}>
|
||||
{stats.moduleDamage}
|
||||
</InfoWithDelta>
|
||||
@@ -388,6 +393,15 @@ export function Characteristics() {
|
||||
>
|
||||
{stats.dispersionGunDamaged}
|
||||
</InfoWithDelta>
|
||||
<InfoWithDelta
|
||||
decimals={3}
|
||||
indent
|
||||
name="Maximum"
|
||||
unit="m"
|
||||
deltaType="lowerIsBetter"
|
||||
>
|
||||
{stats.maxDispersion}
|
||||
</InfoWithDelta>
|
||||
<Info name="Gun flexibility" unit="°" />
|
||||
<InfoWithDelta decimals={1} indent name="Depression">
|
||||
{stats.gunDepression}
|
||||
@@ -520,6 +534,16 @@ export function Characteristics() {
|
||||
>
|
||||
{stats.weight}
|
||||
</InfoWithDelta>
|
||||
<Info name="Terrain coefficients" />
|
||||
<InfoWithDelta decimals={1} indent name="Hard">
|
||||
{stats.hardTerrainCoefficient}
|
||||
</InfoWithDelta>
|
||||
<InfoWithDelta decimals={1} indent name="Medium">
|
||||
{stats.mediumTerrainCoefficient}
|
||||
</InfoWithDelta>
|
||||
<InfoWithDelta decimals={1} indent name="Soft">
|
||||
{stats.softTerrainCoefficient}
|
||||
</InfoWithDelta>
|
||||
<Info name="Power to weight ratio" unit="hp/tn" />
|
||||
<InfoWithDelta decimals={1} indent name="On hard terrain">
|
||||
{stats.powerToWeightRatioHardTerrain}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { modelDefinitions } from '../../../../../core/blitzkit/modelDefinitions'
|
||||
import { Pose, poseEvent } from '../../../../../core/blitzkit/pose';
|
||||
import { useAwait } from '../../../../../hooks/useAwait';
|
||||
import { useDuel } from '../../../../../stores/duel';
|
||||
import { useTankopediaPersistent } from '../../../../../stores/tankopedia';
|
||||
import { useTankopediaTemporary } from '../../../../../stores/tankopedia';
|
||||
|
||||
const poseDistances: Record<Pose, number> = {
|
||||
[Pose.HullDown]: 15,
|
||||
@@ -62,8 +62,8 @@ export function Controls() {
|
||||
}, [camera]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribeTankopedia = useTankopediaPersistent.subscribe(
|
||||
(state) => state.model.visual.controlsEnabled,
|
||||
const unsubscribeTankopedia = useTankopediaTemporary.subscribe(
|
||||
(state) => state.controlsEnabled,
|
||||
(enabled) => {
|
||||
if (orbitControls.current) orbitControls.current.enabled = enabled;
|
||||
},
|
||||
@@ -182,7 +182,7 @@ export function Controls() {
|
||||
maxDistance={20}
|
||||
minDistance={5}
|
||||
ref={orbitControls}
|
||||
enabled={useTankopediaPersistent.getState().model.visual.controlsEnabled}
|
||||
enabled={useTankopediaTemporary.getState().controlsEnabled}
|
||||
rotateSpeed={0.25}
|
||||
enableDamping={false}
|
||||
autoRotate={autoRotate}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChevronLeftIcon, MixIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import { classIcons } from '../../../../../../components/ClassIcon';
|
||||
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||
@@ -11,14 +12,15 @@ import { TankSandbox } from './TankSandbox';
|
||||
import { Options } from './components/Options';
|
||||
|
||||
export function HeroSection() {
|
||||
const protagonistTank = useDuel((state) => state.protagonist!.tank);
|
||||
const antagonistTank = useDuel((state) => state.antagonist!.tank);
|
||||
const protagonist = useDuel((state) => state.protagonist!.tank);
|
||||
const antagonist = useDuel((state) => state.antagonist!.tank);
|
||||
const wideFormat = useWideFormat(880);
|
||||
const compareTanks =
|
||||
protagonistTank.id === antagonistTank.id
|
||||
? [protagonistTank.id]
|
||||
: [protagonistTank.id, antagonistTank.id];
|
||||
protagonist.id === antagonist.id
|
||||
? [protagonist.id]
|
||||
: [protagonist.id, antagonist.id];
|
||||
const isFullScreen = useFullScreen();
|
||||
const Icon = classIcons[protagonist.class];
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -58,18 +60,26 @@ export function HeroSection() {
|
||||
<Heading
|
||||
size={wideFormat ? '9' : '8'}
|
||||
align={wideFormat ? undefined : 'center'}
|
||||
color={
|
||||
protagonist.treeType === 'collector'
|
||||
? 'blue'
|
||||
: protagonist.treeType === 'premium'
|
||||
? 'amber'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{protagonistTank.nameFull ?? protagonistTank.name}
|
||||
<Icon style={{ width: '0.75em', height: '0.75em' }} />{' '}
|
||||
{protagonist.nameFull ?? protagonist.name}
|
||||
</Heading>
|
||||
|
||||
<Text color="gray">
|
||||
Tier {TIER_ROMAN_NUMERALS[protagonistTank.tier]}{' '}
|
||||
Tier {TIER_ROMAN_NUMERALS[protagonist.tier]}{' '}
|
||||
{
|
||||
(strings.common.nations_adjectives as Record<string, string>)[
|
||||
protagonistTank.nation
|
||||
protagonist.nation
|
||||
]
|
||||
}{' '}
|
||||
{strings.common.tank_class_short[protagonistTank.class]}
|
||||
{strings.common.tank_class_short[protagonist.class]}
|
||||
</Text>
|
||||
|
||||
<Flex gap="4" mt="-1">
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useState } from 'react';
|
||||
import { ModuleButton } from '../../../../../../../components/ModuleButtons/ModuleButton';
|
||||
import { SmallTankIcon } from '../../../../../../../components/SmallTankIcon';
|
||||
import { resolveNearPenetration } from '../../../../../../../core/blitz/resolveNearPenetration';
|
||||
import { resolvePenetrationCoefficient } from '../../../../../../../core/blitz/resolvePenetrationCoefficient';
|
||||
import { asset } from '../../../../../../../core/blitzkit/asset';
|
||||
import { Pose, poseEvent } from '../../../../../../../core/blitzkit/pose';
|
||||
import {
|
||||
@@ -113,7 +114,17 @@ export function Options() {
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
align="end"
|
||||
>
|
||||
<Text color="gray" size={{ initial: '1', sm: '2' }}>
|
||||
{(
|
||||
resolvePenetrationCoefficient(
|
||||
hasCalibratedShells,
|
||||
antagonistShell.type,
|
||||
) * resolveNearPenetration(antagonistShell.penetration)
|
||||
).toFixed(0)}
|
||||
mm
|
||||
</Text>
|
||||
<Flex direction="column">
|
||||
{antagonistGun.shells.map((thisShell, shellIndex) => (
|
||||
<IconButton
|
||||
@@ -157,7 +168,6 @@ export function Options() {
|
||||
</IconButton>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" style={{ pointerEvents: 'auto' }}>
|
||||
<IconButton
|
||||
color={hasCalibratedShells ? undefined : 'gray'}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { ThreeEvent, useThree } from '@react-three/fiber';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { Euler, Group, Vector2, Vector3 } from 'three';
|
||||
import {
|
||||
Euler,
|
||||
Group,
|
||||
Mesh,
|
||||
MeshStandardMaterial,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { degToRad } from 'three/src/math/MathUtils';
|
||||
import { I_HAT, J_HAT, K_HAT } from '../../../../../../../constants/axis';
|
||||
import { applyPitchYawLimits } from '../../../../../../../core/blitz/applyPitchYawLimits';
|
||||
@@ -15,9 +22,7 @@ import { normalizeAngleRad } from '../../../../../../../core/math/normalizeAngle
|
||||
import { useAwait } from '../../../../../../../hooks/useAwait';
|
||||
import { useModel } from '../../../../../../../hooks/useModel';
|
||||
import { mutateDuel, useDuel } from '../../../../../../../stores/duel';
|
||||
import mutateTankopediaPersistent, {
|
||||
mutateTankopediaTemporary,
|
||||
} from '../../../../../../../stores/tankopedia';
|
||||
import { mutateTankopediaTemporary } from '../../../../../../../stores/tankopedia';
|
||||
|
||||
export const TankModel = memo(() => {
|
||||
const awaitedModelDefinitions = useAwait(modelDefinitions);
|
||||
@@ -117,11 +122,52 @@ export const TankModel = memo(() => {
|
||||
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;
|
||||
|
||||
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) return;
|
||||
|
||||
position.set(event.clientX, event.clientY);
|
||||
event.stopPropagation();
|
||||
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
window.addEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
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() {
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(
|
||||
@@ -158,8 +204,8 @@ export const TankModel = memo(() => {
|
||||
yaw = protagonist.yaw;
|
||||
pitch = protagonist.pitch;
|
||||
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.controlsEnabled = false;
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.shot = undefined;
|
||||
@@ -194,8 +240,8 @@ export const TankModel = memo(() => {
|
||||
draft.protagonist!.pitch = normalizeAngleRad(pitch);
|
||||
draft.protagonist!.yaw = normalizeAngleRad(yaw);
|
||||
});
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.controlsEnabled = true;
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
window.removeEventListener('pointerup', handlePointerUp);
|
||||
@@ -234,8 +280,8 @@ export const TankModel = memo(() => {
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
event.stopPropagation();
|
||||
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.controlsEnabled = false;
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.controlsEnabled = false;
|
||||
});
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.shot = undefined;
|
||||
@@ -269,8 +315,8 @@ export const TankModel = memo(() => {
|
||||
modelTransformEvent.emit({ pitch, yaw });
|
||||
}
|
||||
function handlePointerUp() {
|
||||
mutateTankopediaPersistent((draft) => {
|
||||
draft.model.visual.controlsEnabled = true;
|
||||
mutateTankopediaTemporary((draft) => {
|
||||
draft.controlsEnabled = true;
|
||||
});
|
||||
mutateDuel((draft) => {
|
||||
draft.protagonist!.pitch = normalizeAngleRad(pitch);
|
||||
|
||||
@@ -38,7 +38,9 @@ export function RotationInputs() {
|
||||
function handleTransformEvent({ pitch, yaw }: ModelTransformEventData) {
|
||||
if (!pitchInput.current || !yawInput.current) return;
|
||||
|
||||
pitchInput.current.value = (-radToDeg(pitch)).toFixed(1);
|
||||
pitchInput.current.value = (
|
||||
-radToDeg(pitch) + (tankModelDefinition.turretRotation?.pitch ?? 0)
|
||||
).toFixed(1);
|
||||
|
||||
if (yaw === undefined) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Flex } from '@radix-ui/themes';
|
||||
import { ShotDisplayCard } from '../../../../../components/Armor/components/ShotDisplayCard';
|
||||
|
||||
export function ShotDisplaySection() {
|
||||
return (
|
||||
<Flex
|
||||
justify="center"
|
||||
display={{
|
||||
initial: 'flex',
|
||||
sm: 'none',
|
||||
}}
|
||||
>
|
||||
<ShotDisplayCard />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { HistorySection } from './components/HistorySection';
|
||||
import { CharacteristicsSection } from './components/Model/CharacteristicsSection';
|
||||
import { HeroSection } from './components/Model/HeroSection';
|
||||
import { TechTreeSection } from './components/Model/TechTreeSection';
|
||||
import { ShotDisplaySection } from './components/ShotDisplaySection';
|
||||
import { VideoSection } from './components/VideoSection';
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
@@ -77,6 +78,7 @@ export default function Page({ params }: { params: { id: string } }) {
|
||||
{assigned && (
|
||||
<>
|
||||
<HeroSection />
|
||||
<ShotDisplaySection />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
<CharacteristicsSection />
|
||||
{!exempt && <AdMidSectionResponsive />}
|
||||
|
||||
Reference in new issue
Block a user