scss instead of css
floating play-pause buttons character model simplification
This commit is contained in:
+2
-2
@@ -1,12 +1,12 @@
|
||||
import { ThreeView } from './components/ThreeView';
|
||||
import './App.css';
|
||||
import './App.scss';
|
||||
import { Toolbar } from './components/Toolbar';
|
||||
|
||||
export const App = function () {
|
||||
return (
|
||||
<>
|
||||
<Toolbar />
|
||||
<ThreeView />
|
||||
<Toolbar />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { state } from "../state";
|
||||
import type { Character, Scene } from "../types";
|
||||
|
||||
export const CharacterView = observer(function () {
|
||||
const character = state.world.currentScene.character;
|
||||
export const CharacterView = observer(function ({ character }: { character: Character }) {
|
||||
|
||||
return <mesh position={character.position} rotation={character.look}>
|
||||
<boxGeometry args={[0.8, 0.8, 0.8]} />
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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} />;
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
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} />
|
||||
</>);
|
||||
});
|
||||
@@ -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 />
|
||||
<CharacterView character={scene.character} />
|
||||
</>);
|
||||
});
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { state } from '../state';
|
||||
import { WorldView } from './WorldView';
|
||||
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>;
|
||||
|
||||
export const ThreeView = observer(function () {
|
||||
const isGame = state.isGamePlaying;
|
||||
return (
|
||||
<div style={{ width: '800px', height: '600px', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden' }}>
|
||||
<div style={{ position: 'relative', 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()}
|
||||
>
|
||||
<WorldView />
|
||||
{isGame ? <GameView /> : <SceneEditorView />}
|
||||
</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>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -14,18 +14,6 @@ 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>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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} />
|
||||
</>)
|
||||
});
|
||||
@@ -15,7 +15,8 @@
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
font: 16px/100% var(--sans);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
@@ -24,10 +25,6 @@
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -66,46 +63,17 @@ body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
svg {
|
||||
fill: currentColor;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
top: 0.125em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { App } from './App.tsx'
|
||||
import './index.css'
|
||||
import './index.scss'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import type { WorldState } from "./worldState";
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { WorldState } from "./worldState";
|
||||
import { WorldEditorState } from "./worldEditorState";
|
||||
import { GameState } from "./gameState";
|
||||
|
||||
export class RootState {
|
||||
public readonly world = new WorldState();
|
||||
@@ -16,6 +17,15 @@ 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();
|
||||
|
||||
@@ -4,6 +4,7 @@ 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',
|
||||
@@ -34,7 +35,7 @@ export class WorldEditorState {
|
||||
}
|
||||
|
||||
public get isEnabled(): boolean {
|
||||
return !this.world.isPlaying;
|
||||
return !state.isGamePlaying;
|
||||
}
|
||||
|
||||
public setCamera(value: Pos3): void {
|
||||
|
||||
+16
-60
@@ -1,19 +1,15 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { WorldFactory } from "../model/worldFactory";
|
||||
import type { ObjectType, RunningGameState, Scene, World } from "../types";
|
||||
import { CharacterState } from "./characterState";
|
||||
import type { Pos3 } from "../types/3d";
|
||||
import { clone } from "../utils";
|
||||
import type { ObjectType, World } from "../types";
|
||||
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();
|
||||
|
||||
public character = new CharacterState(this);
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
@@ -75,60 +71,6 @@ export class WorldState {
|
||||
WorldFactory.save(this.data);
|
||||
}
|
||||
|
||||
public get isPlaying(): boolean {
|
||||
return this.data.state.playing;
|
||||
}
|
||||
|
||||
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 (this.isPlaying) {
|
||||
const state = clone(this.data.state) as RunningGameState;
|
||||
state.paused = false;
|
||||
this.data.state = state;
|
||||
}
|
||||
else {
|
||||
this.data.state = {
|
||||
playing: true,
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: clone(this.data.initialScene),
|
||||
}
|
||||
}
|
||||
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];
|
||||
}
|
||||
@@ -136,4 +78,18 @@ export class WorldState {
|
||||
public getVoxelTypeById(id: string): VoxelType | undefined {
|
||||
return this.data.voxelTypes[id];
|
||||
}
|
||||
|
||||
public play(): void {
|
||||
if (state.game)
|
||||
state.game.resume();
|
||||
else {
|
||||
this.data.state = {
|
||||
playing: true,
|
||||
paused: false,
|
||||
time: 0,
|
||||
scene: clone(this.data.initialScene),
|
||||
}
|
||||
state.worldEditor.resetSelectedObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { Pos3 } from "./3d";
|
||||
|
||||
export type Character = Pos3 & {
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { Pos3 } from "./3d";
|
||||
|
||||
export type CharacterState = Pos3 & {
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ export * from './scene';
|
||||
export * from './world';
|
||||
export * from './gameRules';
|
||||
export * from './gameState';
|
||||
export * from './characterState';
|
||||
export * from './character';
|
||||
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { CharacterState } from "./characterState";
|
||||
import type { Character } from "./character";
|
||||
import type { ObjectInstance } from "./object";
|
||||
|
||||
export type Scene = {
|
||||
character: CharacterState;
|
||||
character: Character;
|
||||
objects: Record<string, ObjectInstance>;
|
||||
}
|
||||
Reference in New Issue
Block a user