refactor: prepare main feature gameplay object and use GLB sky model
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { TriggerObject } from "@/components/three/TriggerObject";
|
||||
import { RepairCaseModel } from "@/components/three/RepairCaseModel";
|
||||
import { AudioManager } from "@/managers/AudioManager";
|
||||
import type { Vector3Tuple } from "@/types/three";
|
||||
|
||||
interface MainFeatureObjectProps {
|
||||
position: Vector3Tuple;
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
const CASE_MODEL_PATH = "/models/packderelance/model.gltf";
|
||||
const CASE_SOUND_PATH = "/sounds/effect/fa.mp3";
|
||||
const CASE_OPEN_SOUND_RATE = 1.08;
|
||||
const CASE_CLOSE_SOUND_RATE = 0.82;
|
||||
|
||||
export function MainFeatureObject({
|
||||
position,
|
||||
open,
|
||||
onToggle,
|
||||
}: MainFeatureObjectProps): React.JSX.Element {
|
||||
return (
|
||||
<TriggerObject
|
||||
position={position}
|
||||
colliders="cuboid"
|
||||
label={open ? "Fermer la mallette" : "Ouvrir la mallette"}
|
||||
onTrigger={() => {
|
||||
AudioManager.getInstance().playSound(CASE_SOUND_PATH, 1, {
|
||||
playbackRate: open ? CASE_CLOSE_SOUND_RATE : CASE_OPEN_SOUND_RATE,
|
||||
});
|
||||
onToggle();
|
||||
}}
|
||||
>
|
||||
<RepairCaseModel
|
||||
modelPath={CASE_MODEL_PATH}
|
||||
open={open}
|
||||
position={[0, -0.45, 0]}
|
||||
scale={1.5}
|
||||
/>
|
||||
</TriggerObject>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { Text } from "@react-three/drei";
|
||||
import { TriggerObject } from "@/components/three/TriggerObject";
|
||||
import { MainFeatureObject } from "@/components/three/MainFeatureObject";
|
||||
import { ModelSelectorPlaceholder } from "@/components/three/ModelSelectorPlaceholder";
|
||||
import { RepairCaseModel } from "@/components/three/RepairCaseModel";
|
||||
|
||||
const ZONE_ORIGIN = [10, 0.4, -8] as const;
|
||||
const CASE_MODEL_PATH = "/models/packderelance/model.gltf";
|
||||
const ZONE_RADIUS = 4.2;
|
||||
|
||||
export function MainFeatureZone(): React.JSX.Element {
|
||||
@@ -44,19 +42,11 @@ export function MainFeatureZone(): React.JSX.Element {
|
||||
Pack de Relance Feature
|
||||
</Text>
|
||||
|
||||
<TriggerObject
|
||||
<MainFeatureObject
|
||||
position={[ZONE_ORIGIN[0], ZONE_ORIGIN[1], ZONE_ORIGIN[2]]}
|
||||
colliders="cuboid"
|
||||
label={caseOpen ? "Fermer la mallette" : "Ouvrir la mallette"}
|
||||
onTrigger={() => setCaseOpen((value) => !value)}
|
||||
>
|
||||
<RepairCaseModel
|
||||
modelPath={CASE_MODEL_PATH}
|
||||
open={caseOpen}
|
||||
position={[0, -0.45, 0]}
|
||||
scale={0.35}
|
||||
/>
|
||||
</TriggerObject>
|
||||
open={caseOpen}
|
||||
onToggle={() => setCaseOpen((value) => !value)}
|
||||
/>
|
||||
|
||||
<ModelSelectorPlaceholder
|
||||
label="Module A"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import gsap from "gsap";
|
||||
import * as THREE from "three";
|
||||
import type { Vector3Tuple } from "@/types/three";
|
||||
|
||||
@@ -13,8 +13,9 @@ interface RepairCaseModelProps {
|
||||
}
|
||||
|
||||
const CASE_LID_NODE_NAME = "partiesup";
|
||||
const CASE_OPEN_ANGLE = THREE.MathUtils.degToRad(115);
|
||||
const CASE_OPEN_SPEED = 7;
|
||||
const CASE_OPEN_ROTATION_OFFSET_Z = 0;
|
||||
const CASE_CLOSED_ROTATION_OFFSET_Z = THREE.MathUtils.degToRad(-115);
|
||||
const CASE_ANIMATION_DURATION = 1.2;
|
||||
|
||||
export function RepairCaseModel({
|
||||
modelPath,
|
||||
@@ -26,29 +27,39 @@ export function RepairCaseModel({
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
const lidRef = useRef<THREE.Object3D | null>(null);
|
||||
const closedRotationX = useRef(0);
|
||||
const initialOpen = useRef(open);
|
||||
const openedRotationZ = useRef(0);
|
||||
const parsedScale =
|
||||
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
||||
|
||||
useEffect(() => {
|
||||
const lid = model.getObjectByName(CASE_LID_NODE_NAME);
|
||||
lidRef.current = lid ?? null;
|
||||
closedRotationX.current = lid?.rotation.x ?? 0;
|
||||
openedRotationZ.current = lid?.rotation.z ?? 0;
|
||||
|
||||
if (lid && !initialOpen.current) {
|
||||
lid.rotation.z = openedRotationZ.current + CASE_CLOSED_ROTATION_OFFSET_Z;
|
||||
}
|
||||
}, [model]);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
useEffect(() => {
|
||||
const lid = lidRef.current;
|
||||
if (!lid) return;
|
||||
|
||||
const targetRotation =
|
||||
closedRotationX.current - (open ? CASE_OPEN_ANGLE : 0);
|
||||
lid.rotation.x = THREE.MathUtils.damp(
|
||||
lid.rotation.x,
|
||||
targetRotation,
|
||||
CASE_OPEN_SPEED,
|
||||
delta,
|
||||
);
|
||||
});
|
||||
openedRotationZ.current +
|
||||
(open ? CASE_OPEN_ROTATION_OFFSET_Z : CASE_CLOSED_ROTATION_OFFSET_Z);
|
||||
gsap.to(lid.rotation, {
|
||||
z: targetRotation,
|
||||
duration: CASE_ANIMATION_DURATION,
|
||||
ease: "power2.inOut",
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
gsap.killTweensOf(lid.rotation);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<group position={position} rotation={rotation} scale={parsedScale}>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { useGLTF } from "@react-three/drei";
|
||||
import { useMemo, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
|
||||
interface SkyModelProps {
|
||||
modelPath: string;
|
||||
}
|
||||
|
||||
const SKY_MODEL_SCALE = 1;
|
||||
|
||||
export function SkyModel({ modelPath }: SkyModelProps): React.JSX.Element {
|
||||
const camera = useThree((state) => state.camera);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const { scene } = useGLTF(modelPath);
|
||||
const model = useMemo(() => scene.clone(true), [scene]);
|
||||
|
||||
useFrame(() => {
|
||||
groupRef.current?.position.copy(camera.position);
|
||||
});
|
||||
|
||||
return (
|
||||
<group ref={groupRef} scale={SKY_MODEL_SCALE} frustumCulled={false}>
|
||||
<primitive object={model} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload("/models/sky/model.glb");
|
||||
@@ -6,6 +6,7 @@ export type { SimpleModelConfig } from "./SimpleModel";
|
||||
|
||||
export { ExplodableModel } from "./ExplodableModel";
|
||||
export { MainFeatureZone } from "./MainFeatureZone";
|
||||
export { MainFeatureObject } from "./MainFeatureObject";
|
||||
export { ModelSelectorPlaceholder } from "./ModelSelectorPlaceholder";
|
||||
export { RepairCaseModel } from "./RepairCaseModel";
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
HandTrackingContext,
|
||||
} from "@/hooks/useHandTrackingSnapshot";
|
||||
import { useRemoteHandTracking } from "@/hooks/useRemoteHandTracking";
|
||||
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
||||
|
||||
export function HandTrackingProvider({
|
||||
children,
|
||||
@@ -15,10 +14,7 @@ export function HandTrackingProvider({
|
||||
}): React.JSX.Element {
|
||||
const sceneMode = useSceneMode();
|
||||
const { nearby, holding, handHolding } = useInteraction();
|
||||
const enabled =
|
||||
isDebugEnabled() &&
|
||||
sceneMode === "physics" &&
|
||||
(nearby || holding || handHolding);
|
||||
const enabled = sceneMode === "physics" && (nearby || holding || handHolding);
|
||||
const snapshot = useRemoteHandTracking({ enabled });
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,7 +13,7 @@ export const TEST_SCENE_GRABBABLE_ROUGHNESS = 0.6;
|
||||
export const TEST_SCENE_GRABBABLE_METALNESS = 0.1;
|
||||
|
||||
export const TEST_SCENE_TRIGGER_POSITION: Vector3Tuple = [3, 2, -3];
|
||||
export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/fa.mp3";
|
||||
export const TEST_SCENE_TRIGGER_SOUND_PATH = "/sounds/effect/fa.mp3";
|
||||
export const TEST_SCENE_TRIGGER_RADIUS = 0.4;
|
||||
export const TEST_SCENE_TRIGGER_SEGMENTS = 32;
|
||||
export const TEST_SCENE_TRIGGER_COLOR = "#3b82f6";
|
||||
|
||||
@@ -99,7 +99,7 @@ Ce document décrit le code réellement présent aujourd'hui dans le dépôt.
|
||||
- soit la carte principale, soit la scène de test physique debug
|
||||
- le rig joueur quand le mode caméra actif est \`player\`
|
||||
- \`src/world/GameMap.tsx\` charge les modèles de carte disponibles et construit l'octree de collision.
|
||||
- \`src/world/debug/TestScene.tsx\` fournit une scène orientée debug pour les interactions et la physique.
|
||||
- \`src/world/debug/TestMap.tsx\` fournit une carte orientée debug pour les interactions et la physique.
|
||||
- \`src/world/player/Player.tsx\` monte la caméra et le contrôleur.
|
||||
- \`src/world/player/PlayerController.tsx\` gère le mouvement pointer lock, le saut et les inputs d'interaction.
|
||||
|
||||
@@ -129,7 +129,7 @@ Ce document décrit le code réellement présent aujourd'hui dans le dépôt.
|
||||
## Limites actuelles
|
||||
|
||||
- Le dépôt est encore un prototype, pas le runtime complet du jeu.
|
||||
- \`src/world/debug/TestScene.tsx\` fait encore partie de la composition active.
|
||||
- \`src/world/debug/TestMap.tsx\` fait encore partie de la composition active.
|
||||
- Il n'existe pas encore d'orchestrateur gameplay central comme \`GameManager\`.
|
||||
- Les systèmes de missions, zones, cinématiques et dialogues ne sont pas implémentés.
|
||||
- Le joueur utilise une collision octree et des règles simples, pas une pile physique gameplay complète.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const GAME_SCENE_SKYBOX_PATH = "/skybox/sky.exr";
|
||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
interface PlaySoundOptions {
|
||||
playbackRate?: number;
|
||||
}
|
||||
|
||||
export class AudioManager {
|
||||
private static _instance: AudioManager | null = null;
|
||||
private readonly _audioPools = new Map<string, HTMLAudioElement[]>();
|
||||
@@ -20,9 +24,10 @@ export class AudioManager {
|
||||
|
||||
private constructor() {}
|
||||
|
||||
playSound(path: string, volume = 1): void {
|
||||
playSound(path: string, volume = 1, options: PlaySoundOptions = {}): void {
|
||||
const audio = this._acquireAudio(path);
|
||||
audio.volume = Math.max(0, Math.min(1, volume));
|
||||
audio.playbackRate = options.playbackRate ?? 1;
|
||||
audio.currentTime = 0;
|
||||
|
||||
void audio.play().catch((error: unknown) => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Environment as DreiEnvironment } from "@react-three/drei";
|
||||
import {
|
||||
GAME_SCENE_SKYBOX_PATH,
|
||||
GAME_SCENE_SKY_MODEL_PATH,
|
||||
PHYSICS_SCENE_BACKGROUND_COLOR,
|
||||
} from "@/data/world/environmentConfig";
|
||||
import { useSceneMode } from "@/hooks/debug/useSceneMode";
|
||||
import { SkyModel } from "@/components/three/SkyModel";
|
||||
|
||||
export function Environment(): React.JSX.Element {
|
||||
const sceneMode = useSceneMode();
|
||||
@@ -14,5 +14,5 @@ export function Environment(): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
return <DreiEnvironment background files={GAME_SCENE_SKYBOX_PATH} />;
|
||||
return <SkyModel modelPath={GAME_SCENE_SKY_MODEL_PATH} />;
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import { Environment } from "@/world/Environment";
|
||||
import { Lighting } from "@/world/Lighting";
|
||||
import { GameMap } from "@/world/GameMap";
|
||||
import { Player } from "@/world/player/Player";
|
||||
import { TestScene } from "@/world/debug/TestScene";
|
||||
import { TestMap } from "@/world/debug/TestMap";
|
||||
|
||||
export function World(): React.JSX.Element {
|
||||
const cameraMode = useCameraMode();
|
||||
@@ -33,7 +33,7 @@ export function World(): React.JSX.Element {
|
||||
{sceneMode === "game" ? (
|
||||
<GameMap onOctreeReady={setOctree} />
|
||||
) : (
|
||||
<TestScene onOctreeReady={setOctree} />
|
||||
<TestMap onOctreeReady={setOctree} />
|
||||
)}
|
||||
|
||||
{cameraMode !== "debug" ? (
|
||||
|
||||
@@ -24,13 +24,11 @@ import {
|
||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||
import type { OctreeReadyHandler } from "@/types/three";
|
||||
|
||||
interface TestSceneProps {
|
||||
interface TestMapProps {
|
||||
onOctreeReady: OctreeReadyHandler;
|
||||
}
|
||||
|
||||
export function TestScene({
|
||||
onOctreeReady,
|
||||
}: TestSceneProps): React.JSX.Element {
|
||||
export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
||||
const floorRef = useRef<THREE.Group>(null);
|
||||
|
||||
useOctreeGraphNode(floorRef, onOctreeReady);
|
||||
Reference in New Issue
Block a user