mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-07 18:00:38 +01:00
Compare commits
4
Commits
efb479fece
...
fced1e96ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fced1e96ef | ||
|
|
778efb2372 | ||
|
|
b67fd5b58c | ||
|
|
2559782a28 |
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import { Box, Text } from "@radix-ui/themes";
|
||||
import { Html } from "@react-three/drei";
|
||||
import { memo, type ComponentProps } from "react";
|
||||
|
||||
interface Props extends ComponentProps<"spotLight"> {
|
||||
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
|
||||
return (
|
||||
<>
|
||||
{debug && (
|
||||
<mesh position={props.position}>
|
||||
<icosahedronGeometry args={[0.1, 2]} />
|
||||
<meshBasicMaterial color={props.color} />
|
||||
</mesh>
|
||||
<group position={props.position}>
|
||||
<mesh>
|
||||
<icosahedronGeometry args={[0.1, 2]} />
|
||||
<meshBasicMaterial color={props.color} />
|
||||
</mesh>
|
||||
|
||||
<Html center>
|
||||
<Box pb="9">
|
||||
<Text wrap="nowrap">
|
||||
{props.intensity} @ {JSON.stringify(props.userData)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Html>
|
||||
</group>
|
||||
)}
|
||||
|
||||
<spotLight {...props} />
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ type ThicknessesProps = MaybeSkeletonComponentProps & {
|
||||
};
|
||||
|
||||
export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const display = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const showExternalModules = TankopediaPersistent.use(
|
||||
(state) => state.showExternalModules
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
const hasCustomShell = Tankopedia.use(
|
||||
(state) => state.customShell !== undefined
|
||||
);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const display = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const isFullScreen = useFullScreen();
|
||||
const advancedHighlighting = TankopediaPersistent.use(
|
||||
(state) => state.advancedHighlighting
|
||||
@@ -388,7 +388,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
onValueChange={(value) => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.disturbed = true;
|
||||
draft.display = Number(value);
|
||||
draft.requestedDisplay = Number(value);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import { invalidate, useThree } from "@react-three/fiber";
|
||||
import { useEffect } from "react";
|
||||
import type { Fog } from "three";
|
||||
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
|
||||
export function InitialFogReveal() {
|
||||
const fog = useThree((state) => state.scene.fog as Fog | null);
|
||||
|
||||
useEffect(() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
|
||||
if (!fog) return;
|
||||
|
||||
const t0 = Date.now();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const t = (Date.now() - t0) / 1000;
|
||||
const x = t / fogAnimationTime;
|
||||
const near = forNear1 + (forNear0 - forNear1) * x;
|
||||
const far = fogFar1 + (fogFar0 - fogFar1) * x;
|
||||
|
||||
fog.near = near;
|
||||
fog.far = far;
|
||||
invalidate();
|
||||
|
||||
if (x >= 1) {
|
||||
fog.near = forNear0;
|
||||
fog.far = fogFar0;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000 / 60);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
+77
-66
@@ -1,82 +1,93 @@
|
||||
import { useHelper } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { times } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import { Color, SpotLight, SpotLightHelper } from "three";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { HemisphereLight, SpotLight, type Group } from "three";
|
||||
import { clamp } from "three/src/math/MathUtils.js";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
|
||||
|
||||
const LIGHTS_COUNT = 5;
|
||||
const LIGHT_DISTANCE = 13;
|
||||
const LIGHT_HEIGHT_0 = 4;
|
||||
const LIGHT_HEIGHT_1 = 2;
|
||||
const INTENSITY = 50;
|
||||
|
||||
const N = 128;
|
||||
const TARGET_FRAME_RATE = 30;
|
||||
const ANGLE = Math.PI / 3;
|
||||
const HEMISPHERE_INTENSITY = 4;
|
||||
const REVEAL_ANIMATION_TIME = 5;
|
||||
const TRANSITION_ANIMATION_TIME = 0.5;
|
||||
|
||||
export function Lighting() {
|
||||
const [cast, setCast] = useState(true);
|
||||
|
||||
const wrapper = useRef<Group>(null!);
|
||||
const t0 = useRef(0);
|
||||
const topLight = useRef<SpotLight>(null!);
|
||||
const frontLight = useRef<SpotLight>(null!);
|
||||
const clock = useThree((state) => state.clock);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||
|
||||
useHelper(topLight, SpotLightHelper, "red");
|
||||
useHelper(frontLight, SpotLightHelper, "green");
|
||||
useEffect(() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
|
||||
const last = useRef<number>(0);
|
||||
const samples = useRef<number[]>([]);
|
||||
t0.current = clock.elapsedTime - REVEAL_ANIMATION_TIME;
|
||||
}, []);
|
||||
|
||||
useFrame(({ clock, frameloop }) => {
|
||||
if (frameloop === "never") {
|
||||
t0.current = clock.elapsedTime;
|
||||
samples.current = [];
|
||||
return;
|
||||
}
|
||||
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
|
||||
return;
|
||||
useFrame(({ clock, invalidate }) => {
|
||||
const x = clamp(
|
||||
(clock.elapsedTime - t0.current) / animationTime.current,
|
||||
0,
|
||||
2
|
||||
);
|
||||
const t = 0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5;
|
||||
|
||||
for (const child of wrapper.current.children) {
|
||||
if (child instanceof HemisphereLight) {
|
||||
child.intensity = HEMISPHERE_INTENSITY * t;
|
||||
} else if (child instanceof SpotLight) {
|
||||
child.angle = ANGLE * t;
|
||||
}
|
||||
}
|
||||
|
||||
const dt = clock.elapsedTime - last.current;
|
||||
last.current = clock.elapsedTime;
|
||||
if (x < 2) invalidate();
|
||||
|
||||
const fps = 1 / dt;
|
||||
samples.current.push(fps);
|
||||
|
||||
if (samples.current.length > N) samples.current.shift();
|
||||
|
||||
const avg =
|
||||
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
|
||||
|
||||
if (avg < TARGET_FRAME_RATE) setCast(false);
|
||||
if (x === 2 && display !== requestedDisplay) {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.display = requestedDisplay;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{times(LIGHTS_COUNT, (index) => {
|
||||
const x = index / (LIGHTS_COUNT - 1);
|
||||
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152);
|
||||
const position = [
|
||||
LIGHT_DISTANCE * Math.sin(theta),
|
||||
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
|
||||
LIGHT_DISTANCE * Math.cos(theta),
|
||||
] as const;
|
||||
const color = new Color();
|
||||
useEffect(() => {
|
||||
if (requestedDisplay === display) return;
|
||||
|
||||
return (
|
||||
<HelpingSpotLight
|
||||
key={index}
|
||||
position={position}
|
||||
intensity={INTENSITY}
|
||||
penumbra={1}
|
||||
castShadow={cast}
|
||||
decay={1}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
t0.current = clock.elapsedTime;
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
}, [requestedDisplay]);
|
||||
|
||||
return (
|
||||
<group ref={wrapper}>
|
||||
<hemisphereLight
|
||||
position={[0, 1, 0]}
|
||||
color="#dadaff"
|
||||
groundColor="#856331"
|
||||
intensity={0}
|
||||
/>
|
||||
|
||||
<HelpingSpotLight
|
||||
debug
|
||||
position={[-6, 6, -10]}
|
||||
intensity={4}
|
||||
penumbra={1}
|
||||
castShadow
|
||||
color="#ffffff"
|
||||
decay={0}
|
||||
angle={0}
|
||||
/>
|
||||
|
||||
<HelpingSpotLight
|
||||
debug
|
||||
position={[6, 6, -10]}
|
||||
intensity={1}
|
||||
penumbra={1}
|
||||
castShadow
|
||||
color="#ffffff"
|
||||
decay={0}
|
||||
angle={0}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
+3
-2
@@ -4,7 +4,7 @@ import { Quicklime, type QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Mesh, MeshStandardMaterial } from "three";
|
||||
|
||||
const SIZE = 2 ** 5;
|
||||
const SIZE = 2 ** 6;
|
||||
|
||||
export const screenshotReadyEvent = new Quicklime(false);
|
||||
|
||||
@@ -36,6 +36,7 @@ export function SceneProps() {
|
||||
receiveShadow
|
||||
visible={!screenshotReadyEvent.last!}
|
||||
ref={mesh}
|
||||
renderOrder={-1}
|
||||
>
|
||||
<planeGeometry args={[SIZE, SIZE]} />
|
||||
<meshStandardMaterial
|
||||
@@ -43,7 +44,7 @@ export function SceneProps() {
|
||||
ref={material}
|
||||
color="black"
|
||||
roughness={1}
|
||||
metalness={1}
|
||||
metalness={0}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
|
||||
+7
-2
@@ -59,7 +59,10 @@ export function TankModel() {
|
||||
}
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
if (isTrack && Tankopedia.state.display === TankopediaDisplay.Model) {
|
||||
if (
|
||||
isTrack &&
|
||||
Tankopedia.state.requestedDisplay === TankopediaDisplay.Model
|
||||
) {
|
||||
position.set(event.clientX, event.clientY);
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -106,7 +109,9 @@ export function TankModel() {
|
||||
const isTurret = node.name.startsWith("turret_");
|
||||
const isCurrentTurret =
|
||||
node.name ===
|
||||
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`;
|
||||
`turret_${turretModelDefinition.model_id
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
const isVisible = isCurrentTurret;
|
||||
const position = new Vector2();
|
||||
const delta = new Vector2();
|
||||
|
||||
+2
-44
@@ -1,12 +1,10 @@
|
||||
import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
|
||||
import { invalidate } from "@react-three/fiber";
|
||||
import {
|
||||
forwardRef,
|
||||
Suspense,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Fog } from "three";
|
||||
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
|
||||
@@ -29,26 +27,17 @@ import { SmartCanvas } from "../../../../SmartCanvas";
|
||||
import { AutoClear } from "./components/AutoClear";
|
||||
import { Controls } from "./components/Control";
|
||||
import { InitialAligner } from "./components/InitialAligner";
|
||||
import { InitialFogReveal } from "./components/InitialFogReveal";
|
||||
import { Lighting } from "./components/Lighting";
|
||||
import { SceneProps } from "./components/SceneProps";
|
||||
import { TankModel } from "./components/TankModel";
|
||||
import { TransitionSkeleton } from "./components/TransitionSkeleton";
|
||||
|
||||
interface TankSandboxProps {
|
||||
thicknessRange: ThicknessRange;
|
||||
naked?: boolean;
|
||||
}
|
||||
|
||||
export const forNear0 = 30;
|
||||
export const fogFar0 = 50;
|
||||
export const forNear1 = 0;
|
||||
export const fogFar1 = 0;
|
||||
export const fogAnimationTime = 2.5;
|
||||
|
||||
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
({ thicknessRange, naked }, ref) => {
|
||||
const fog = useRef(new Fog("black", forNear1, fogFar1));
|
||||
const canvas = useRef<HTMLCanvasElement>(null);
|
||||
const hasImprovedVerticalStabilizer = useEquipment(122);
|
||||
const hasDownImprovedVerticalStabilizer = useEquipment(124);
|
||||
@@ -58,40 +47,11 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
const turretModelDefinition =
|
||||
tankModelDefinition.turrets[protagonistTurret.id];
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
|
||||
const rawDisplay = Tankopedia.use((state) => state.display);
|
||||
const [display, setDisplay] = useState(rawDisplay);
|
||||
const display = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const hideTankModelUnderArmor = TankopediaPersistent.use(
|
||||
(state) => state.hideTankModelUnderArmor
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (rawDisplay === display) return;
|
||||
|
||||
const t0 = Date.now();
|
||||
const interval = setInterval(() => {
|
||||
const t = (Date.now() - t0) / 1000;
|
||||
const x = t / fogAnimationTime;
|
||||
const y = Math.sin(Math.PI * x);
|
||||
|
||||
const near = forNear0 + (forNear1 - forNear0) * y;
|
||||
const far = fogFar0 + (fogFar1 - fogFar0) * y;
|
||||
|
||||
fog.current.near = near;
|
||||
fog.current.far = far;
|
||||
|
||||
invalidate();
|
||||
|
||||
if (rawDisplay !== display && x >= 0.5) setDisplay(rawDisplay);
|
||||
if (x >= 1) {
|
||||
fog.current.near = forNear0;
|
||||
fog.current.far = fogFar0;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000 / 60);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [rawDisplay]);
|
||||
|
||||
useImperativeHandle(ref, () => canvas.current!, []);
|
||||
|
||||
function handlePointerDown() {
|
||||
@@ -191,7 +151,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
return (
|
||||
<SmartCanvas
|
||||
ref={canvas}
|
||||
scene={{ fog: naked ? undefined : fog.current }}
|
||||
scene={{ fog: naked ? undefined : new Fog("black", 30, 50) }}
|
||||
gl={{
|
||||
clippingPlanes: [],
|
||||
localClippingEnabled: true,
|
||||
@@ -216,7 +176,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
{(display === TankopediaDisplay.Model ||
|
||||
(display === TankopediaDisplay.DynamicArmor &&
|
||||
!hideTankModelUnderArmor)) && <TankModel />}
|
||||
<TransitionSkeleton />
|
||||
<ShotDisplay />
|
||||
<ArmorPlateDisplay />
|
||||
<AutoClear />
|
||||
@@ -224,7 +183,6 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
|
||||
<Suspense>
|
||||
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
|
||||
<Controls naked={naked} />
|
||||
<InitialFogReveal />
|
||||
|
||||
{display === TankopediaDisplay.DynamicArmor && <Armor />}
|
||||
{display === TankopediaDisplay.StaticArmor && (
|
||||
|
||||
@@ -100,6 +100,7 @@ interface Tankopedia {
|
||||
);
|
||||
xpMultiplier: (typeof XP_MULTIPLIERS)[number];
|
||||
customShell?: ShellDefinition;
|
||||
requestedDisplay: TankopediaDisplay;
|
||||
display: TankopediaDisplay;
|
||||
statSearch?: string;
|
||||
}
|
||||
@@ -114,5 +115,6 @@ export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
model,
|
||||
xpMultiplier: 1,
|
||||
requestedDisplay: TankopediaDisplay.Model,
|
||||
display: TankopediaDisplay.Model,
|
||||
}));
|
||||
Reference in new issue
Block a user