refactor: prepare main feature gameplay object and use GLB sky model
This commit is contained in:
@@ -14,7 +14,7 @@ This document describes the code that exists today in the repository.
|
||||
- either the map scene or the debug physics test scene
|
||||
- the player rig when the active camera mode is `player`
|
||||
- `src/world/GameMap.tsx` loads map nodes from `public/map.json`, resolves available models, and builds the collision octree.
|
||||
- `src/world/debug/TestScene.tsx` provides a debug-oriented interaction and physics scene.
|
||||
- `src/world/debug/TestMap.tsx` provides a debug-oriented interaction and physics map.
|
||||
- `src/world/player/PlayerComponent.tsx` mounts the camera and controller.
|
||||
- `src/world/player/PlayerController.tsx` owns pointer lock movement, jump handling, and interaction input.
|
||||
|
||||
@@ -64,7 +64,7 @@ This document describes the code that exists today in the repository.
|
||||
## Current Limitations
|
||||
|
||||
- The repository is a prototype, not the full intended game runtime.
|
||||
- `src/world/debug/TestScene.tsx` is part of the active scene composition.
|
||||
- `src/world/debug/TestMap.tsx` is part of the active scene composition.
|
||||
- There is no central gameplay orchestrator such as `GameManager`.
|
||||
- Missions, zones, cinematics, and dialogue systems are not implemented.
|
||||
- The player uses octree collision and simple movement rules, not a complete gameplay physics stack.
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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