diff --git a/packages/website/public/assets/images/3d/drag-slicer.png b/packages/website/public/assets/images/3d/drag-slicer.png new file mode 100644 index 000000000..52b9b3fb7 Binary files /dev/null and b/packages/website/public/assets/images/3d/drag-slicer.png differ diff --git a/packages/website/public/assets/images/icons/tankopedia-modules.png b/packages/website/public/assets/images/icons/tankopedia-dissector.png similarity index 100% rename from packages/website/public/assets/images/icons/tankopedia-modules.png rename to packages/website/public/assets/images/icons/tankopedia-dissector.png diff --git a/packages/website/public/assets/images/tools/api.webp b/packages/website/public/assets/images/tools/api.webp new file mode 100644 index 000000000..2423626e8 Binary files /dev/null and b/packages/website/public/assets/images/tools/api.webp differ diff --git a/packages/website/public/assets/images/tools/tankopedia.webp b/packages/website/public/assets/images/tools/tanks.webp similarity index 100% rename from packages/website/public/assets/images/tools/tankopedia.webp rename to packages/website/public/assets/images/tools/tanks.webp diff --git a/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx b/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx index daa4f71b0..0ce05ee9f 100644 --- a/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx +++ b/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx @@ -22,7 +22,6 @@ import { useThree } from '@react-three/fiber'; import { Fragment, useEffect, useRef } from 'react'; import type { Mesh } from 'three'; import { - Box3, Color, DoubleSide, Group, @@ -109,6 +108,12 @@ export function DissectorSceneComponent({ const armorName = mesh.material.name; const armorPlate = groupArmor.armors[armorName]; + + if (armorPlate === undefined) { + console.warn(`Missing armor: ${armorName}`); + return null; + } + const isModule = armorPlate.type !== 'Armor' && armorPlate.type !== 'ArmorScreen'; const isTrack = @@ -149,8 +154,6 @@ export function DissectorSceneComponent({ }), ); const worldPosition = new Vector3(); - const boundsTop = new Vector3(); - const bounds = new Box3(); useEffect(() => { const offset = new Vector3(); diff --git a/packages/website/src/components/Armor/components/SpacedArmorScene.tsx b/packages/website/src/components/Armor/components/SpacedArmorScene.tsx index b3a84d454..718c2de5a 100644 --- a/packages/website/src/components/Armor/components/SpacedArmorScene.tsx +++ b/packages/website/src/components/Armor/components/SpacedArmorScene.tsx @@ -1,15 +1,8 @@ -import { memo, useRef } from 'react'; -import { Group, Plane, Scene, Vector3 } from 'three'; -import { correctZYTuple } from '../../../core/blitz/correctZYTuple'; -import { nameToArmorId } from '../../../core/blitzkit/nameToArmorId'; -import { resolveArmor } from '../../../core/blitzkit/resolveThickness'; -import { useArmor } from '../../../hooks/useArmor'; -import { useModel } from '../../../hooks/useModel'; -import { useTankModelDefinition } from '../../../hooks/useTankModelDefinition'; -import { useTankTransform } from '../../../hooks/useTankTransform'; +import { Scene } from 'three'; +import { arrayfy } from '../../../core/three/arrayfy'; import { Duel } from '../../../stores/duel'; -import { ModelTankWrapper } from './ModelTankWrapper'; -import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent'; +import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; +import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent/index'; export enum ArmorType { Primary, @@ -21,185 +14,25 @@ interface SpacedArmorSceneProps { scene: Scene; } -export const SpacedArmorScene = memo(({ scene }) => { - const wrapper = useRef(null); - const turretContainer = useRef(null); - const gunContainer = useRef(null); - const tank = Duel.use((state) => state.protagonist.tank); - const track = Duel.use((state) => state.protagonist.track); +export function SpacedArmorScene({ scene }: SpacedArmorSceneProps) { + const model = TankopediaEphemeral.use((state) => state.model); const turret = Duel.use((state) => state.protagonist.turret); const gun = Duel.use((state) => state.protagonist.gun); - const { gltf: armorGltf } = useArmor(tank.id); - const { gltf: modelGltf } = useModel(tank.id); - const armorNodes = Object.values(armorGltf.nodes); - const modelNodes = Object.values(modelGltf.nodes); - const tankModelDefinition = useTankModelDefinition(); - const trackModelDefinition = tankModelDefinition.tracks[track.id]; - const turretModelDefinition = tankModelDefinition.turrets[turret.id]; - const gunModelDefinition = - turretModelDefinition.guns[gun.gun_type!.value.base.id]; - const hullOrigin = correctZYTuple(trackModelDefinition.origin); - const turretOrigin = correctZYTuple(tankModelDefinition.turret_origin); - const gunOrigin = correctZYTuple(turretModelDefinition.gun_origin); - const maskOrigin = - gunModelDefinition.mask === undefined - ? undefined - : gunModelDefinition.mask + hullOrigin.y + turretOrigin.y + gunOrigin.y; - const isDynamicArmorActive = Duel.use((state) => - state.protagonist.consumables.includes(73), - ); - - useTankTransform(track, turret, turretContainer, gunContainer); + const turretTranslation = arrayfy(model.modules[turret.id].translation); + const gunTranslation = arrayfy(model.modules[gun.id].translation); return ( - - - {armorNodes.map((node) => { - const isHull = node.name.startsWith('hull_'); - const isVisible = isHull; - const armorId = nameToArmorId(node.name); - const { spaced, thickness } = resolveArmor( - tankModelDefinition.armor, - armorId, - ); + + + - if ( - !isVisible || - thickness === undefined || - (isDynamicArmorActive && node.name.includes('state_00')) || - (!isDynamicArmorActive && node.name.includes('state_01')) - ) - return null; - - return ( - - ); - })} + + - {modelNodes.map((node) => { - const isWheel = node.name.startsWith('chassis_wheel_'); - const isTrack = node.name.startsWith('chassis_track_'); - const isVisible = isWheel || isTrack; - - if (!isVisible) return null; - - return ( - - ); - })} - - - {armorNodes.map((node) => { - const isCurrentTurret = node.name.startsWith( - `turret_${turretModelDefinition.model_id.toString().padStart(2, '0')}`, - ); - const isVisible = isCurrentTurret; - const armorId = nameToArmorId(node.name); - const { spaced, thickness } = resolveArmor( - turretModelDefinition.armor, - armorId, - ); - - if ( - !isVisible || - thickness === undefined || - (isDynamicArmorActive && node.name.includes('state_00')) || - (!isDynamicArmorActive && node.name.includes('state_01')) - ) - return null; - - return ( - - - - - - ); - })} - - - {armorNodes.map((node) => { - const isCurrentGun = node.name.startsWith( - `gun_${gunModelDefinition.model_id.toString().padStart(2, '0')}`, - ); - const isVisible = isCurrentGun; - const armorId = nameToArmorId(node.name); - const { spaced, thickness } = resolveArmor( - gunModelDefinition.armor, - armorId, - ); - - if ( - !isVisible || - thickness === undefined || - (isDynamicArmorActive && node.name.includes('state_00')) || - (!isDynamicArmorActive && node.name.includes('state_01')) - ) - return null; - - return ( - - - - - - ); - })} - - {modelNodes.map((node) => { - const gunString = `gun_${gunModelDefinition.model_id - .toString() - .padStart(2, '0')}`; - const isCurrentGun = gunModelDefinition.mask - ? node.name.startsWith(gunString) - : node.name === gunString; - const isVisible = isCurrentGun; - - if (!isVisible) return null; - - return ( - - ); - })} - + + - + ); -}); +} diff --git a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx index 983692e2a..0333b1d52 100644 --- a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx +++ b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubExternal/index.tsx @@ -7,12 +7,12 @@ import { Plane, ShaderMaterial, } from 'three'; -import type { ArmorUserData, ExternalModuleVariant } from '../..'; import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; import { jsxTree } from '../../../../../../core/blitzkit/jsxTree'; import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; import { ArmorType } from '../../../SpacedArmorScene'; +import type { ArmorUserData, ExternalModuleVariant } from '../../_index'; import fragmentShader from './shaders/fragment.glsl?raw'; import vertexShader from './shaders/vertex.glsl?raw'; diff --git a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx index 3c3904e71..7a89c3215 100644 --- a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx +++ b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/components/SpacedArmorSubSpaced/index.tsx @@ -1,24 +1,14 @@ -import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core'; -import { useEffect } from 'react'; -import { - AdditiveBlending, - MeshBasicMaterial, - ShaderMaterial, - type Object3D, -} from 'three'; -import { degToRad } from 'three/src/math/MathUtils.js'; -import type { ArmorUserData } from '../..'; -import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment'; -import { jsxTree } from '../../../../../../core/blitzkit/jsxTree'; -import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel'; +import { ArmorPlate } from '@blitzkit/core'; +import type { MeshProps } from '@react-three/fiber'; +import { Fragment } from 'react'; +import { AdditiveBlending, MeshBasicMaterial, ShaderMaterial } from 'three'; +import { Duel } from '../../../../../../stores/duel'; import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; -import { ArmorType } from '../../../SpacedArmorScene'; import fragmentShader from './shaders/fragment.glsl?raw'; import vertexShader from './shaders/vertex.glsl?raw'; -interface SpacedArmorSubSpacedProps { - node: Object3D; - thickness: number; +interface SpacedArmorSubSpacedProps extends MeshProps { + armorPlate: ArmorPlate; } const depthWriteMaterial = new MeshBasicMaterial({ @@ -27,8 +17,8 @@ const depthWriteMaterial = new MeshBasicMaterial({ }); export function SpacedArmorSubSpaced({ - node, - thickness, + armorPlate, + ...props }: SpacedArmorSubSpacedProps) { const tankopediaEphemeralStore = TankopediaEphemeral.useStore(); const duelStore = Duel.useStore(); @@ -49,107 +39,89 @@ export function SpacedArmorSubSpaced({ }, }); - useEffect(() => { - function handleShellChange() { - const duel = duelStore.getState(); - const tankopediaEphemeral = tankopediaEphemeralStore.getState(); - const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; + // useEffect(() => { + // function handleShellChange() { + // const duel = duelStore.getState(); + // const tankopediaEphemeral = tankopediaEphemeralStore.getState(); + // const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; - material.uniforms.penetration.value = shell.penetration.near; - material.uniforms.caliber.value = shell.caliber; - material.uniforms.ricochet.value = degToRad( - isExplosive(shell.type) ? 90 : shell.ricochet!, - ); - material.uniforms.normalization.value = degToRad( - shell.normalization ?? 0, - ); - } - async function handleProtagonistEquipmentChange( - equipment: EquipmentMatrix, - ) { - const duel = duelStore.getState(); - const hasEnhancedArmor = await hasEquipment( - 110, - duel.protagonist.tank.equipment_preset, - equipment, - ); - material.uniforms.thickness.value = hasEnhancedArmor - ? thickness * 1.03 - : thickness; - } - async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { - const duel = duelStore.getState(); - const tankopediaEphemeral = tankopediaEphemeralStore.getState(); - const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; - const penetration = shell.penetration.near; - const hasCalibratedShells = await hasEquipment( - 103, - duel.antagonist.tank.equipment_preset, - equipment, - ); + // material.uniforms.penetration.value = shell.penetration.near; + // material.uniforms.caliber.value = shell.caliber; + // material.uniforms.ricochet.value = degToRad( + // isExplosive(shell.type) ? 90 : shell.ricochet!, + // ); + // material.uniforms.normalization.value = degToRad( + // shell.normalization ?? 0, + // ); + // } + // async function handleProtagonistEquipmentChange( + // equipment: EquipmentMatrix, + // ) { + // const duel = duelStore.getState(); + // const hasEnhancedArmor = await hasEquipment( + // 110, + // duel.protagonist.tank.equipment_preset, + // equipment, + // ); + // material.uniforms.thickness.value = hasEnhancedArmor + // ? thickness * 1.03 + // : thickness; + // } + // async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) { + // const duel = duelStore.getState(); + // const tankopediaEphemeral = tankopediaEphemeralStore.getState(); + // const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell; + // const penetration = shell.penetration.near; + // const hasCalibratedShells = await hasEquipment( + // 103, + // duel.antagonist.tank.equipment_preset, + // equipment, + // ); - material.uniforms.penetration.value = - penetration * - resolvePenetrationCoefficient(hasCalibratedShells, shell.type); - } + // material.uniforms.penetration.value = + // penetration * + // resolvePenetrationCoefficient(hasCalibratedShells, shell.type); + // } - handleShellChange(); - handleProtagonistEquipmentChange( - duelStore.getState().protagonist.equipmentMatrix, - ); - handleAntagonistEquipmentChange( - duelStore.getState().antagonist.equipmentMatrix, - ); + // handleShellChange(); + // handleProtagonistEquipmentChange( + // duelStore.getState().protagonist.equipmentMatrix, + // ); + // handleAntagonistEquipmentChange( + // duelStore.getState().antagonist.equipmentMatrix, + // ); - const unsubscribes = [ - duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), - tankopediaEphemeralStore.subscribe( - (state) => state.customShell, - handleShellChange, - ), - duelStore.subscribe( - (state) => state.protagonist.equipmentMatrix, - handleProtagonistEquipmentChange, - ), - duelStore.subscribe( - (state) => state.antagonist.equipmentMatrix, - handleAntagonistEquipmentChange, - ), - ]; + // const unsubscribes = [ + // duelStore.subscribe((state) => state.antagonist.shell, handleShellChange), + // tankopediaEphemeralStore.subscribe( + // (state) => state.customShell, + // handleShellChange, + // ), + // duelStore.subscribe( + // (state) => state.protagonist.equipmentMatrix, + // handleProtagonistEquipmentChange, + // ), + // duelStore.subscribe( + // (state) => state.antagonist.equipmentMatrix, + // handleAntagonistEquipmentChange, + // ), + // ]; - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); + // return () => { + // unsubscribes.forEach((unsubscribe) => unsubscribe()); + // }; + // }); return ( - <> - {jsxTree( - node, - { - mesh: { - renderOrder: 2, - material, - onClick() {}, - userData: { - type: ArmorType.Spaced, - thickness, - } satisfies ArmorUserData, - }, - }, - `${node.uuid}-exclude`, - )} - - {jsxTree( - node, - { - mesh: { - renderOrder: 5, - material: depthWriteMaterial, - }, - }, - `${node.uuid}-include`, - )} - + + {}} + userData={armorPlate} + /> + + ); } diff --git a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx index 2d8666347..38aa0501b 100644 --- a/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx +++ b/packages/website/src/components/Armor/components/SpacedArmorSceneComponent/index.tsx @@ -1,83 +1,42 @@ -import { - ShellType, - isExplosive, - resolvePenetrationCoefficient, -} from '@blitzkit/core'; +import { asset } from '@blitzkit/core'; +import { useGLTF } from '@react-three/drei'; import { useThree } from '@react-three/fiber'; import { useCallback } from 'react'; -import { - type Intersection, - MeshBasicMaterial, - Object3D, - Plane, - Quaternion, - Raycaster, - Scene, - Vector3, -} from 'three'; -import { degToRad } from 'three/src/math/MathUtils.js'; -import { hasEquipment } from '../../../../core/blitzkit/hasEquipment'; +import { Material, MeshBasicMaterial } from 'three'; import { jsxTree } from '../../../../core/blitzkit/jsxTree'; -import { discardClippingPlane } from '../../../../core/three/discardClippingPlane'; import { Duel } from '../../../../stores/duel'; -import { - type Shot, - type ShotLayerBase, - type ShotLayerNonExternal, - TankopediaEphemeral, -} from '../../../../stores/tankopediaEphemeral'; -import { ArmorType } from '../SpacedArmorScene'; +import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; +import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent'; +import { groupNameRegex } from '../StaticArmorSceneComponent'; import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal'; import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced'; -type SpacedArmorSceneComponentProps = { - node: Object3D; - thickness: number; - clip?: Plane; - scene: Scene; -} & ( - | { - type: Exclude; - } - | { - type: ArmorType.External; - variant: ExternalModuleVariant; - } -); - -export type ExternalModuleVariant = 'gun' | 'track'; -export type ArmorUserData = { - thickness: number; -} & ( - | { - type: Exclude; - } - | { - type: ArmorType.External; - variant: ExternalModuleVariant; - } -); +interface StaticArmorSceneComponentsProp { + group: string; +} const omitMaterial = new MeshBasicMaterial({ colorWrite: false, depthWrite: true, }); -/** - * Render orders allocations: - * - 0 : core omission - * - 1-2: spaced without depth buffer write but do test to allow additive blending - * - 3-4: external with depth buffer write in first pass and read in second to simplify geometry - * - 5 : spaced depth write - */ export function SpacedArmorSceneComponent({ - node, - thickness, - clip, - ...props -}: SpacedArmorSceneComponentProps) { + group, +}: StaticArmorSceneComponentsProp) { const tankopediaEphemeralStore = TankopediaEphemeral.useStore(); - const duelStore = Duel.useStore(); + const tank = Duel.use((state) => state.protagonist.tank); + const showModules = TankopediaPersistent.use((state) => state.showModules); + const showArmorScreen = TankopediaPersistent.use( + (state) => state.showArmorScreen, + ); + const showArmor = TankopediaPersistent.use((state) => state.showArmor); + const gltf = useGLTF(asset(`tanks/${tank.id}/collision/${group}.glb`)); + const armor = TankopediaEphemeral.use((state) => state.armor); + const groupName = group.match(groupNameRegex)![1]; + const groupArmor = armor.groups[groupName]; + const thicknessRange = TankopediaEphemeral.use( + (state) => state.thicknessRange, + ); const camera = useThree((state) => state.camera); const shoot = useCallback( @@ -369,20 +328,30 @@ export function SpacedArmorSceneComponent({ [camera], ); - return ( - <> - {props.type === ArmorType.Primary && - jsxTree( - node, - { - mesh: { - renderOrder: 0, - material: omitMaterial, - userData: { - type: ArmorType.Primary, - thickness, - } satisfies ArmorUserData, - async onClick(event) { + return jsxTree(gltf.scene, { + mesh(mesh, props, key) { + if (!(mesh.material instanceof Material)) { + return null; + } + + const armorName = mesh.material.name; + const armorPlate = groupArmor.armors[armorName]; + + if (armorPlate === undefined) { + console.warn(`Missing armor: ${armorName}`); + return null; + } + + switch (armorPlate.type) { + case 'Armor': + return ( + { event.stopPropagation(); const shot = (await shoot( event.point, @@ -390,24 +359,30 @@ export function SpacedArmorSceneComponent({ true, ))!; tankopediaEphemeralStore.setState({ shot }); - }, - }, - }, - node.uuid, - )} + }} + /> + ); - {props.type === ArmorType.Spaced && ( - - )} + case 'ArmorScreen': + return ( + + ); - {props.type === ArmorType.External && ( - - )} - - ); + default: + return null; + return ( + + ); + } + }, + }); } diff --git a/packages/website/src/components/Armor/components/StaticArmor.tsx b/packages/website/src/components/Armor/components/StaticArmor.tsx index 7a82ed179..d3b49b804 100644 --- a/packages/website/src/components/Armor/components/StaticArmor.tsx +++ b/packages/website/src/components/Armor/components/StaticArmor.tsx @@ -1,4 +1,3 @@ -import { memo } from 'react'; import { arrayfy } from '../../../core/three/arrayfy'; import { Duel } from '../../../stores/duel'; import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral'; @@ -8,8 +7,7 @@ export interface ThicknessRange { value: number; } -export const StaticArmor = memo(() => { - const tank = Duel.use((state) => state.protagonist); +export function StaticArmor() { const model = TankopediaEphemeral.use((state) => state.model); const turret = Duel.use((state) => state.protagonist.turret); const gun = Duel.use((state) => state.protagonist.gun); @@ -30,4 +28,4 @@ export const StaticArmor = memo(() => { ); -}); +} diff --git a/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx b/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx index d838b3797..d009b6045 100644 --- a/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx +++ b/packages/website/src/components/Armor/components/StaticArmorSceneComponent.tsx @@ -1,5 +1,6 @@ import { asset } from '@blitzkit/core'; import { useGLTF } from '@react-three/drei'; +import { Fragment } from 'react/jsx-runtime'; import { Color, LineBasicMaterial, @@ -37,7 +38,7 @@ export function StaticArmorSceneComponent({ ); return jsxTree(gltf.scene, { - mesh(mesh, props) { + mesh(mesh, props, key) { if (!(mesh.material instanceof Material)) { return null; } @@ -109,13 +110,13 @@ export function StaticArmorSceneComponent({ material.transparent = material.opacity < 1; return ( - <> + - + ); }, }); diff --git a/packages/website/src/components/Armor/index.tsx b/packages/website/src/components/Armor/index.tsx index fe1927fd5..16d3b2b9e 100644 --- a/packages/website/src/components/Armor/index.tsx +++ b/packages/website/src/components/Armor/index.tsx @@ -6,6 +6,8 @@ import { spacedArmorRenderTarget } from './components/PrimaryArmorSceneComponent import { SpacedArmorScene } from './components/SpacedArmorScene'; export const Armor = memo(() => { + return ; + const [spacedArmorScene] = useState(() => new Scene()); const [primaryArmorScene] = useState(() => new Scene()); const spacedArmorPortal = createPortal( diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx index d51c33295..f3bd4e6fe 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/Options/index.tsx @@ -403,18 +403,6 @@ export function Options({ canvas }: OptionsProps) { - - - - - - - apiBlobProxy(path); -} - export async function jsonMirror( path: string, message: MessageFns, diff --git a/packages/website/src/core/blitzkit/jsxTree.tsx b/packages/website/src/core/blitzkit/jsxTree.tsx index 6319d7c08..253527522 100644 --- a/packages/website/src/core/blitzkit/jsxTree.tsx +++ b/packages/website/src/core/blitzkit/jsxTree.tsx @@ -5,13 +5,16 @@ import { Mesh, Object3D } from 'three'; export function jsxTree( node: Object3D, mergers?: { - mesh?: (mesh: Mesh, props: MeshProps) => ReactNode; - group?: (object3d: Object3D, props: Object3DProps) => ReactNode; + mesh?: (mesh: Mesh, props: MeshProps, key: string) => ReactNode; + group?: ( + object3d: Object3D, + props: Object3DProps, + key: string, + ) => ReactNode; }, ): ReactNode { if (node instanceof Mesh) { const props = { - key: node.uuid, geometry: node.geometry, material: node.material, position: node.position, @@ -19,17 +22,24 @@ export function jsxTree( scale: node.scale, }; - return mergers?.mesh ? mergers.mesh(node, props) : ; + return mergers?.mesh ? ( + mergers.mesh(node, props, node.uuid) + ) : ( + + ); } else if (node instanceof Object3D) { const props = { - key: node.uuid, position: node.position, rotation: node.rotation, scale: node.scale, children: node.children.map((child) => jsxTree(child, mergers)), }; - return mergers?.group ? mergers.group(node, props) : ; + return mergers?.group ? ( + mergers.group(node, props, node.uuid) + ) : ( + + ); } return null; diff --git a/packages/website/src/pages/[...locale]/tanks/index.astro b/packages/website/src/pages/[...locale]/tanks/index.astro index c04910733..35b8f5838 100644 --- a/packages/website/src/pages/[...locale]/tanks/index.astro +++ b/packages/website/src/pages/[...locale]/tanks/index.astro @@ -1,5 +1,5 @@ --- -import { fetchTanksFull } from '@blitzkit/core'; +import { asset, fetchTanksFull } from '@blitzkit/core'; import { Box, Flex, Text } from '@radix-ui/themes'; import { LinkI18n } from '../../../components/LinkI18n'; import { PageWrapper } from '../../../components/PageWrapper'; @@ -25,7 +25,7 @@ const tanksFull = await fetchTanksFull(); width="8rem" height="6rem" style={{ - backgroundImage: `url(/api/tanks/${tank.id}/icons/big.png)`, + backgroundImage: `url(${asset(`tanks/${tank.id}/icons/big.webp`)})`, backgroundSize: '150%', backgroundPosition: 'center', }} diff --git a/packages/website/src/pages/api/tanks/full.json.ts b/packages/website/src/pages/api/tanks/full.json.ts index 546a5accf..e150c020e 100644 --- a/packages/website/src/pages/api/tanks/full.json.ts +++ b/packages/website/src/pages/api/tanks/full.json.ts @@ -2,9 +2,10 @@ import { TanksFull } from '@blitzkit/core'; import { jsonMirrorCurry } from '../../../core/blitzkit/blobMirror'; /** - * Identical to tanks/full.pb but in JSON. + * Returns meta data similar to tanks/[id].json but for all tanks as an array. There is no guarantee of order. * * @warning This is a large response. Consider using tanks/[id]/meta.json for individual tanks. + * @proto tanks_full * @returns JSON. */ export const GET = jsonMirrorCurry('/tanks/full.pb', TanksFull); diff --git a/packages/website/src/pages/api/tanks/full.pb.ts b/packages/website/src/pages/api/tanks/full.pb.ts deleted file mode 100644 index 9c8c3a049..000000000 --- a/packages/website/src/pages/api/tanks/full.pb.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { apiBlobProxyCurry } from '../../../core/blitzkit/blobMirror'; - -/** - * Returns meta data similar to tanks/[id].json but for all tanks as an array. There is no guarantee of order. - * - * @proto tanks_full - */ -export const GET = apiBlobProxyCurry('/tanks/full.pb'); diff --git a/packages/website/src/stores/tankopediaEphemeral.ts b/packages/website/src/stores/tankopediaEphemeral.ts index cac8ee59d..a67114217 100644 --- a/packages/website/src/stores/tankopediaEphemeral.ts +++ b/packages/website/src/stores/tankopediaEphemeral.ts @@ -8,7 +8,7 @@ import type { Vector3 } from 'three'; import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; import type { ArmorType } from '../components/Armor/components/SpacedArmorScene'; -import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent'; +import type { ExternalModuleVariant } from '../components/Armor/components/SpacedArmorSceneComponent/_index'; import type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection'; import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions'; import { createContextualStore } from '../core/zustand/createContextualStore'; @@ -148,7 +148,7 @@ export const TankopediaEphemeral = createContextualStore( model, controlsEnabled: true, xpMultiplier: 1, - display: TankopediaDisplay.StaticArmor, + display: TankopediaDisplay.DynamicArmor, })), ); }, diff --git a/src/BlitzKit.CLI/Functions/Api.cs b/src/BlitzKit.CLI/Functions/Api.cs deleted file mode 100644 index 45d7c0c42..000000000 --- a/src/BlitzKit.CLI/Functions/Api.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Threading.Tasks; -using AssetRipper.TextureDecoder.Rgb.Channels; -using Blitzkit; -using Google.Protobuf; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace BlitzKit.CLI.Functions -{ - public class Api(string[] args) - { - readonly WebApplication app = WebApplication.Create(); - readonly Mangler mangler = new(args); - - void Map(string path, Func> func) - where T : IMessage => - app.MapGet(path, async context => await Octet(context, await func())); - - void Map(string path, Func func) - where T : IMessage => Map(path, () => Task.FromResult(func())); - - static async Task Octet(HttpContext context, T message) - where T : IMessage - { - var bytes = message.ToByteArray(); - - context.Response.ContentType = "application/octet-stream"; - await context.Response.Body.WriteAsync(bytes); - } - - public async Task Run() - { - await mangler.Initialize(); - - Map("/tanks/full.pb", mangler.TanksFull); - Map("/tanks/list.pb", mangler.Tanks); - - app.MapGet( - "/tanks/{id}.pb", - (HttpContext context, string id) => Octet(context, mangler.Tank(id)) - ); - - app.MapGet( - "/tanks/{id}/icons/big.pb", - (HttpContext context, string id) => Octet(context, mangler.TankIconBig(id)) - ); - - app.Run(); - } - } -} diff --git a/src/BlitzKit.CLI/Functions/Mangler.cs b/src/BlitzKit.CLI/Functions/Mangler.cs index 1b474ab78..0d246788f 100644 --- a/src/BlitzKit.CLI/Functions/Mangler.cs +++ b/src/BlitzKit.CLI/Functions/Mangler.cs @@ -49,81 +49,15 @@ namespace BlitzKit.CLI.Functions readonly BlitzProvider provider = new(args.Contains("--depot")); Locales? locales; readonly Dictionary> strings = []; - TanksFull tanksFull = null!; + readonly AssetUploader assetUploader = new("mangled ue assets") { enabled = true }; - public async Task Initialize() + public async Task Mangle() { await FetchStrings(); - tanksFull = TanksFull(); + await MangleNations(); + await assetUploader.Flush(); } - public TanksFull TanksFull() - { - if (tanksFull != null) - return tanksFull; - - var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories; - tanksFull = new(); - - foreach (var dir in dirs) - { - if (dir.Key == "TankStub") - continue; - - foreach (var tankDir in dir.Value.Directories) - { - var tank = Tank(tankDir.Value); - tanksFull.Tanks.Add(tank); - } - } - - DeduplicateSlugs(tanksFull); - - return tanksFull; - } - - public Tanks Tanks() - { - var tanksFull = TanksFull(); - Tanks tanks = new(); - - foreach (var tank in tanksFull.Tanks) - { - tanks.Tanks_.Add(tank.Id); - } - - return tanks; - } - - public Tank Tank(string id) - { - return tanksFull.Tanks.First(tank => tank.Id == id); - } - - Tank Tank(VFS tankDir) - { - var pdaName = $"PDA_{tankDir.Name}"; - var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}"); - - var id = pda.Get("TankId").Text; - var name = GetString($"TankEntity__{id.ToLowerInvariant()}__Short_Name"); - var slug = Diacritics.Remove(name.Locales["en"]).ToLower(); - slug = NonAlphanumericRegex().Replace(slug, "-"); - slug = MultipleDashesRegex().Replace(slug, "-"); - slug = TrailingDashRegex().Replace(slug, ""); - - Tank tank = new() - { - Id = id, - Name = name, - Slug = slug, - }; - - return tank; - } - - /////////////////////////////////////////////////////////////////////////// - I18n GetString(string name) { I18n i18n = new(); @@ -175,6 +109,42 @@ namespace BlitzKit.CLI.Functions } } + async Task MangleNations() + { + var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories; + TanksFull tanksFull = new(); + + foreach (var dir in dirs) + { + if (dir.Key == "TankStub") + continue; + + foreach (var tankDir in dir.Value.Directories) + { + // if (tankDir.Value.Name != "R90_IS_4") + // continue; + + // if (tankDir.Value.Name != "R81_IS_8") + // continue; + + var tank = await MangleTank(tankDir.Value); + tanksFull.Tanks.Add(tank); + } + } + + DeduplicateSlugs(tanksFull); + Tanks tanks = new(); + + foreach (var tank in tanksFull.Tanks) + { + tanks.Tanks_.Add(tank.Id); + await assetUploader.Add($"tanks/{tank.Id}/meta.pb", tank.ToByteArray()); + } + + await assetUploader.Add("definitions/tanks.pb", tanks.ToByteArray()); + await assetUploader.Add("definitions/tanks-full.pb", tanksFull.ToByteArray()); + } + void DeduplicateSlugs(TanksFull tanksFull) { List checkedSlugs = []; @@ -245,6 +215,33 @@ namespace BlitzKit.CLI.Functions } } + async Task MangleTank(VFS tankDir) + { + var pdaName = $"PDA_{tankDir.Name}"; + var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}"); + + var id = pda.Get("TankId").Text; + var name = GetString($"TankEntity__{id.ToLowerInvariant()}__Short_Name"); + var slug = Diacritics.Remove(name.Locales["en"]).ToLower(); + slug = NonAlphanumericRegex().Replace(slug, "-"); + slug = MultipleDashesRegex().Replace(slug, "-"); + slug = TrailingDashRegex().Replace(slug, ""); + + await MangleIcon(pda); + await MangleArmor(pda); + + Tank tank = new() + { + Id = id, + Name = name, + Slug = slug, + }; + + await MangleModules(tank, pda); + + return tank; + } + async Task MangleArmor(UObject pda) { var id = pda.Get("TankId").Text; @@ -285,7 +282,7 @@ namespace BlitzKit.CLI.Functions PrettyLog.Warn($"Penetration groups missing for {id}; creating empty list"); } - // await assetUploader.Add($"tanks/{id}/armor.pb", tankArmor.ToByteArray()); + await assetUploader.Add($"tanks/{id}/armor.pb", tankArmor.ToByteArray()); } async Task MangleIcon(UObject pda) @@ -295,7 +292,7 @@ namespace BlitzKit.CLI.Functions if (pda.TryGet("SmallIcon", out var smallIconTexture)) { var smallIcon = BlitzKitExporter.Texture2D(smallIconTexture); - // await assetUploader.Add($"tanks/{id}/icons/small.png", smallIcon); + await assetUploader.Add($"tanks/{id}/icons/small.webp", smallIcon); } else { @@ -305,7 +302,7 @@ namespace BlitzKit.CLI.Functions if (pda.TryGet("BigIcon", out var bigIconTexture)) { var bigIcon = BlitzKitExporter.Texture2D(bigIconTexture); - // await assetUploader.Add($"tanks/{id}/icons/big.png", bigIcon); + await assetUploader.Add($"tanks/{id}/icons/big.webp", bigIcon); } else { @@ -357,9 +354,9 @@ namespace BlitzKit.CLI.Functions { MonoGltf gltf = new(collision); - // await assetUploader.Add( - // new($"tanks/{tank.Id}/collision/{moduleName}.glb", gltf.Write()) - // ); + await assetUploader.Add( + new($"tanks/{tank.Id}/collision/{moduleName}.glb", gltf.Write()) + ); } else { @@ -368,7 +365,7 @@ namespace BlitzKit.CLI.Functions } } - // await assetUploader.Add($"tanks/{tank.Id}/model.pb", tankModel.ToByteArray()); + await assetUploader.Add($"tanks/{tank.Id}/model.pb", tankModel.ToByteArray()); } } } diff --git a/src/BlitzKit.CLI/Models/AssetUploader.cs b/src/BlitzKit.CLI/Models/AssetUploader.cs index 186fc858c..2b3054a76 100644 --- a/src/BlitzKit.CLI/Models/AssetUploader.cs +++ b/src/BlitzKit.CLI/Models/AssetUploader.cs @@ -87,9 +87,9 @@ namespace BlitzKit.CLI.Models } else { - PrettyLog.Background( - $"🔵 {blobPath} ({change.Content.Count.ToString("N0", Program.Culture)}B)" - ); + // PrettyLog.Background( + // $"🔵 {blobPath} ({change.Content.Count.ToString("N0", Program.Culture)}B)" + // ); return; } } diff --git a/src/BlitzKit.CLI/Program.cs b/src/BlitzKit.CLI/Program.cs index 84ca6f0fd..41af977f4 100644 --- a/src/BlitzKit.CLI/Program.cs +++ b/src/BlitzKit.CLI/Program.cs @@ -22,10 +22,10 @@ namespace BlitzKit.CLI break; } - case "api": + case "mangle": { - Api api = new(args); - await api.Run(); + Mangler mangler = new(args); + await mangler.Mangle(); break; } diff --git a/src/BlitzKit.CLI/Utils/Exporter.cs b/src/BlitzKit.CLI/Utils/Exporter.cs index b428b0d4a..63942517c 100644 --- a/src/BlitzKit.CLI/Utils/Exporter.cs +++ b/src/BlitzKit.CLI/Utils/Exporter.cs @@ -14,7 +14,7 @@ namespace BlitzKit.CLI.Utils { return texture .Decode(ETexturePlatform.DesktopMobile)! - .Encode(SKEncodedImageFormat.Webp, 100) + .Encode(SKEncodedImageFormat.Webp, 80) .ToArray(); }