Compare commits

..

No commits in common. "e56f92a4ac32d96a84c5528f08448c55d6efbc99" and "e520eb07c5a050029567f83bc91582809d9a962d" have entirely different histories.

25 changed files with 218 additions and 402 deletions

View File

@ -32,7 +32,6 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"sass": "^1.100.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"

View File

@ -56,7 +56,7 @@ importers:
version: 19.2.3(@types/react@19.2.15)
'@vitejs/plugin-react':
specifier: ^6.0.1
version: 6.0.2(vite@8.0.14(@types/node@24.12.4)(sass@1.100.0))
version: 6.0.2(vite@8.0.14(@types/node@24.12.4))
eslint:
specifier: ^10.3.0
version: 10.4.1
@ -69,9 +69,6 @@ importers:
globals:
specifier: ^17.6.0
version: 17.6.0
sass:
specifier: ^1.100.0
version: 1.100.0
typescript:
specifier: ~6.0.2
version: 6.0.3
@ -80,7 +77,7 @@ importers:
version: 8.60.0(eslint@10.4.1)(typescript@6.0.3)
vite:
specifier: ^8.0.12
version: 8.0.14(@types/node@24.12.4)(sass@1.100.0)
version: 8.0.14(@types/node@24.12.4)
packages:
@ -259,94 +256,6 @@ packages:
'@oxc-project/types@0.132.0':
resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==}
'@parcel/watcher-android-arm64@2.5.6':
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [android]
'@parcel/watcher-darwin-arm64@2.5.6':
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.6':
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.6':
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [freebsd]
'@parcel/watcher-linux-arm-glibc@2.5.6':
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [win32]
'@parcel/watcher-win32-ia32@2.5.6':
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
engines: {node: '>= 10.0.0'}
cpu: [ia32]
os: [win32]
'@parcel/watcher-win32-x64@2.5.6':
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.6':
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'}
'@react-three/drei@10.7.7':
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
peerDependencies:
@ -656,10 +565,6 @@ packages:
caniuse-lite@1.0.30001793:
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@ -843,9 +748,6 @@ packages:
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@5.1.6:
resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
@ -1031,9 +933,6 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-releases@2.0.46:
resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
engines: {node: '>=18'}
@ -1172,10 +1071,6 @@ packages:
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
engines: {node: '>=0.10.0'}
readdirp@5.0.0:
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
engines: {node: '>= 20.19.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
@ -1185,11 +1080,6 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
sass@1.100.0:
resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==}
engines: {node: '>=20.19.0'}
hasBin: true
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@ -1625,67 +1515,6 @@ snapshots:
'@oxc-project/types@0.132.0': {}
'@parcel/watcher-android-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-arm64@2.5.6':
optional: true
'@parcel/watcher-darwin-x64@2.5.6':
optional: true
'@parcel/watcher-freebsd-x64@2.5.6':
optional: true
'@parcel/watcher-linux-arm-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm-musl@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-arm64-musl@2.5.6':
optional: true
'@parcel/watcher-linux-x64-glibc@2.5.6':
optional: true
'@parcel/watcher-linux-x64-musl@2.5.6':
optional: true
'@parcel/watcher-win32-arm64@2.5.6':
optional: true
'@parcel/watcher-win32-ia32@2.5.6':
optional: true
'@parcel/watcher-win32-x64@2.5.6':
optional: true
'@parcel/watcher@2.5.6':
dependencies:
detect-libc: 2.1.2
is-glob: 4.0.3
node-addon-api: 7.1.1
picomatch: 4.0.4
optionalDependencies:
'@parcel/watcher-android-arm64': 2.5.6
'@parcel/watcher-darwin-arm64': 2.5.6
'@parcel/watcher-darwin-x64': 2.5.6
'@parcel/watcher-freebsd-x64': 2.5.6
'@parcel/watcher-linux-arm-glibc': 2.5.6
'@parcel/watcher-linux-arm-musl': 2.5.6
'@parcel/watcher-linux-arm64-glibc': 2.5.6
'@parcel/watcher-linux-arm64-musl': 2.5.6
'@parcel/watcher-linux-x64-glibc': 2.5.6
'@parcel/watcher-linux-x64-musl': 2.5.6
'@parcel/watcher-win32-arm64': 2.5.6
'@parcel/watcher-win32-ia32': 2.5.6
'@parcel/watcher-win32-x64': 2.5.6
optional: true
'@react-three/drei@10.7.7(@react-three/fiber@9.6.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(three@0.184.0))(@types/react@19.2.15)(@types/three@0.184.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(three@0.184.0)':
dependencies:
'@babel/runtime': 7.29.7
@ -1934,10 +1763,10 @@ snapshots:
'@use-gesture/core': 10.3.1
react: 19.2.6
'@vitejs/plugin-react@6.0.2(vite@8.0.14(@types/node@24.12.4)(sass@1.100.0))':
'@vitejs/plugin-react@6.0.2(vite@8.0.14(@types/node@24.12.4))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.0.14(@types/node@24.12.4)(sass@1.100.0)
vite: 8.0.14(@types/node@24.12.4)
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
@ -1985,10 +1814,6 @@ snapshots:
caniuse-lite@1.0.30001793: {}
chokidar@5.0.0:
dependencies:
readdirp: 5.0.0
convert-source-map@2.0.0: {}
cross-env@7.0.3:
@ -2161,8 +1986,6 @@ snapshots:
immediate@3.0.6: {}
immutable@5.1.6: {}
imurmurhash@0.1.4: {}
install@0.13.0: {}
@ -2297,9 +2120,6 @@ snapshots:
natural-compare@1.4.0: {}
node-addon-api@7.1.1:
optional: true
node-releases@2.0.46: {}
npm@11.16.0: {}
@ -2359,8 +2179,6 @@ snapshots:
react@19.2.6: {}
readdirp@5.0.0: {}
require-from-string@2.0.2: {}
rolldown@1.0.2:
@ -2384,14 +2202,6 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.0.2
'@rolldown/binding-win32-x64-msvc': 1.0.2
sass@1.100.0:
dependencies:
chokidar: 5.0.0
immutable: 5.1.6
source-map-js: 1.2.1
optionalDependencies:
'@parcel/watcher': 2.5.6
scheduler@0.27.0: {}
semver@6.3.1: {}
@ -2504,7 +2314,7 @@ snapshots:
uuid@14.0.0: {}
vite@8.0.14(@types/node@24.12.4)(sass@1.100.0):
vite@8.0.14(@types/node@24.12.4):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@ -2514,7 +2324,6 @@ snapshots:
optionalDependencies:
'@types/node': 24.12.4
fsevents: 2.3.3
sass: 1.100.0
webgl-constants@1.1.1: {}

