Merge branch 'main' into dev

This commit is contained in:
TrèsAbhi committed 2024-07-01 20:31:30 -05:00
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>
);
}
+2
View File
@@ -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 />}
@@ -10,9 +10,10 @@ import { Sort } from './Sort';
interface SearchBarProps {
topResult?: TankDefinition;
onSelect?: (tank: TankDefinition) => void;
}
export function SearchBar({ topResult }: SearchBarProps) {
export function SearchBar({ topResult, onSelect }: SearchBarProps) {
const router = useRouter();
const input = useRef<HTMLInputElement>(null);
const searching = useTankopediaFilters((state) => state.searching);
@@ -42,7 +43,12 @@ export function SearchBar({ topResult }: SearchBarProps) {
if (event.key !== 'Enter' || !topResult || searching) return;
event.preventDefault();
router.push(`/tools/tankopedia/${topResult.id}`);
if (onSelect) {
onSelect(topResult);
} else {
router.push(`/tools/tankopedia/${topResult.id}`);
}
},
[topResult],
);
+11 -1
View File
@@ -43,7 +43,17 @@ export function Sort() {
});
}}
>
DPM
Standard DPM
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={sort.by === 'fire.dpmPremium'}
onClick={() => {
mutateTankopediaFilters((draft) => {
draft.sort.by = 'fire.dpmPremium';
});
}}
>
Premium DPM
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={sort.by === 'fire.reload'}
@@ -51,6 +51,8 @@ export function TankCard({ tank, onSelect }: TankCardProps) {
return gun.dispersion.base.toFixed(3);
case 'fire.dpm':
return resolveDpm(gun, shell0).toFixed(0);
case 'fire.dpmPremium':
return shell1 ? resolveDpm(gun, shell1).toFixed(0) : '--';
case 'fire.reload':
return resolveReload(gun).toFixed(2);
case 'fire.standardPenetration':
@@ -69,7 +69,8 @@ export const TankSearch = memo<TankSearchProps>(
(a, b) =>
awaitedGameDefinitions.nations.indexOf(b.nation) -
awaitedGameDefinitions.nations.indexOf(a.nation),
);
)
.sort((a, b) => a.tier - b.tier);
break;
case 'survivability.health': {
@@ -179,6 +180,24 @@ export const TankSearch = memo<TankSearchProps>(
);
break;
case 'fire.dpmPremium':
sorted = filtered.sort(
(a, b) =>
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
? resolveDpm(
a.turrets.at(-1)!.guns.at(-1)!,
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
)
: 0) -
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
? resolveDpm(
b.turrets.at(-1)!.guns.at(-1)!,
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
)
: 0),
);
break;
case 'fire.reload':
sorted = filtered.sort(
(a, b) =>
@@ -360,7 +379,7 @@ export const TankSearch = memo<TankSearchProps>(
return (
<Flex direction="column" gap="4" flexGrow="1" {...props}>
<SearchBar topResult={tanks?.[0]} />
<SearchBar topResult={tanks?.[0]} onSelect={onSelect} />
{!filters.search && !filters.searching && (
<FilterControl compact={compact} />