From 41f7b2ad19de0d73db3dcb8489a5342aff8b9bf7 Mon Sep 17 00:00:00 2001 From: math-pixel <59537610+math-pixel@users.noreply.github.com> Date: Mon, 11 May 2026 10:28:39 +0200 Subject: [PATCH] update step --- src/components/zone/ZoneDetection.tsx | 151 ++++++++++++++++++++++++++ src/data/zones.ts | 12 ++ src/hooks/useGameStep.ts | 12 ++ src/stateManager/GameStepManager.ts | 47 ++++++++ src/types/game.ts | 20 ++++ src/world/World.tsx | 6 + 6 files changed, 248 insertions(+) create mode 100644 src/components/zone/ZoneDetection.tsx create mode 100644 src/data/zones.ts create mode 100644 src/hooks/useGameStep.ts create mode 100644 src/stateManager/GameStepManager.ts create mode 100644 src/types/game.ts diff --git a/src/components/zone/ZoneDetection.tsx b/src/components/zone/ZoneDetection.tsx new file mode 100644 index 0000000..9a53d29 --- /dev/null +++ b/src/components/zone/ZoneDetection.tsx @@ -0,0 +1,151 @@ +import { useEffect, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import { ZONES } from "@/data/zones"; +import { GameStepManager } from "@/stateManager/GameStepManager"; +import { Debug } from "@/utils/debug/Debug"; + +const _playerPos = new THREE.Vector3(); +const _zonePos = new THREE.Vector3(); + +export function ZoneDetection(): null { + const camera = useThree((state) => state.camera); + const manager = GameStepManager.getInstance(); + const triggeredZones = useRef>(new Set()); + const debug = Debug.getInstance(); + + useEffect(() => { + if (!debug.active) return; + + const folder = debug.createFolder("Game"); + if (!folder) return; + + const gameState = { step: manager.getStep() }; + const playerPos = { x: 0, y: 0, z: 0 }; + + folder + .add(gameState, "step", ["intro", "outOfFabrik"]) + .name("Game Step") + .disable(); + + folder.add(playerPos, "x").name("Player X").listen().disable(); + folder.add(playerPos, "y").name("Player Y").listen().disable(); + folder.add(playerPos, "z").name("Player Z").listen().disable(); + + const unsubManager = manager.subscribe(() => { + gameState.step = manager.getStep(); + folder.controllersRecursive().forEach((c) => c.updateDisplay()); + }); + + let frameId: number; + const updatePlayerPos = (): void => { + camera.getWorldPosition(_playerPos); + playerPos.x = Math.round(_playerPos.x * 100) / 100; + playerPos.y = Math.round(_playerPos.y * 100) / 100; + playerPos.z = Math.round(_playerPos.z * 100) / 100; + folder.controllersRecursive().forEach((c) => c.updateDisplay()); + frameId = requestAnimationFrame(updatePlayerPos); + }; + updatePlayerPos(); + + return () => { + cancelAnimationFrame(frameId); + debug.destroyFolder("Game"); + unsubManager(); + }; + }, [debug, manager, camera]); + + useFrame(() => { + camera.getWorldPosition(_playerPos); + + for (const zone of ZONES) { + if (triggeredZones.current.has(zone.id)) continue; + + _zonePos.set(...zone.position); + + const distanceSq = _playerPos.distanceToSquared(_zonePos); + + if (distanceSq <= zone.radius * zone.radius) { + manager.transitionTo(zone.targetStep); + triggeredZones.current.add(zone.id); + break; + } + } + }); + + return null; +} + +interface ZoneVisualProps { + position: [number, number, number]; + radius: number; + height: number; + triggered: boolean; +} + +function ZoneVisual({ + position, + radius, + height, + triggered, +}: ZoneVisualProps): React.JSX.Element { + const color = triggered ? "#00ff00" : "#ff0000"; + + return ( + + + + + + + + + + + ); +} + +export function ZoneDebugVisuals(): React.JSX.Element | null { + const debug = Debug.getInstance(); + const camera = useThree((state) => state.camera); + const [triggeredZones, setTriggeredZones] = useState>(new Set()); + + useFrame(() => { + camera.getWorldPosition(_playerPos); + + for (const zone of ZONES) { + if (triggeredZones.has(zone.id)) continue; + + _zonePos.set(...zone.position); + + const distanceSq = _playerPos.distanceToSquared(_zonePos); + + if (distanceSq <= zone.radius * zone.radius) { + setTriggeredZones((prev) => new Set(prev).add(zone.id)); + break; + } + } + }); + + if (!debug.active) return null; + + return ( + <> + {ZONES.map((zone) => ( + + ))} + + ); +} diff --git a/src/data/zones.ts b/src/data/zones.ts new file mode 100644 index 0000000..06b254a --- /dev/null +++ b/src/data/zones.ts @@ -0,0 +1,12 @@ +import type { Zone } from "@/types/game"; +import type { Vector3Tuple } from "@/types/3d"; + +export const ZONES: Zone[] = [ + { + id: "fabrikExit", + position: [-5, 25, -15] as Vector3Tuple, + radius: 10, + height: 20, + targetStep: "outOfFabrik", + }, +]; diff --git a/src/hooks/useGameStep.ts b/src/hooks/useGameStep.ts new file mode 100644 index 0000000..03d07ef --- /dev/null +++ b/src/hooks/useGameStep.ts @@ -0,0 +1,12 @@ +import { useSyncExternalStore } from "react"; +import { GameStepManager } from "@/stateManager/GameStepManager"; +import type { GameStepSnapshot } from "@/types/game"; + +const manager = GameStepManager.getInstance(); + +export function useGameStep(): GameStepSnapshot { + return useSyncExternalStore(manager.subscribe.bind(manager), () => ({ + step: manager.getStep(), + transitionTo: manager.transitionTo.bind(manager), + })); +} diff --git a/src/stateManager/GameStepManager.ts b/src/stateManager/GameStepManager.ts new file mode 100644 index 0000000..aae5527 --- /dev/null +++ b/src/stateManager/GameStepManager.ts @@ -0,0 +1,47 @@ +import type { GameStep } from "@/types/game"; + +export class GameStepManager { + private static _instance: GameStepManager | null = null; + + private _currentStep: GameStep = "intro"; + private readonly _listeners = new Set<() => void>(); + + static getInstance(): GameStepManager { + if (!GameStepManager._instance) { + GameStepManager._instance = new GameStepManager(); + } + + return GameStepManager._instance; + } + + private constructor() {} + + getStep(): GameStep { + return this._currentStep; + } + + transitionTo(step: GameStep): void { + if (this._currentStep === step) return; + + this._currentStep = step; + this._emit(); + } + + subscribe(listener: () => void): () => void { + this._listeners.add(listener); + + return () => { + this._listeners.delete(listener); + }; + } + + destroy(): void { + this._currentStep = "intro"; + this._listeners.clear(); + GameStepManager._instance = null; + } + + private _emit(): void { + this._listeners.forEach((cb) => cb()); + } +} diff --git a/src/types/game.ts b/src/types/game.ts new file mode 100644 index 0000000..5f79fe5 --- /dev/null +++ b/src/types/game.ts @@ -0,0 +1,20 @@ +import type { Vector3Tuple } from "@/types/3d"; + +export type GameStep = "intro" | "outOfFabrik"; + +export interface Zone { + id: string; + position: Vector3Tuple; + radius: number; + height: number; + targetStep: GameStep; +} + +export interface GameState { + step: GameStep; +} + +export interface GameStepSnapshot { + step: GameStep; + transitionTo: (step: GameStep) => void; +} diff --git a/src/world/World.tsx b/src/world/World.tsx index af7c39f..ab24ce6 100644 --- a/src/world/World.tsx +++ b/src/world/World.tsx @@ -6,6 +6,10 @@ import { } from "@/data/playerConfig"; import { useCameraMode } from "@/hooks/debug/useCameraMode"; import { useSceneMode } from "@/hooks/debug/useSceneMode"; +import { + ZoneDebugVisuals, + ZoneDetection, +} from "@/components/zone/ZoneDetection"; import { DebugCameraControls } from "@/utils/debug/scene/DebugCameraControls"; import { DebugHelpers } from "@/utils/debug/scene/DebugHelpers"; import { Environment } from "@/world/Environment"; @@ -28,6 +32,8 @@ export function World(): React.JSX.Element { + + {cameraMode === "debug" ? : null} {sceneMode === "game" ? (