View File

@ -1,2 +0,0 @@
allowBuilds:
'@parcel/watcher': set this to true or false

View File

@ -1,12 +1,12 @@
import { ThreeView } from './components/ThreeView';
import './App.scss';
import './App.css';
import { Toolbar } from './components/Toolbar';
export const App = function () {
return (
<>
<ThreeView />
<Toolbar />
<ThreeView />
</>
)
}

View File

@ -1,7 +1,8 @@
import { observer } from "mobx-react-lite";
import type { Character, Scene } from "../types";
import { state } from "../state";
export const CharacterView = observer(function ({ character }: { character: Character }) {
export const CharacterView = observer(function () {
const character = state.world.currentScene.character;
return <mesh position={character.position} rotation={character.look}>
<boxGeometry args={[0.8, 0.8, 0.8]} />

View File

@ -1,17 +0,0 @@
import { observer } from "mobx-react-lite";
import { SceneView } from "./SceneView";
import { state } from "../state";
import { useFrame } from "@react-three/fiber";
export const GameView = observer(function () {
const game = state.game;
useFrame((_, delta) => {
state.game?.tick(delta);
});
if (!game)
return null;
return <SceneView scene={game.scene} />;
});

View File

@ -5,7 +5,7 @@ import type { Group } from "three";
import { Edges, TransformControls } from "@react-three/drei";
import type { ThreeEvent } from "@react-three/fiber";
import { state } from "../state";
import { nextSelectionEditMode } from "../state/worldEditorState";
import { nextSelectionEditMode } from "../state/worldEditor";
import type { R3 } from "../types/3d";
type ObjectViewProps = {

View File

@ -1,47 +0,0 @@
import { useLayoutEffect, useRef } from 'react';
import type React from 'react';
import { useThree } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import { observer } from 'mobx-react-lite';
import { state } from '../state';
import { SceneView } from './SceneView';
import type { Pos3 } from '../types/3d';
const CameraSync = observer(function ({ camera }: { camera: Pos3 }) {
const { camera: threeCamera } = useThree();
useLayoutEffect(() => {
threeCamera.position.set(camera.position[0], camera.position[1], camera.position[2]);
threeCamera.rotation.set(camera.look[0], camera.look[1], camera.look[2]);
threeCamera.updateProjectionMatrix();
});
return null;
});
export const SceneEditorView = observer(function () {
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
const handleEnd = () => {
const controls = controlsRef.current;
if (!controls)
return;
const [x, y, z] = controls.object.rotation.toArray();
state.worldEditor.setCamera({
position: controls.object.position.toArray(),
look: [x, y, z],
});
};
return (<>
<OrbitControls
ref={controlsRef}
enabled={!state.worldEditor.isDragging}
onEnd={handleEnd}
makeDefault
enableDamping={false}
/>
<CameraSync camera={state.worldEditor.camera} />
<SceneView scene={state.worldEditor.scene} />
</>);
});

View File

@ -13,6 +13,6 @@ export const SceneView = observer(function ({ scene }: SceneViewProps) {
<directionalLight position={[5, 5, 5]} intensity={1} />
{Object.values(scene.objects).map((obj) =>
<ObjectView key={obj.id} object={obj} />)}
<CharacterView character={scene.character} />
<CharacterView />
</>);
});

View File

@ -1,37 +1,17 @@
import { Canvas } from '@react-three/fiber';
import { observer } from 'mobx-react-lite';
import { state } from '../state';
import { GameView } from './GameView';
import { SceneEditorView } from './SceneEditorView';
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
const IconPause = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="4" height="10" fill="currentColor" /><rect x="8" y="2" width="4" height="10" fill="currentColor" /></svg >;
const IconPlay = () => <svg viewBox="0 0 14 14"><polygon points="3,1 13,7 3,13" fill="currentColor" /></svg>;
import { WorldView } from './WorldView';
export const ThreeView = observer(function () {
const isGame = state.isGamePlaying;
return (
<div style={{ position: 'relative', width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
<div style={{ width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
<Canvas
// camera={state.world.character.camera}
camera={state.world.character.camera}
onPointerMissed={() => state.worldEditor.resetSelectedObject()}
>
{isGame ? <GameView /> : <SceneEditorView />}
<WorldView />
</Canvas>
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
{
state.game
? <>
<button onClick={() => state.game!.stop()}><IconStop /></button>
{
state.game!.isPaused
? <button onClick={() => state.game!.resume()}><IconPlay /></button>
: <button onClick={() => state.game!.pause()}><IconPause /></button>
}
</>
: <button onClick={() => state.world.play()}><IconPlay /></button>
}
</div>
</div>
)
});

View File

@ -14,6 +14,18 @@ export const Toolbar = observer(function () {
}
return <div className="toolbar">
{state.worldEditor.isEnabled && <div>EDITOR MODE</div>}
{state.world.isPlaying
? <>
<button onClick={() => state.world.stop()}>Stop</button>
{
(state.world.data.state as RunningGameState).paused
? <button onClick={() => state.world.play()}>Play</button>
: <button onClick={() => state.world.pause()}>Pause</button>
}
</>
: <button onClick={() => state.world.play()}>Play</button>
}
<button onClick={handleLoadMockWorld}>Load mock world</button>
<button onClick={handleCloneTest1Object}>Clone test1</button>
</div>

View File

@ -0,0 +1,52 @@
import { useLayoutEffect, useRef } from 'react';
import type React from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import { observer } from 'mobx-react-lite';
import { state } from '../state';
import { SceneView } from './SceneView';
const CameraSync = observer(function () {
const { camera } = useThree();
const cam = state.world.currentCamera; // MobX tracks this; re-renders on change
useLayoutEffect(() => {
camera.position.set(cam.position[0], cam.position[1], cam.position[2]);
camera.rotation.set(cam.look[0], cam.look[1], cam.look[2]);
camera.updateProjectionMatrix();
});
return null;
});
export const WorldView = observer(function () {
const world = state.world;
const controlsRef = useRef<React.ComponentRef<typeof OrbitControls>>(null);
useFrame((_, delta) => {
world.tick(delta);
});
const handleEnd = () => {
const controls = controlsRef.current;
if (!controls || world.isPlaying)
return;
const [x, y, z] = controls.object.rotation.toArray();
state.worldEditor.setCamera({
position: controls.object.position.toArray(),
look: [x, y, z],
});
};
return (<>
<OrbitControls
ref={controlsRef}
enabled={!world.isPlaying && !state.worldEditor.isDragging}
onEnd={handleEnd}
makeDefault
enableDamping={false}
/>
<CameraSync />
<SceneView scene={state.world.currentScene} />
</>)
});

View File

@ -15,8 +15,7 @@
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 16px/100% var(--sans);
line-height: 1;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
@ -25,6 +24,10 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
@ -63,17 +66,46 @@ body {
margin: 0;
}
svg {
fill: currentColor;
display: inline-block;
height: 1em;
top: 0.125em;
vertical-align: bottom;
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
button,
input,
select,
textarea {
font: inherit;
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}

View File

@ -1,7 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App.tsx'
import './index.scss'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>

25
src/state/character.ts Normal file
View File

@ -0,0 +1,25 @@
import { makeAutoObservable } from "mobx";
import type { WorldState } from "./world";
import type { CameraProps } from "@react-three/fiber";
export class CharacterState {
private readonly world: WorldState;
constructor(world: WorldState) {
this.world = world;
makeAutoObservable(
this,
{
},
);
}
public get camera(): CameraProps {
const cam = this.world.currentCamera;
return {
position: cam.position,
fov: 50,
rotation: cam.look,
}
}
}

View File

@ -1,61 +0,0 @@
import { makeAutoObservable } from "mobx";
import type { WorldState } from "./worldState";
import type { RunningGameState, Scene } from "../types";
import { clone } from "../utils";
import { state } from "./rootState";
import type { Pos3 } from "../types/3d";
import type { CameraProps } from "@react-three/fiber";
export class GameState {
private readonly world: WorldState;
constructor(world: WorldState) {
this.world = world;
makeAutoObservable(this);
}
public get state(): RunningGameState {
return this.world.data.state as RunningGameState;
}
public get isPaused(): boolean {
return this.state.paused;
}
public get scene(): Scene {
return this.state.scene;
}
public get camera(): Pos3 {
return this.scene.character;
}
public get cameraAsThree(): CameraProps {
const cam = this.camera;
return {
position: cam.position,
fov: 50,
rotation: cam.look,
}
}
public resume(): void {
const state = clone(this.world.data.state) as RunningGameState;
state.paused = false;
this.world.data.state = state;
}
public pause(): void {
const state = clone(this.world.data.state) as RunningGameState;
state.paused = true;
this.world.data.state = state;
}
public stop(): void {
this.world.data.state = { playing: false };
}
public tick(deltaTime: number): void {
//TODO
}
}

View File

@ -1 +1 @@
export * from './rootState';
export * from './root';

View File

@ -1,7 +1,6 @@
import { makeAutoObservable } from "mobx";
import { WorldState } from "./worldState";
import { WorldEditorState } from "./worldEditorState";
import { GameState } from "./gameState";
import { WorldState } from "./world";
import { WorldEditorState } from "./worldEditor";
export class RootState {
public readonly world = new WorldState();
@ -17,15 +16,6 @@ export class RootState {
},
);
}
public get isGamePlaying(): boolean {
return this.world.data.state.playing;
}
public get game(): GameState | undefined {
if (this.isGamePlaying)
return new GameState(this.world);
}
}
export const state = new RootState();

View File

@ -1,15 +1,19 @@
import { makeAutoObservable } from "mobx";
import { WorldFactory } from "../model/worldFactory";
import type { ObjectType, World } from "../types";
import type { ObjectType, RunningGameState, Scene, World } from "../types";
import { CharacterState } from "./character";
import type { Pos3 } from "../types/3d";
import { clone } from "../utils";
import type { VoxelType } from "../types/voxel";
import { state } from "./rootState";
import { state } from "./root";
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();
public character = new CharacterState(this);
constructor() {
makeAutoObservable(this);
}
@ -71,17 +75,35 @@ export class WorldState {
WorldFactory.save(this.data);
}
public getObjectTypeById(id: string): ObjectType | undefined {
return this.data.objectTypes[id];
public get isPlaying(): boolean {
return this.data.state.playing;
}
public getVoxelTypeById(id: string): VoxelType | undefined {
return this.data.voxelTypes[id];
public get currentScene(): Scene {
return this.isPlaying
? (this.data.state as RunningGameState).scene
: this.data.initialScene;
}
public get currentCamera(): Pos3 {
return this.isPlaying
? (this.data.state as RunningGameState).scene.character
: this.data.editorCamera;
}
public tick(_delta: number) {
if (!this.isPlaying)
return;
//TODO
}
public play(): void {
if (state.game)
state.game.resume();
if (this.isPlaying) {
const state = clone(this.data.state) as RunningGameState;
state.paused = false;
this.data.state = state;
}
else {
this.data.state = {
playing: true,
@ -89,7 +111,29 @@ export class WorldState {
time: 0,
scene: clone(this.data.initialScene),
}
state.worldEditor.resetSelectedObject();
}
state.worldEditor.resetSelectedObject();
}
public pause(): void {
if (!this.isPlaying)
return;
const state = clone(this.data.state) as RunningGameState;
state.paused = true;
this.data.state = state;
}
public stop(): void {
if (!this.isPlaying)
return;
this.data.state = { playing: false };
}
public getObjectTypeById(id: string): ObjectType | undefined {
return this.data.objectTypes[id];
}
public getVoxelTypeById(id: string): VoxelType | undefined {
return this.data.voxelTypes[id];
}
}

View File

@ -1,10 +1,9 @@
import { makeAutoObservable } from "mobx";
import type { WorldState } from "./worldState";
import type { WorldState } from "./world";
import type { ObjectInstance, Scene, World } from "../types";
import { createObjectInstance } from "../utils/object";
import { randomId } from "../utils";
import type { Pos3, R3, V3 } from "../types/3d";
import { state } from "./rootState";
export const SelectionEditModeEnum = [
'translate',
@ -35,7 +34,7 @@ export class WorldEditorState {
}
public get isEnabled(): boolean {
return !state.isGamePlaying;
return !this.world.isPlaying;
}
public setCamera(value: Pos3): void {

View File

@ -1,4 +0,0 @@
import type { Pos3 } from "./3d";
export type Character = Pos3 & {
}

View File

@ -0,0 +1,4 @@
import type { Pos3 } from "./3d";
export type CharacterState = Pos3 & {
}

View File

@ -3,7 +3,7 @@ export * from './scene';
export * from './world';
export * from './gameRules';
export * from './gameState';
export * from './character';
export * from './characterState';

View File

@ -1,7 +1,7 @@
import type { Character } from "./character";
import type { CharacterState } from "./characterState";
import type { ObjectInstance } from "./object";
export type Scene = {
character: Character;
character: CharacterState;
objects: Record<string, ObjectInstance>;
}