object move blocks

debug floaters
This commit is contained in:
azykov@mail.ru
2026-06-12 21:11:35 +03:00
parent 20a4edad5a
commit 209ff3f710
31 changed files with 527 additions and 53 deletions
@@ -1 +1,3 @@
export * from './applyImpulse';
export * from './moveInDirection';
export * from './moveToPosition';
@@ -0,0 +1,30 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['move_in_direction_action'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Move me in direction');
this.appendValueInput('DIRECTION')
.setCheck('DPos');
this.appendDummyInput()
.appendField('for')
.appendField(new Blockly.FieldNumber(5, 0.01, 10000, 0.1), 'DISTANCE')
.appendField('with speed')
.appendField(new Blockly.FieldNumber(5, 0.01, 1000, 0.1), 'SPEED');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Move this object in the given direction for the given distance at the given speed (units/sec), blocking until done');
this.setColour(20);
}
};
javascriptGenerator.forBlock['move_in_direction_action'] = function (block, generator) {
const direction = generator.valueToCode(block, 'DIRECTION', Order.ATOMIC) || '[0,0,1]';
const distance = block.getFieldValue('DISTANCE');
const speed = block.getFieldValue('SPEED');
return `yield helpers.actions.MoveInDirection(${direction}, ${distance}, ${speed});\n`;
};
export { };
@@ -0,0 +1,27 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['move_to_position_action'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Move me to position');
this.appendValueInput('TARGET_POSITION')
.setCheck('Pos');
this.appendDummyInput()
.appendField('with speed')
.appendField(new Blockly.FieldNumber(5, 0.01, 1000, 0.1), 'SPEED');
this.setInputsInline(true);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Move this object to the target position at the given speed (units/sec), blocking until arrived');
this.setColour(20);
}
};
javascriptGenerator.forBlock['move_to_position_action'] = function (block, generator) {
const targetPosition = generator.valueToCode(block, 'TARGET_POSITION', Order.ATOMIC) || '[0,0,0]';
const speed = block.getFieldValue('SPEED');
return `yield helpers.actions.MoveToPosition(${targetPosition}, ${speed});\n`;
};
export { };
+5 -2
View File
@@ -2,6 +2,7 @@ import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
export function generateCode(workspace: Blockly.WorkspaceSvg): string {
javascriptGenerator.init(workspace);
let result = '';
for (const block of workspace.getTopBlocks(true)) {
if (block.type.endsWith('_event')) {
@@ -21,9 +22,11 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
//TODO use args
const eventName = block.type.replace(/_event$/, '');
result += `eventBus.on(
'${block.type.replace(/_event$/, '')}',
function*(context) {
'${eventName}',
function* handle_${eventName}(context) {
if (context.object && (context.object.id !== thisObject.id))
return;
${body} },
+1
View File
@@ -4,3 +4,4 @@ export * from './actions';
export * from './theme';
export * from './generateCode';
export * from './fieldTypes';
export * from './loops';
+1
View File
@@ -0,0 +1 @@
export * from './repeatForever';
@@ -0,0 +1,22 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
Blockly.Blocks['loop_forever'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('Repeat forever');
this.appendStatementInput('BODY')
.setCheck(null);
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Repeat the enclosed actions forever');
this.setColour(120);
}
};
javascriptGenerator.forBlock['loop_forever'] = function (block, generator) {
const body = generator.statementToCode(block, 'BODY');
return `while (true) {\n${body}${generator.INDENT}yield helpers.actions.WaitFrames(1);\n}\n`;
};
export { };
@@ -16,7 +16,7 @@ Blockly.Blocks['direction_to_position_value'] = {
javascriptGenerator.forBlock['direction_to_position_value'] = function (block, generator) {
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
return [`((a,b)=>[a[0]-b[0],a[1]-b[1],a[2]-b[2]])(${targetValue},object.position)`, Order.NONE];
return [`(t=>{const p=helpers.positionOfObject(thisObject); const dir = [t[0]-p[0],t[1]-p[1],t[2]-p[2]]; /* helpers.debug.show('${block.id}.targetPosition', ${targetValue}); helpers.debug.show('${block.id}.result', dir); helpers.debug.show('${block.id}.thisObject.position', p); */ return dir;})(${targetValue})`, Order.NONE];
};
export { };
@@ -16,7 +16,7 @@ Blockly.Blocks['position_of_object_value'] = {
javascriptGenerator.forBlock['position_of_object_value'] = function (block, generator) {
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
return [`${targetValue}.position`, Order.NONE];
return [`helpers.positionOfObject(${targetValue})`, Order.NONE];
};
export { };
+1 -1
View File
@@ -48,7 +48,7 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
};
}, [object.cache.boundingBox]);
const yawRef = useRef(0);
const yawRef = useRef(object.rotation[1]);
const pitchRef = useRef(0);
const mouseRef = useRef({ x: 0, y: 0 });
const jumpPressedRef = useRef(false);
+67
View File
@@ -0,0 +1,67 @@
import { Html } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import * as THREE from "three";
import type { RuntimeObjectInstance } from "../types";
import { debugLabelsStore } from "../state/debugLabelsStore";
import { state } from "../state";
import { observer } from "mobx-react-lite";
export const DebugLabel = observer(function ({ obj }: { obj: RuntimeObjectInstance; }) {
const groupRef = useRef<THREE.Group>(null);
const rb = obj.rb();
const texts = [
`${state.world.getObjectTypeById(obj.typeId)?.name} ${obj.id}`,
`position: ${obj.position}`,
`rotation: ${obj.rotation}`,
...Object.entries(debugLabelsStore.getAll(obj.id))
.map(([k, v]) => `${k}: ${v}`),
];
if (rb) {
try {
const t = rb.translation();
texts.push(...[
`rb position: ${[t.x, t.y, t.z]}`,
]);
} catch {
texts.push('rb: stale handle');
}
}
useFrame(() => {
if (!groupRef.current)
return;
const [px, py, pz] = obj.position;
// const bb = obj.cache.boundingBox;
// const halfHeight = bb ? (bb.max[1] - bb.min[1]) / 2 : 1;
// groupRef.current.position.set(px, py + halfHeight + 0.5, pz);
groupRef.current.position.set(px, py, pz);
});
const text = texts.join('\n');
if (!text)
return null;
return (
<group ref={groupRef}>
<Html center sprite>
<div style={{
background: 'rgba(0,0,0,0.65)',
color: '#fff',
padding: '2px 7px',
borderRadius: 4,
fontSize: 12,
fontFamily: 'monospace',
whiteSpace: 'pre',
pointerEvents: 'none',
userSelect: 'none',
}}>
{text}
</div>
</Html>
</group>
);
});
@@ -0,0 +1,25 @@
import { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { debugLabelsStore } from "../state/debugLabelsStore";
import { DebugLabel } from "./DebugLabel";
import type { RuntimeScene } from "../types";
export const DebugLabelsView = observer(function ({ scene }: { scene: RuntimeScene }) {
useEffect(
() => {
return () => { debugLabelsStore.clear(); };
},
[],
);
return <>
{
Object.entries(scene.objects)
.map(([objectId, obj]) => {
return <DebugLabel key={objectId} obj={obj} />;
})
}
</>;
});
+1
View File
@@ -1,5 +1,6 @@
import { observer } from "mobx-react-lite";
import { SceneView } from "./SceneView";
import { DebugLabelsView } from "./DebugLabelsView";
import { state } from "../state";
import { useFrame } from "@react-three/fiber";
import { Suspense } from "react";
+29 -8
View File
@@ -1,7 +1,9 @@
import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
import type { ObjectInstance, R3, Runtime } from "../types";
import { useMemo, useRef, type RefObject } from "react";
import type { Group } from "three";
import { Euler, Quaternion, Vector3 } from "three";
import { TransformControls, useHelper } from "@react-three/drei";
import { BoxHelper } from "three";
import { state } from "../state";
@@ -16,12 +18,13 @@ type ObjectEditorViewProps = {
type SelectionOverlayProps = {
objectId: string;
ref: RefObject<ObjectViewInternalHandle | null>;
onTransformChange: () => void;
onTransformEnd: () => void;
}
// Separate observer so only the 2 affected instances (selected/deselected)
// re-render on selection change, not all N objects in the scene.
const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: SelectionOverlayProps) {
const SelectionOverlay = observer(function ({ objectId, ref, onTransformChange, onTransformEnd }: SelectionOverlayProps) {
const isSelected = state.worldEditor.isEnabled &&
state.worldEditor.selection?.type === 'object' &&
state.worldEditor.selection.id === objectId;
@@ -45,6 +48,7 @@ const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: S
<TransformControls
object={groupRef as RefObject<Group>}
mode={selectionMode}
onChange={onTransformChange}
onMouseUp={onTransformEnd}
/>
);
@@ -72,21 +76,38 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
}
}
function handleTransformChange() {
const group = dataRef.current?.group;
if (!group) return;
const pos = group.getWorldPosition(new Vector3());
const quat = group.getWorldQuaternion(new Quaternion());
const euler = new Euler().setFromQuaternion(quat);
runInAction(() => {
object.position = pos.toArray() as R3;
object.rotation = [euler.x, euler.y, euler.z];
});
}
function handleTransformEnd() {
const group = dataRef.current?.group;
if (group)
state.worldEditor.setObjectTransform(
object.id,
group.position.toArray(),
group.rotation.toArray().slice(0, 3) as R3,
group.scale.toArray(),
);
if (!group) return;
const pos = group.getWorldPosition(new Vector3());
const quat = group.getWorldQuaternion(new Quaternion());
const euler = new Euler().setFromQuaternion(quat);
const scale = group.getWorldScale(new Vector3());
state.worldEditor.setObjectTransform(
object.id,
pos.toArray(),
[euler.x, euler.y, euler.z],
scale.toArray(),
);
}
return (<>
<SelectionOverlay
objectId={object.id}
ref={dataRef}
onTransformChange={handleTransformChange}
onTransformEnd={handleTransformEnd}
/>
<ObjectViewInternal
+4
View File
@@ -2,9 +2,13 @@ import { observer } from "mobx-react-lite";
import { LeftPanel } from "./LeftPanel";
import { MainPanel } from "./MainPanel";
import './Panels.scss';
import { state } from "../state";
export const Panels = observer(function () {
if (state.game)
return null;
return <div className="overlay-panels">
<LeftPanel />
<MainPanel />
+6 -4
View File
@@ -1,7 +1,8 @@
import { observer } from "mobx-react-lite";
import type { RuntimeScene } from "../types";
import type { RuntimeGameObjectInstance, RuntimeScene } from "../types";
import { GameObjectView } from "./GameObjectView";
import { ObjectEditorView } from "./ObjectEditorView";
import { DebugLabelsView } from "./DebugLabelsView";
type SceneViewProps = {
scene: RuntimeScene,
@@ -14,10 +15,11 @@ export const SceneView = observer(function (props: SceneViewProps) {
<directionalLight position={[5, 5, 5]} intensity={1} />
{
Object.values(props.scene.objects).map((obj) => (
props.editMode
? <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
: <GameObjectView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
!props.editMode
? <GameObjectView key={obj.id} object={obj as RuntimeGameObjectInstance} isPlayer={props.scene.playerObjectId === obj.id} />
: <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
))
}
<DebugLabelsView scene={props.scene} />
</>);
});
+5 -2
View File
@@ -7,7 +7,7 @@ import { GameView } from './GameView';
import { SceneEditorView } from './SceneEditorView';
import { JoystickView } from './JoystickView';
import type { RefObject } from 'react';
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled } from '@tabler/icons-react';
import { IconPlayerPlayFilled, IconPlayerPauseFilled, IconPlayerStopFilled, IconPlayerSkipForwardFilled } from '@tabler/icons-react';
import { RenderInfoUpdater } from './renderInfo';
export const ThreeView = observer(function () {
@@ -37,7 +37,10 @@ export const ThreeView = observer(function () {
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
{
state.game!.isPaused
? <button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
? <>
<button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
<button onClick={() => state.game!.step()}><IconPlayerSkipForwardFilled size="1em" /></button>
</>
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
}
</>
@@ -4,8 +4,80 @@
display: flex;
flex-direction: column;
.script-editor-toolbar {
display: flex;
gap: 6px;
padding: 4px 6px;
background: #2a2a2a;
button {
padding: 2px 10px;
font-size: 12px;
cursor: pointer;
}
}
.blockly-container {
flex: 1;
min-height: 0;
}
.script-editor-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.script-editor-popup {
background: #1e1e1e;
color: #d4d4d4;
border: 1px solid #444;
border-radius: 6px;
width: 70vw;
max-height: 75vh;
display: flex;
flex-direction: column;
overflow: hidden;
.script-editor-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #444;
font-size: 13px;
font-weight: bold;
.script-editor-popup-actions {
display: flex;
gap: 8px;
align-items: center;
}
button {
background: none;
border: none;
color: #aaa;
cursor: pointer;
font-size: 13px;
line-height: 1;
&:hover { color: #fff; }
}
}
.script-editor-popup-content {
flex: 1;
overflow: auto;
margin: 0;
padding: 12px;
font-size: 12px;
font-family: monospace;
white-space: pre;
}
}
}
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { runInAction } from "mobx";
import * as Blockly from "blockly";
@@ -39,12 +39,22 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
{ kind: 'block', type: 'player_object_value' },
],
},
{
kind: 'category',
name: 'Loops',
colour: '50',
contents: [
{ kind: 'block', type: 'loop_forever' },
],
},
{
kind: 'category',
name: 'Actions',
colour: '20',
contents: [
{ kind: 'block', type: 'console_log_action' },
{ kind: 'block', type: 'move_to_position_action' },
{ kind: 'block', type: 'move_in_direction_action' },
{ kind: 'block', type: 'physics_apply_impulse_action' },
{ kind: 'block', type: 'wait_seconds_action' },
],
@@ -65,6 +75,7 @@ type ScriptEditorViewProps = {
}
export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorViewProps) {
const [popup, setPopup] = useState<{ title: string; content: string } | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const workspaceRef = useRef<Blockly.WorkspaceSvg | null>(null);
const objectTypeRef = useRef(objectType);
@@ -86,8 +97,6 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
const serialized = JSON.stringify(Blockly.serialization.workspaces.save(workspace));
current.program = serialized;
console.log(serialized);
try {
current.javascript = generateCode(workspace);
}
@@ -95,7 +104,6 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
console.log(err);
current.javascript = undefined;
}
console.log(current.javascript);
});
};
workspace.addChangeListener(listener);
@@ -139,7 +147,34 @@ export const ScriptEditorView = observer(function ({ objectType }: ScriptEditorV
}
}, [objectType.id, objectType.program]);
const showProgram = () => {
const raw = objectType.program;
setPopup({ title: 'Program JSON', content: raw ? JSON.stringify(JSON.parse(raw), null, 2) : '(empty)' });
};
const showJavascript = () => {
setPopup({ title: 'JavaScript', content: objectType.javascript ?? '(empty)' });
};
return <div className="script-editor">
<div className="script-editor-toolbar">
<button onClick={showProgram}>Program JSON</button>
<button onClick={showJavascript}>JavaScript</button>
</div>
<div ref={containerRef} className="blockly-container" />
{popup && (
<div className="script-editor-overlay" onClick={() => setPopup(null)}>
<div className="script-editor-popup" onClick={e => e.stopPropagation()}>
<div className="script-editor-popup-header">
<span>{popup.title}</span>
<div className="script-editor-popup-actions">
<button onClick={() => navigator.clipboard.writeText(popup.content)}>Copy all</button>
<button onClick={() => setPopup(null)}>✕</button>
</div>
</div>
<pre className="script-editor-popup-content">{popup.content}</pre>
</div>
</div>
)}
</div>;
});
+10 -3
View File
@@ -4,6 +4,8 @@ import { clone } from "../utils";
import { populateRuntimeScene } from "../utils/runtime";
import { ScriptGameEventBus } from "../scripts/eventBus";
import * as ScriptActions from "../scripts/actions";
import { positionOfObject } from "../scripts/positionOfObject";
import { debugLabelsStore } from "../state/debugLabelsStore";
import type { ScriptCoroutines } from "../scripts/coroutines";
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -51,6 +53,7 @@ export class GameFactory {
const eventBus = new ScriptGameEventBus(coroutines);
const scriptHelpers = {
positionOfObject,
// convert all ScriptActions imported to a structure factory(coroutine):
// {
// WaitSeconds: (seconds) => new WaitSecondsScriptAction(coroutine, seconds),
@@ -71,7 +74,7 @@ export class GameFactory {
.filter((ot) => ot.javascript)
.forEach((ot) => {
try {
const javascript = `(function gameScript(eventBus, helpers, thisObject, thisObjectType) {${ot.javascript}})`;
const javascript = `(function gameScript(eventBus, helpers, thisObject, thisObjectType) {${ot.javascript}})\n//# sourceURL=GameScript:${ot.name}.js`;
const gameScript = eval(javascript);
Object.values(scene.objects)
@@ -86,16 +89,20 @@ export class GameFactory {
const localScriptHelpers = {
...scriptHelpers,
actions: scriptHelpers.actions({ thisObject: o, thisObjectType: ot }),
debug: {
show: (key: string, text: string) => debugLabelsStore.setOne(o.id, key, text),
clear: (key: string) => debugLabelsStore.deleteOne(o.id, key),
},
};
gameScript(eventBus, localScriptHelpers, o, ot);
}
catch (err) {
console.log('Error running object instance script:\n' + err);
console.error(`[GameScript:${ot.name}] Error running script for object ${o.id}:`, err);
}
});
}
catch (err) {
console.log('Error evaluating object type script:\n' + err);
console.error(`[GameScript:${ot.name}] Error evaluating script:`, err);
}
});
+2
View File
@@ -2,3 +2,5 @@ export * from './waitConditionAction';
export * from './waitFramesAction';
export * from './waitSecondsAction';
export * from './applyImpulseAction';
export * from './moveToPositionAction';
export * from './moveInDirectionAction';
@@ -0,0 +1,44 @@
import { ScriptAction, type ScriptCoroutineContext, type V3 } from "../../types";
const EPS = 0.05;
export class MoveInDirectionScriptAction extends ScriptAction {
private direction: V3;
private distance: number;
private speed: number;
private traveled: number = 0;
constructor(coroutine: ScriptCoroutineContext, direction: V3, distance: number, speed: number) {
super(coroutine);
const len = Math.sqrt(direction[0] ** 2 + direction[1] ** 2 + direction[2] ** 2);
this.direction = len > 1e-6
? [direction[0] / len, direction[1] / len, direction[2] / len]
: [0, 0, 0];
this.distance = distance;
this.speed = speed;
}
public tick(deltaTime: number): boolean {
const rb = this.coroutine.thisObject.rb();
if (!rb)
return true;
const remaining = this.distance - this.traveled;
if (remaining < EPS)
return true;
const step = Math.min(this.speed * deltaTime, remaining);
const { x, y, z } = rb.translation();
rb.setTranslation({
x: x + this.direction[0] * step,
y: y + this.direction[1] * step,
z: z + this.direction[2] * step,
}, true);
rb.setLinvel({ x: 0, y: 0, z: 0 }, true);
this.traveled += step;
return this.traveled >= this.distance;
}
}
@@ -0,0 +1,44 @@
import { ScriptAction, type ScriptCoroutineContext, type V3 } from "../../types";
export class MoveToPositionScriptAction extends ScriptAction {
private targetPosition: V3;
private speed: number;
constructor(coroutine: ScriptCoroutineContext, targetPosition: V3, speed: number) {
super(coroutine);
this.targetPosition = targetPosition;
this.speed = speed;
}
public tick(deltaTime: number): boolean {
const rb = this.coroutine.thisObject.rb();
if (!rb)
return true;
const { x: px, y: py, z: pz } = rb.translation();
const dx = this.targetPosition[0] - px;
const dy = this.targetPosition[1] - py;
const dz = this.targetPosition[2] - pz;
const distSq = dx * dx + dy * dy + dz * dz;
if (distSq < 0.0001) {
rb.setLinvel({ x: 0, y: 0, z: 0 }, true);
return true;
}
const dist = Math.sqrt(distSq);
const step = this.speed * deltaTime;
if (step >= dist) {
rb.setTranslation({ x: this.targetPosition[0], y: this.targetPosition[1], z: this.targetPosition[2] }, true);
rb.setLinvel({ x: 0, y: 0, z: 0 }, true);
return true;
}
const scale = step / dist;
rb.setTranslation({ x: px + dx * scale, y: py + dy * scale, z: pz + dz * scale }, true);
rb.setLinvel({ x: 0, y: 0, z: 0 }, true);
return false;
}
}
+14 -15
View File
@@ -1,33 +1,32 @@
import type { ScriptAction, ScriptCoroutine } from "../types";
import type { NamedScriptCoroutine, ScriptAction } from "../types";
type CoroutineItem = NamedScriptCoroutine & {
currentAction?: ScriptAction;
};
export class ScriptCoroutines {
private items: { coroutine: ScriptCoroutine, currentAction?: ScriptAction }[] = [];
private items: CoroutineItem[] = [];
public push(coroutines: ScriptCoroutine[]) {
this.items.push(
...coroutines
.map((c) => ({ coroutine: c })),
);
public push(coroutines: NamedScriptCoroutine[]) {
this.items.push(...coroutines.map((c) => ({ ...c })));
}
public tick(deltaTime: number): void {
// execute coroutines one by one and remove the ones that have finished
this.items = this.items
.filter((c) => !this.tickItem(c, deltaTime));
this.items = this.items.filter((c) => !this.tickItem(c, deltaTime));
}
private tickItem(item: { coroutine: ScriptCoroutine, currentAction?: ScriptAction }, deltaTime: number): boolean {
private tickItem(item: CoroutineItem, deltaTime: number): boolean {
if (!item.currentAction) {
try {
const yieldResult = item.coroutine.next();
if (yieldResult.done)
return true; // coroutine exhausted
return true;
item.currentAction = yieldResult.value;
}
catch (err) {
console.error(err);
return true; // coroutine failed to yield an action or return
console.error(`[${item.name}] Script error:`, err);
return true;
}
}
@@ -37,7 +36,7 @@ export class ScriptCoroutines {
item.currentAction = undefined;
}
catch (err) {
console.error(err);
console.error(`[${item.name}] Action error in ${item.currentAction?.constructor.name}:`, err);
item.currentAction = undefined;
}
+5 -8
View File
@@ -36,15 +36,12 @@ export class ScriptGameEventBus {
}
emit(event: string, context: Omit<ScriptContext, keyof ScriptCoroutineContext>): void {
console.log('emitting ' + event);
const coroutines = this.handlers[event]
?.map((h) => {
console.log(`passing ${event} to ${h.handler}(${JSON.stringify(context)}, ${JSON.stringify(h.coroutine)})`);
return h.handler({ ...context, ...h.coroutine });
});
console.log(coroutines);
?.map((h) => ({
owner: h.coroutine.thisObject.id,
name: `${h.coroutine.thisObjectType.name}[${h.coroutine.thisObject.id}]:${event}`,
coroutine: h.handler({ ...context, ...h.coroutine }),
}));
if (coroutines)
this.coroutines.push(coroutines);
+10
View File
@@ -0,0 +1,10 @@
import type { RuntimeGameObjectInstance, V3 } from "../types";
export function positionOfObject(obj: RuntimeGameObjectInstance): V3 {
const t = obj.rb()?.translation();
const result: V3 = t
? [t.x, t.y, t.z]
: [obj.position[0], obj.position[1], obj.position[2]];
return result;
}
+39
View File
@@ -0,0 +1,39 @@
import { makeAutoObservable } from "mobx";
class DebugLabelsStore {
public labels = new Map<string, Record<string, string>>();
constructor() {
makeAutoObservable(this);
}
public getAll(objectId: string): Record<string, string> {
return this.labels.get(objectId) ?? {};
}
public getOne(objectId: string, key: string): string | undefined {
return this.getAll(objectId)[key];
}
public setOne(objectId: string, key: string, text: string) {
if (!this.labels.has(objectId))
this.labels.set(objectId, {});
this.labels.get(objectId)![key] = text;
}
public deleteAll(objectId: string) {
this.labels.delete(objectId);
}
public deleteOne(objectId: string, key: string) {
if (this.labels.has(objectId)) {
delete (this.labels.get(objectId)![key]);
}
}
public clear() {
this.labels.clear();
}
}
export const debugLabelsStore = new DebugLabelsStore();
+4
View File
@@ -124,6 +124,10 @@ export class GameState {
if (this.isPaused)
return;
this.step(deltaTime);
}
public step(deltaTime: number = 1 / 60): void {
this.coroutines.tick(deltaTime);
this.time += deltaTime;
+2
View File
@@ -4,6 +4,8 @@ import type { ObjectInstance } from "./object";
export type RigidBodyHandle = {
applyImpulse(impulse: { x: number; y: number; z: number }, wakeUp: boolean): void;
setLinvel(vel: { x: number; y: number; z: number }, wakeUp: boolean): void;
setTranslation(translation: { x: number; y: number; z: number }, wakeUp: boolean): void;
translation(): { x: number; y: number; z: number };
linvel(): { x: number; y: number; z: number };
angvel(): { x: number; y: number; z: number };
+6 -1
View File
@@ -1,7 +1,12 @@
import type { ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model";
import type { ObjectInstance, ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model";
import type { ScriptAction } from "./actions";
export type ScriptCoroutine = Generator<ScriptAction, void, void>;
export type NamedScriptCoroutine = {
name: string;
owner: ObjectInstance['id'];
coroutine: ScriptCoroutine;
}
export type ScriptCoroutineContext = {
thisObject: RuntimeGameObjectInstance,
+6 -1
View File
@@ -1,5 +1,5 @@
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, V3, World } from "../../types";
import type { GameObjectInstance, ObjectInstance, ObjectType, RuntimeGameObjectInstance, RuntimeObjectInstance, V3, World } from "../../types";
import { buildObjectTrimesh } from "../graphics/mesh";
export function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
@@ -37,3 +37,8 @@ export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: W
const { cache: _cache, rb: _rb, ...result } = object;
return result;
}
export function isGameObject(obj: ObjectInstance): obj is GameObjectInstance {
return 'version' in obj;
}