diff --git a/src/components/three/gameplay/RepairGame.tsx b/src/components/three/gameplay/RepairGame.tsx index b6b2827..81529ae 100644 --- a/src/components/three/gameplay/RepairGame.tsx +++ b/src/components/three/gameplay/RepairGame.tsx @@ -13,6 +13,7 @@ import { REPAIR_FRAGMENTATION_SEQUENCE_SECONDS } from "@/data/gameplay/repairGam import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions"; import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput"; import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep"; +import { useTerrainSnappedPosition } from "@/hooks/three/useTerrainHeight"; import type { MissionStep, RepairMissionConfig, @@ -66,6 +67,7 @@ export function RepairGame({ readonly RepairScannedBrokenPart[] >([]); const parsedScale = toVector3Scale(scale); + const snappedPosition = useTerrainSnappedPosition(position); const readyForFragmentation = step === "inspected"; useRepairFragmentationInput({ @@ -105,7 +107,7 @@ export function RepairGame({ if (step === "locked") return null; return ( - + @@ -113,7 +115,7 @@ export function RepairGame({ {step === "waiting" ? ( setMissionStep(mission, "inspected")} /> ) : null} diff --git a/src/components/ui/debug/GameStateDebugPanel.tsx b/src/components/ui/debug/GameStateDebugPanel.tsx index f7bcf43..3b87077 100644 --- a/src/components/ui/debug/GameStateDebugPanel.tsx +++ b/src/components/ui/debug/GameStateDebugPanel.tsx @@ -18,15 +18,15 @@ function toPascalCase(value: string): string { export function GameStateDebugPanel(): React.JSX.Element { const mainState = useGameStore((state) => state.mainState); - const bikeStep = useGameStore((state) => state.bike.currentStep); + const ebikeStep = useGameStore((state) => state.ebike.currentStep); const pyloneStep = useGameStore((state) => state.pylone.currentStep); const fermeStep = useGameStore((state) => state.ferme.currentStep); const detail = useGameStore((state) => { switch (state.mainState) { case "intro": return state.intro.currentStep; - case "bike": - return state.bike.currentStep; + case "ebike": + return state.ebike.currentStep; case "pylone": return state.pylone.currentStep; case "ferme": @@ -37,7 +37,7 @@ export function GameStateDebugPanel(): React.JSX.Element { }); const setMainState = useGameStore((state) => state.setMainState); const setIntroStep = useGameStore((state) => state.setIntroStep); - const setBikeState = useGameStore((state) => state.setBikeState); + const setEbikeState = useGameStore((state) => state.setEbikeState); const setPyloneState = useGameStore((state) => state.setPyloneState); const setFermeState = useGameStore((state) => state.setFermeState); const setOutroState = useGameStore((state) => state.setOutroState); @@ -67,8 +67,8 @@ export function GameStateDebugPanel(): React.JSX.Element { if (!isMissionStep(nextSubState)) return; - if (mainState === "bike") { - setBikeState({ currentStep: nextSubState }); + if (mainState === "ebike") { + setEbikeState({ currentStep: nextSubState }); return; } @@ -86,8 +86,8 @@ export function GameStateDebugPanel(): React.JSX.Element { function setDebugMainState(nextMainState: MainGameState): void { setMainState(nextMainState); - if (nextMainState === "bike" && bikeStep === "locked") { - setBikeState({ currentStep: "waiting" }); + if (nextMainState === "ebike" && ebikeStep === "locked") { + setEbikeState({ currentStep: "waiting" }); return; } diff --git a/src/data/debug/testSceneConfig.ts b/src/data/debug/testSceneConfig.ts index 3edd84b..4fb9c80 100644 --- a/src/data/debug/testSceneConfig.ts +++ b/src/data/debug/testSceneConfig.ts @@ -25,8 +25,8 @@ export const TEST_SCENE_REPAIR_ZONE_MARKER_TUBE_RADIUS = 0.045; export const TEST_SCENE_REPAIR_ZONES = [ { - mission: "bike", - label: "Bike", + mission: "ebike", + label: "E-bike", color: "#38bdf8", position: [-12, 0, -12], }, @@ -43,7 +43,7 @@ export const TEST_SCENE_REPAIR_ZONES = [ position: [12, 0, -12], }, ] as const satisfies readonly { - mission: "bike" | "pylone" | "ferme"; + mission: "ebike" | "pylone" | "ferme"; label: string; color: string; position: Vector3Tuple; diff --git a/src/data/gameplay/repairMissionAnchors.ts b/src/data/gameplay/repairMissionAnchors.ts index 695025f..7edbe92 100644 --- a/src/data/gameplay/repairMissionAnchors.ts +++ b/src/data/gameplay/repairMissionAnchors.ts @@ -1,18 +1,18 @@ import type { Vector3Tuple } from "@/types/three/three"; import type { RepairMissionId } from "@/types/gameplay/repairMission"; -export const BIKE_REPAIR_POSITION = [ +export const EBIKE_REPAIR_POSITION = [ 42.2399, 4.5484, 34.6468, ] as const satisfies Vector3Tuple; const REPAIR_MISSION_POSITIONS = { - bike: BIKE_REPAIR_POSITION, + ebike: EBIKE_REPAIR_POSITION, pylone: [64, 0, -66], ferme: [-24, 0, 42], } as const satisfies Record; export const REPAIR_MISSION_POSITION_ENTRIES = [ - { mission: "bike", position: REPAIR_MISSION_POSITIONS.bike }, + { mission: "ebike", position: REPAIR_MISSION_POSITIONS.ebike }, { mission: "pylone", position: REPAIR_MISSION_POSITIONS.pylone }, { mission: "ferme", position: REPAIR_MISSION_POSITIONS.ferme }, ] as const satisfies readonly { diff --git a/src/data/gameplay/repairMissions.ts b/src/data/gameplay/repairMissions.ts index 3e864ee..fcf49d9 100644 --- a/src/data/gameplay/repairMissions.ts +++ b/src/data/gameplay/repairMissions.ts @@ -14,8 +14,8 @@ const DEFAULT_REPAIR_CASE = { } satisfies RepairMissionCaseConfig; export const REPAIR_MISSIONS: Record = { - bike: { - id: "bike", + ebike: { + id: "ebike", label: "E-bike", description: "Repair the damaged cooling module before relaunching the bike", @@ -25,10 +25,10 @@ export const REPAIR_MISSIONS: Record = { interactUiPath: REPAIR_INTERACT_UI_PATH, brokenUiPath: REPAIR_BROKEN_UI_PATH, case: DEFAULT_REPAIR_CASE, - requiredReplacementPartId: "bike-cooling-core-replacement", + requiredReplacementPartId: "ebike-cooling-core-replacement", brokenParts: [ { - id: "bike-cooling-core", + id: "ebike-cooling-core", label: "Cooling core", modelPath: "/models/refroidisseur/model.gltf", nodeName: "refroidisseur", @@ -37,17 +37,17 @@ export const REPAIR_MISSIONS: Record = { ], replacementParts: [ { - id: "bike-cooling-core-replacement", + id: "ebike-cooling-core-replacement", label: "Replacement cooling core", modelPath: "/models/refroidisseur/model.gltf", }, { - id: "bike-radio-distractor", + id: "ebike-radio-distractor", label: "Radio module", modelPath: "/models/talkie/model.gltf", }, { - id: "bike-glove-distractor", + id: "ebike-glove-distractor", label: "Insulation glove", modelPath: "/models/gant_l/model.gltf", }, diff --git a/src/data/world/terrainConfig.ts b/src/data/world/terrainConfig.ts index bdce845..9cde64a 100644 --- a/src/data/world/terrainConfig.ts +++ b/src/data/world/terrainConfig.ts @@ -1,9 +1,15 @@ -import type { TerrainSurfaceColorConfig } from "@/types/world/terrainSurface"; +import type { + TerrainSurfaceColorConfig, + TerrainSurfaceProjectionConfig, +} from "@/types/world/terrainSurface"; export const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf"; export const TERRAIN_WATER_HEIGHT = 0.8; -const TERRAIN_TILE_SIZE = 1; +export const TERRAIN_TILE_SIZE = 1; +export const TERRAIN_SURFACE_COLOR_TOLERANCE = 5; +export const TERRAIN_SURFACE_PROJECTION = + {} satisfies TerrainSurfaceProjectionConfig; export const TERRAIN_COLORS = { grass1: { @@ -54,3 +60,5 @@ export const TERRAIN_COLORS = { kind: "rock", }, } satisfies Record; + +export type TerrainColorKey = keyof typeof TERRAIN_COLORS; diff --git a/src/hooks/gameplay/useRepairMovementLocked.ts b/src/hooks/gameplay/useRepairMovementLocked.ts index 11953ce..2d19c11 100644 --- a/src/hooks/gameplay/useRepairMovementLocked.ts +++ b/src/hooks/gameplay/useRepairMovementLocked.ts @@ -4,8 +4,8 @@ import type { MissionStep } from "@/types/gameplay/repairMission"; export function useRepairMovementLocked(): boolean { return useGameStore((state) => { switch (state.mainState) { - case "bike": - return isRepairMovementLocked(state.bike.currentStep); + case "ebike": + return isRepairMovementLocked(state.ebike.currentStep); case "pylone": return isRepairMovementLocked(state.pylone.currentStep); case "ferme": diff --git a/src/managers/stores/useGameStore.ts b/src/managers/stores/useGameStore.ts index 9e89f26..2849702 100644 --- a/src/managers/stores/useGameStore.ts +++ b/src/managers/stores/useGameStore.ts @@ -26,7 +26,7 @@ interface IntroState { currentStep: GameStep; dialogueAudio: string | null; hasCompleted: boolean; - isBikeUnlocked: boolean; + isEbikeUnlocked: boolean; } interface MissionState { @@ -46,7 +46,7 @@ export interface GameState { isCinematicPlaying: boolean; missionFlow: MissionFlowState; intro: IntroState; - bike: MissionState & { + ebike: MissionState & { isRepaired: boolean; }; pylone: MissionState & { @@ -70,13 +70,13 @@ interface GameActions { setIntroStep: (step: GameStep) => void; setIntroState: (intro: Partial) => void; setPlayerName: (playerName: string) => void; - setBikeState: (bike: Partial) => void; + setEbikeState: (ebike: Partial) => void; setPyloneState: (pylone: Partial) => void; setFermeState: (ferme: Partial) => void; setOutroState: (outro: Partial) => void; setMissionStep: (mission: RepairMissionId, step: MissionStep) => void; completeIntro: () => void; - completeBike: () => void; + completeEbike: () => void; completePylone: () => void; completeFerme: () => void; completeMission: (mission: RepairMissionId) => void; @@ -104,24 +104,24 @@ function isBoolean(value: unknown): value is boolean { function completeIntroState(state: GameState): GameStateUpdate { return { - mainState: "bike", + mainState: "ebike", intro: { ...state.intro, hasCompleted: true, - isBikeUnlocked: true, + isEbikeUnlocked: true, }, - bike: { - ...state.bike, + ebike: { + ...state.ebike, currentStep: "locked", }, }; } -function completeBikeState(state: GameState): GameStateUpdate { +function completeEbikeState(state: GameState): GameStateUpdate { return { mainState: "pylone", - bike: { - ...state.bike, + ebike: { + ...state.ebike, currentStep: "done", isRepaired: true, }, @@ -180,8 +180,8 @@ function completeMissionState( mission: RepairMissionId, ): GameStateUpdate { switch (mission) { - case "bike": - return completeBikeState(state); + case "ebike": + return completeEbikeState(state); case "pylone": return completePyloneState(state); case "ferme": @@ -236,9 +236,9 @@ function createInitialGameState(): GameState { currentStep: "intro", dialogueAudio: null, hasCompleted: false, - isBikeUnlocked: false, + isEbikeUnlocked: false, }, - bike: { + ebike: { currentStep: "locked", dialogueAudio: null, isRepaired: false, @@ -273,9 +273,9 @@ function hydrateIntroState(initial: IntroState, value: unknown): IntroState { hasCompleted: isBoolean(value.hasCompleted) ? value.hasCompleted : initial.hasCompleted, - isBikeUnlocked: isBoolean(value.isBikeUnlocked) - ? value.isBikeUnlocked - : initial.isBikeUnlocked, + isEbikeUnlocked: isBoolean(value.isEbikeUnlocked) + ? value.isEbikeUnlocked + : initial.isEbikeUnlocked, }; } @@ -320,7 +320,7 @@ function hydrateMissionFlowState( function hydrateDebugGameState(initial: GameState, value: unknown): GameState { if (!isRecord(value)) return initial; - const bike = hydrateMissionState(initial.bike, value.bike); + const ebike = hydrateMissionState(initial.ebike, value.ebike); const pylone = hydrateMissionState(initial.pylone, value.pylone); const ferme = hydrateMissionState(initial.ferme, value.ferme); const outro = isRecord(value.outro) ? value.outro : null; @@ -337,12 +337,12 @@ function hydrateDebugGameState(initial: GameState, value: unknown): GameState { value.missionFlow, ), intro: hydrateIntroState(initial.intro, value.intro), - bike: { - ...bike, + ebike: { + ...ebike, isRepaired: - isRecord(value.bike) && isBoolean(value.bike.isRepaired) - ? value.bike.isRepaired - : initial.bike.isRepaired, + isRecord(value.ebike) && isBoolean(value.ebike.isRepaired) + ? value.ebike.isRepaired + : initial.ebike.isRepaired, }, pylone: { ...pylone, @@ -384,7 +384,7 @@ function pickGameState(state: GameStore): GameState { isCinematicPlaying: state.isCinematicPlaying, missionFlow: state.missionFlow, intro: state.intro, - bike: state.bike, + ebike: state.ebike, pylone: state.pylone, ferme: state.ferme, outro: state.outro, @@ -415,8 +415,8 @@ export const useGameStore = create()((set) => ({ set((state) => ({ missionFlow: { ...state.missionFlow, playerName }, })), - setBikeState: (bike) => - set((state) => ({ bike: { ...state.bike, ...bike } })), + setEbikeState: (ebike) => + set((state) => ({ ebike: { ...state.ebike, ...ebike } })), setPyloneState: (pylone) => set((state) => ({ pylone: { ...state.pylone, ...pylone } })), setFermeState: (ferme) => @@ -426,7 +426,7 @@ export const useGameStore = create()((set) => ({ setMissionStep: (mission, step) => set((state) => setMissionStepState(state, mission, step)), completeIntro: () => set(completeIntroState), - completeBike: () => set((state) => completeMissionState(state, "bike")), + completeEbike: () => set((state) => completeMissionState(state, "ebike")), completePylone: () => set((state) => completeMissionState(state, "pylone")), completeFerme: () => set((state) => completeMissionState(state, "ferme")), completeMission: (mission) => diff --git a/src/providers/gameplay/HandTrackingProvider.tsx b/src/providers/gameplay/HandTrackingProvider.tsx index a5993f9..4fd56f9 100644 --- a/src/providers/gameplay/HandTrackingProvider.tsx +++ b/src/providers/gameplay/HandTrackingProvider.tsx @@ -26,8 +26,8 @@ export function HandTrackingProvider({ const sceneMode = useSceneMode(); const repairNeedsHands = useGameStore((state) => { switch (state.mainState) { - case "bike": - return REPAIR_HAND_TRACKING_STEPS.has(state.bike.currentStep); + case "ebike": + return REPAIR_HAND_TRACKING_STEPS.has(state.ebike.currentStep); case "pylone": return REPAIR_HAND_TRACKING_STEPS.has(state.pylone.currentStep); case "ferme": diff --git a/src/types/game.ts b/src/types/game.ts index dd2a56c..5813d01 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -27,11 +27,11 @@ export const GAME_STEPS: readonly GameStep[] = [ const GAME_STEP_VALUES: ReadonlySet = new Set(GAME_STEPS); -export type MainGameState = "intro" | "bike" | "pylone" | "ferme" | "outro"; +export type MainGameState = "intro" | "ebike" | "pylone" | "ferme" | "outro"; export const MAIN_GAME_STATES: readonly MainGameState[] = [ "intro", - "bike", + "ebike", "pylone", "ferme", "outro", diff --git a/src/types/gameplay/repairMission.ts b/src/types/gameplay/repairMission.ts index e82b5d0..2f8b0ad 100644 --- a/src/types/gameplay/repairMission.ts +++ b/src/types/gameplay/repairMission.ts @@ -4,7 +4,7 @@ import type { Vector3Tuple, } from "@/types/three/three"; -export type RepairMissionId = "bike" | "pylone" | "ferme"; +export type RepairMissionId = "ebike" | "pylone" | "ferme"; export interface RepairMissionCaseConfig { position: Vector3Tuple; @@ -54,7 +54,7 @@ export type MissionStep = | "reassembling" | "done"; -const REPAIR_MISSION_IDS = ["bike", "pylone", "ferme"] as const; +const REPAIR_MISSION_IDS = ["ebike", "pylone", "ferme"] as const; const REPAIR_MISSION_ID_VALUES: ReadonlySet = new Set( REPAIR_MISSION_IDS, ); diff --git a/src/types/world/terrainSurface.ts b/src/types/world/terrainSurface.ts index aa6a812..1984c77 100644 --- a/src/types/world/terrainSurface.ts +++ b/src/types/world/terrainSurface.ts @@ -1,4 +1,6 @@ -type TerrainSurfaceKind = +import type * as THREE from "three"; + +export type TerrainSurfaceKind = | "grass" | "path" | "water" @@ -6,7 +8,19 @@ type TerrainSurfaceKind = | "dirt" | "rock"; -type TerrainSurfaceRgb = readonly [number, number, number]; +export type TerrainSurfaceRgb = readonly [number, number, number]; + +export interface TerrainSurfaceUv { + u: number; + v: number; +} + +export interface TerrainSurfaceProjectionConfig { + flipX?: boolean; + flipZ?: boolean; + offsetX?: number; + offsetZ?: number; +} export interface TerrainSurfaceBounds { minX: number; @@ -23,3 +37,15 @@ export interface TerrainSurfaceColorConfig { modelPath?: string; tileSize?: number; } + +export interface TerrainSurfaceSample { + rgb: TerrainSurfaceRgb; + key: string | null; + config: TerrainSurfaceColorConfig | null; +} + +export interface TerrainSurfaceData { + bounds: TerrainSurfaceBounds; + imageData: ImageData; + raycastTarget: THREE.Object3D; +} diff --git a/src/world/GameMap.tsx b/src/world/GameMap.tsx index 2af7b0a..132a102 100644 --- a/src/world/GameMap.tsx +++ b/src/world/GameMap.tsx @@ -301,9 +301,9 @@ function MapNodeInstance({ }): React.JSX.Element | null { const isGeneratedModel = isGeneratedMapModelName(node.name); const mainState = useGameStore((state) => state.mainState); - const bikeStep = useGameStore((state) => state.bike.currentStep); + const ebikeStep = useGameStore((state) => state.ebike.currentStep); const hideEbikeMapModel = - node.name === "ebike" && mainState === "bike" && bikeStep !== "locked"; + node.name === "ebike" && mainState === "ebike" && ebikeStep !== "locked"; useEffect(() => { if (modelUrl !== null || isGeneratedModel) return; diff --git a/src/world/GameStageContent.tsx b/src/world/GameStageContent.tsx index 1a5ab4a..e1f36c7 100644 --- a/src/world/GameStageContent.tsx +++ b/src/world/GameStageContent.tsx @@ -1,7 +1,7 @@ import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { RepairGame } from "@/components/three/gameplay/RepairGame"; import { - BIKE_REPAIR_POSITION, + EBIKE_REPAIR_POSITION, REPAIR_MISSION_POSITION_ENTRIES, } from "@/data/gameplay/repairMissionAnchors"; import { useGameStore } from "@/managers/stores/useGameStore"; @@ -34,19 +34,19 @@ function StageAnchor({ function EbikeMissionTrigger(): React.JSX.Element | null { const mainState = useGameStore((state) => state.mainState); - const bikeStep = useGameStore((state) => state.bike.currentStep); + const ebikeStep = useGameStore((state) => state.ebike.currentStep); const setMissionStep = useGameStore((state) => state.setMissionStep); - if (mainState !== "bike" || bikeStep !== "locked") return null; + if (mainState !== "ebike" || ebikeStep !== "locked") return null; return ( - + setMissionStep("bike", "waiting")} + onPress={() => setMissionStep("ebike", "waiting")} >