linting
This commit is contained in:
parent
3bdde094f8
commit
88b36988ff
|
|
@ -19,13 +19,12 @@ export default defineConfig([
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"padding-line-between-statements": [
|
"semi": ["error", "always"],
|
||||||
"warn",
|
"nonblock-statement-body-position": ["error", "below"],
|
||||||
{ blankLine: 'always', prev: '*', next: 'block' },
|
"@typescript-eslint/no-unused-vars": ["error", {
|
||||||
{ blankLine: 'always', prev: 'block', next: '*' },
|
"varsIgnorePattern": "^_",
|
||||||
{ blankLine: 'always', prev: '*', next: 'block-like' },
|
"argsIgnorePattern": "^_",
|
||||||
{ blankLine: 'always', prev: 'block-like', next: '*' },
|
}],
|
||||||
]
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
||||||
35
src/App.tsx
35
src/App.tsx
|
|
@ -1,6 +1,6 @@
|
||||||
import { BrowserRouter, Link, Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { BrowserRouter, Link, Outlet, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import { VoxelEditorPage } from './components/voxelEditor/VoxelEditorPage';
|
import { VoxelEditorPage } from './components/voxelEditor/VoxelEditorPage';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import { reaction } from 'mobx';
|
import { reaction } from 'mobx';
|
||||||
import { ThreeView } from './components/ThreeView';
|
import { ThreeView } from './components/ThreeView';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
|
|
@ -11,7 +11,7 @@ function StateToUrlSync() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const pathnameRef = useRef(pathname);
|
const pathnameRef = useRef(pathname);
|
||||||
pathnameRef.current = pathname;
|
useLayoutEffect(() => { pathnameRef.current = pathname; });
|
||||||
|
|
||||||
useEffect(() => reaction(
|
useEffect(() => reaction(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -21,22 +21,28 @@ function StateToUrlSync() {
|
||||||
}),
|
}),
|
||||||
({ isGame, selectedObjectId, selectedObjectTypeId }) => {
|
({ isGame, selectedObjectId, selectedObjectTypeId }) => {
|
||||||
let target: string;
|
let target: string;
|
||||||
if (isGame) target = '/game';
|
if (isGame)
|
||||||
else if (selectedObjectId) target = `/editor/object/${selectedObjectId}`;
|
target = '/game';
|
||||||
else if (selectedObjectTypeId) target = `/editor/objectType/${selectedObjectTypeId}`;
|
else if (selectedObjectId)
|
||||||
else target = '/editor';
|
target = `/editor/object/${selectedObjectId}`;
|
||||||
|
else if (selectedObjectTypeId)
|
||||||
|
target = `/editor/objectType/${selectedObjectTypeId}`;
|
||||||
|
else
|
||||||
|
target = '/editor';
|
||||||
|
|
||||||
const current = pathnameRef.current;
|
const current = pathnameRef.current;
|
||||||
if (current === target || current.startsWith(target + '/')) return;
|
if (current === target || current.startsWith(target + '/'))
|
||||||
|
return;
|
||||||
navigate(target);
|
navigate(target);
|
||||||
},
|
},
|
||||||
), []);
|
), [navigate]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRoute() {
|
function EditorRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!state.game) state.stopGame();
|
if (state.game)
|
||||||
|
state.stopGame();
|
||||||
state.worldEditor.resetSelection();
|
state.worldEditor.resetSelection();
|
||||||
}, []);
|
}, []);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -45,8 +51,9 @@ function EditorRoute() {
|
||||||
function EditorObjectRoute() {
|
function EditorObjectRoute() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id)
|
||||||
if (!!state.game)
|
return;
|
||||||
|
if (state.game)
|
||||||
state.stopGame();
|
state.stopGame();
|
||||||
const editMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id === id
|
const editMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection.id === id
|
||||||
? state.worldEditor.selection.editMode ?? 'translate'
|
? state.worldEditor.selection.editMode ?? 'translate'
|
||||||
|
|
@ -59,8 +66,10 @@ function EditorObjectRoute() {
|
||||||
function EditorObjectTypeRoute() {
|
function EditorObjectTypeRoute() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id)
|
||||||
if (!!state.game) state.stopGame();
|
return;
|
||||||
|
if (state.game)
|
||||||
|
state.stopGame();
|
||||||
const editMode = state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id === id
|
const editMode = state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection.id === id
|
||||||
? state.worldEditor.selection.editMode ?? 'scripts'
|
? state.worldEditor.selection.editMode ?? 'scripts'
|
||||||
: 'scripts';
|
: 'scripts';
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Blockly.Blocks['console_log_action'] = {
|
||||||
.setAlign(Blockly.inputs.Align.RIGHT)
|
.setAlign(Blockly.inputs.Align.RIGHT)
|
||||||
// .setCheck('String')
|
// .setCheck('String')
|
||||||
.appendField('print');
|
.appendField('print');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false);
|
||||||
this.setPreviousStatement(true, null);
|
this.setPreviousStatement(true, null);
|
||||||
this.setNextStatement(true, null);
|
this.setNextStatement(true, null);
|
||||||
this.setTooltip('Print value to console');
|
this.setTooltip('Print value to console');
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Blockly.Blocks['physics_apply_impulse_action'] = {
|
||||||
.appendField(new Blockly.FieldNumber(1, -10, 10, 0.01), 'FORCE');
|
.appendField(new Blockly.FieldNumber(1, -10, 10, 0.01), 'FORCE');
|
||||||
|
|
||||||
|
|
||||||
this.setInputsInline(true)
|
this.setInputsInline(true);
|
||||||
this.setPreviousStatement(true, null);
|
this.setPreviousStatement(true, null);
|
||||||
this.setNextStatement(true, null);
|
this.setNextStatement(true, null);
|
||||||
this.setTooltip('Push me in a direction');
|
this.setTooltip('Push me in a direction');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import * as Blockly from "blockly";
|
import * as Blockly from "blockly";
|
||||||
|
|
||||||
export class ObjecTypeField extends Blockly.Field {
|
export class ObjecTypeField extends Blockly.Field {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
constructor(value: any, validator: any) {
|
constructor(value: any, validator: any) {
|
||||||
super(value, validator);
|
super(value, validator);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Blockly.Blocks['current_object_value'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendEndRowInput('NAME')
|
this.appendEndRowInput('NAME')
|
||||||
.appendField('Me');
|
.appendField('Me');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false);
|
||||||
this.setOutput(true, 'Object');
|
this.setOutput(true, 'Object');
|
||||||
this.setTooltip('Returns current object instance');
|
this.setTooltip('Returns current object instance');
|
||||||
this.setColour(315);
|
this.setColour(315);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Blockly.Blocks['current_object_type_value'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendEndRowInput('NAME')
|
this.appendEndRowInput('NAME')
|
||||||
.appendField('My type');
|
.appendField('My type');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false);
|
||||||
this.setOutput(true, 'ObjectType');
|
this.setOutput(true, 'ObjectType');
|
||||||
this.setTooltip('Returns current object type');
|
this.setTooltip('Returns current object type');
|
||||||
this.setColour(315);
|
this.setColour(315);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Blockly.Blocks['object_by_id_value'] = {
|
||||||
this.appendEndRowInput()
|
this.appendEndRowInput()
|
||||||
.appendField('Object with id')
|
.appendField('Object with id')
|
||||||
.appendField(new Blockly.FieldTextInput(''), 'TARGET_ID');
|
.appendField(new Blockly.FieldTextInput(''), 'TARGET_ID');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false);
|
||||||
this.setOutput(true, 'Object');
|
this.setOutput(true, 'Object');
|
||||||
this.setTooltip('Returns object by id, if any');
|
this.setTooltip('Returns object by id, if any');
|
||||||
this.setColour(315);
|
this.setColour(315);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Blockly.Blocks['player_object_value'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendEndRowInput()
|
this.appendEndRowInput()
|
||||||
.appendField('Player');
|
.appendField('Player');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false);
|
||||||
this.setOutput(true, 'Object');
|
this.setOutput(true, 'Object');
|
||||||
this.setTooltip('Returns object that is controlled by player');
|
this.setTooltip('Returns object that is controlled by player');
|
||||||
this.setColour(315);
|
this.setColour(315);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
shoulderOffset: new Vector3(W * 0.1, centerY + H * 0.3, bb.max[2] + radius),
|
shoulderOffset: new Vector3(W * 0.1, centerY + H * 0.3, bb.max[2] + radius),
|
||||||
lookAtY: centerY,
|
lookAtY: centerY,
|
||||||
};
|
};
|
||||||
}, [object.id]);
|
}, [object.cache.boundingBox]);
|
||||||
|
|
||||||
const yawRef = useRef(0);
|
const yawRef = useRef(0);
|
||||||
const pitchRef = useRef(0);
|
const pitchRef = useRef(0);
|
||||||
|
|
@ -69,9 +69,11 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = gl.domElement;
|
const canvas = gl.domElement;
|
||||||
const onClick = () => { if (document.pointerLockElement !== canvas) canvas.requestPointerLock()?.catch(() => {}); };
|
const onClick = () => { if (document.pointerLockElement !== canvas)
|
||||||
|
canvas.requestPointerLock()?.catch(() => {}); };
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
if (document.pointerLockElement !== canvas) return;
|
if (document.pointerLockElement !== canvas)
|
||||||
|
return;
|
||||||
mouseRef.current.x += e.movementX;
|
mouseRef.current.x += e.movementX;
|
||||||
mouseRef.current.y += e.movementY;
|
mouseRef.current.y += e.movementY;
|
||||||
};
|
};
|
||||||
|
|
@ -85,12 +87,16 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
|
|
||||||
useBeforePhysicsStep((world) => {
|
useBeforePhysicsStep((world) => {
|
||||||
const rb = handleRef.current?.rb;
|
const rb = handleRef.current?.rb;
|
||||||
if (!rb) return;
|
if (!rb)
|
||||||
|
return;
|
||||||
const collider = rb.collider(0);
|
const collider = rb.collider(0);
|
||||||
if (!collider) return;
|
if (!collider)
|
||||||
|
return;
|
||||||
const controller = controllerRef.current;
|
const controller = controllerRef.current;
|
||||||
if (!controller) return;
|
if (!controller)
|
||||||
if (state.game?.isPaused) return;
|
return;
|
||||||
|
if (state.game?.isPaused)
|
||||||
|
return;
|
||||||
|
|
||||||
const dt = world.timestep;
|
const dt = world.timestep;
|
||||||
const yaw = yawRef.current;
|
const yaw = yawRef.current;
|
||||||
|
|
@ -128,7 +134,8 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
|
|
||||||
useFrame(({ camera }, delta) => {
|
useFrame(({ camera }, delta) => {
|
||||||
const rb = handleRef.current?.rb;
|
const rb = handleRef.current?.rb;
|
||||||
if (!rb) return;
|
if (!rb)
|
||||||
|
return;
|
||||||
|
|
||||||
mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta;
|
mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta;
|
||||||
mouseRef.current.y += -joystickValues.look.y * LOOK_RATE * delta;
|
mouseRef.current.y += -joystickValues.look.y * LOOK_RATE * delta;
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ export const LeftPanel = observer(function () {
|
||||||
{/* <button onClick={handleLoadWorld}>Load</button> */}
|
{/* <button onClick={handleLoadWorld}>Load</button> */}
|
||||||
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
<button onClick={handleLoadMockWorld}>Load mock world</button>
|
||||||
<div className="debug"><RenderInfoView /></div>
|
<div className="debug"><RenderInfoView /></div>
|
||||||
</Panel>
|
</Panel>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,5 @@ export const MainPanel = observer(function () {
|
||||||
<div><span className="title">{objectType?.name}</span> ({objects.length} object{objects.length > 1 ? 's' : ''})</div>
|
<div><span className="title">{objectType?.name}</span> ({objects.length} object{objects.length > 1 ? 's' : ''})</div>
|
||||||
</header>
|
</header>
|
||||||
<ScriptEditorView objectType={objectType} />
|
<ScriptEditorView objectType={objectType} />
|
||||||
</Panel>
|
</Panel>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: S
|
||||||
[ref],
|
[ref],
|
||||||
);
|
);
|
||||||
|
|
||||||
useHelper(isSelected ? groupRef : { current: null } as any, BoxHelper, 'white');
|
useHelper(isSelected ? groupRef : { current: null }, BoxHelper, 'white');
|
||||||
|
|
||||||
if (!isSelected || selectionMode === undefined || !groupRef.current)
|
if (!isSelected || selectionMode === undefined || !groupRef.current)
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (props.isPlayer) return;
|
if (props.isPlayer)
|
||||||
|
return;
|
||||||
return reaction(
|
return reaction(
|
||||||
() => 'version' in object ? object.version : 0,
|
() => 'version' in object ? object.version : 0,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -53,19 +54,20 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[object.id]
|
[object, object.id, props.isPlayer]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gameObj = object as RuntimeGameObjectInstance;
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
const gameObj = object as RuntimeGameObjectInstance;
|
|
||||||
|
|
||||||
return reaction(
|
return reaction(
|
||||||
() => gameObj.pendingActions.impulse,
|
() => gameObj.pendingActions.impulse,
|
||||||
(impulse) => {
|
(impulse) => {
|
||||||
if (!impulse || !rbRef.current)
|
if (!impulse || !rbRef.current)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
let { direction, amplitude } = impulse;
|
let { direction, amplitude } = impulse;
|
||||||
amplitude *= 100;
|
amplitude *= 100;
|
||||||
const v = { x: direction[0] * amplitude, y: direction[1] * amplitude, z: direction[2] * amplitude };
|
const v = { x: direction[0] * amplitude, y: direction[1] * amplitude, z: direction[2] * amplitude };
|
||||||
|
|
@ -76,7 +78,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[object.id]
|
[gameObj.pendingActions, object.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleClick(e: ThreeEvent<MouseEvent>) {
|
function handleClick(e: ThreeEvent<MouseEvent>) {
|
||||||
|
|
@ -126,4 +128,4 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
||||||
</group>
|
</group>
|
||||||
</SyncRigidBody>
|
</SyncRigidBody>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,5 @@ export type PanelProps = {
|
||||||
export const Panel = observer(function ({ children, side = 'left' }: PanelProps) {
|
export const Panel = observer(function ({ children, side = 'left' }: PanelProps) {
|
||||||
return <div className={`panel ${side}`}>
|
return <div className={`panel ${side}`}>
|
||||||
{children}
|
{children}
|
||||||
</div >
|
</div >;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,5 @@ export const Panels = observer(function () {
|
||||||
return <div className="overlay-panels">
|
return <div className="overlay-panels">
|
||||||
<LeftPanel />
|
<LeftPanel />
|
||||||
<MainPanel />
|
<MainPanel />
|
||||||
</div>
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ export const RenderInfoView = observer(function () {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!el) return;
|
if (!el)
|
||||||
|
return;
|
||||||
el.appendChild(chartRef.current);
|
el.appendChild(chartRef.current);
|
||||||
return () => { el.removeChild(chartRef.current); };
|
return () => { el.removeChild(chartRef.current); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -25,5 +26,5 @@ export const RenderInfoView = observer(function () {
|
||||||
<span>textures: {info.textures}</span>
|
<span>textures: {info.textures}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
import { Canvas } from '@react-three/fiber';
|
||||||
import { KeyboardControls, Stats } from '@react-three/drei';
|
import { KeyboardControls, Stats } from '@react-three/drei';
|
||||||
import { action } from 'mobx';
|
|
||||||
import { chartRef } from './chartRef';
|
import { chartRef } from './chartRef';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { state } from '../state';
|
import { state } from '../state';
|
||||||
|
|
@ -9,23 +8,7 @@ import { SceneEditorView } from './SceneEditorView';
|
||||||
import { JoystickView } from './JoystickView';
|
import { JoystickView } from './JoystickView';
|
||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled } from '@tabler/icons-react';
|
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled } from '@tabler/icons-react';
|
||||||
|
import { RenderInfoUpdater } from './renderInfo';
|
||||||
function RenderInfoUpdater() {
|
|
||||||
const { gl } = useThree();
|
|
||||||
useFrame(action(() => {
|
|
||||||
const { calls, triangles } = gl.info.render;
|
|
||||||
const shaders = gl.info.programs?.length ?? 0;
|
|
||||||
const { geometries, textures } = gl.info.memory;
|
|
||||||
state.setRenderInfo({
|
|
||||||
calls,
|
|
||||||
triangles,
|
|
||||||
shaders,
|
|
||||||
geometries,
|
|
||||||
textures,
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThreeView = observer(function () {
|
export const ThreeView = observer(function () {
|
||||||
const isGame = !!state.game;
|
const isGame = !!state.game;
|
||||||
|
|
@ -63,5 +46,5 @@ export const ThreeView = observer(function () {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</KeyboardControls>
|
</KeyboardControls>
|
||||||
)
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
|
import { state } from "../state";
|
||||||
|
import { action } from "mobx";
|
||||||
|
|
||||||
|
export function RenderInfoUpdater() {
|
||||||
|
const { gl } = useThree();
|
||||||
|
useFrame(action(() => {
|
||||||
|
const { calls, triangles } = gl.info.render;
|
||||||
|
const shaders = gl.info.programs?.length ?? 0;
|
||||||
|
const { geometries, textures } = gl.info.memory;
|
||||||
|
state.setRenderInfo({
|
||||||
|
calls,
|
||||||
|
triangles,
|
||||||
|
shaders,
|
||||||
|
geometries,
|
||||||
|
textures,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
import * as Blockly from "blockly";
|
import * as Blockly from "blockly";
|
||||||
|
|
@ -68,10 +68,11 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
|
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
|
||||||
const objectTypeRef = useRef(objectType);
|
const objectTypeRef = useRef(objectType);
|
||||||
objectTypeRef.current = objectType;
|
useLayoutEffect(() => { objectTypeRef.current = objectType; });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current)
|
||||||
|
return;
|
||||||
|
|
||||||
const workspace = Blockly.inject(containerRef.current, { toolbox: TOOLBOX, theme: gameTheme });
|
const workspace = Blockly.inject(containerRef.current, { toolbox: TOOLBOX, theme: gameTheme });
|
||||||
workspaceRef.current = workspace;
|
workspaceRef.current = workspace;
|
||||||
|
|
@ -116,11 +117,12 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
|
||||||
workspace.dispose();
|
workspace.dispose();
|
||||||
workspaceRef.current = null;
|
workspaceRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [objectType.program]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const workspace = workspaceRef.current;
|
const workspace = workspaceRef.current;
|
||||||
if (!workspace) return;
|
if (!workspace)
|
||||||
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Blockly.Events.disable();
|
Blockly.Events.disable();
|
||||||
|
|
@ -132,7 +134,7 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
|
||||||
} finally {
|
} finally {
|
||||||
Blockly.Events.enable();
|
Blockly.Events.enable();
|
||||||
}
|
}
|
||||||
}, [objectType.id]);
|
}, [objectType.id, objectType.program]);
|
||||||
|
|
||||||
return <div className="script-editor">
|
return <div className="script-editor">
|
||||||
<div ref={containerRef} className="blockly-container" />
|
<div ref={containerRef} className="blockly-container" />
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ export const VoxelEditorPage = observer(function () {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (id) state.worldEditor.refreshObjectTypeCaches(id);
|
if (id)
|
||||||
|
state.worldEditor.refreshObjectTypeCaches(id);
|
||||||
};
|
};
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
|
|
||||||
10
src/main.tsx
10
src/main.tsx
|
|
@ -1,10 +1,10 @@
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import { App } from './App.tsx'
|
import { App } from './App.tsx';
|
||||||
import './index.scss'
|
import './index.scss';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ export class GameScheduler {
|
||||||
tick(deltaTime: number): void {
|
tick(deltaTime: number): void {
|
||||||
const next: Coroutine[] = [];
|
const next: Coroutine[] = [];
|
||||||
for (const co of this.coroutines) {
|
for (const co of this.coroutines) {
|
||||||
if (this.isReady(co)) this.step(co, next);
|
if (this.isReady(co))
|
||||||
else next.push(this.countdown(co, deltaTime));
|
this.step(co, next);
|
||||||
|
else
|
||||||
|
next.push(this.countdown(co, deltaTime));
|
||||||
}
|
}
|
||||||
this.coroutines = next;
|
this.coroutines = next;
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +49,8 @@ export class GameScheduler {
|
||||||
|
|
||||||
private step(co: Coroutine, list: Coroutine[]): void {
|
private step(co: Coroutine, list: Coroutine[]): void {
|
||||||
const result = co.gen.next();
|
const result = co.gen.next();
|
||||||
if (!result.done) list.push(this.make(co.gen, result.value));
|
if (!result.done)
|
||||||
|
list.push(this.make(co.gen, result.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private make(gen: Generator<YieldCommand, void, void>, cmd: YieldCommand): Coroutine {
|
private make(gen: Generator<YieldCommand, void, void>, cmd: YieldCommand): Coroutine {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,6 @@ export const DEFAULT_VOXEL_TYPES = {
|
||||||
[dirt.id]: dirt,
|
[dirt.id]: dirt,
|
||||||
[water.id]: water,
|
[water.id]: water,
|
||||||
[glass.id]: glass,
|
[glass.id]: glass,
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DEFAULT_VOXEL_TYPE = stone;
|
export const DEFAULT_VOXEL_TYPE = stone;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
import type { Game, RuntimeGameScene, RuntimeScene, World } from "../types";
|
||||||
import type { GameEventContext, GameEventHandler, InternalGameEventBus } from "../types/runtime/gameBus";
|
import type { GameEventContext, GameEventHandler, GeneratorEventHandler, InternalGameEventBus } from "../types/runtime/gameBus";
|
||||||
import { clone } from "../utils";
|
import { clone } from "../utils";
|
||||||
import { populateRuntimeScene } from "../utils/runtime";
|
import { populateRuntimeScene } from "../utils/runtime";
|
||||||
import { ObjectApi } from "../utils/runtime/objectApi";
|
import { ObjectApi } from "../utils/runtime/objectApi";
|
||||||
import type { GameScheduler, YieldCommand } from "./GameScheduler";
|
import type { GameScheduler, YieldCommand } from "./gameScheduler";
|
||||||
|
|
||||||
export class GameEventBus {
|
export class GameEventBus {
|
||||||
private handlers = new Map<string, GameEventHandler[]>();
|
private handlers = new Map<string, GameEventHandler[]>();
|
||||||
|
|
@ -70,7 +70,7 @@ export class GameFactory {
|
||||||
scheduler: GameScheduler,
|
scheduler: GameScheduler,
|
||||||
): void {
|
): void {
|
||||||
|
|
||||||
const handlerWrappers = new WeakMap<Function, (ctx: GameEventContext) => void>();
|
const handlerWrappers = new WeakMap<GeneratorEventHandler, GameEventHandler>();
|
||||||
|
|
||||||
const wait = (seconds: number): YieldCommand => ({ type: 'waitSeconds', seconds });
|
const wait = (seconds: number): YieldCommand => ({ type: 'waitSeconds', seconds });
|
||||||
const waitFrames = (frames: number): YieldCommand => ({ type: 'waitFrames', frames });
|
const waitFrames = (frames: number): YieldCommand => ({ type: 'waitFrames', frames });
|
||||||
|
|
@ -102,11 +102,11 @@ export class GameFactory {
|
||||||
.forEach((o) => {
|
.forEach((o) => {
|
||||||
const api = new ObjectApi(o, ot, world, scene);
|
const api = new ObjectApi(o, ot, world, scene);
|
||||||
gameScript({ ...internalBus, wait, waitFrames, waitUntil }, { api, object: o, objectType: ot });
|
gameScript({ ...internalBus, wait, waitFrames, waitUntil }, { api, object: o, objectType: ot });
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.log('Error running game script:\n' + err)
|
console.log('Error running game script:\n' + err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { WorldState } from "./worldState";
|
||||||
import type { Game, Pos3, RuntimeGameScene } from "../types";
|
import type { Game, Pos3, RuntimeGameScene } from "../types";
|
||||||
import type { CameraProps } from "@react-three/fiber";
|
import type { CameraProps } from "@react-three/fiber";
|
||||||
import { GameEventBus, GameFactory } from "../model/gameFactory";
|
import { GameEventBus, GameFactory } from "../model/gameFactory";
|
||||||
import { GameScheduler } from "../model/GameScheduler";
|
import { GameScheduler } from "../model/gameScheduler";
|
||||||
import type { GameEventContext } from "../types/runtime/gameBus";
|
import type { GameEventContext } from "../types/runtime/gameBus";
|
||||||
|
|
||||||
export class GameState {
|
export class GameState {
|
||||||
|
|
@ -25,8 +25,7 @@ export class GameState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// @ts-expect-error -- for future use
|
||||||
// @ts-ignore
|
|
||||||
private withoutAutoSave(fn: () => void) {
|
private withoutAutoSave(fn: () => void) {
|
||||||
this._stopAutoSave();
|
this._stopAutoSave();
|
||||||
fn();
|
fn();
|
||||||
|
|
@ -72,7 +71,7 @@ export class GameState {
|
||||||
paused: toJS(this.isPaused),
|
paused: toJS(this.isPaused),
|
||||||
time: toJS(this.time),
|
time: toJS(this.time),
|
||||||
scene: toJS(this.scene),
|
scene: toJS(this.scene),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public setGame(value: Game) {
|
public setGame(value: Game) {
|
||||||
|
|
@ -95,7 +94,7 @@ export class GameState {
|
||||||
position: cam.position,
|
position: cam.position,
|
||||||
fov: 50,
|
fov: 50,
|
||||||
rotation: cam.look,
|
rotation: cam.look,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// public load() {
|
// public load() {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export class MenuState {
|
||||||
</>,
|
</>,
|
||||||
className: isPlayer ? 'player-controlled-object' : undefined,
|
className: isPlayer ? 'player-controlled-object' : undefined,
|
||||||
actions,
|
actions,
|
||||||
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
|
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }); },
|
||||||
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
||||||
} as MenuNode;
|
} as MenuNode;
|
||||||
});
|
});
|
||||||
|
|
@ -89,10 +89,10 @@ export class MenuState {
|
||||||
onClick: () => { editor.deleteObjectType(ot.id); },
|
onClick: () => { editor.deleteObjectType(ot.id); },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onClick: () => { editor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
|
onClick: () => { editor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }); },
|
||||||
selected: () => editor.selection?.type === 'objectType' && editor.selection?.id === ot.id,
|
selected: () => editor.selection?.type === 'objectType' && editor.selection?.id === ot.id,
|
||||||
children,
|
children,
|
||||||
} as MenuNode
|
} as MenuNode;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ export class MenuState {
|
||||||
{
|
{
|
||||||
id: 'editor-scene-menu',
|
id: 'editor-scene-menu',
|
||||||
title: 'Scene',
|
title: 'Scene',
|
||||||
onClick: () => { state.worldEditor.resetSelection() },
|
onClick: () => { state.worldEditor.resetSelection(); },
|
||||||
selected: () => !state.worldEditor.selection,
|
selected: () => !state.worldEditor.selection,
|
||||||
children: this.editorObjectTypesMenu,
|
children: this.editorObjectTypesMenu,
|
||||||
actions: [{
|
actions: [{
|
||||||
|
|
@ -111,13 +111,13 @@ export class MenuState {
|
||||||
onClick: () => { state.worldEditor.addObjectType(); },
|
onClick: () => { state.worldEditor.addObjectType(); },
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public get nodes(): MenuNode[] {
|
public get nodes(): MenuNode[] {
|
||||||
return [
|
return [
|
||||||
...this.editorMenu,
|
...this.editorMenu,
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public nodeContainsSelected(node: MenuNode): boolean {
|
public nodeContainsSelected(node: MenuNode): boolean {
|
||||||
|
|
|
||||||
|
|
@ -165,33 +165,39 @@ export class WorldEditorState {
|
||||||
|
|
||||||
public addVoxelToObjectType(typeId: string, voxel: Voxel): void {
|
public addVoxelToObjectType(typeId: string, voxel: Voxel): void {
|
||||||
const objectType = this.world.getObjectTypeById(typeId);
|
const objectType = this.world.getObjectTypeById(typeId);
|
||||||
if (!objectType) return;
|
if (!objectType)
|
||||||
|
return;
|
||||||
const occupied = objectType.voxels.some(v =>
|
const occupied = objectType.voxels.some(v =>
|
||||||
v.position[0] === voxel.position[0] &&
|
v.position[0] === voxel.position[0] &&
|
||||||
v.position[1] === voxel.position[1] &&
|
v.position[1] === voxel.position[1] &&
|
||||||
v.position[2] === voxel.position[2]
|
v.position[2] === voxel.position[2]
|
||||||
);
|
);
|
||||||
if (!occupied) objectType.voxels.push(voxel);
|
if (!occupied)
|
||||||
|
objectType.voxels.push(voxel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeVoxelFromObjectType(typeId: string, position: V3): void {
|
public removeVoxelFromObjectType(typeId: string, position: V3): void {
|
||||||
const objectType = this.world.getObjectTypeById(typeId);
|
const objectType = this.world.getObjectTypeById(typeId);
|
||||||
if (!objectType) return;
|
if (!objectType)
|
||||||
|
return;
|
||||||
const idx = objectType.voxels.findIndex(v =>
|
const idx = objectType.voxels.findIndex(v =>
|
||||||
v.position[0] === position[0] &&
|
v.position[0] === position[0] &&
|
||||||
v.position[1] === position[1] &&
|
v.position[1] === position[1] &&
|
||||||
v.position[2] === position[2]
|
v.position[2] === position[2]
|
||||||
);
|
);
|
||||||
if (idx !== -1) objectType.voxels.splice(idx, 1);
|
if (idx !== -1)
|
||||||
|
objectType.voxels.splice(idx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public refreshObjectTypeCaches(typeId: string): void {
|
public refreshObjectTypeCaches(typeId: string): void {
|
||||||
const objectType = this.world.getObjectTypeById(typeId);
|
const objectType = this.world.getObjectTypeById(typeId);
|
||||||
if (!objectType) return;
|
if (!objectType)
|
||||||
|
return;
|
||||||
const world = this.world.data;
|
const world = this.world.data;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const obj of Object.values(this.scene.objects)) {
|
for (const obj of Object.values(this.scene.objects)) {
|
||||||
if (obj.typeId !== typeId) continue;
|
if (obj.typeId !== typeId)
|
||||||
|
continue;
|
||||||
obj.cache.voxelGroups = getObjectVoxelGroups(objectType, world.voxelTypes);
|
obj.cache.voxelGroups = getObjectVoxelGroups(objectType, world.voxelTypes);
|
||||||
obj.cache.colliderMesh = buildObjectTrimesh(objectType, world.voxelTypes);
|
obj.cache.colliderMesh = buildObjectTrimesh(objectType, world.voxelTypes);
|
||||||
obj.cache.boundingBox = computeBoundingBox(objectType);
|
obj.cache.boundingBox = computeBoundingBox(objectType);
|
||||||
|
|
@ -207,7 +213,7 @@ export class WorldEditorState {
|
||||||
if (this.scene.objects[id].typeId === typeId)
|
if (this.scene.objects[id].typeId === typeId)
|
||||||
this.deleteObject(id);
|
this.deleteObject(id);
|
||||||
|
|
||||||
delete (this.world.data.objectTypes[typeId])
|
delete (this.world.data.objectTypes[typeId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export class WorldState {
|
||||||
} as ObjectInstance));
|
} as ObjectInstance));
|
||||||
const objectMap = Object.fromEntries(
|
const objectMap = Object.fromEntries(
|
||||||
objects.map((obj) => [obj.id, obj]),
|
objects.map((obj) => [obj.id, obj]),
|
||||||
) as Record<string, ObjectInstance>
|
) as Record<string, ObjectInstance>;
|
||||||
|
|
||||||
this.data = populateRuntimeWorld({
|
this.data = populateRuntimeWorld({
|
||||||
objectTypes: {
|
objectTypes: {
|
||||||
|
|
@ -106,7 +106,7 @@ export class WorldState {
|
||||||
private saveData(data: World): void {
|
private saveData(data: World): void {
|
||||||
console.log('Saving world...');
|
console.log('Saving world...');
|
||||||
const stack = new Error('Saving world...').stack!.split('\n').slice(1);
|
const stack = new Error('Saving world...').stack!.split('\n').slice(1);
|
||||||
const { voxelTypes, initialScene, ...debug } = toJS(data);
|
const { voxelTypes: _vts, initialScene: _is, ...debug } = toJS(data);
|
||||||
console.dir({ stack, debug });
|
console.dir({ stack, debug });
|
||||||
WorldFactory.save(toJS(this.data));
|
WorldFactory.save(toJS(this.data));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,5 @@ export function v3toRapier(value: V3): Vector3Object {
|
||||||
x: value[0],
|
x: value[0],
|
||||||
y: value[1],
|
y: value[1],
|
||||||
z: value[2],
|
z: value[2],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,6 @@ export function populateRuntimeObject(object: ObjectInstance, world: World): Run
|
||||||
}
|
}
|
||||||
|
|
||||||
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
||||||
const { cache, ...result } = object;
|
const { cache: _cache, ...result } = object;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ export class ObjectApi {
|
||||||
const dy = targetPosition[1] - py;
|
const dy = targetPosition[1] - py;
|
||||||
const dz = targetPosition[2] - pz;
|
const dz = targetPosition[2] - pz;
|
||||||
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
if (len < 1e-6) return;
|
if (len < 1e-6)
|
||||||
|
return;
|
||||||
const direction: V3 = [dx / len, dy / len, dz / len];
|
const direction: V3 = [dx / len, dy / len, dz / len];
|
||||||
this.object.pendingActions.impulse = { direction, amplitude };
|
this.object.pendingActions.impulse = { direction, amplitude };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ export function populateRuntimeScene(scene: Scene, world: World): RuntimeScene {
|
||||||
return {
|
return {
|
||||||
...scene,
|
...scene,
|
||||||
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, populateRuntimeObject(obj, world)])),
|
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, populateRuntimeObject(obj, world)])),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function depopulateRuntimeScene(scene: RuntimeScene, world: World): Scene {
|
export function depopulateRuntimeScene(scene: RuntimeScene, world: World): Scene {
|
||||||
return {
|
return {
|
||||||
...scene,
|
...scene,
|
||||||
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, depopulateRuntimeObject(obj, world)])),
|
objects: Object.fromEntries(Object.entries(scene.objects).map(([id, obj]) => [id, depopulateRuntimeObject(obj, world)])),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue