diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 502139b03..cc76295ef 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -861,6 +861,12 @@ }, "tools": { + "mixer": { + "name": "Mixer", + "button": "Mix and match", + "description": "Mix tank modules" + }, + "guess": { "name": "Guess the tank", "button": "Guess away", diff --git a/packages/website/src/components/MixerScene/components/Model.tsx b/packages/website/src/components/MixerScene/components/Model.tsx new file mode 100644 index 000000000..b4825b06e --- /dev/null +++ b/packages/website/src/components/MixerScene/components/Model.tsx @@ -0,0 +1,137 @@ +import { Vector3 } from 'three'; +import { awaitableModelDefinitions } from '../../../core/awaitables/modelDefinitions'; +import { jsxTree } from '../../../core/blitzkit/jsxTree'; +import { useModel } from '../../../hooks/useModel'; +import { MixerEphemeral } from '../../../stores/mixerEphemeral'; + +const modelDefinitions = await awaitableModelDefinitions; + +export function Model() { + const hull = MixerEphemeral.use((state) => state.hull); + const turret = MixerEphemeral.use((state) => state.turret); + const gun = MixerEphemeral.use((state) => state.gun); + + const hullGltf = useModel(hull.id); + const turretGltf = useModel(turret.tank.id); + const gunGltf = useModel(gun.tank.id); + + const hullNodes = Object.values(hullGltf.gltf.nodes); + const turretNodes = Object.values(turretGltf.gltf.nodes); + const gunNodes = Object.values(gunGltf.gltf.nodes); + + const tankModel = modelDefinitions.models[hull.id]; + const trackModel = tankModel.tracks[hull.tracks.at(-1)!.id]; + + const turretTankModel = modelDefinitions.models[turret.tank.id]; + const turretTrackModel = + turretTankModel.tracks[turret.tank.tracks.at(-1)!.id]; + const turretModel = turretTankModel.turrets[turret.turret.id]; + + const gunTankModel = modelDefinitions.models[gun.tank.id]; + const gunTrackModel = gunTankModel.tracks[gun.tank.tracks.at(-1)!.id]; + const gunTurretModel = gunTankModel.turrets[gun.turret.id]; + const gunModel = gunTurretModel.guns[gun.gun.id]; + + const turretDelta = new Vector3( + tankModel.turret_origin.x - turretTankModel.turret_origin.x, + tankModel.turret_origin.y - turretTankModel.turret_origin.y, + tankModel.turret_origin.z - turretTankModel.turret_origin.z, + ); + const gunDelta = new Vector3( + turretModel.gun_origin.x - gunTurretModel.gun_origin.x, + turretModel.gun_origin.z - gunTurretModel.gun_origin.z, + turretModel.gun_origin.y - gunTurretModel.gun_origin.y, + ); + + return ( + + {hullNodes.map((node) => { + const isHull = node.name === 'hull'; + const isWheel = node.name.startsWith('chassis_wheel_'); + const isTrack = node.name.startsWith('chassis_track_'); + const isVisible = isHull || isWheel || isTrack; + + if (!isVisible) return null; + + return jsxTree(node, { + mesh(_, props, key) { + return ; + }, + }); + })} + + + {turretNodes.map((node) => { + const isCurrentTurret = + node.name === + `turret_${turretModel.model_id.toString().padStart(2, '0')}`; + const isVisible = isCurrentTurret; + + if (!isVisible) return null; + + return jsxTree(node, { + mesh(_, props, key) { + return ; + }, + }); + })} + + + {gunNodes.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; + + if (!isVisible) return null; + + return jsxTree(node, { + mesh(_, props, key) { + return ; + }, + }); + })} + + + + ); +} diff --git a/packages/website/src/components/MixerScene/index.tsx b/packages/website/src/components/MixerScene/index.tsx new file mode 100644 index 000000000..7c37e7076 --- /dev/null +++ b/packages/website/src/components/MixerScene/index.tsx @@ -0,0 +1,25 @@ +import { Environment, OrbitControls } from '@react-three/drei'; +import { Canvas } from '@react-three/fiber'; +import { Suspense } from 'react'; +import { Model } from './components/Model'; + +export function MixerScene() { + return ( + + + + + {/* + */} + + + + + + ); +} diff --git a/packages/website/src/constants/tools.ts b/packages/website/src/constants/tools.ts index 4f9efe129..24a4daff7 100644 --- a/packages/website/src/constants/tools.ts +++ b/packages/website/src/constants/tools.ts @@ -127,11 +127,18 @@ export const guessTool: Tool = { branches: ['main', 'dev', 'opentest', 'preview'], }; +export const mixerTool: Tool = { + id: 'mixer', + button: { color: 'red' }, + branches: ['main', 'dev', 'opentest', 'preview'], +}; + export const TOOLS: Tool[] = [ tankopediaTool, compareTool, performanceTool, guessTool, + mixerTool, // chartsTool, galleryTool, sessionTool, diff --git a/packages/website/src/pages/[...locale]/mixer/_index.tsx b/packages/website/src/pages/[...locale]/mixer/_index.tsx new file mode 100644 index 000000000..f2c0dbf1d --- /dev/null +++ b/packages/website/src/pages/[...locale]/mixer/_index.tsx @@ -0,0 +1,64 @@ +import { Box } from '@radix-ui/themes'; +import { useRef } from 'react'; +import { MixerScene } from '../../../components/MixerScene'; +import { PageWrapper } from '../../../components/PageWrapper'; +import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions'; +import { + LocaleProvider, + type LocaleAcceptorProps, +} from '../../../hooks/useLocale'; +import { MixerEphemeral } from '../../../stores/mixerEphemeral'; + +const tankDefinitions = await awaitableTankDefinitions; +const tanks = Object.values(tankDefinitions.tanks); + +export function Page({ locale }: LocaleAcceptorProps) { + const hull = useRef(tanks[Math.floor(Math.random() * tanks.length)]); + const turretTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]); + const turret = useRef( + turretTank.current.turrets[ + Math.floor(Math.random() * turretTank.current.turrets.length) + ], + ); + const gunTank = useRef(tanks[Math.floor(Math.random() * tanks.length)]); + const gunTurret = useRef( + gunTank.current.turrets[ + Math.floor(Math.random() * gunTank.current.turrets.length) + ], + ); + const gun = useRef( + gunTurret.current.guns[ + Math.floor(Math.random() * gunTurret.current.guns.length) + ], + ); + + return ( + + + + + + ); +} + +function Content() { + return ( + + + + + + + + ); +} diff --git a/packages/website/src/pages/[...locale]/mixer/index.astro b/packages/website/src/pages/[...locale]/mixer/index.astro new file mode 100644 index 000000000..ec26fd5f3 --- /dev/null +++ b/packages/website/src/pages/[...locale]/mixer/index.astro @@ -0,0 +1,15 @@ +--- +import { PageWrapper } from '../../../components/PageWrapper'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import { Page } from './_index'; + +export { getStaticPaths } from '../index.astro'; + +const { locale } = Astro.params; +--- + + + + + + diff --git a/packages/website/src/stores/mixerEphemeral.ts b/packages/website/src/stores/mixerEphemeral.ts new file mode 100644 index 000000000..2b9801650 --- /dev/null +++ b/packages/website/src/stores/mixerEphemeral.ts @@ -0,0 +1,24 @@ +import type { + GunDefinition, + TankDefinition, + TurretDefinition, +} from '@blitzkit/core'; +import { create } from 'zustand'; +import { createContextualStore } from '../core/zustand/createContextualStore'; + +export interface MixerEphemeral { + hull: TankDefinition; + turret: { + tank: TankDefinition; + turret: TurretDefinition; + }; + gun: { + tank: TankDefinition; + turret: TurretDefinition; + gun: GunDefinition; + }; +} + +export const MixerEphemeral = createContextualStore((data: MixerEphemeral) => + create()(() => data), +);