mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 20:19:49 +02:00
feat: starting work on on-click armor
This commit is contained in:
1 parent
35788657a8
commit
69c388f243
16 files changed
+585
-471
No files matched your search
@@ -1,3 +1,3 @@
|
|||||||
# Blitzkrieg 1.16.1
|
# Blitzkrieg 1.16.1
|
||||||
|
|
||||||
Rewritten the armor sandbox for a 25%+ performance boost and more accuracy.
|
Armor sandbox gains 25% rendering speed and 40% memory efficiency.
|
||||||
@@ -12,118 +12,18 @@ import { useArmor } from '../../../hooks/useArmor';
|
|||||||
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
|
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
|
||||||
import { useDuel } from '../../../stores/duel';
|
import { useDuel } from '../../../stores/duel';
|
||||||
import { useTankopediaPersistent } from '../../../stores/tankopedia';
|
import { useTankopediaPersistent } from '../../../stores/tankopedia';
|
||||||
import { CoreArmor } from './CoreArmor';
|
import { CoreArmorSceneComponent } from './CoreArmorSceneComponent';
|
||||||
|
|
||||||
interface CoreArmorSceneProps {
|
export const CoreArmorScene = memo(() => {
|
||||||
ornamental?: boolean;
|
const wrapper = useRef<Group>(null);
|
||||||
}
|
const modelDefinitions = useModelDefinitions();
|
||||||
|
const turretContainer = useRef<Group>(null);
|
||||||
|
const gunContainer = useRef<Group>(null);
|
||||||
|
const initialTankopediaState = useTankopediaPersistent.getState();
|
||||||
|
|
||||||
export const CoreArmorScene = memo<CoreArmorSceneProps>(
|
useEffect(() => {
|
||||||
({ ornamental = false }) => {
|
if (!modelDefinitions) return;
|
||||||
const wrapper = useRef<Group>(null);
|
|
||||||
const modelDefinitions = useModelDefinitions();
|
|
||||||
const turretContainer = useRef<Group>(null);
|
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!modelDefinitions) return;
|
|
||||||
|
|
||||||
const hullOrigin = new Vector3(
|
|
||||||
trackModelDefinition.origin[0],
|
|
||||||
trackModelDefinition.origin[1],
|
|
||||||
-trackModelDefinition.origin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretOrigin = new Vector3(
|
|
||||||
tankModelDefinition.turretOrigin[0],
|
|
||||||
tankModelDefinition.turretOrigin[1],
|
|
||||||
-tankModelDefinition.turretOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const gunOrigin = new Vector3(
|
|
||||||
turretModelDefinition.gunOrigin[0],
|
|
||||||
turretModelDefinition.gunOrigin[1],
|
|
||||||
-turretModelDefinition.gunOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretPosition = new Vector3();
|
|
||||||
const turretRotation = new Euler();
|
|
||||||
const gunPosition = new Vector3();
|
|
||||||
const gunRotation = new Euler();
|
|
||||||
|
|
||||||
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
|
||||||
gunPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.sub(gunOrigin)
|
|
||||||
.applyAxisAngle(I_HAT, pitch)
|
|
||||||
.add(gunOrigin)
|
|
||||||
.add(turretOrigin)
|
|
||||||
.add(hullOrigin);
|
|
||||||
gunRotation.set(pitch, 0, 0);
|
|
||||||
gunContainer.current?.position.copy(gunPosition);
|
|
||||||
gunContainer.current?.rotation.copy(gunRotation);
|
|
||||||
|
|
||||||
if (yaw === undefined) return;
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
|
||||||
turretRotation.set(0, 0, yaw);
|
|
||||||
|
|
||||||
if (tankModelDefinition.turretRotation) {
|
|
||||||
const initialPitch = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.pitch,
|
|
||||||
);
|
|
||||||
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
|
||||||
const initialRoll = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.roll,
|
|
||||||
);
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.applyAxisAngle(I_HAT, initialPitch)
|
|
||||||
.applyAxisAngle(J_HAT, initialRoll)
|
|
||||||
.applyAxisAngle(K_HAT, initialYaw);
|
|
||||||
turretRotation.x += initialPitch;
|
|
||||||
turretRotation.y += initialRoll;
|
|
||||||
turretRotation.z += initialYaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
turretPosition.add(turretOrigin).add(hullOrigin);
|
|
||||||
turretContainer.current?.position.copy(turretPosition);
|
|
||||||
turretContainer.current?.rotation.copy(turretRotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModelTransform(useDuel.getState().protagonist!);
|
|
||||||
modelTransformEvent.on(handleModelTransform);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
modelTransformEvent.off(handleModelTransform);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.mode,
|
|
||||||
(mode) => {
|
|
||||||
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tank = useDuel((state) => state.protagonist!.tank);
|
|
||||||
const track = useDuel((state) => state.protagonist!.track);
|
|
||||||
const turret = useDuel((state) => state.protagonist!.turret);
|
|
||||||
const gun = useDuel((state) => state.protagonist!.gun);
|
|
||||||
const armorGltf = useArmor(tank.id);
|
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
|
||||||
const tankModelDefinition = modelDefinitions[tank.id];
|
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
|
||||||
const hullOrigin = new Vector3(
|
const hullOrigin = new Vector3(
|
||||||
trackModelDefinition.origin[0],
|
trackModelDefinition.origin[0],
|
||||||
trackModelDefinition.origin[1],
|
trackModelDefinition.origin[1],
|
||||||
@@ -139,86 +39,181 @@ export const CoreArmorScene = memo<CoreArmorSceneProps>(
|
|||||||
turretModelDefinition.gunOrigin[1],
|
turretModelDefinition.gunOrigin[1],
|
||||||
-turretModelDefinition.gunOrigin[2],
|
-turretModelDefinition.gunOrigin[2],
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretPosition = new Vector3();
|
||||||
|
const turretRotation = new Euler();
|
||||||
|
const gunPosition = new Vector3();
|
||||||
|
const gunRotation = new Euler();
|
||||||
|
|
||||||
return (
|
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
||||||
<group
|
gunPosition
|
||||||
ref={wrapper}
|
.set(0, 0, 0)
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
.sub(hullOrigin)
|
||||||
visible={initialTankopediaState.mode === 'armor'}
|
.sub(turretOrigin)
|
||||||
>
|
.sub(gunOrigin)
|
||||||
|
.applyAxisAngle(I_HAT, pitch)
|
||||||
|
.add(gunOrigin)
|
||||||
|
.add(turretOrigin)
|
||||||
|
.add(hullOrigin);
|
||||||
|
gunRotation.set(pitch, 0, 0);
|
||||||
|
gunContainer.current?.position.copy(gunPosition);
|
||||||
|
gunContainer.current?.rotation.copy(gunRotation);
|
||||||
|
|
||||||
|
if (yaw === undefined) return;
|
||||||
|
|
||||||
|
turretPosition
|
||||||
|
.set(0, 0, 0)
|
||||||
|
.sub(hullOrigin)
|
||||||
|
.sub(turretOrigin)
|
||||||
|
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||||
|
turretRotation.set(0, 0, yaw);
|
||||||
|
|
||||||
|
if (tankModelDefinition.turretRotation) {
|
||||||
|
const initialPitch = -degToRad(
|
||||||
|
tankModelDefinition.turretRotation.pitch,
|
||||||
|
);
|
||||||
|
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
||||||
|
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
|
||||||
|
|
||||||
|
turretPosition
|
||||||
|
.applyAxisAngle(I_HAT, initialPitch)
|
||||||
|
.applyAxisAngle(J_HAT, initialRoll)
|
||||||
|
.applyAxisAngle(K_HAT, initialYaw);
|
||||||
|
turretRotation.x += initialPitch;
|
||||||
|
turretRotation.y += initialRoll;
|
||||||
|
turretRotation.z += initialYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
turretPosition.add(turretOrigin).add(hullOrigin);
|
||||||
|
turretContainer.current?.position.copy(turretPosition);
|
||||||
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModelTransform(useDuel.getState().protagonist!);
|
||||||
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransform);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = useTankopediaPersistent.subscribe(
|
||||||
|
(state) => state.mode,
|
||||||
|
(mode) => {
|
||||||
|
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tank = useDuel((state) => state.protagonist!.tank);
|
||||||
|
const track = useDuel((state) => state.protagonist!.track);
|
||||||
|
const turret = useDuel((state) => state.protagonist!.turret);
|
||||||
|
const gun = useDuel((state) => state.protagonist!.gun);
|
||||||
|
const armorGltf = useArmor(tank.id);
|
||||||
|
const armorNodes = Object.values(armorGltf.nodes);
|
||||||
|
const tankModelDefinition = modelDefinitions[tank.id];
|
||||||
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
|
const hullOrigin = new Vector3(
|
||||||
|
trackModelDefinition.origin[0],
|
||||||
|
trackModelDefinition.origin[1],
|
||||||
|
-trackModelDefinition.origin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretOrigin = new Vector3(
|
||||||
|
tankModelDefinition.turretOrigin[0],
|
||||||
|
tankModelDefinition.turretOrigin[1],
|
||||||
|
-tankModelDefinition.turretOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const gunOrigin = new Vector3(
|
||||||
|
turretModelDefinition.gunOrigin[0],
|
||||||
|
turretModelDefinition.gunOrigin[1],
|
||||||
|
-turretModelDefinition.gunOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
ref={wrapper}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
visible={initialTankopediaState.mode === 'armor'}
|
||||||
|
>
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
{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 || spaced || thickness === undefined) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CoreArmorSceneComponent
|
||||||
|
key={node.uuid}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group ref={turretContainer}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isCurrentTurret = node.name.startsWith(
|
||||||
const isVisible = isHull;
|
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
const isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || spaced || thickness === undefined) return null;
|
if (!isVisible || spaced || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CoreArmor key={node.uuid} thickness={thickness} node={node} />
|
<group key={node.uuid} position={turretOrigin}>
|
||||||
|
<CoreArmorSceneComponent
|
||||||
|
key={node.uuid}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
|
<group ref={gunContainer}>
|
||||||
<group ref={turretContainer}>
|
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
const isCurrentGun = node.name.startsWith(
|
||||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentTurret;
|
const isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || spaced || thickness === undefined) return null;
|
if (!isVisible || spaced || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={node.uuid} position={turretOrigin}>
|
<group
|
||||||
<CoreArmor
|
key={node.uuid}
|
||||||
key={node.uuid}
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
thickness={thickness}
|
>
|
||||||
node={node}
|
<CoreArmorSceneComponent thickness={thickness} node={node} />
|
||||||
/>
|
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
<group ref={gunContainer}>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentGun = node.name.startsWith(
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
gunModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || spaced || thickness === undefined)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group
|
|
||||||
key={node.uuid}
|
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
|
||||||
>
|
|
||||||
<CoreArmor thickness={thickness} node={node} />
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
);
|
</group>
|
||||||
},
|
);
|
||||||
);
|
});
|
||||||
+5
-4
@@ -24,7 +24,7 @@ import vertexShader from './shaders/vertex.glsl';
|
|||||||
|
|
||||||
export const spacedArmorRenderTarget = new WebGLRenderTarget();
|
export const spacedArmorRenderTarget = new WebGLRenderTarget();
|
||||||
|
|
||||||
interface CoreArmorProps {
|
interface CoreArmorSceneComponentProps {
|
||||||
thickness: number;
|
thickness: number;
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,10 @@ const excludeMaterial = new MeshBasicMaterial({
|
|||||||
colorWrite: false,
|
colorWrite: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function CoreArmor({ node, thickness }: CoreArmorProps) {
|
export function CoreArmorSceneComponent({
|
||||||
|
node,
|
||||||
|
thickness,
|
||||||
|
}: CoreArmorSceneComponentProps) {
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
vertexShader,
|
vertexShader,
|
||||||
@@ -51,7 +54,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) {
|
|||||||
damage: { value: null },
|
damage: { value: null },
|
||||||
explosionRadius: { value: null },
|
explosionRadius: { value: null },
|
||||||
greenPenetration: { value: null },
|
greenPenetration: { value: null },
|
||||||
wireframe: { value: null },
|
|
||||||
opaque: { value: null },
|
opaque: { value: null },
|
||||||
|
|
||||||
resolution: { value: new Vector2() },
|
resolution: { value: new Vector2() },
|
||||||
@@ -79,7 +81,6 @@ export function CoreArmor({ node, thickness }: CoreArmorProps) {
|
|||||||
visual: TankopediaPersistent['model']['visual'],
|
visual: TankopediaPersistent['model']['visual'],
|
||||||
) {
|
) {
|
||||||
material.uniforms.greenPenetration.value = visual.greenPenetration;
|
material.uniforms.greenPenetration.value = visual.greenPenetration;
|
||||||
material.uniforms.wireframe.value = visual.wireframe;
|
|
||||||
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
|
material.uniforms.opaque.value = visual.opaque || visual.wireframe;
|
||||||
material.wireframe = visual.wireframe;
|
material.wireframe = visual.wireframe;
|
||||||
}
|
}
|
||||||
+2
-3
@@ -13,7 +13,6 @@ uniform bool canSplash;
|
|||||||
uniform float damage;
|
uniform float damage;
|
||||||
uniform float explosionRadius;
|
uniform float explosionRadius;
|
||||||
uniform bool greenPenetration;
|
uniform bool greenPenetration;
|
||||||
uniform bool wireframe;
|
|
||||||
uniform bool opaque;
|
uniform bool opaque;
|
||||||
|
|
||||||
uniform highp sampler2D spacedArmorBuffer;
|
uniform highp sampler2D spacedArmorBuffer;
|
||||||
@@ -35,8 +34,8 @@ void main() {
|
|||||||
vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates);
|
vec4 spacedArmorBufferFragment = texture2D(spacedArmorBuffer, screenCoordinates);
|
||||||
bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0;
|
bool isUnderSpacedArmor = spacedArmorBufferFragment.a != 0.0;
|
||||||
|
|
||||||
bool threeCalibersRule = caliber > thickness * 3.0;
|
bool threeCalibersRule = caliber > thickness * 3.0 || isUnderSpacedArmor;
|
||||||
if (!threeCalibersRule && !isUnderSpacedArmor && angle >= ricochet) {
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
penetrationChance = 0.0;
|
penetrationChance = 0.0;
|
||||||
} else {
|
} else {
|
||||||
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
|
bool twoCalibersRule = caliber > thickness * 2.0 && thickness > 0.0;
|
||||||
File renamed without changes.
@@ -1,44 +0,0 @@
|
|||||||
import { MeshBasicMaterial, Object3D } from 'three';
|
|
||||||
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
|
||||||
import { ArmorType } from '../SpacedArmorScene';
|
|
||||||
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
|
||||||
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
|
||||||
interface SpacedArmorProps {
|
|
||||||
node: Object3D;
|
|
||||||
ornamental?: boolean;
|
|
||||||
type: ArmorType;
|
|
||||||
thickness: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const omitMaterial = new MeshBasicMaterial({
|
|
||||||
colorWrite: false,
|
|
||||||
depthWrite: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function SpacedArmor({ node, type, thickness }: SpacedArmorProps) {
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{type === ArmorType.Core &&
|
|
||||||
jsxTree(node, {
|
|
||||||
renderOrder: 0,
|
|
||||||
material: omitMaterial,
|
|
||||||
})}
|
|
||||||
|
|
||||||
{type === ArmorType.Spaced && (
|
|
||||||
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === ArmorType.External && (
|
|
||||||
<SpacedArmorSubExternal node={node} thickness={thickness} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ import { useModel } from '../../../hooks/useModel';
|
|||||||
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
|
import { useModelDefinitions } from '../../../hooks/useModelDefinitions';
|
||||||
import { useDuel } from '../../../stores/duel';
|
import { useDuel } from '../../../stores/duel';
|
||||||
import { useTankopediaPersistent } from '../../../stores/tankopedia';
|
import { useTankopediaPersistent } from '../../../stores/tankopedia';
|
||||||
import { SpacedArmor } from './SpacedArmor';
|
import { SpacedArmorSceneComponent } from './SpacedArmorSceneComponent';
|
||||||
|
|
||||||
export enum ArmorType {
|
export enum ArmorType {
|
||||||
Core,
|
Core,
|
||||||
@@ -21,118 +21,16 @@ export enum ArmorType {
|
|||||||
External,
|
External,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpacedArmorSceneProps {
|
export const SpacedArmorScene = memo(() => {
|
||||||
ornamental?: boolean;
|
const wrapper = useRef<Group>(null);
|
||||||
}
|
const modelDefinitions = useModelDefinitions();
|
||||||
|
const turretContainer = useRef<Group>(null);
|
||||||
|
const gunContainer = useRef<Group>(null);
|
||||||
|
const initialTankopediaState = useTankopediaPersistent.getState();
|
||||||
|
|
||||||
export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
|
useEffect(() => {
|
||||||
({ ornamental = false }) => {
|
if (!modelDefinitions) return;
|
||||||
const wrapper = useRef<Group>(null);
|
|
||||||
const modelDefinitions = useModelDefinitions();
|
|
||||||
const turretContainer = useRef<Group>(null);
|
|
||||||
const gunContainer = useRef<Group>(null);
|
|
||||||
const initialTankopediaState = useTankopediaPersistent.getState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!modelDefinitions) return;
|
|
||||||
|
|
||||||
const hullOrigin = new Vector3(
|
|
||||||
trackModelDefinition.origin[0],
|
|
||||||
trackModelDefinition.origin[1],
|
|
||||||
-trackModelDefinition.origin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretOrigin = new Vector3(
|
|
||||||
tankModelDefinition.turretOrigin[0],
|
|
||||||
tankModelDefinition.turretOrigin[1],
|
|
||||||
-tankModelDefinition.turretOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const gunOrigin = new Vector3(
|
|
||||||
turretModelDefinition.gunOrigin[0],
|
|
||||||
turretModelDefinition.gunOrigin[1],
|
|
||||||
-turretModelDefinition.gunOrigin[2],
|
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
|
||||||
const turretPosition = new Vector3();
|
|
||||||
const turretRotation = new Euler();
|
|
||||||
const gunPosition = new Vector3();
|
|
||||||
const gunRotation = new Euler();
|
|
||||||
|
|
||||||
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
|
||||||
gunPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.sub(gunOrigin)
|
|
||||||
.applyAxisAngle(I_HAT, pitch)
|
|
||||||
.add(gunOrigin)
|
|
||||||
.add(turretOrigin)
|
|
||||||
.add(hullOrigin);
|
|
||||||
gunRotation.set(pitch, 0, 0);
|
|
||||||
gunContainer.current?.position.copy(gunPosition);
|
|
||||||
gunContainer.current?.rotation.copy(gunRotation);
|
|
||||||
|
|
||||||
if (yaw === undefined) return;
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.set(0, 0, 0)
|
|
||||||
.sub(hullOrigin)
|
|
||||||
.sub(turretOrigin)
|
|
||||||
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
|
||||||
turretRotation.set(0, 0, yaw);
|
|
||||||
|
|
||||||
if (tankModelDefinition.turretRotation) {
|
|
||||||
const initialPitch = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.pitch,
|
|
||||||
);
|
|
||||||
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
|
||||||
const initialRoll = -degToRad(
|
|
||||||
tankModelDefinition.turretRotation.roll,
|
|
||||||
);
|
|
||||||
|
|
||||||
turretPosition
|
|
||||||
.applyAxisAngle(I_HAT, initialPitch)
|
|
||||||
.applyAxisAngle(J_HAT, initialRoll)
|
|
||||||
.applyAxisAngle(K_HAT, initialYaw);
|
|
||||||
turretRotation.x += initialPitch;
|
|
||||||
turretRotation.y += initialRoll;
|
|
||||||
turretRotation.z += initialYaw;
|
|
||||||
}
|
|
||||||
|
|
||||||
turretPosition.add(turretOrigin).add(hullOrigin);
|
|
||||||
turretContainer.current?.position.copy(turretPosition);
|
|
||||||
turretContainer.current?.rotation.copy(turretRotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModelTransform(useDuel.getState().protagonist!);
|
|
||||||
modelTransformEvent.on(handleModelTransform);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
modelTransformEvent.off(handleModelTransform);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = useTankopediaPersistent.subscribe(
|
|
||||||
(state) => state.mode,
|
|
||||||
(mode) => {
|
|
||||||
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tank = useDuel((state) => state.protagonist!.tank);
|
|
||||||
const track = useDuel((state) => state.protagonist!.track);
|
|
||||||
const turret = useDuel((state) => state.protagonist!.turret);
|
|
||||||
const gun = useDuel((state) => state.protagonist!.gun);
|
|
||||||
const armorGltf = useArmor(tank.id);
|
|
||||||
const { gltf: modelGltf } = useModel(tank.id);
|
|
||||||
const armorNodes = Object.values(armorGltf.nodes);
|
|
||||||
const modelNodes = Object.values(modelGltf.nodes);
|
|
||||||
const tankModelDefinition = modelDefinitions[tank.id];
|
|
||||||
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
|
||||||
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
|
||||||
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
|
||||||
const hullOrigin = new Vector3(
|
const hullOrigin = new Vector3(
|
||||||
trackModelDefinition.origin[0],
|
trackModelDefinition.origin[0],
|
||||||
trackModelDefinition.origin[1],
|
trackModelDefinition.origin[1],
|
||||||
@@ -148,74 +46,196 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
|
|||||||
turretModelDefinition.gunOrigin[1],
|
turretModelDefinition.gunOrigin[1],
|
||||||
-turretModelDefinition.gunOrigin[2],
|
-turretModelDefinition.gunOrigin[2],
|
||||||
).applyAxisAngle(I_HAT, Math.PI / 2);
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretPosition = new Vector3();
|
||||||
|
const turretRotation = new Euler();
|
||||||
|
const gunPosition = new Vector3();
|
||||||
|
const gunRotation = new Euler();
|
||||||
|
|
||||||
return (
|
function handleModelTransform({ yaw, pitch }: ModelTransformEventData) {
|
||||||
<group
|
gunPosition
|
||||||
ref={wrapper}
|
.set(0, 0, 0)
|
||||||
rotation={[-Math.PI / 2, 0, 0]}
|
.sub(hullOrigin)
|
||||||
visible={initialTankopediaState.mode === 'armor'}
|
.sub(turretOrigin)
|
||||||
>
|
.sub(gunOrigin)
|
||||||
|
.applyAxisAngle(I_HAT, pitch)
|
||||||
|
.add(gunOrigin)
|
||||||
|
.add(turretOrigin)
|
||||||
|
.add(hullOrigin);
|
||||||
|
gunRotation.set(pitch, 0, 0);
|
||||||
|
gunContainer.current?.position.copy(gunPosition);
|
||||||
|
gunContainer.current?.rotation.copy(gunRotation);
|
||||||
|
|
||||||
|
if (yaw === undefined) return;
|
||||||
|
|
||||||
|
turretPosition
|
||||||
|
.set(0, 0, 0)
|
||||||
|
.sub(hullOrigin)
|
||||||
|
.sub(turretOrigin)
|
||||||
|
.applyAxisAngle(new Vector3(0, 0, 1), yaw);
|
||||||
|
turretRotation.set(0, 0, yaw);
|
||||||
|
|
||||||
|
if (tankModelDefinition.turretRotation) {
|
||||||
|
const initialPitch = -degToRad(
|
||||||
|
tankModelDefinition.turretRotation.pitch,
|
||||||
|
);
|
||||||
|
const initialYaw = -degToRad(tankModelDefinition.turretRotation.yaw);
|
||||||
|
const initialRoll = -degToRad(tankModelDefinition.turretRotation.roll);
|
||||||
|
|
||||||
|
turretPosition
|
||||||
|
.applyAxisAngle(I_HAT, initialPitch)
|
||||||
|
.applyAxisAngle(J_HAT, initialRoll)
|
||||||
|
.applyAxisAngle(K_HAT, initialYaw);
|
||||||
|
turretRotation.x += initialPitch;
|
||||||
|
turretRotation.y += initialRoll;
|
||||||
|
turretRotation.z += initialYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
turretPosition.add(turretOrigin).add(hullOrigin);
|
||||||
|
turretContainer.current?.position.copy(turretPosition);
|
||||||
|
turretContainer.current?.rotation.copy(turretRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleModelTransform(useDuel.getState().protagonist!);
|
||||||
|
modelTransformEvent.on(handleModelTransform);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modelTransformEvent.off(handleModelTransform);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = useTankopediaPersistent.subscribe(
|
||||||
|
(state) => state.mode,
|
||||||
|
(mode) => {
|
||||||
|
if (wrapper.current) wrapper.current.visible = mode === 'armor';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tank = useDuel((state) => state.protagonist!.tank);
|
||||||
|
const track = useDuel((state) => state.protagonist!.track);
|
||||||
|
const turret = useDuel((state) => state.protagonist!.turret);
|
||||||
|
const gun = useDuel((state) => state.protagonist!.gun);
|
||||||
|
const armorGltf = useArmor(tank.id);
|
||||||
|
const { gltf: modelGltf } = useModel(tank.id);
|
||||||
|
const armorNodes = Object.values(armorGltf.nodes);
|
||||||
|
const modelNodes = Object.values(modelGltf.nodes);
|
||||||
|
const tankModelDefinition = modelDefinitions[tank.id];
|
||||||
|
const trackModelDefinition = tankModelDefinition.tracks[track.id];
|
||||||
|
const turretModelDefinition = tankModelDefinition.turrets[turret.id];
|
||||||
|
const gunModelDefinition = turretModelDefinition.guns[gun.id];
|
||||||
|
const hullOrigin = new Vector3(
|
||||||
|
trackModelDefinition.origin[0],
|
||||||
|
trackModelDefinition.origin[1],
|
||||||
|
-trackModelDefinition.origin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const turretOrigin = new Vector3(
|
||||||
|
tankModelDefinition.turretOrigin[0],
|
||||||
|
tankModelDefinition.turretOrigin[1],
|
||||||
|
-tankModelDefinition.turretOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
const gunOrigin = new Vector3(
|
||||||
|
turretModelDefinition.gunOrigin[0],
|
||||||
|
turretModelDefinition.gunOrigin[1],
|
||||||
|
-turretModelDefinition.gunOrigin[2],
|
||||||
|
).applyAxisAngle(I_HAT, Math.PI / 2);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
ref={wrapper}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
visible={initialTankopediaState.mode === 'armor'}
|
||||||
|
>
|
||||||
|
<group position={hullOrigin}>
|
||||||
|
{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) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
key={node.uuid}
|
||||||
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
|
thickness={thickness}
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</group>
|
||||||
|
{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 (
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
|
key={node.uuid}
|
||||||
|
type={ArmorType.External}
|
||||||
|
thickness={trackModelDefinition.thickness}
|
||||||
|
variant="track"
|
||||||
|
node={node}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<group ref={turretContainer}>
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isHull = node.name.startsWith('hull_');
|
const isCurrentTurret = node.name.startsWith(
|
||||||
const isVisible = isHull;
|
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
|
);
|
||||||
|
const isVisible = isCurrentTurret;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
tankModelDefinition.armor,
|
turretModelDefinition.armor,
|
||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined) return null;
|
if (!isVisible || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SpacedArmor
|
<group key={node.uuid} position={turretOrigin}>
|
||||||
key={node.uuid}
|
<SpacedArmorSceneComponent
|
||||||
ornamental={ornamental}
|
key={node.uuid}
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
/>
|
/>
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
{modelNodes.map((node) => {
|
<group ref={gunContainer}>
|
||||||
const isWheel = node.name.startsWith('chassis_wheel_');
|
|
||||||
const isTrack = node.name.startsWith('chassis_track_');
|
|
||||||
const isVisible = isWheel || isTrack;
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpacedArmor
|
|
||||||
key={node.uuid}
|
|
||||||
ornamental={ornamental}
|
|
||||||
type={ArmorType.External}
|
|
||||||
thickness={trackModelDefinition.thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<group ref={turretContainer}>
|
|
||||||
<group position={hullOrigin}>
|
<group position={hullOrigin}>
|
||||||
{armorNodes.map((node) => {
|
{armorNodes.map((node) => {
|
||||||
const isCurrentTurret = node.name.startsWith(
|
const isCurrentGun = node.name.startsWith(
|
||||||
`turret_${turretModelDefinition.model.toString().padStart(2, '0')}`,
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
||||||
);
|
);
|
||||||
const isVisible = isCurrentTurret;
|
const isVisible = isCurrentGun;
|
||||||
const armorId = nameToArmorId(node.name);
|
const armorId = nameToArmorId(node.name);
|
||||||
const { spaced, thickness } = resolveArmor(
|
const { spaced, thickness } = resolveArmor(
|
||||||
turretModelDefinition.armor,
|
gunModelDefinition.armor,
|
||||||
armorId,
|
armorId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined) return null;
|
if (!isVisible || thickness === undefined) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={node.uuid} position={turretOrigin}>
|
<group
|
||||||
<SpacedArmor
|
key={node.uuid}
|
||||||
key={node.uuid}
|
position={turretOrigin.clone().add(gunOrigin)}
|
||||||
ornamental={ornamental}
|
>
|
||||||
|
<SpacedArmorSceneComponent
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
||||||
thickness={thickness}
|
thickness={thickness}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -224,58 +244,27 @@ export const SpacedArmorScene = memo<SpacedArmorSceneProps>(
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</group>
|
</group>
|
||||||
<group ref={gunContainer}>
|
|
||||||
<group position={hullOrigin}>
|
|
||||||
{armorNodes.map((node) => {
|
|
||||||
const isCurrentGun = node.name.startsWith(
|
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`,
|
|
||||||
);
|
|
||||||
const isVisible = isCurrentGun;
|
|
||||||
const armorId = nameToArmorId(node.name);
|
|
||||||
const { spaced, thickness } = resolveArmor(
|
|
||||||
gunModelDefinition.armor,
|
|
||||||
armorId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isVisible || thickness === undefined) return null;
|
{modelNodes.map((node) => {
|
||||||
|
const isCurrentGun =
|
||||||
|
node.name ===
|
||||||
|
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
||||||
|
const isVisible = isCurrentGun;
|
||||||
|
|
||||||
return (
|
if (!isVisible) return null;
|
||||||
<group
|
|
||||||
key={node.uuid}
|
|
||||||
position={turretOrigin.clone().add(gunOrigin)}
|
|
||||||
>
|
|
||||||
<SpacedArmor
|
|
||||||
ornamental={ornamental}
|
|
||||||
type={spaced ? ArmorType.Spaced : ArmorType.Core}
|
|
||||||
thickness={thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
|
|
||||||
{modelNodes.map((node) => {
|
return (
|
||||||
const isCurrentGun =
|
<SpacedArmorSceneComponent
|
||||||
node.name ===
|
key={node.uuid}
|
||||||
`gun_${gunModelDefinition.model.toString().padStart(2, '0')}`;
|
type={ArmorType.External}
|
||||||
const isVisible = isCurrentGun;
|
thickness={gunModelDefinition.thickness}
|
||||||
|
variant="gun"
|
||||||
if (!isVisible) return null;
|
node={node}
|
||||||
|
/>
|
||||||
return (
|
);
|
||||||
<SpacedArmor
|
})}
|
||||||
key={node.uuid}
|
|
||||||
ornamental={ornamental}
|
|
||||||
type={ArmorType.External}
|
|
||||||
thickness={gunModelDefinition.thickness}
|
|
||||||
node={node}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
);
|
</group>
|
||||||
},
|
);
|
||||||
);
|
});
|
||||||
+9
@@ -5,6 +5,7 @@ import {
|
|||||||
Object3D,
|
Object3D,
|
||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from 'three';
|
} from 'three';
|
||||||
|
import { ArmorUserData, ExternalModuleVariant } from '../..';
|
||||||
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||||
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
||||||
@@ -17,11 +18,13 @@ import vertexShader from './shaders/vertex.glsl';
|
|||||||
interface SpacedArmorSubExternalProps {
|
interface SpacedArmorSubExternalProps {
|
||||||
node: Object3D;
|
node: Object3D;
|
||||||
thickness: number;
|
thickness: number;
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpacedArmorSubExternal({
|
export function SpacedArmorSubExternal({
|
||||||
node,
|
node,
|
||||||
thickness,
|
thickness,
|
||||||
|
variant,
|
||||||
}: SpacedArmorSubExternalProps) {
|
}: SpacedArmorSubExternalProps) {
|
||||||
const material = new ShaderMaterial({
|
const material = new ShaderMaterial({
|
||||||
fragmentShader,
|
fragmentShader,
|
||||||
@@ -90,6 +93,12 @@ export function SpacedArmorSubExternal({
|
|||||||
depthTest: true,
|
depthTest: true,
|
||||||
depthWrite: true,
|
depthWrite: true,
|
||||||
}),
|
}),
|
||||||
|
onClick() {},
|
||||||
|
userData: {
|
||||||
|
type: 'external',
|
||||||
|
thickness,
|
||||||
|
variant,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
})}
|
})}
|
||||||
{jsxTree(node, {
|
{jsxTree(node, {
|
||||||
renderOrder: 4,
|
renderOrder: 4,
|
||||||
File renamed without changes.
File renamed without changes.
+6
@@ -6,6 +6,7 @@ import {
|
|||||||
ShaderMaterial,
|
ShaderMaterial,
|
||||||
} from 'three';
|
} from 'three';
|
||||||
import { degToRad } from 'three/src/math/MathUtils';
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { ArmorUserData } from '../..';
|
||||||
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
import { isExplosive } from '../../../../../../core/blitz/isExplosive';
|
||||||
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
import { resolveNearPenetration } from '../../../../../../core/blitz/resolveNearPenetration';
|
||||||
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
import { hasEquipment } from '../../../../../../core/blitzkrieg/hasEquipment';
|
||||||
@@ -100,6 +101,11 @@ export function SpacedArmorSubSpaced({
|
|||||||
{jsxTree(node, {
|
{jsxTree(node, {
|
||||||
renderOrder: 2,
|
renderOrder: 2,
|
||||||
material,
|
material,
|
||||||
|
onClick() {},
|
||||||
|
userData: {
|
||||||
|
type: 'spaced',
|
||||||
|
thickness,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
})}
|
})}
|
||||||
{jsxTree(node, {
|
{jsxTree(node, {
|
||||||
renderOrder: 5,
|
renderOrder: 5,
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,212 @@
|
|||||||
|
import { useThree } from '@react-three/fiber';
|
||||||
|
import { Intersection, MeshBasicMaterial, Object3D, Vector3 } from 'three';
|
||||||
|
import { degToRad } from 'three/src/math/MathUtils';
|
||||||
|
import { I_HAT } from '../../../../constants/axis';
|
||||||
|
import { isExplosive } from '../../../../core/blitz/isExplosive';
|
||||||
|
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
|
||||||
|
import { hasEquipment } from '../../../../core/blitzkrieg/hasEquipment';
|
||||||
|
import { jsxTree } from '../../../../core/blitzkrieg/jsxTree';
|
||||||
|
import { useDuel } from '../../../../stores/duel';
|
||||||
|
import { ArmorType } from '../SpacedArmorScene';
|
||||||
|
import { SpacedArmorSubExternal } from './components/SpacedArmorSubExternal';
|
||||||
|
import { SpacedArmorSubSpaced } from './components/SpacedArmorSubSpaced';
|
||||||
|
|
||||||
|
type SpacedArmorSceneComponentProps = {
|
||||||
|
node: Object3D;
|
||||||
|
thickness: number;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ArmorType.External;
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ExternalModuleVariant = 'gun' | 'track';
|
||||||
|
export type ArmorUserData = {
|
||||||
|
thickness: number;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: 'core' | 'spaced';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'external';
|
||||||
|
variant: ExternalModuleVariant;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
type ArmorMeshIntersection = Intersection & {
|
||||||
|
eventObject: Object3D;
|
||||||
|
object: Object3D & { userData: ArmorUserData };
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
...props
|
||||||
|
}: SpacedArmorSceneComponentProps) {
|
||||||
|
const camera = useThree((state) => state.camera);
|
||||||
|
const cameraNormal = new Vector3();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.type === ArmorType.Core &&
|
||||||
|
jsxTree(node, {
|
||||||
|
renderOrder: 0,
|
||||||
|
material: omitMaterial,
|
||||||
|
userData: {
|
||||||
|
type: 'core',
|
||||||
|
thickness,
|
||||||
|
} satisfies ArmorUserData,
|
||||||
|
|
||||||
|
async onClick(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const intersectionsAll = event.intersections.filter(
|
||||||
|
({ object }) => object.userData.type !== undefined,
|
||||||
|
) as ArmorMeshIntersection[];
|
||||||
|
const intersectionsWithDuplicates = intersectionsAll.slice(
|
||||||
|
0,
|
||||||
|
intersectionsAll.findIndex(
|
||||||
|
({ object }) => object.userData.type === 'core',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const firstGunIntersectionIndex =
|
||||||
|
intersectionsWithDuplicates.findIndex(
|
||||||
|
({ object }) =>
|
||||||
|
object.userData.type === 'external' &&
|
||||||
|
object.userData.variant === 'gun',
|
||||||
|
);
|
||||||
|
const firstTrackIntersectionIndex =
|
||||||
|
intersectionsWithDuplicates.findIndex(
|
||||||
|
({ object }) =>
|
||||||
|
object.userData.type === 'external' &&
|
||||||
|
object.userData.variant === 'track',
|
||||||
|
);
|
||||||
|
let intersections = intersectionsWithDuplicates;
|
||||||
|
|
||||||
|
if (firstGunIntersectionIndex !== -1) {
|
||||||
|
intersections = intersections.filter(({ object }, index) => {
|
||||||
|
return object.userData.type === 'external' &&
|
||||||
|
object.userData.variant === 'gun'
|
||||||
|
? index === firstGunIntersectionIndex
|
||||||
|
: true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (firstTrackIntersectionIndex !== -1) {
|
||||||
|
intersections = intersections.filter(({ object }, index) => {
|
||||||
|
return object.userData.type === 'external' &&
|
||||||
|
object.userData.variant === 'track'
|
||||||
|
? index === firstTrackIntersectionIndex
|
||||||
|
: true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCalibratedShells = await hasEquipment(103, true);
|
||||||
|
const hasEnhancedArmor = await hasEquipment(110);
|
||||||
|
const thicknessCoefficient = hasEnhancedArmor ? 1.04 : 1;
|
||||||
|
const shell = useDuel.getState().antagonist!.shell;
|
||||||
|
const penetration =
|
||||||
|
resolveNearPenetration(shell.penetration) *
|
||||||
|
(hasCalibratedShells
|
||||||
|
? isExplosive(shell.type)
|
||||||
|
? 1.1
|
||||||
|
: 1.05
|
||||||
|
: 1);
|
||||||
|
const shotLog: ({
|
||||||
|
thickness: number;
|
||||||
|
status: 'blocked' | 'penetration' | 'ricochet';
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: ArmorType.External;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: Exclude<ArmorType, ArmorType.External>;
|
||||||
|
thicknessAngled: number;
|
||||||
|
}
|
||||||
|
))[] = []; // TODO: type this
|
||||||
|
let remainingPenetration = penetration;
|
||||||
|
let penetrationChance = -1;
|
||||||
|
let splashChance = -1;
|
||||||
|
|
||||||
|
camera.getWorldDirection(cameraNormal).multiplyScalar(-1);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (const intersection of intersections) {
|
||||||
|
const thickness =
|
||||||
|
thicknessCoefficient * intersection.object.userData.thickness;
|
||||||
|
|
||||||
|
if (intersection.object.userData.type === 'external') {
|
||||||
|
remainingPenetration -= thickness;
|
||||||
|
const blocked = remainingPenetration < 0;
|
||||||
|
|
||||||
|
shotLog.push({
|
||||||
|
type: ArmorType.External,
|
||||||
|
thickness: intersection.object.userData.thickness,
|
||||||
|
status: blocked ? 'blocked' : 'penetration',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blocked) break;
|
||||||
|
} else if (intersection.object.userData.type === 'spaced') {
|
||||||
|
const normal = intersection.normal!.applyAxisAngle(
|
||||||
|
I_HAT,
|
||||||
|
-Math.PI / 2,
|
||||||
|
);
|
||||||
|
const angle = normal.angleTo(cameraNormal);
|
||||||
|
const ricochet = degToRad(shell.ricochet ?? 90);
|
||||||
|
const normalization = degToRad(shell.normalization ?? 0);
|
||||||
|
const threeCalibersRule =
|
||||||
|
shell.caliber > thickness || index > 0;
|
||||||
|
const twoCalibersRule = shell.caliber > thickness * 2;
|
||||||
|
const finalNormalization = twoCalibersRule
|
||||||
|
? (1.4 * normalization * shell.caliber) / (2.0 * thickness)
|
||||||
|
: normalization;
|
||||||
|
const finalThickness =
|
||||||
|
thickness / Math.cos(angle - finalNormalization);
|
||||||
|
remainingPenetration -= finalThickness;
|
||||||
|
|
||||||
|
if (!threeCalibersRule && angle >= ricochet) {
|
||||||
|
shotLog.push({
|
||||||
|
type: ArmorType.Spaced,
|
||||||
|
thickness,
|
||||||
|
thicknessAngled: finalThickness,
|
||||||
|
status: 'ricochet',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else throw new Error('Core should be omitted');
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(shotLog, null, 2));
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
|
||||||
|
{props.type === ArmorType.Spaced && (
|
||||||
|
<SpacedArmorSubSpaced node={node} thickness={thickness} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.type === ArmorType.External && (
|
||||||
|
<SpacedArmorSubExternal
|
||||||
|
node={node}
|
||||||
|
thickness={thickness}
|
||||||
|
variant={props.variant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createPortal, useFrame } from '@react-three/fiber';
|
import { createPortal, useFrame } from '@react-three/fiber';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { DepthTexture, Scene, Vector2 } from 'three';
|
import { DepthTexture, Scene, Vector2 } from 'three';
|
||||||
import { spacedArmorRenderTarget } from './components/CoreArmor';
|
|
||||||
import { CoreArmorScene } from './components/CoreArmorScene';
|
import { CoreArmorScene } from './components/CoreArmorScene';
|
||||||
|
import { spacedArmorRenderTarget } from './components/CoreArmorSceneComponent';
|
||||||
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
import { SpacedArmorScene } from './components/SpacedArmorScene';
|
||||||
|
|
||||||
export const Armor = memo(() => {
|
export const Armor = memo(() => {
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { createPortal, useFrame, useThree } from '@react-three/fiber';
|
|
||||||
import { isInteger } from 'lodash';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { Scene } from 'three';
|
|
||||||
|
|
||||||
interface HeadsUpDisplayProps {
|
|
||||||
priority?: number;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Renders `children` above the default scene, even if the priority is a
|
|
||||||
* negative number.
|
|
||||||
*/
|
|
||||||
export function HeadsUpDisplay({
|
|
||||||
priority = 1,
|
|
||||||
children,
|
|
||||||
}: HeadsUpDisplayProps) {
|
|
||||||
const { gl: renderer, scene: defaultScene, camera } = useThree();
|
|
||||||
const scene = new Scene();
|
|
||||||
const portal = createPortal(children, scene, {
|
|
||||||
events: { priority: priority + 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
useFrame(() => {
|
|
||||||
if (priority === 1) {
|
|
||||||
renderer.clear();
|
|
||||||
renderer.render(defaultScene, camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.clearDepth();
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
}, priority);
|
|
||||||
|
|
||||||
renderer.autoClear = false;
|
|
||||||
if (!isInteger(priority)) {
|
|
||||||
throw new TypeError('Priority property must be an integer');
|
|
||||||
}
|
|
||||||
|
|
||||||
return portal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this component if the default scene disappears on usage of the
|
|
||||||
* HeadsUpDisplay; caused because there are not HeadsUpDisplay components
|
|
||||||
* with `1` priority.
|
|
||||||
*/
|
|
||||||
export function RenderDefault() {
|
|
||||||
const { gl: renderer, scene, camera } = useThree();
|
|
||||||
|
|
||||||
useFrame(() => renderer.render(scene, camera));
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user