This commit is contained in:
azykov@mail.ru 2026-06-11 11:34:12 +03:00
parent 3bdde094f8
commit 88b36988ff
No known key found for this signature in database
34 changed files with 150 additions and 116 deletions

View File

@ -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: '*' }, }],
]
}, },
}, },
]) ])

View File

@ -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';

View File

@ -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');

View File

@ -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');

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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>;
}); });

View File

@ -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>;
}); });

View File

@ -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;

View File

@ -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>
); );
} };

View File

@ -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 >;
}); });

View File

@ -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>;
}); });

View File

@ -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>
)} )}
</> </>;
}); });

View File

@ -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>
) );
}); });

View File

@ -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;
}

View File

@ -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" />

View File

@ -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]);

View File

@ -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>,
) );

View File

@ -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 {

View File

@ -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;

View File

@ -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);
} }
} }
} }

View File

@ -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() {

View File

@ -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 {

View File

@ -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]);
} }
} }

View File

@ -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));
} }

View File

@ -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],
} };
} }

View File

@ -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;
} }

View File

@ -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 };
} }

View File

@ -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)])),
} };
} }

View File

@ -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()],
}) });