mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: start work on dynamic armor
This commit is contained in:
1 parent
1d301bf2fb
commit
1ee232d104
26 files changed
+296
-583
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 706 KiB |
File renamed without changes.
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
File renamed without changes.
@@ -22,7 +22,6 @@ import { useThree } from '@react-three/fiber';
|
|||||||
import { Fragment, useEffect, useRef } from 'react';
|
import { Fragment, useEffect, useRef } from 'react';
|
||||||
import type { Mesh } from 'three';
|
import type { Mesh } from 'three';
|
||||||
import {
|
import {
|
||||||
Box3,
|
|
||||||
Color,
|
Color,
|
||||||
DoubleSide,
|
DoubleSide,
|
||||||
Group,
|
Group,
|
||||||
@@ -109,6 +108,12 @@ export function DissectorSceneComponent({
|
|||||||
|
|
||||||
const armorName = mesh.material.name;
|
const armorName = mesh.material.name;
|
||||||
const armorPlate = groupArmor.armors[armorName];
|
const armorPlate = groupArmor.armors[armorName];
|
||||||
|
|
||||||
|
if (armorPlate === undefined) {
|
||||||
|
console.warn(`Missing armor: ${armorName}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isModule =
|
const isModule =
|
||||||
armorPlate.type !== 'Armor' && armorPlate.type !== 'ArmorScreen';
|
armorPlate.type !== 'Armor' && armorPlate.type !== 'ArmorScreen';
|
||||||
const isTrack =
|
const isTrack =
|
||||||
@@ -149,8 +154,6 @@ export function DissectorSceneComponent({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const worldPosition = new Vector3();
|
const worldPosition = new Vector3();
|
||||||
const boundsTop = new Vector3();
|
|
||||||
const bounds = new Box3();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const offset = new Vector3();
|
const offset = new Vector3();
|
||||||
|
|||||||
@@ -1,15 +1,8 @@
|
|||||||
import { memo, useRef } from 'react';
|
import { Scene } from 'three';
|
||||||
import { Group, Plane, Scene, Vector3 } from 'three';
|
import { arrayfy } from '../../../core/three/arrayfy';
|
||||||
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 { Duel } from '../../../stores/duel';
|
import { Duel } from '../../../stores/duel';
|
||||||
import { ModelTankWrapper } from './ModelTankWrapper';
|
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||||
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
|
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent/index';
|
||||||
|
|
||||||
export enum ArmorType {
|
export enum ArmorType {
|
||||||
Primary,
|
Primary,
|
||||||
@@ -21,185 +14,25 @@ interface SpacedArmorSceneProps {
|
|||||||
scene: Scene;
|
scene: Scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(({ scene }) => {
|
export function SpacedArmorScene({ scene }: SpacedArmorSceneProps) {
|
||||||
const wrapper = useRef<Group>(null);
|
const model = TankopediaEphemeral.use((state) => state.model);
|
||||||
const turretContainer = useRef<Group>(null);
|
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
|
||||||
const track = Duel.use((state) => state.protagonist.track);
|
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
const { gltf: armorGltf } = useArmor(tank.id);
|
const turretTranslation = arrayfy(model.modules[turret.id].translation);
|
||||||
const { gltf: modelGltf } = useModel(tank.id);
|
const gunTranslation = arrayfy(model.modules[gun.id].translation);
|
||||||
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);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModelTankWrapper ref={wrapper}>
|
<group>
|
||||||
<group position={hullOrigin}>
|
<SpacedArmorSceneComponent group="hull" />
|
||||||
{armorNodes.map((node) => {
|
<SpacedArmorSceneComponent group="chassis" />
|
||||||
const isHull = node.name.startsWith('hull_');
|
|
||||||
const isVisible = isHull;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
tankModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
<group position={turretTranslation}>
|
||||||
!isVisible ||
|
<SpacedArmorSceneComponent group={turret.id} />
|
||||||
thickness === undefined ||
|
|
||||||
(isDynamicArmorActive && node.name.includes('state_00')) ||
|
|
||||||
(!isDynamicArmorActive && node.name.includes('state_01'))
|
|
||||||
)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
scene={scene}
|
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
<group position={gunTranslation}>
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
<SpacedArmorSceneComponent group={gun.id} />
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
const isVisible = isWheel || isTrack;
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
scene={scene}
|
|
||||||
key={node.uuid}
|
|
||||||
type={ArmorType.External}
|
|
||||||
thickness={trackModelDefinition.thickness}
|
|
||||||
variant="track"
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
|
||||||
{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 (
|
|
||||||
<group position={hullOrigin} key={node.uuid}>
|
|
||||||
<group position={turretOrigin}>
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
scene={scene}
|
|
||||||
key={node.uuid}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={gunContainer}>
|
|
||||||
{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 (
|
|
||||||
<group position={hullOrigin} key={node.uuid}>
|
|
||||||
<group position={turretOrigin.clone().add(gunOrigin)}>
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
scene={scene}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{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 (
|
|
||||||
<SpacedArmorSceneComponent
|
|
||||||
scene={scene}
|
|
||||||
key={node.uuid}
|
|
||||||
type={ArmorType.External}
|
|
||||||
thickness={gunModelDefinition.thickness}
|
|
||||||
variant="gun"
|
|
||||||
node={node}
|
|
||||||
clip={
|
|
||||||
maskOrigin === undefined
|
|
||||||
? undefined
|
|
||||||
: new Plane(new Vector3(0, 0, -1), -maskOrigin)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</ModelTankWrapper>
|
</group>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
+1
-1
@@ -7,12 +7,12 @@ import {
|
|||||||
Plane,
|
Plane,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from 'three';
|
} from 'three';
|
||||||
import type { ArmorUserData, ExternalModuleVariant } from '../..';
|
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
import { hasEquipment } from '../../../../../../core/blitzkit/hasEquipment';
|
||||||
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from '../../../../../../core/blitzkit/jsxTree';
|
||||||
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
import { Duel, type EquipmentMatrix } from '../../../../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
import { ArmorType } from '../../../SpacedArmorScene';
|
||||||
|
import type { ArmorUserData, ExternalModuleVariant } from '../../_index';
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from './shaders/vertex.glsl?raw';
|
||||||
|
|
||||||
|
|||||||
+86
-114
@@ -1,24 +1,14 @@
|
|||||||
import { isExplosive, resolvePenetrationCoefficient } from '@blitzkit/core';
|
import { ArmorPlate } from '@blitzkit/core';
|
||||||
import { useEffect } from 'react';
|
import type { MeshProps } from '@react-three/fiber';
|
||||||
import {
|
import { Fragment } from 'react';
|
||||||
AdditiveBlending,
|
import { AdditiveBlending, MeshBasicMaterial, ShaderMaterial } from 'three';
|
||||||
MeshBasicMaterial,
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
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 { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral';
|
||||||
import { ArmorType } from '../../../SpacedArmorScene';
|
|
||||||
import fragmentShader from './shaders/fragment.glsl?raw';
|
import fragmentShader from './shaders/fragment.glsl?raw';
|
||||||
import vertexShader from './shaders/vertex.glsl?raw';
|
import vertexShader from './shaders/vertex.glsl?raw';
|
||||||
|
|
||||||
interface SpacedArmorSubSpacedProps {
|
interface SpacedArmorSubSpacedProps extends MeshProps {
|
||||||
node: Object3D;
|
armorPlate: ArmorPlate;
|
||||||
thickness: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const depthWriteMaterial = new MeshBasicMaterial({
|
const depthWriteMaterial = new MeshBasicMaterial({
|
||||||
@@ -27,8 +17,8 @@ const depthWriteMaterial = new MeshBasicMaterial({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function SpacedArmorSubSpaced({
|
export function SpacedArmorSubSpaced({
|
||||||
node,
|
armorPlate,
|
||||||
thickness,
|
...props
|
||||||
}: SpacedArmorSubSpacedProps) {
|
}: SpacedArmorSubSpacedProps) {
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
@@ -49,107 +39,89 @@ export function SpacedArmorSubSpaced({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
function handleShellChange() {
|
// function handleShellChange() {
|
||||||
const duel = duelStore.getState();
|
// const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
// const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
// const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
|
|
||||||
material.uniforms.penetration.value = shell.penetration.near;
|
// material.uniforms.penetration.value = shell.penetration.near;
|
||||||
material.uniforms.caliber.value = shell.caliber;
|
// material.uniforms.caliber.value = shell.caliber;
|
||||||
material.uniforms.ricochet.value = degToRad(
|
// material.uniforms.ricochet.value = degToRad(
|
||||||
isExplosive(shell.type) ? 90 : shell.ricochet!,
|
// isExplosive(shell.type) ? 90 : shell.ricochet!,
|
||||||
);
|
// );
|
||||||
material.uniforms.normalization.value = degToRad(
|
// material.uniforms.normalization.value = degToRad(
|
||||||
shell.normalization ?? 0,
|
// shell.normalization ?? 0,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
async function handleProtagonistEquipmentChange(
|
// async function handleProtagonistEquipmentChange(
|
||||||
equipment: EquipmentMatrix,
|
// equipment: EquipmentMatrix,
|
||||||
) {
|
// ) {
|
||||||
const duel = duelStore.getState();
|
// const duel = duelStore.getState();
|
||||||
const hasEnhancedArmor = await hasEquipment(
|
// const hasEnhancedArmor = await hasEquipment(
|
||||||
110,
|
// 110,
|
||||||
duel.protagonist.tank.equipment_preset,
|
// duel.protagonist.tank.equipment_preset,
|
||||||
equipment,
|
// equipment,
|
||||||
);
|
// );
|
||||||
material.uniforms.thickness.value = hasEnhancedArmor
|
// material.uniforms.thickness.value = hasEnhancedArmor
|
||||||
? thickness * 1.03
|
// ? thickness * 1.03
|
||||||
: thickness;
|
// : thickness;
|
||||||
}
|
// }
|
||||||
async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
// async function handleAntagonistEquipmentChange(equipment: EquipmentMatrix) {
|
||||||
const duel = duelStore.getState();
|
// const duel = duelStore.getState();
|
||||||
const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
// const tankopediaEphemeral = tankopediaEphemeralStore.getState();
|
||||||
const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
// const shell = tankopediaEphemeral.customShell ?? duel.antagonist.shell;
|
||||||
const penetration = shell.penetration.near;
|
// const penetration = shell.penetration.near;
|
||||||
const hasCalibratedShells = await hasEquipment(
|
// const hasCalibratedShells = await hasEquipment(
|
||||||
103,
|
// 103,
|
||||||
duel.antagonist.tank.equipment_preset,
|
// duel.antagonist.tank.equipment_preset,
|
||||||
equipment,
|
// equipment,
|
||||||
);
|
// );
|
||||||
|
|
||||||
material.uniforms.penetration.value =
|
// material.uniforms.penetration.value =
|
||||||
penetration *
|
// penetration *
|
||||||
resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
|
// resolvePenetrationCoefficient(hasCalibratedShells, shell.type);
|
||||||
}
|
// }
|
||||||
|
|
||||||
handleShellChange();
|
// handleShellChange();
|
||||||
handleProtagonistEquipmentChange(
|
// handleProtagonistEquipmentChange(
|
||||||
duelStore.getState().protagonist.equipmentMatrix,
|
// duelStore.getState().protagonist.equipmentMatrix,
|
||||||
);
|
// );
|
||||||
handleAntagonistEquipmentChange(
|
// handleAntagonistEquipmentChange(
|
||||||
duelStore.getState().antagonist.equipmentMatrix,
|
// duelStore.getState().antagonist.equipmentMatrix,
|
||||||
);
|
// );
|
||||||
|
|
||||||
const unsubscribes = [
|
// const unsubscribes = [
|
||||||
duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
// duelStore.subscribe((state) => state.antagonist.shell, handleShellChange),
|
||||||
tankopediaEphemeralStore.subscribe(
|
// tankopediaEphemeralStore.subscribe(
|
||||||
(state) => state.customShell,
|
// (state) => state.customShell,
|
||||||
handleShellChange,
|
// handleShellChange,
|
||||||
),
|
// ),
|
||||||
duelStore.subscribe(
|
// duelStore.subscribe(
|
||||||
(state) => state.protagonist.equipmentMatrix,
|
// (state) => state.protagonist.equipmentMatrix,
|
||||||
handleProtagonistEquipmentChange,
|
// handleProtagonistEquipmentChange,
|
||||||
),
|
// ),
|
||||||
duelStore.subscribe(
|
// duelStore.subscribe(
|
||||||
(state) => state.antagonist.equipmentMatrix,
|
// (state) => state.antagonist.equipmentMatrix,
|
||||||
handleAntagonistEquipmentChange,
|
// handleAntagonistEquipmentChange,
|
||||||
),
|
// ),
|
||||||
];
|
// ];
|
||||||
|
|
||||||
return () => {
|
// return () => {
|
||||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
// unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
{jsxTree(
|
<mesh
|
||||||
node,
|
{...props}
|
||||||
{
|
renderOrder={2}
|
||||||
mesh: {
|
material={material}
|
||||||
renderOrder: 2,
|
onClick={() => {}}
|
||||||
material,
|
userData={armorPlate}
|
||||||
onClick() {},
|
/>
|
||||||
userData: {
|
<mesh {...props} renderOrder={5} material={depthWriteMaterial} />
|
||||||
type: ArmorType.Spaced,
|
</Fragment>
|
||||||
thickness,
|
|
||||||
} satisfies ArmorUserData,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
`${node.uuid}-exclude`,
|
|
||||||
)}
|
|
||||||
|
|
||||||
{jsxTree(
|
|
||||||
node,
|
|
||||||
{
|
|
||||||
mesh: {
|
|
||||||
renderOrder: 5,
|
|
||||||
material: depthWriteMaterial,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
`${node.uuid}-include`,
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+72
-97
@@ -1,83 +1,42 @@
|
|||||||
import {
|
import { asset } from '@blitzkit/core';
|
||||||
ShellType,
|
import { useGLTF } from '@react-three/drei';
|
||||||
isExplosive,
|
|
||||||
resolvePenetrationCoefficient,
|
|
||||||
} from '@blitzkit/core';
|
|
||||||
import { useThree } from '@react-three/fiber';
|
import { useThree } from '@react-three/fiber';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import {
|
import { Material, MeshBasicMaterial } from 'three';
|
||||||
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 { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
import { jsxTree } from '../../../../core/blitzkit/jsxTree';
|
||||||
import { discardClippingPlane } from '../../../../core/three/discardClippingPlane';
|
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from '../../../../stores/duel';
|
||||||
import {
|
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||||
type Shot,
|
import { TankopediaPersistent } from '../../../../stores/tankopediaPersistent';
|
||||||
type ShotLayerBase,
|
import { groupNameRegex } from '../StaticArmorSceneComponent';
|
||||||
type ShotLayerNonExternal,
|
|
||||||
TankopediaEphemeral,
|
|
||||||
} from '../../../../stores/tankopediaEphemeral';
|
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
||||||
|
|
||||||
type SpacedArmorSceneComponentProps = {
|
interface StaticArmorSceneComponentsProp {
|
||||||
node: Object3D;
|
group: string;
|
||||||
thickness: number;
|
}
|
||||||
clip?: Plane;
|
|
||||||
scene: Scene;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ArmorType.External;
|
|
||||||
variant: ExternalModuleVariant;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ExternalModuleVariant = 'gun' | 'track';
|
|
||||||
export type ArmorUserData = {
|
|
||||||
thickness: number;
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: Exclude<ArmorType, ArmorType.External>;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: ArmorType.External;
|
|
||||||
variant: ExternalModuleVariant;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const omitMaterial = new MeshBasicMaterial({
|
const omitMaterial = new MeshBasicMaterial({
|
||||||
colorWrite: false,
|
colorWrite: false,
|
||||||
depthWrite: true,
|
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({
|
export function SpacedArmorSceneComponent({
|
||||||
node,
|
group,
|
||||||
thickness,
|
}: StaticArmorSceneComponentsProp) {
|
||||||
clip,
|
|
||||||
...props
|
|
||||||
}: SpacedArmorSceneComponentProps) {
|
|
||||||
const tankopediaEphemeralStore = TankopediaEphemeral.useStore();
|
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 camera = useThree((state) => state.camera);
|
||||||
|
|
||||||
const shoot = useCallback(
|
const shoot = useCallback(
|
||||||
@@ -369,20 +328,30 @@ export function SpacedArmorSceneComponent({
|
|||||||
[camera],
|
[camera],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return jsxTree(gltf.scene, {
|
||||||
<>
|
mesh(mesh, props, key) {
|
||||||
{props.type === ArmorType.Primary &&
|
if (!(mesh.material instanceof Material)) {
|
||||||
jsxTree(
|
return null;
|
||||||
node,
|
}
|
||||||
{
|
|
||||||
mesh: {
|
const armorName = mesh.material.name;
|
||||||
renderOrder: 0,
|
const armorPlate = groupArmor.armors[armorName];
|
||||||
material: omitMaterial,
|
|
||||||
userData: {
|
if (armorPlate === undefined) {
|
||||||
type: ArmorType.Primary,
|
console.warn(`Missing armor: ${armorName}`);
|
||||||
thickness,
|
return null;
|
||||||
} satisfies ArmorUserData,
|
}
|
||||||
async onClick(event) {
|
|
||||||
|
switch (armorPlate.type) {
|
||||||
|
case 'Armor':
|
||||||
|
return (
|
||||||
|
<mesh
|
||||||
|
{...props}
|
||||||
|
key={key}
|
||||||
|
renderOrder={0}
|
||||||
|
material={omitMaterial}
|
||||||
|
userData={armorPlate}
|
||||||
|
onClick={async (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const shot = (await shoot(
|
const shot = (await shoot(
|
||||||
event.point,
|
event.point,
|
||||||
@@ -390,24 +359,30 @@ export function SpacedArmorSceneComponent({
|
|||||||
true,
|
true,
|
||||||
))!;
|
))!;
|
||||||
tankopediaEphemeralStore.setState({ shot });
|
tankopediaEphemeralStore.setState({ shot });
|
||||||
},
|
}}
|
||||||
},
|
/>
|
||||||
},
|
);
|
||||||
node.uuid,
|
|
||||||
)}
|
|
||||||
|
|
||||||
{props.type === ArmorType.Spaced && (
|
case 'ArmorScreen':
|
||||||
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
return (
|
||||||
)}
|
<SpacedArmorSubSpaced
|
||||||
|
{...props}
|
||||||
|
key={key}
|
||||||
|
armorPlate={armorPlate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
{props.type === ArmorType.External && (
|
default:
|
||||||
<SpacedArmorSubExternal
|
return null;
|
||||||
node={node}
|
return (
|
||||||
thickness={thickness}
|
<SpacedArmorSubExternal
|
||||||
variant={props.variant}
|
node={node}
|
||||||
clip={clip}
|
thickness={thickness}
|
||||||
/>
|
variant={props.variant}
|
||||||
)}
|
clip={clip}
|
||||||
</>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
import { arrayfy } from '../../../core/three/arrayfy';
|
import { arrayfy } from '../../../core/three/arrayfy';
|
||||||
import { Duel } from '../../../stores/duel';
|
import { Duel } from '../../../stores/duel';
|
||||||
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
||||||
@@ -8,8 +7,7 @@ export interface ThicknessRange {
|
|||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StaticArmor = memo(() => {
|
export function StaticArmor() {
|
||||||
const tank = Duel.use((state) => state.protagonist);
|
|
||||||
const model = TankopediaEphemeral.use((state) => state.model);
|
const model = TankopediaEphemeral.use((state) => state.model);
|
||||||
const turret = Duel.use((state) => state.protagonist.turret);
|
const turret = Duel.use((state) => state.protagonist.turret);
|
||||||
const gun = Duel.use((state) => state.protagonist.gun);
|
const gun = Duel.use((state) => state.protagonist.gun);
|
||||||
@@ -30,4 +28,4 @@ export const StaticArmor = memo(() => {
|
|||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { asset } from '@blitzkit/core';
|
import { asset } from '@blitzkit/core';
|
||||||
import { useGLTF } from '@react-three/drei';
|
import { useGLTF } from '@react-three/drei';
|
||||||
|
import { Fragment } from 'react/jsx-runtime';
|
||||||
import {
|
import {
|
||||||
Color,
|
Color,
|
||||||
LineBasicMaterial,
|
LineBasicMaterial,
|
||||||
@@ -37,7 +38,7 @@ export function StaticArmorSceneComponent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return jsxTree(gltf.scene, {
|
return jsxTree(gltf.scene, {
|
||||||
mesh(mesh, props) {
|
mesh(mesh, props, key) {
|
||||||
if (!(mesh.material instanceof Material)) {
|
if (!(mesh.material instanceof Material)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -109,13 +110,13 @@ export function StaticArmorSceneComponent({
|
|||||||
material.transparent = material.opacity < 1;
|
material.transparent = material.opacity < 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment key={key}>
|
||||||
<mesh {...props} material={material} />
|
<mesh {...props} material={material} />
|
||||||
|
|
||||||
<lineSegments material={outlineMaterial}>
|
<lineSegments material={outlineMaterial}>
|
||||||
<edgesGeometry args={[props.geometry, 45]} />
|
<edgesGeometry args={[props.geometry, 45]} />
|
||||||
</lineSegments>
|
</lineSegments>
|
||||||
</>
|
</Fragment>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { spacedArmorRenderTarget } from './components/PrimaryArmorSceneComponent
|
|||||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||||
|
|
||||||
export const Armor = memo(() => {
|
export const Armor = memo(() => {
|
||||||
|
return <SpacedArmorScene scene={new Scene()} />;
|
||||||
|
|
||||||
const [spacedArmorScene] = useState(() => new Scene());
|
const [spacedArmorScene] = useState(() => new Scene());
|
||||||
const [primaryArmorScene] = useState(() => new Scene());
|
const [primaryArmorScene] = useState(() => new Scene());
|
||||||
const spacedArmorPortal = createPortal(
|
const spacedArmorPortal = createPortal(
|
||||||
|
|||||||
@@ -403,18 +403,6 @@ export function Options({ canvas }: OptionsProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</SegmentedControl.Item>
|
</SegmentedControl.Item>
|
||||||
<SegmentedControl.Item value={`${TankopediaDisplay.Dissector}`}>
|
|
||||||
<Tooltip
|
|
||||||
content={strings.website.tools.tanks.sandbox.dissector.name}
|
|
||||||
>
|
|
||||||
<Flex height="100%" align="center">
|
|
||||||
<img
|
|
||||||
src="/assets/images/icons/tankopedia-static-armor.png"
|
|
||||||
style={{ height: '1.25em' }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Tooltip>
|
|
||||||
</SegmentedControl.Item>
|
|
||||||
<SegmentedControl.Item value={`${TankopediaDisplay.Dissector}`}>
|
<SegmentedControl.Item value={`${TankopediaDisplay.Dissector}`}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={strings.website.tools.tanks.sandbox.dissector.name}
|
content={strings.website.tools.tanks.sandbox.dissector.name}
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { asset } from '@blitzkit/core';
|
||||||
import { Box, Flex, type FlexProps, Progress } from '@radix-ui/themes';
|
import { Box, Flex, type FlexProps, Progress } from '@radix-ui/themes';
|
||||||
import { useProgress } from '@react-three/drei';
|
import { useProgress } from '@react-three/drei';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export function TankSandboxLoader({ id, ...props }: TankSandboxLoaderProps) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
style={{
|
||||||
filter: 'blur(1rem)',
|
filter: 'blur(1rem)',
|
||||||
backgroundImage: `url(/api/tanks/${id}/icons/big.png)`,
|
backgroundImage: `url(${asset(`tanks/${id}/icons/big.webp`)})`,
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -124,8 +124,7 @@ export const apiTool: Tool = {
|
|||||||
id: 'api',
|
id: 'api',
|
||||||
button: {
|
button: {
|
||||||
color: 'indigo',
|
color: 'indigo',
|
||||||
},
|
},
|
||||||
image: 'dIfvWnV',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TOOLS: Tool[] = [
|
export const TOOLS: Tool[] = [
|
||||||
|
|||||||
@@ -10,14 +10,6 @@ export async function blobProxy(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apiBlobProxy(path: string) {
|
|
||||||
return blobProxy(`http://localhost:5000${path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apiBlobProxyCurry(path: string) {
|
|
||||||
return () => apiBlobProxy(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function jsonMirror<Type>(
|
export async function jsonMirror<Type>(
|
||||||
path: string,
|
path: string,
|
||||||
message: MessageFns<Type>,
|
message: MessageFns<Type>,
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import { Mesh, Object3D } from 'three';
|
|||||||
export function jsxTree(
|
export function jsxTree(
|
||||||
node: Object3D,
|
node: Object3D,
|
||||||
mergers?: {
|
mergers?: {
|
||||||
mesh?: (mesh: Mesh, props: MeshProps) => ReactNode;
|
mesh?: (mesh: Mesh, props: MeshProps, key: string) => ReactNode;
|
||||||
group?: (object3d: Object3D, props: Object3DProps) => ReactNode;
|
group?: (
|
||||||
|
object3d: Object3D,
|
||||||
|
props: Object3DProps,
|
||||||
|
key: string,
|
||||||
|
) => ReactNode;
|
||||||
},
|
},
|
||||||
): ReactNode {
|
): ReactNode {
|
||||||
if (node instanceof Mesh) {
|
if (node instanceof Mesh) {
|
||||||
const props = {
|
const props = {
|
||||||
key: node.uuid,
|
|
||||||
geometry: node.geometry,
|
geometry: node.geometry,
|
||||||
material: node.material,
|
material: node.material,
|
||||||
position: node.position,
|
position: node.position,
|
||||||
@@ -19,17 +22,24 @@ export function jsxTree(
|
|||||||
scale: node.scale,
|
scale: node.scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
return mergers?.mesh ? mergers.mesh(node, props) : <mesh {...props} />;
|
return mergers?.mesh ? (
|
||||||
|
mergers.mesh(node, props, node.uuid)
|
||||||
|
) : (
|
||||||
|
<mesh key={node.uuid} {...props} />
|
||||||
|
);
|
||||||
} else if (node instanceof Object3D) {
|
} else if (node instanceof Object3D) {
|
||||||
const props = {
|
const props = {
|
||||||
key: node.uuid,
|
|
||||||
position: node.position,
|
position: node.position,
|
||||||
rotation: node.rotation,
|
rotation: node.rotation,
|
||||||
scale: node.scale,
|
scale: node.scale,
|
||||||
children: node.children.map((child) => jsxTree(child, mergers)),
|
children: node.children.map((child) => jsxTree(child, mergers)),
|
||||||
};
|
};
|
||||||
|
|
||||||
return mergers?.group ? mergers.group(node, props) : <group {...props} />;
|
return mergers?.group ? (
|
||||||
|
mergers.group(node, props, node.uuid)
|
||||||
|
) : (
|
||||||
|
<group key={node.uuid} {...props} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { fetchTanksFull } from '@blitzkit/core';
|
import { asset, fetchTanksFull } from '@blitzkit/core';
|
||||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||||
import { LinkI18n } from '../../../components/LinkI18n';
|
import { LinkI18n } from '../../../components/LinkI18n';
|
||||||
import { PageWrapper } from '../../../components/PageWrapper';
|
import { PageWrapper } from '../../../components/PageWrapper';
|
||||||
@@ -25,7 +25,7 @@ const tanksFull = await fetchTanksFull();
|
|||||||
width="8rem"
|
width="8rem"
|
||||||
height="6rem"
|
height="6rem"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(/api/tanks/${tank.id}/icons/big.png)`,
|
backgroundImage: `url(${asset(`tanks/${tank.id}/icons/big.webp`)})`,
|
||||||
backgroundSize: '150%',
|
backgroundSize: '150%',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { TanksFull } from '@blitzkit/core';
|
|||||||
import { jsonMirrorCurry } from '../../../core/blitzkit/blobMirror';
|
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.
|
* @warning This is a large response. Consider using tanks/[id]/meta.json for individual tanks.
|
||||||
|
* @proto tanks_full
|
||||||
* @returns JSON.
|
* @returns JSON.
|
||||||
*/
|
*/
|
||||||
export const GET = jsonMirrorCurry('/tanks/full.pb', TanksFull);
|
export const GET = jsonMirrorCurry('/tanks/full.pb', TanksFull);
|
||||||
@@ -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');
|
|
||||||
@@ -8,7 +8,7 @@ import type { Vector3 } from 'three';
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { subscribeWithSelector } from 'zustand/middleware';
|
import { subscribeWithSelector } from 'zustand/middleware';
|
||||||
import type { ArmorType } from '../components/Armor/components/SpacedArmorScene';
|
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 type { XP_MULTIPLIERS } from '../components/Tankopedia/TechTreeSection';
|
||||||
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions';
|
import { awaitableSkillDefinitions } from '../core/awaitables/skillDefinitions';
|
||||||
import { createContextualStore } from '../core/zustand/createContextualStore';
|
import { createContextualStore } from '../core/zustand/createContextualStore';
|
||||||
@@ -148,7 +148,7 @@ export const TankopediaEphemeral = createContextualStore(
|
|||||||
model,
|
model,
|
||||||
controlsEnabled: true,
|
controlsEnabled: true,
|
||||||
xpMultiplier: 1,
|
xpMultiplier: 1,
|
||||||
display: TankopediaDisplay.StaticArmor,
|
display: TankopediaDisplay.DynamicArmor,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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<T>(string path, Func<Task<T>> func)
|
|
||||||
where T : IMessage<T> =>
|
|
||||||
app.MapGet(path, async context => await Octet(context, await func()));
|
|
||||||
|
|
||||||
void Map<T>(string path, Func<T> func)
|
|
||||||
where T : IMessage<T> => Map(path, () => Task.FromResult(func()));
|
|
||||||
|
|
||||||
static async Task Octet<T>(HttpContext context, T message)
|
|
||||||
where T : IMessage<T>
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -49,81 +49,15 @@ namespace BlitzKit.CLI.Functions
|
|||||||
readonly BlitzProvider provider = new(args.Contains("--depot"));
|
readonly BlitzProvider provider = new(args.Contains("--depot"));
|
||||||
Locales? locales;
|
Locales? locales;
|
||||||
readonly Dictionary<string, Dictionary<string, string>> strings = [];
|
readonly Dictionary<string, Dictionary<string, string>> strings = [];
|
||||||
TanksFull tanksFull = null!;
|
readonly AssetUploader assetUploader = new("mangled ue assets") { enabled = true };
|
||||||
|
|
||||||
public async Task Initialize()
|
public async Task Mangle()
|
||||||
{
|
{
|
||||||
await FetchStrings();
|
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<FName>("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 GetString(string name)
|
||||||
{
|
{
|
||||||
I18n i18n = new();
|
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)
|
void DeduplicateSlugs(TanksFull tanksFull)
|
||||||
{
|
{
|
||||||
List<string> checkedSlugs = [];
|
List<string> checkedSlugs = [];
|
||||||
@@ -245,6 +215,33 @@ namespace BlitzKit.CLI.Functions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task<Tank> MangleTank(VFS tankDir)
|
||||||
|
{
|
||||||
|
var pdaName = $"PDA_{tankDir.Name}";
|
||||||
|
var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}");
|
||||||
|
|
||||||
|
var id = pda.Get<FName>("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)
|
async Task MangleArmor(UObject pda)
|
||||||
{
|
{
|
||||||
var id = pda.Get<FName>("TankId").Text;
|
var id = pda.Get<FName>("TankId").Text;
|
||||||
@@ -285,7 +282,7 @@ namespace BlitzKit.CLI.Functions
|
|||||||
PrettyLog.Warn($"Penetration groups missing for {id}; creating empty list");
|
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)
|
async Task MangleIcon(UObject pda)
|
||||||
@@ -295,7 +292,7 @@ namespace BlitzKit.CLI.Functions
|
|||||||
if (pda.TryGet<UTexture2D>("SmallIcon", out var smallIconTexture))
|
if (pda.TryGet<UTexture2D>("SmallIcon", out var smallIconTexture))
|
||||||
{
|
{
|
||||||
var smallIcon = BlitzKitExporter.Texture2D(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
|
else
|
||||||
{
|
{
|
||||||
@@ -305,7 +302,7 @@ namespace BlitzKit.CLI.Functions
|
|||||||
if (pda.TryGet<UTexture2D>("BigIcon", out var bigIconTexture))
|
if (pda.TryGet<UTexture2D>("BigIcon", out var bigIconTexture))
|
||||||
{
|
{
|
||||||
var bigIcon = BlitzKitExporter.Texture2D(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
|
else
|
||||||
{
|
{
|
||||||
@@ -357,9 +354,9 @@ namespace BlitzKit.CLI.Functions
|
|||||||
{
|
{
|
||||||
MonoGltf gltf = new(collision);
|
MonoGltf gltf = new(collision);
|
||||||
|
|
||||||
// await assetUploader.Add(
|
await assetUploader.Add(
|
||||||
// new($"tanks/{tank.Id}/collision/{moduleName}.glb", gltf.Write())
|
new($"tanks/{tank.Id}/collision/{moduleName}.glb", gltf.Write())
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
else
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,9 +87,9 @@ namespace BlitzKit.CLI.Models
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PrettyLog.Background(
|
// PrettyLog.Background(
|
||||||
$"🔵 {blobPath} ({change.Content.Count.ToString("N0", Program.Culture)}B)"
|
// $"🔵 {blobPath} ({change.Content.Count.ToString("N0", Program.Culture)}B)"
|
||||||
);
|
// );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ namespace BlitzKit.CLI
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "api":
|
case "mangle":
|
||||||
{
|
{
|
||||||
Api api = new(args);
|
Mangler mangler = new(args);
|
||||||
await api.Run();
|
await mangler.Mangle();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace BlitzKit.CLI.Utils
|
|||||||
{
|
{
|
||||||
return texture
|
return texture
|
||||||
.Decode(ETexturePlatform.DesktopMobile)!
|
.Decode(ETexturePlatform.DesktopMobile)!
|
||||||
.Encode(SKEncodedImageFormat.Webp, 100)
|
.Encode(SKEncodedImageFormat.Webp, 80)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user