mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: revamp
This commit is contained in:
1 parent
468308ad22
commit
e3d69ab83c
4 files changed
+137
-198
No files matched your search
@@ -0,0 +1,103 @@
|
|||||||
|
import { EnterFullScreenIcon, ExitFullScreenIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Checkbox, Flex, Heading, IconButton, Text } from '@radix-ui/themes';
|
||||||
|
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
|
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||||
|
import strings from '../../../../../../lang/en-US.json';
|
||||||
|
import { theme } from '../../../../../../stitches.config';
|
||||||
|
import { useDuel } from '../../../../../../stores/duel';
|
||||||
|
import mutateTankopediaPersistent, {
|
||||||
|
useTankopediaPersistent,
|
||||||
|
} from '../../../../../../stores/tankopedia';
|
||||||
|
import { TankSandbox } from './TankSandbox';
|
||||||
|
|
||||||
|
export function HeroSection() {
|
||||||
|
const tank = useDuel((state) => state.protagonist!.tank);
|
||||||
|
const mode = useTankopediaPersistent((state) => state.mode);
|
||||||
|
const isFullScreen = useFullScreen();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
pl="9"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(90deg, ${theme.colors.appBackground1}, ${theme.colors.appBackground2})`,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
maxWidth: 1600,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading size="7">{tank.name}</Heading>
|
||||||
|
<Text color="gray">
|
||||||
|
Tier {TIER_ROMAN_NUMERALS[tank.tier]}{' '}
|
||||||
|
{(strings.common.nations as Record<string, string>)[tank.nation]}{' '}
|
||||||
|
{strings.common.tank_class_short[tank.class]}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 512,
|
||||||
|
flex: 3,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TankSandbox />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 16,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
cursor: 'default',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
align="center"
|
||||||
|
gap="2"
|
||||||
|
onClick={() => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.mode = draft.mode === 'armor' ? 'model' : 'armor';
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox checked={mode === 'armor'} />
|
||||||
|
Armor
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
document.body.requestFullscreen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { LightningBoltIcon } from '@radix-ui/react-icons';
|
|
||||||
import { Button, Card, Flex, Tabs, Text, Theme } from '@radix-ui/themes';
|
|
||||||
import { PerspectiveCamera } from '@react-three/drei';
|
import { PerspectiveCamera } from '@react-three/drei';
|
||||||
import { Canvas } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
import { Suspense, use, useEffect, useRef, useState } from 'react';
|
||||||
@@ -10,24 +8,18 @@ import { applyPitchYawLimits } from '../../../../../../core/blitz/applyPitchYawL
|
|||||||
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
import { modelDefinitions } from '../../../../../../core/blitzkit/modelDefinitions';
|
||||||
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
import { modelTransformEvent } from '../../../../../../core/blitzkit/modelTransform';
|
||||||
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
import { Pose, poseEvent } from '../../../../../../core/blitzkit/pose';
|
||||||
import { tankIcon } from '../../../../../../core/blitzkit/tankIcon';
|
|
||||||
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
import { useEquipment } from '../../../../../../hooks/useEquipment';
|
||||||
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
import { useFullScreen } from '../../../../../../hooks/useFullScreen';
|
||||||
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
import { useWideFormat } from '../../../../../../hooks/useWideFormat';
|
||||||
import { BlitzkitWide } from '../../../../../../icons/BlitzkitWide';
|
|
||||||
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../../../stores/duel';
|
||||||
import mutateTankopediaPersistent, {
|
import {
|
||||||
TankopediaMode,
|
|
||||||
mutateTankopediaTemporary,
|
mutateTankopediaTemporary,
|
||||||
useTankopediaPersistent,
|
useTankopediaPersistent,
|
||||||
} from '../../../../../../stores/tankopedia';
|
} from '../../../../../../stores/tankopedia';
|
||||||
import { AntagonistBar } from '../AntagonistBar';
|
|
||||||
import { Controls } from '../Control';
|
import { Controls } from '../Control';
|
||||||
import { Lighting } from '../Lighting';
|
import { Lighting } from '../Lighting';
|
||||||
import { RotationInputs } from '../QuickInputs';
|
|
||||||
import { SceneProps } from '../SceneProps';
|
import { SceneProps } from '../SceneProps';
|
||||||
import { ModelLoader } from './components/ModelLoader';
|
import { ModelLoader } from './components/ModelLoader';
|
||||||
import { Options } from './components/Options';
|
|
||||||
import { TankModel } from './components/TankModel';
|
import { TankModel } from './components/TankModel';
|
||||||
|
|
||||||
export function TankSandbox() {
|
export function TankSandbox() {
|
||||||
@@ -138,137 +130,30 @@ export function TankSandbox() {
|
|||||||
}, [protagonist.gun, protagonist.turret]);
|
}, [protagonist.gun, protagonist.turret]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Theme radius={isFullScreen ? 'none' : undefined}>
|
<Canvas
|
||||||
<Card
|
gl={{
|
||||||
style={{
|
clippingPlanes: Object.freeze([]),
|
||||||
position: isFullScreen ? 'fixed' : 'relative',
|
localClippingEnabled: true,
|
||||||
border: isFullScreen ? 'none' : 'unset',
|
}}
|
||||||
width: isFullScreen ? '100vw' : 'unset',
|
shadows
|
||||||
height: isFullScreen ? 'calc(100vh)' : 'unset',
|
ref={canvas}
|
||||||
top: isFullScreen ? 0 : 'unset',
|
onPointerDown={handlePointerDown}
|
||||||
left: isFullScreen ? 0 : 'unset',
|
onPointerMissed={() => {
|
||||||
zIndex: isFullScreen ? 2 : 'unset',
|
mutateTankopediaTemporary((draft) => {
|
||||||
}}
|
draft.shot = undefined;
|
||||||
ref={canvasWrapper}
|
});
|
||||||
>
|
}}
|
||||||
<Flex
|
>
|
||||||
style={{
|
<PerspectiveCamera makeDefault fov={25} far={32} />
|
||||||
height: isFullScreen ? '100%' : '75vh',
|
<Controls />
|
||||||
maxHeight: isFullScreen ? 'unset' : 576,
|
<SceneProps />
|
||||||
}}
|
|
||||||
direction="column"
|
|
||||||
gap="2"
|
|
||||||
>
|
|
||||||
<Tabs.Root
|
|
||||||
value={mode}
|
|
||||||
onValueChange={(mode) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.mode = mode as TankopediaMode;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs.List>
|
|
||||||
<Tabs.Trigger value="model">
|
|
||||||
<Flex gap="2" align="center">
|
|
||||||
Model
|
|
||||||
</Flex>
|
|
||||||
</Tabs.Trigger>
|
|
||||||
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
|
|
||||||
</Tabs.List>
|
|
||||||
</Tabs.Root>
|
|
||||||
|
|
||||||
{wideFormat && (
|
<Suspense fallback={<ModelLoader />}>
|
||||||
<Text
|
<Lighting />
|
||||||
color="gray"
|
<TankModel />
|
||||||
style={{
|
<Armor />
|
||||||
position: 'absolute',
|
<ShotDisplay />
|
||||||
left: '50%',
|
</Suspense>
|
||||||
top: 10,
|
</Canvas>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BlitzkitWide height={32} />
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: 'calc(100% - 64px)',
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loadModel ? (
|
|
||||||
<div style={{ width: '100%', height: '100%' }}>
|
|
||||||
<Canvas
|
|
||||||
gl={{
|
|
||||||
clippingPlanes: Object.freeze([]),
|
|
||||||
localClippingEnabled: true,
|
|
||||||
}}
|
|
||||||
shadows
|
|
||||||
ref={canvas}
|
|
||||||
onPointerDown={handlePointerDown}
|
|
||||||
onPointerMissed={() => {
|
|
||||||
mutateTankopediaTemporary((draft) => {
|
|
||||||
draft.shot = undefined;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PerspectiveCamera makeDefault fov={25} far={32} />
|
|
||||||
<Controls />
|
|
||||||
<SceneProps />
|
|
||||||
|
|
||||||
<Suspense fallback={<ModelLoader />}>
|
|
||||||
<Lighting />
|
|
||||||
<TankModel />
|
|
||||||
<Armor />
|
|
||||||
<ShotDisplay />
|
|
||||||
</Suspense>
|
|
||||||
</Canvas>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
style={{ height: '100%', position: 'relative' }}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
style={{
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setLoadModel(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LightningBoltIcon /> Load model
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
style={{
|
|
||||||
filter: 'blur(16px)',
|
|
||||||
width: 256,
|
|
||||||
height: 128,
|
|
||||||
display: 'flex',
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
transformOrigin: 'center center',
|
|
||||||
transform: 'translate(25%, -25%) scale(300%)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={tankIcon(duel.antagonist!.tank.id, 'big')} />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Options canvasWrapper={canvasWrapper} isFullScreen={isFullScreen} />
|
|
||||||
|
|
||||||
{isFullScreen && <AntagonistBar floating />}
|
|
||||||
|
|
||||||
{loadModel && <RotationInputs isFullScreen={isFullScreen} />}
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Theme>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Flex } from '@radix-ui/themes';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import PageWrapper from '../../../../components/PageWrapper';
|
import PageWrapper from '../../../../components/PageWrapper';
|
||||||
import { assignDuelMember } from '../../../../core/blitzkit/assignDuelMember';
|
import { assignDuelMember } from '../../../../core/blitzkit/assignDuelMember';
|
||||||
import { useWideFormat } from '../../../../hooks/useWideFormat';
|
import { useWideFormat } from '../../../../hooks/useWideFormat';
|
||||||
import { mutateDuel, useDuel } from '../../../../stores/duel';
|
import { mutateDuel, useDuel } from '../../../../stores/duel';
|
||||||
import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
|
import { mutateTankopediaTemporary } from '../../../../stores/tankopedia';
|
||||||
import { AntagonistBar } from './components/AntagonistBar';
|
import { HeroSection } from './components/Model/HeroSection';
|
||||||
import { Characteristics } from './components/Characteristics';
|
|
||||||
import { Consumables } from './components/Characteristics/components/Consumables';
|
|
||||||
import { Equipment } from './components/Characteristics/components/Equipment';
|
|
||||||
import { Miscellaneous } from './components/Characteristics/components/Miscellaneous';
|
|
||||||
import { Modules } from './components/Characteristics/components/Modules';
|
|
||||||
import { Provisions } from './components/Characteristics/components/Provisions';
|
|
||||||
import { Skills } from './components/Characteristics/components/Skills';
|
|
||||||
import { TankSandbox } from './components/Model/TankSandbox';
|
|
||||||
import { Title } from './components/Title';
|
|
||||||
import { Videos } from './components/Videos';
|
|
||||||
|
|
||||||
export default function Page({ params }: { params: { id: string } }) {
|
export default function Page({ params }: { params: { id: string } }) {
|
||||||
const initialId = parseInt(params.id);
|
const initialId = parseInt(params.id);
|
||||||
@@ -66,52 +55,10 @@ export default function Page({ params }: { params: { id: string } }) {
|
|||||||
}, [initialId, assigned]);
|
}, [initialId, assigned]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="purple" size={1600}>
|
<PageWrapper noPadding noMaxWidth color="purple" size={1600}>
|
||||||
{assigned && (
|
{assigned && (
|
||||||
<>
|
<>
|
||||||
<Title />
|
<HeroSection />
|
||||||
|
|
||||||
<Flex
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
gap="8"
|
|
||||||
direction={wideFormat ? 'row' : 'column'}
|
|
||||||
align="start"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
gap="8"
|
|
||||||
direction="column"
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
width: '100%',
|
|
||||||
top: 64 + 16,
|
|
||||||
position: wideFormat ? 'sticky' : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex gap="4" direction="column">
|
|
||||||
<TankSandbox />
|
|
||||||
<AntagonistBar />
|
|
||||||
</Flex>
|
|
||||||
<Flex gap="5" wrap="wrap">
|
|
||||||
<Modules />
|
|
||||||
<Provisions />
|
|
||||||
<Consumables />
|
|
||||||
|
|
||||||
<Flex gap="5" wrap="wrap">
|
|
||||||
<Skills />
|
|
||||||
<Equipment />
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Miscellaneous />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex style={{ width: wideFormat ? 320 : '100%' }}>
|
|
||||||
<Characteristics />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Videos />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ComponentProps } from 'react';
|
|||||||
type PageWrapperProps = FlexProps & {
|
type PageWrapperProps = FlexProps & {
|
||||||
color?: ComponentProps<typeof Theme>['accentColor'];
|
color?: ComponentProps<typeof Theme>['accentColor'];
|
||||||
size?: number | string;
|
size?: number | string;
|
||||||
|
noPadding?: boolean;
|
||||||
|
noMaxWidth?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PageWrapper({
|
export default function PageWrapper({
|
||||||
@@ -11,6 +13,8 @@ export default function PageWrapper({
|
|||||||
color,
|
color,
|
||||||
size = 800,
|
size = 800,
|
||||||
children,
|
children,
|
||||||
|
noPadding = false,
|
||||||
|
noMaxWidth = false,
|
||||||
...props
|
...props
|
||||||
}: PageWrapperProps) {
|
}: PageWrapperProps) {
|
||||||
return (
|
return (
|
||||||
@@ -27,9 +31,9 @@ export default function PageWrapper({
|
|||||||
gap="4"
|
gap="4"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: size,
|
maxWidth: noMaxWidth ? undefined : size,
|
||||||
margin: 'auto',
|
margin: 'auto',
|
||||||
padding: 16,
|
padding: noPadding ? 0 : 16,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
...style,
|
...style,
|
||||||
|
|||||||
Reference in new issue
Block a user