update: add runtine camera keyframe

This commit is contained in:
Tom Boullay
2026-05-11 11:13:49 +02:00
parent 241e4140e7
commit d74de82cae
8 changed files with 273 additions and 5 deletions
+119
View File
@@ -0,0 +1,119 @@
import { useEffect, useRef, useState } from "react";
import type { MutableRefObject } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import gsap from "gsap";
import * as THREE from "three";
import { useGameStore } from "@/managers/stores/useGameStore";
import type {
CinematicDefinition,
CinematicManifest,
} from "@/types/cinematics/cinematics";
import { logger } from "@/utils/core/logger";
import { loadCinematicManifest } from "@/utils/cinematics/loadCinematicManifest";
export function GameCinematics(): null {
const camera = useThree((state) => state.camera);
const [manifest, setManifest] = useState<CinematicManifest | null>(null);
const playedCinematicsRef = useRef(new Set<string>());
const timelineRef = useRef<gsap.core.Timeline | null>(null);
useEffect(() => {
let mounted = true;
void loadCinematicManifest()
.then((loadedManifest) => {
if (mounted) setManifest(loadedManifest);
})
.catch((error: unknown) => {
logger.error("GameCinematics", "Failed to load cinematic manifest", {
error: error instanceof Error ? error : String(error),
});
});
return () => {
mounted = false;
stopActiveCinematic(timelineRef);
useGameStore.getState().setCinematicPlaying(false);
};
}, []);
useFrame(({ clock }) => {
if (!manifest) return;
const elapsedTime = clock.getElapsedTime();
manifest.cinematics.forEach((cinematic) => {
if (cinematic.timecode === undefined) return;
if (cinematic.timecode > elapsedTime) return;
if (playedCinematicsRef.current.has(cinematic.id)) return;
playedCinematicsRef.current.add(cinematic.id);
playCinematic(camera, cinematic, timelineRef);
});
});
return null;
}
function stopActiveCinematic(
timelineRef: MutableRefObject<gsap.core.Timeline | null>,
): void {
timelineRef.current?.kill();
timelineRef.current = null;
}
function playCinematic(
camera: THREE.Camera,
cinematic: CinematicDefinition,
timelineRef: MutableRefObject<gsap.core.Timeline | null>,
): void {
const firstKeyframe = cinematic.cameraKeyframes[0];
if (!firstKeyframe) return;
document.exitPointerLock();
timelineRef.current?.kill();
useGameStore.getState().setCinematicPlaying(true);
const target = new THREE.Vector3(...firstKeyframe.target);
camera.position.set(...firstKeyframe.position);
camera.lookAt(target);
const timeline = gsap.timeline({
onUpdate: () => camera.lookAt(target),
onComplete: () => {
timelineRef.current = null;
useGameStore.getState().setCinematicPlaying(false);
},
});
cinematic.cameraKeyframes.slice(1).forEach((keyframe, index) => {
const previousKeyframe = cinematic.cameraKeyframes[index];
if (!previousKeyframe) return;
const duration = keyframe.time - previousKeyframe.time;
timeline.to(
camera.position,
{
x: keyframe.position[0],
y: keyframe.position[1],
z: keyframe.position[2],
duration,
ease: "power2.inOut",
},
previousKeyframe.time,
);
timeline.to(
target,
{
x: keyframe.target[0],
y: keyframe.target[1],
z: keyframe.target[2],
duration,
ease: "power2.inOut",
},
previousKeyframe.time,
);
});
timelineRef.current = timeline;
}
+2
View File
@@ -10,6 +10,7 @@ import { DebugCameraControls } from "@/components/debug/scene/DebugCameraControl
import { DebugHelpers } from "@/components/debug/scene/DebugHelpers";
import { HandTrackingGlove } from "@/components/three/handTracking/HandTrackingGlove";
import { Environment } from "@/world/Environment";
import { GameCinematics } from "@/world/GameCinematics";
import { GameDialogues } from "@/world/GameDialogues";
import { GameMusic } from "@/world/GameMusic";
import { Lighting } from "@/world/Lighting";
@@ -43,6 +44,7 @@ export function World(): React.JSX.Element {
{sceneMode === "game" ? (
<>
<GameMusic />
<GameCinematics />
<GameDialogues />
<GameMap onOctreeReady={setOctree} />
<GameStageContent />
+13 -5
View File
@@ -24,6 +24,7 @@ import {
PLAYER_XZ_DAMPING_FACTOR,
} from "@/data/player/playerConfig";
import { InteractionManager } from "@/managers/InteractionManager";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useSettingsStore } from "@/managers/stores/useSettingsStore";
import type { Vector3Tuple } from "@/types/three/three";
@@ -55,6 +56,13 @@ const _up = new THREE.Vector3(0, 1, 0);
const _translateVec = new THREE.Vector3();
const _collisionCorrection = new THREE.Vector3();
function isPlayerInputLocked(): boolean {
return (
useSettingsStore.getState().isSettingsMenuOpen ||
useGameStore.getState().isCinematicPlaying
);
}
function setMovementKey(keys: Keys, key: string, pressed: boolean): boolean {
switch (key.toLowerCase()) {
case MOVE_FORWARD_KEY:
@@ -109,7 +117,7 @@ export function PlayerController({
const interaction = InteractionManager.getInstance();
const handleKeyDown = (event: KeyboardEvent): void => {
if (useSettingsStore.getState().isSettingsMenuOpen) return;
if (isPlayerInputLocked()) return;
if (setMovementKey(keys.current, event.key, true)) {
event.preventDefault();
@@ -131,7 +139,7 @@ export function PlayerController({
};
const handleKeyUp = (event: KeyboardEvent): void => {
if (useSettingsStore.getState().isSettingsMenuOpen) return;
if (isPlayerInputLocked()) return;
if (setMovementKey(keys.current, event.key, false)) {
event.preventDefault();
@@ -139,7 +147,7 @@ export function PlayerController({
};
const handleMouseDown = (event: MouseEvent): void => {
if (useSettingsStore.getState().isSettingsMenuOpen) return;
if (isPlayerInputLocked()) return;
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
if (interaction.getState().focused?.kind === "grab") {
interaction.pressInteract();
@@ -147,7 +155,7 @@ export function PlayerController({
};
const handleMouseUp = (event: MouseEvent): void => {
if (useSettingsStore.getState().isSettingsMenuOpen) return;
if (isPlayerInputLocked()) return;
if (event.button !== PRIMARY_INTERACT_MOUSE_BUTTON) return;
if (interaction.getState().holding) {
interaction.releaseInteract();
@@ -169,7 +177,7 @@ export function PlayerController({
}, []);
useFrame((_, delta) => {
if (useSettingsStore.getState().isSettingsMenuOpen) {
if (isPlayerInputLocked()) {
keys.current = { ...DEFAULT_KEYS };
velocity.current.set(0, 0, 0);
wantsJump.current = false;