Merge branch 'main' into data-driven

This commit is contained in:
tresabhi committed 2026-10-31 13:38:57 -05:00
commit a15e331681
18 files changed
+552 -130

No files matched your search

+10 -7
View File
@@ -48,7 +48,9 @@ export function Lighting({ hasPbr }: Props) {
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const revealed = Tankopedia.use((state) => state.revealed);
const skinPending = Tankopedia.use(
(state) => state.loadedSkin !== state.skin,
(state) =>
state.loaders.skin.requested !== state.loaders.skin.loaded ||
state.loaders.attachment.requested !== state.loaders.attachment.loaded,
);
const animationTime = useRef(REVEAL_ANIMATION_TIME);
@@ -63,6 +65,7 @@ export function Lighting({ hasPbr }: Props) {
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
draft.loaders.model.loaded = true;
});
}, []);
@@ -162,6 +165,7 @@ export function Lighting({ hasPbr }: Props) {
key={index}
position={position}
intensity={intensity}
userData={{ intensity }}
penumbra={1}
castShadow={highGraphics}
decay={1}
@@ -212,6 +216,11 @@ function Animator({
for (const child of wrapper.current.children) {
if (child instanceof SpotLight) {
child.angle = ANGLE * t * factor;
child.intensity = child.userData.intensity;
if (child.angle === 0) {
child.intensity = 0;
}
} else if (child instanceof HemisphereLight) {
child.intensity = HEMISPHERE_INTENSITY * t * factor;
}
@@ -244,12 +253,6 @@ function Animator({
draft.display = requestedDisplay;
});
}
if (x >= 1 && Tankopedia.state.skin !== Tankopedia.state.loadedSkin) {
Tankopedia.mutate((draft) => {
draft.skin = draft.loadedSkin;
});
}
});
return null;
+42 -4
View File
@@ -1,5 +1,6 @@
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
import { useRef } from "react";
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
@@ -13,6 +14,7 @@ import { useSkin } from "../hooks/useSkin";
import { useTankTransform } from "../hooks/useTankTransform";
import { Tankopedia } from "../stores/tankopedia";
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
import { transitionEvent } from "./Lighting";
export function TankModel() {
const protagonist = Tankopedia.use((state) => state.protagonist);
@@ -28,9 +30,45 @@ export function TankModel() {
const { gltf } = useModel(protagonist.tank);
const nodes = Object.values(gltf.nodes);
const skinGltf = useSkin(protagonist.tank);
const attachmentGltf = useSkin(protagonist.tank, "attachment");
const skins = [skinGltf, attachmentGltf].filter((skin) => skin !== undefined);
useTankTransform(track, turret, turretContainer, gunContainer);
useEffect(() => {
const emissiveMaterials = new Set<MeshStandardMaterial>();
for (const scene of [gltf.scene, ...skins.map((skin) => skin.scene)]) {
scene.traverse((object) => {
if (
object instanceof Mesh &&
object.material instanceof MeshStandardMaterial &&
object.material.emissiveMap
) {
emissiveMaterials.add(object.material);
}
});
}
// emissive parts fade with the lights instead of glowing in the dark
function applyIntensity(intensity: number) {
for (const material of emissiveMaterials) {
material.emissiveIntensity = intensity;
}
}
function handleTransitionEvent(event: QuicklimeEvent<number>) {
applyIntensity(event.data);
}
applyIntensity(transitionEvent.last!);
transitionEvent.on(handleTransitionEvent);
return () => {
transitionEvent.off(handleTransitionEvent);
};
}, [gltf, skinGltf, attachmentGltf]);
return (
<>
{nodes.map((node) => {
@@ -92,7 +130,7 @@ export function TankModel() {
window.removeEventListener("pointerup", handlePointerUp);
}
return skinTree(node, skinGltf, {
return skinTree(node, skins, {
mesh(_, props, key) {
return (
<mesh
@@ -171,7 +209,7 @@ export function TankModel() {
window.removeEventListener("pointerup", handlePointerUp);
}
return skinTree(node, skinGltf, {
return skinTree(node, skins, {
mesh(_, props, key) {
return (
<mesh
@@ -254,7 +292,7 @@ export function TankModel() {
window.removeEventListener("pointerup", handlePointerUp);
}
return skinTree(node, skinGltf, {
return skinTree(node, skins, {
mesh(_, props, key) {
return (
<mesh
@@ -44,7 +44,9 @@ export function GunFlexibilityVisualizer({
const tank = Duel.use((state) => state.protagonist.tank);
const turret = Duel.use((state) => state.protagonist.turret);
const gun = Duel.use((state) => state.protagonist.gun);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const modelRequested = Tankopedia.use(
(state) => state.loaders.model.requested,
);
const tankModel = modelDefinition.models[tank.id];
const turretModel = tankModel.turrets[turret.id];
@@ -13,18 +13,25 @@ const camouflageDefinitions = await api.camouflageDefinitions();
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
const { unwrap, strings } = useLocale();
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const modelRequested = Tankopedia.use(
(state) => state.loaders.model.requested,
);
const protagonist = Duel.use((state) => state.protagonist.tank);
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const hasRevealed = useRef(false);
const skin = Tankopedia.use((state) => state.requestedSkin);
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
const camouflage =
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
const tankName = unwrap(protagonist.name!);
const name = camouflage
? `${tankName} ${unwrap(camouflage.name!).replace("%(camo_num)", SKIN_NUMBER)}`
: tankName;
// legendary camos carry the tank's in-battle name
const name = (
camouflage
? camouflage.tank_name
? unwrap(camouflage.tank_name)
: `${tankName} ${unwrap(camouflage.name!)}`
: tankName
).replace("%(camo_num)", SKIN_NUMBER);
const fontSize = revealed
? disturbed
? "1.5rem"
@@ -102,7 +109,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) {
onPointerDown={(event) => event.stopPropagation()}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.modelRequested = true;
draft.loaders.model.requested = true;
});
}}
>
@@ -20,7 +20,9 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
const canvas = useRef<HTMLCanvasElement>(null!);
const isFullScreen = useFullScreen();
const protagonist = Duel.use((state) => state.protagonist.tank);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const modelRequested = Tankopedia.use(
(state) => state.loaders.model.requested,
);
const thicknessRange = useMemo(() => {
const entries = Object.values(tankDefinitions.tanks);
const filtered = entries.filter(
@@ -17,8 +17,10 @@ export function TankopediaSkinSwitcher() {
const tank = useDuel("protagonist", DuelModule.Tank);
const skin = Tankopedia.use((state) => state.requestedSkin);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
const requestedDisplay = Tankopedia.use(
(state) => state.loaders.display.requested,
);
const revealed = Tankopedia.use((state) => state.revealed);
const skins = uniqBy(
@@ -53,8 +55,8 @@ export function TankopediaSkinSwitcher() {
size="major"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.requestedSkin =
draft.requestedSkin === camouflage.id
draft.loaders.skin.requested =
draft.loaders.skin.requested === camouflage.id
? undefined
: camouflage.id;
});