feat: start of mixer

This commit is contained in:
TrèsAbhi committed 2025-08-28 16:12:40 -05:00
1 parent 5747d7549f
commit f3762e356f
7 files changed
+278

No files matched your search

+6
View File
@@ -861,6 +861,12 @@
},
"tools": {
"mixer": {
"name": "Mixer",
"button": "Mix and match",
"description": "Mix tank modules"
},
"guess": {
"name": "Guess the tank",
"button": "Guess away",
@@ -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 (
<group rotation={[-Math.PI / 2, 0, 0]}>
{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 <mesh {...props} key={key} />;
},
});
})}
<group
position={[
tankModel.turret_origin.x +
trackModel.origin.x -
turretTankModel.turret_origin.x -
turretTrackModel.origin.x,
tankModel.turret_origin.z +
trackModel.origin.z -
turretTankModel.turret_origin.z -
turretTrackModel.origin.z,
tankModel.turret_origin.y +
trackModel.origin.y -
turretTankModel.turret_origin.y -
turretTrackModel.origin.y,
]}
>
{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 <mesh {...props} key={key} />;
},
});
})}
<group
position={[
turretTankModel.turret_origin.x +
turretTrackModel.origin.x +
turretModel.gun_origin.x -
gunTankModel.turret_origin.x -
gunTrackModel.origin.x -
gunTurretModel.gun_origin.x,
turretTankModel.turret_origin.z +
turretTrackModel.origin.z +
turretModel.gun_origin.z -
gunTankModel.turret_origin.z -
gunTrackModel.origin.z -
gunTurretModel.gun_origin.z,
turretTankModel.turret_origin.y +
turretTrackModel.origin.y +
turretModel.gun_origin.y -
gunTankModel.turret_origin.y -
gunTrackModel.origin.y -
gunTurretModel.gun_origin.y,
]}
>
{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 <mesh {...props} key={key} />;
},
});
})}
</group>
</group>
</group>
);
}
@@ -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 (
<Canvas
camera={{
fov: 65,
position: [5, 5, -5],
}}
>
<OrbitControls />
<Environment preset="lobby" />
{/* <ambientLight intensity={6} />
<pointLight position={[10, 10, 10]} /> */}
<Suspense fallback={null}>
<Model />
</Suspense>
</Canvas>
);
}
+7
View File
@@ -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,
@@ -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 (
<LocaleProvider locale={locale}>
<MixerEphemeral.Provider
data={{
hull: hull.current,
turret: { tank: turretTank.current, turret: turret.current },
gun: {
tank: gunTank.current,
turret: gunTurret.current,
gun: gun.current,
},
}}
>
<Content />
</MixerEphemeral.Provider>
</LocaleProvider>
);
}
function Content() {
return (
<PageWrapper p="0">
<Box flexGrow="1" position="relative">
<Box position="absolute" width="100%" height="100%" top="0" left="0">
<MixerScene />
</Box>
</Box>
</PageWrapper>
);
}
@@ -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;
---
<BaseLayout>
<PageWrapper p="0" maxWidth="unset">
<Page {locale} client:load />
</PageWrapper>
</BaseLayout>
@@ -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<MixerEphemeral>()(() => data),
);