diff --git a/src/components/CharacterView.tsx b/src/components/CharacterView.tsx index 7dfa982..db0cb31 100644 --- a/src/components/CharacterView.tsx +++ b/src/components/CharacterView.tsx @@ -3,11 +3,11 @@ import type { Character } from "../types"; export const CharacterView = observer(function ({ character }: { character: Character }) { - const pos = character.position; + const pos = character.transform.position; return diff --git a/src/components/GameView.tsx b/src/components/GameView.tsx index e62f6c0..9657d51 100644 --- a/src/components/GameView.tsx +++ b/src/components/GameView.tsx @@ -1,10 +1,46 @@ import { observer } from "mobx-react-lite"; import { SceneView } from "./SceneView"; import { state } from "../state"; -import { useFrame } from "@react-three/fiber"; +import { useFrame, useThree } from "@react-three/fiber"; +import { PointerLockControls, useKeyboardControls } from "@react-three/drei"; +import { useEffect, useRef } from "react"; + +function PlayerMovement() { + const [, get] = useKeyboardControls(); + const dirty = useRef(false); + + useFrame(({ camera }, dt) => { + const { forward, backward, left, right } = get(); + const speed = 5 * dt; + if (forward) { camera.translateZ(-speed); dirty.current = true; } + if (backward) { camera.translateZ( speed); dirty.current = true; } + if (left) { camera.translateX(-speed); dirty.current = true; } + if (right) { camera.translateX( speed); dirty.current = true; } + + if (!dirty.current) return; + dirty.current = false; + + const [rx, ry, rz] = camera.rotation.toArray(); + state.game?.setCharacterTransform({ + position: camera.position.toArray(), + look: [rx, ry, rz], + }); + }); + + return { dirty.current = true; }} />; +} export const GameView = observer(function () { const game = state.game; + const { camera } = useThree(); + + useEffect(() => { + if (!game) + return; + const { position, look } = game.camera; + camera.position.set(position[0], position[1], position[2]); + camera.rotation.set(look[0], look[1], look[2]); + }, []); useFrame((_, delta) => { state.game?.tick(delta); @@ -13,5 +49,8 @@ export const GameView = observer(function () { if (!game) return null; - return ; + return (<> + + + ); }); diff --git a/src/components/ThreeView.tsx b/src/components/ThreeView.tsx index 72f140d..f0001df 100644 --- a/src/components/ThreeView.tsx +++ b/src/components/ThreeView.tsx @@ -1,5 +1,5 @@ import { Canvas, useFrame, useThree } from '@react-three/fiber'; -import { Stats } from '@react-three/drei'; +import { KeyboardControls, Stats } from '@react-three/drei'; import { useRef } from 'react'; function RenderInfoUpdater({ domRef }: { domRef: React.RefObject }) { @@ -30,6 +30,12 @@ export const ThreeView = observer(function () { const isGame = state.isGamePlaying; const infoRef = useRef(null); return ( +
+
) }); diff --git a/src/model/gameFactory.ts b/src/model/gameFactory.ts new file mode 100644 index 0000000..9acac42 --- /dev/null +++ b/src/model/gameFactory.ts @@ -0,0 +1,24 @@ +import type { Game, World } from "../types"; +import { clone } from "../utils"; + +export class GameFactory { + + public static create(world: World): Game { + return { + paused: false, + time: 0, + scene: clone(world.initialScene), + } + } + + public static load(): Game | undefined { + const json = localStorage.getItem("game"); + if (json) { + return JSON.parse(json) as Game; + } + } + + public static save(game: Game): void { + localStorage.setItem("game", JSON.stringify(game)); + } +} diff --git a/src/model/worldFactory.ts b/src/model/worldFactory.ts index 715430f..5f54e83 100644 --- a/src/model/worldFactory.ts +++ b/src/model/worldFactory.ts @@ -9,8 +9,10 @@ export class WorldFactory { voxelTypes: DEFAULT_VOXEL_TYPES, initialScene: { character: { - position: [0, 0, 0], - look: [0, 0, 0], + transform: { + position: [0, 0, 0], + look: [0, 0, 0], + }, }, objects: {}, }, diff --git a/src/state/gameState.ts b/src/state/gameState.ts index ed4cc0d..bf56f62 100644 --- a/src/state/gameState.ts +++ b/src/state/gameState.ts @@ -1,24 +1,32 @@ -import { makeAutoObservable, toJS } from "mobx"; +import { makeAutoObservable, reaction, toJS } from "mobx"; import type { WorldState } from "./worldState"; import type { Game, Scene } from "../types"; -import type { Pos3 } from "../types/3d"; +import type { Pos3, R3, V3 } from "../types/3d"; import type { CameraProps } from "@react-three/fiber"; -import { clone } from "../utils"; -import { state } from "./rootState"; +import { GameFactory } from "../model/gameFactory"; export class GameState { private readonly world: WorldState; - public readonly data: Game; + public data: Game; + + private startAutoSave() { + return reaction(() => toJS(this.data), (data) => this.saveData(data), { delay: 5000 }); + } + + private withoutAutoSave(fn: () => void) { + this._stopAutoSave(); + fn(); + this._stopAutoSave = this.startAutoSave(); + } + + private _stopAutoSave: () => void = () => { }; constructor(world: WorldState) { this.world = world; - this.data = { - paused: false, - time: 0, - scene: clone(toJS(this.world.data.initialScene)), - }; + this.data = GameFactory.create(toJS(this.world.data)); + this._stopAutoSave = this.startAutoSave(); makeAutoObservable(this); } @@ -31,7 +39,7 @@ export class GameState { } public get camera(): Pos3 { - return this.scene.character; + return this.scene.character.transform; } public get cameraAsThree(): CameraProps { @@ -43,6 +51,25 @@ export class GameState { } } + // public load() { + // console.log('Loading game...'); + // this.withoutAutoSave(() => { + // this.data = GameFactory.load() ?? GameFactory.create(this.world.data); + // }); + // } + + private saveData(data: Game): void { + console.log('Saving game...'); + const stack = new Error('Saving game...').stack!.split('\n').slice(1); + const { ...debug } = toJS(data); + console.dir({ stack, debug }); + GameFactory.save(toJS(this.data)); + } + + public save(): void { + this.saveData(this.data); + } + public resume(): void { this.data.paused = false; } @@ -51,6 +78,13 @@ export class GameState { this.data.paused = true; } + public setCharacterTransform(transform: Pos3): void { + if (this.isPaused) + return; + + this.scene.character.transform = transform; + } + public tick(deltaTime: number): void { if (this.isPaused) return; diff --git a/src/state/worldState.ts b/src/state/worldState.ts index 703d3b8..25bcf94 100644 --- a/src/state/worldState.ts +++ b/src/state/worldState.ts @@ -5,7 +5,6 @@ import type { VoxelType } from "../types/voxel"; import { state } from "./rootState"; import { DEFAULT_VOXEL_TYPES } from "../model/defaultVoxelTypes"; import { wolf } from "../model/objectPrefabs/wolf"; -import { clone } from "../utils"; export class WorldState { public data: World = WorldFactory.create(); @@ -66,8 +65,10 @@ export class WorldState { }, initialScene: { character: { - position: [0, 0, 0], - look: [0, 0, 0], + transform: { + position: [0, 0, 0], + look: [0, 0, 0], + } }, objects: objectMap, }, diff --git a/src/types/character.ts b/src/types/character.ts index 000f940..ba7422a 100644 --- a/src/types/character.ts +++ b/src/types/character.ts @@ -1,4 +1,5 @@ import type { Pos3 } from "./3d"; -export type Character = Pos3 & { +export type Character = { + transform: Pos3, }