Compare commits
No commits in common. "b017beca2d8808cc1b61776b908ffd81c6774f54" and "b3ed15fa30830f4a1ee98b00650dcdf0fd5e8f93" have entirely different histories.
b017beca2d
...
b3ed15fa30
|
|
@ -13,7 +13,6 @@
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.6.1",
|
"@react-three/fiber": "^9.6.1",
|
||||||
"@react-three/rapier": "^2.2.0",
|
"@react-three/rapier": "^2.2.0",
|
||||||
"@tabler/icons-react": "^3.44.0",
|
|
||||||
"@types/three": "^0.184.1",
|
"@types/three": "^0.184.1",
|
||||||
"blockly": "^12.5.1",
|
"blockly": "^12.5.1",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@ importers:
|
||||||
'@react-three/rapier':
|
'@react-three/rapier':
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0(@react-three/fiber@9.6.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(three@0.184.0))(react@19.2.6)(three@0.184.0)
|
version: 2.2.0(@react-three/fiber@9.6.1(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(three@0.184.0))(react@19.2.6)(three@0.184.0)
|
||||||
'@tabler/icons-react':
|
|
||||||
specifier: ^3.44.0
|
|
||||||
version: 3.44.0(react@19.2.6)
|
|
||||||
'@types/three':
|
'@types/three':
|
||||||
specifier: ^0.184.1
|
specifier: ^0.184.1
|
||||||
version: 0.184.1
|
version: 0.184.1
|
||||||
|
|
@ -540,14 +537,6 @@ packages:
|
||||||
'@rolldown/pluginutils@1.0.1':
|
'@rolldown/pluginutils@1.0.1':
|
||||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||||
|
|
||||||
'@tabler/icons-react@3.44.0':
|
|
||||||
resolution: {integrity: sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>= 16'
|
|
||||||
|
|
||||||
'@tabler/icons@3.44.0':
|
|
||||||
resolution: {integrity: sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==}
|
|
||||||
|
|
||||||
'@tweenjs/tween.js@23.1.3':
|
'@tweenjs/tween.js@23.1.3':
|
||||||
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||||
|
|
||||||
|
|
@ -2056,13 +2045,6 @@ snapshots:
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.1': {}
|
'@rolldown/pluginutils@1.0.1': {}
|
||||||
|
|
||||||
'@tabler/icons-react@3.44.0(react@19.2.6)':
|
|
||||||
dependencies:
|
|
||||||
'@tabler/icons': 3.44.0
|
|
||||||
react: 19.2.6
|
|
||||||
|
|
||||||
'@tabler/icons@3.44.0': {}
|
|
||||||
|
|
||||||
'@tweenjs/tween.js@23.1.3': {}
|
'@tweenjs/tween.js@23.1.3': {}
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ Blockly.Blocks['console_log_action'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendValueInput('VALUE')
|
this.appendValueInput('VALUE')
|
||||||
.setAlign(Blockly.inputs.Align.RIGHT)
|
.setAlign(Blockly.inputs.Align.RIGHT)
|
||||||
.setCheck('ObjectType')
|
|
||||||
.appendField('print');
|
.appendField('print');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false)
|
||||||
this.setPreviousStatement(true, null);
|
this.setPreviousStatement(true, null);
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export * from './consoleLog';
|
export * from './consoleLog';
|
||||||
export * from './physics';
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['physics_apply_impulse_action'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput('')
|
|
||||||
.appendField('Push me in direction of');
|
|
||||||
this.appendValueInput('POSITION')
|
|
||||||
.setCheck('DPos');
|
|
||||||
this.appendDummyInput('')
|
|
||||||
.appendField('with force')
|
|
||||||
.appendField(new Blockly.FieldNumber(1, -10, 10, 0.01), 'FORCE');
|
|
||||||
|
|
||||||
|
|
||||||
this.setInputsInline(true)
|
|
||||||
this.setPreviousStatement(true, null);
|
|
||||||
this.setNextStatement(true, null);
|
|
||||||
this.setTooltip('Push me to a position');
|
|
||||||
this.setColour(150);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['physics_apply_impulse_action'] = function (block, generator) {
|
|
||||||
const positionValue = generator.valueToCode(block, 'POSITION', Order.ATOMIC);
|
|
||||||
const forceValue = block.getFieldValue('FORCE');
|
|
||||||
return `api.applyImpulse(${positionValue}, ${forceValue});\n`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from './applyImpulse';
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['object_clicked_event'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput()
|
|
||||||
.appendField('When I\'m clicked');
|
|
||||||
this.setStyle('event_hat');
|
|
||||||
this.setNextStatement(true);
|
|
||||||
this.setTooltip('Fires when an object is clicked');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['object_clicked_event'] = function (_block, _generator) {
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './gameStart';
|
export * from './gameStart';
|
||||||
export * from './objectTouchedMe';
|
export * from './objectTouchedMe';
|
||||||
export * from './playerTouchedMe';
|
export * from './playerTouchedMe';
|
||||||
export * from './imClicked';
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import * as Blockly from "blockly";
|
import * as Blockly from "blockly";
|
||||||
import { javascriptGenerator } from "blockly/javascript";
|
import { javascriptGenerator } from "blockly/javascript";
|
||||||
|
import { getObjectTypeDropDownOptions } from "../fieldTypes/objectType";
|
||||||
|
|
||||||
Blockly.Blocks['object_touch_start_event'] = {
|
Blockly.Blocks['object_touch_start_event'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendDummyInput('NAME')
|
this.appendDummyInput()
|
||||||
.appendField('When');
|
.appendField('When')
|
||||||
this.appendValueInput('NAME')
|
.appendField(
|
||||||
.setCheck('Object');
|
new Blockly.FieldDropdown(getObjectTypeDropDownOptions),
|
||||||
this.appendDummyInput('NAME')
|
'OBJECT_TYPE',
|
||||||
|
)
|
||||||
.appendField('touches me');
|
.appendField('touches me');
|
||||||
this.setStyle('event_hat');
|
this.setStyle('event_hat');
|
||||||
this.setNextStatement(true);
|
this.setNextStatement(true);
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from './objectType';
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import * as Blockly from "blockly";
|
import { state } from "../../state/rootState";
|
||||||
|
import { toJS } from "mobx";
|
||||||
|
|
||||||
export class ObjecTypeField extends Blockly.Field {
|
export function getObjectTypeDropDownOptions(): [string, string][] {
|
||||||
constructor(value: any, validator: any) {
|
const types = Object.values(toJS(state.world.data.objectTypes));
|
||||||
super(value, validator);
|
const values: [string, string][] = [
|
||||||
|
['Any object', '*'],
|
||||||
this.SERIALIZABLE = true;
|
...types.map(t => [t.name, t.id] as [string, string]),
|
||||||
}
|
];
|
||||||
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
Blockly.fieldRegistry.register('field_object_type', ObjecTypeField);
|
|
||||||
|
|
@ -14,23 +14,14 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
|
||||||
}
|
}
|
||||||
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT);
|
body = javascriptGenerator.prefixLines(body, javascriptGenerator.INDENT);
|
||||||
|
|
||||||
// const args = Array.from(block.getFields())
|
const args = Array.from(block.getFields())
|
||||||
// .filter(field => field.name !== undefined)
|
.filter(field => field.name !== undefined)
|
||||||
// .map(field => `${field.name}: ${JSON.stringify(field.getValue())}`)
|
.map(field => `${field.name}: ${JSON.stringify(field.getValue())}`)
|
||||||
// .join(', ');
|
.join(', ');
|
||||||
|
|
||||||
//TODO use args
|
//TODO use args
|
||||||
|
|
||||||
result += `onGameEvent(
|
result += `onGameEvent('${block.type.replace(/_event$/, '')}', function(context) {\n${body}});\n`;
|
||||||
'${block.type.replace(/_event$/, '')}',
|
|
||||||
function(context) {
|
|
||||||
// console.dir({context, api, object, objectType});
|
|
||||||
if (context.object && (context.object.id !== object.id))
|
|
||||||
return;
|
|
||||||
${body}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,3 @@ export * from './values';
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
export * from './generateCode';
|
export * from './generateCode';
|
||||||
export * from './fieldTypes';
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['any_object_of_type_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput('NAME')
|
|
||||||
.appendField('Any object of type');
|
|
||||||
this.appendValueInput('NAME')
|
|
||||||
.setCheck('ObjectType');
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setOutput(true, 'Object');
|
|
||||||
this.setTooltip('Any object of type');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['any_object_of_type_value'] = function (_block, _generator) {
|
|
||||||
return ['123', Order.NONE];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -4,16 +4,16 @@ import { javascriptGenerator, Order } from "blockly/javascript";
|
||||||
Blockly.Blocks['current_object_value'] = {
|
Blockly.Blocks['current_object_value'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendEndRowInput('NAME')
|
this.appendEndRowInput('NAME')
|
||||||
.appendField('Me');
|
.appendField('Current object');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false)
|
||||||
this.setOutput(true, 'Object');
|
this.setOutput(true, 'String');
|
||||||
this.setTooltip('Returns current object instance');
|
this.setTooltip('Returns id of current object instance');
|
||||||
this.setColour(315);
|
this.setColour(315);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) {
|
javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) {
|
||||||
return ['object', Order.NONE];
|
return ['object.id', Order.NONE];
|
||||||
};
|
};
|
||||||
|
|
||||||
export { };
|
export { };
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ import { javascriptGenerator, Order } from "blockly/javascript";
|
||||||
Blockly.Blocks['current_object_type_value'] = {
|
Blockly.Blocks['current_object_type_value'] = {
|
||||||
init(this: Blockly.Block) {
|
init(this: Blockly.Block) {
|
||||||
this.appendEndRowInput('NAME')
|
this.appendEndRowInput('NAME')
|
||||||
.appendField('My type');
|
.appendField('Current object type');
|
||||||
this.setInputsInline(false)
|
this.setInputsInline(false)
|
||||||
this.setOutput(true, 'ObjectType');
|
this.setOutput(true, 'String');
|
||||||
this.setTooltip('Returns current object type');
|
this.setTooltip('Returns id of current object type');
|
||||||
this.setColour(315);
|
this.setColour(315);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) {
|
javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) {
|
||||||
return ['objectType', Order.NONE];
|
return ['objectType.id', Order.NONE];
|
||||||
};
|
};
|
||||||
|
|
||||||
export { };
|
export { };
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['dpos_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput()
|
|
||||||
.appendField('Direction [')
|
|
||||||
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'X')
|
|
||||||
.appendField(',')
|
|
||||||
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Y')
|
|
||||||
.appendField(',')
|
|
||||||
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Z');
|
|
||||||
this.appendDummyInput()
|
|
||||||
.appendField(']');
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setOutput(true, 'DPos');
|
|
||||||
this.setTooltip('Direction [dx,dy,dz]');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['dpos_value'] = function (block, _generator) {
|
|
||||||
const xValue = block.getFieldValue('X');
|
|
||||||
const yValue = block.getFieldValue('Y');
|
|
||||||
const zValue = block.getFieldValue('Z');
|
|
||||||
return [`[${xValue},${yValue},${zValue}]`, Order.ATOMIC];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['direction_to_position_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput()
|
|
||||||
.appendField('Direction to');
|
|
||||||
this.appendValueInput('TARGET')
|
|
||||||
.setCheck('Pos');
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setOutput(true, 'DPos');
|
|
||||||
this.setTooltip('Direction to a position');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,9 +1,2 @@
|
||||||
export * from './currentObject';
|
export * from './currentObject';
|
||||||
export * from './currentObjectType';
|
export * from './currentObjectType';
|
||||||
export * from './anyObjectOfType';
|
|
||||||
export * from './objectType';
|
|
||||||
export * from './positionOfObject';
|
|
||||||
export * from './position';
|
|
||||||
export * from './direction';
|
|
||||||
export * from './objectById';
|
|
||||||
export * from './directionToPosition';
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['object_by_id_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendEndRowInput()
|
|
||||||
.appendField('Object with id')
|
|
||||||
.appendField(new Blockly.FieldTextInput(''), 'TARGET_ID');
|
|
||||||
this.setInputsInline(false)
|
|
||||||
this.setOutput(true, 'Object');
|
|
||||||
this.setTooltip('Returns object by id, if any');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['object_by_id_value'] = function (block, _generator) {
|
|
||||||
const targetIdValue = block.getFieldValue('TARGET_ID');
|
|
||||||
return [`context.scene.objects['${targetIdValue}']`, Order.ATOMIC];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
import { toJS } from "mobx";
|
|
||||||
import { state } from "../../state";
|
|
||||||
|
|
||||||
export function getObjectTypeDropDownOptions(): [string, string][] {
|
|
||||||
const types = Object.values(toJS(state.world.data.objectTypes));
|
|
||||||
const values: [string, string][] = [
|
|
||||||
['Any object', '*'],
|
|
||||||
...types.map(t => [t.name, t.id] as [string, string]),
|
|
||||||
];
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
Blockly.Blocks['object_type_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendEndRowInput('NAME')
|
|
||||||
.appendField(
|
|
||||||
new Blockly.FieldDropdown(getObjectTypeDropDownOptions),
|
|
||||||
'OBJECT_TYPE',
|
|
||||||
);
|
|
||||||
this.setInputsInline(false);
|
|
||||||
this.setOutput(true, 'ObjectType');
|
|
||||||
this.setTooltip('Object type');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) {
|
|
||||||
return ['objectType', Order.NONE];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['pos_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput('POSITION')
|
|
||||||
.appendField('Position [')
|
|
||||||
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'X')
|
|
||||||
.appendField(',')
|
|
||||||
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Y')
|
|
||||||
.appendField(',')
|
|
||||||
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 0.01), 'Z');
|
|
||||||
this.appendDummyInput()
|
|
||||||
.appendField(']');
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setOutput(true, 'Pos');
|
|
||||||
this.setTooltip('Position [x,y,z]');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['pos_value'] = function (block, _generator) {
|
|
||||||
const xValue = block.getFieldValue('X');
|
|
||||||
const yValue = block.getFieldValue('Y');
|
|
||||||
const zValue = block.getFieldValue('Z');
|
|
||||||
return [`[${xValue},${yValue},${zValue}]`, Order.ATOMIC];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import * as Blockly from "blockly";
|
|
||||||
import { javascriptGenerator, Order } from "blockly/javascript";
|
|
||||||
|
|
||||||
Blockly.Blocks['position_of_object_value'] = {
|
|
||||||
init(this: Blockly.Block) {
|
|
||||||
this.appendDummyInput()
|
|
||||||
.appendField('Position of');
|
|
||||||
this.appendValueInput('TARGET')
|
|
||||||
.setCheck('Object');
|
|
||||||
this.setInputsInline(true);
|
|
||||||
this.setOutput(true, 'Pos');
|
|
||||||
this.setTooltip('Position of an object');
|
|
||||||
this.setColour(315);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
javascriptGenerator.forBlock['position_of_object_value'] = function (block, generator) {
|
|
||||||
const targetValue = generator.valueToCode(block, 'TARGET', Order.ATOMIC);
|
|
||||||
return [`${targetValue}.position`, Order.NONE];
|
|
||||||
};
|
|
||||||
|
|
||||||
export { };
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { ObjectType, RuntimeGameObjectInstance } from "../types";
|
import type { Character } from "../types";
|
||||||
import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewInternal";
|
import { SyncRigidBody } from "./SyncRigidBody";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { state } from "../state";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { Euler, Quaternion, Vector3 } from "three";
|
import { Euler, Quaternion, Vector3 } from "three";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useKeyboardControls } from "@react-three/drei";
|
import { useKeyboardControls } from "@react-three/drei";
|
||||||
import { useRapier, useBeforePhysicsStep } from "@react-three/rapier";
|
import { useRapier, useBeforePhysicsStep, type RapierRigidBody, type RapierCollider, RoundCuboidCollider } from "@react-three/rapier";
|
||||||
import { joystickValues } from "../joystickInput";
|
import { joystickValues } from "../joystickInput";
|
||||||
import { state } from "../state";
|
|
||||||
|
|
||||||
const SPEED = 5;
|
const SPEED = 5;
|
||||||
const JUMP_SPEED = 8;
|
const JUMP_SPEED = 8;
|
||||||
const GRAVITY = 20;
|
const GRAVITY = 20;
|
||||||
const SENSITIVITY = 0.002;
|
const SENSITIVITY = 0.002;
|
||||||
|
const SHOULDER_OFFSET = new Vector3(0.1, 1.5, 2.5);
|
||||||
const LOOK_RATE = 2000;
|
const LOOK_RATE = 2000;
|
||||||
|
|
||||||
|
// recreate private types
|
||||||
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
type RapierWorldCreateCharacterControllerFunction = ReturnType<typeof useRapier>['world']['createCharacterController'];
|
||||||
type KinematicCharacterController = ReturnType<RapierWorldCreateCharacterControllerFunction>;
|
type KinematicCharacterController = ReturnType<RapierWorldCreateCharacterControllerFunction>;
|
||||||
|
|
||||||
|
|
@ -24,30 +26,19 @@ const _offset = new Vector3();
|
||||||
const _charPos = new Vector3();
|
const _charPos = new Vector3();
|
||||||
const _lookAt = new Vector3();
|
const _lookAt = new Vector3();
|
||||||
|
|
||||||
type PlayerObjectViewProps = {
|
type CharacterViewProps = {
|
||||||
object: RuntimeGameObjectInstance;
|
character: Character;
|
||||||
objectType: ObjectType;
|
editMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerObjectView = observer(function ({ object, objectType }: PlayerObjectViewProps) {
|
export const CharacterView = observer(function ({ character, editMode }: CharacterViewProps) {
|
||||||
const handleRef = useRef<ObjectViewInternalHandle>(null);
|
const pos = character.transform.position;
|
||||||
|
const rbRef = useRef<RapierRigidBody>(null);
|
||||||
|
const colliderRef = useRef<RapierCollider>(null);
|
||||||
const [, get] = useKeyboardControls();
|
const [, get] = useKeyboardControls();
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
const { world } = useRapier();
|
const { world } = useRapier();
|
||||||
|
|
||||||
const { shoulderOffset, lookAtY } = useMemo(() => {
|
|
||||||
const bb = object.cache.boundingBox;
|
|
||||||
const W = bb.max[0] - bb.min[0];
|
|
||||||
const H = bb.max[1] - bb.min[1];
|
|
||||||
const D = bb.max[2] - bb.min[2];
|
|
||||||
const radius = Math.sqrt(W * W + H * H + D * D);
|
|
||||||
const centerY = (bb.min[1] + bb.max[1]) / 2;
|
|
||||||
return {
|
|
||||||
shoulderOffset: new Vector3(W * 0.1, centerY + H * 0.3, bb.max[2] + radius),
|
|
||||||
lookAtY: centerY,
|
|
||||||
};
|
|
||||||
}, [object.id]);
|
|
||||||
|
|
||||||
const yawRef = useRef(0);
|
const yawRef = useRef(0);
|
||||||
const pitchRef = useRef(0);
|
const pitchRef = useRef(0);
|
||||||
const mouseRef = useRef({ x: 0, y: 0 });
|
const mouseRef = useRef({ x: 0, y: 0 });
|
||||||
|
|
@ -68,6 +59,7 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
}, [world]);
|
}, [world]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (editMode) return;
|
||||||
const canvas = gl.domElement;
|
const canvas = gl.domElement;
|
||||||
const onClick = () => canvas.requestPointerLock();
|
const onClick = () => canvas.requestPointerLock();
|
||||||
const onMouseMove = (e: MouseEvent) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
|
@ -81,15 +73,14 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
canvas.removeEventListener('click', onClick);
|
canvas.removeEventListener('click', onClick);
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
};
|
};
|
||||||
}, [gl]);
|
}, [gl, editMode]);
|
||||||
|
|
||||||
useBeforePhysicsStep((world) => {
|
useBeforePhysicsStep((world) => {
|
||||||
const rb = handleRef.current?.rb;
|
if (editMode) return;
|
||||||
if (!rb) return;
|
const rb = rbRef.current;
|
||||||
const collider = rb.collider(0);
|
const collider = colliderRef.current;
|
||||||
if (!collider) return;
|
|
||||||
const controller = controllerRef.current;
|
const controller = controllerRef.current;
|
||||||
if (!controller) return;
|
if (!rb || !collider || !controller) return;
|
||||||
if (state.game?.isPaused) return;
|
if (state.game?.isPaused) return;
|
||||||
|
|
||||||
const dt = world.timestep;
|
const dt = world.timestep;
|
||||||
|
|
@ -127,7 +118,8 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
});
|
});
|
||||||
|
|
||||||
useFrame(({ camera }, delta) => {
|
useFrame(({ camera }, delta) => {
|
||||||
const rb = handleRef.current?.rb;
|
if (editMode) return;
|
||||||
|
const rb = rbRef.current;
|
||||||
if (!rb) return;
|
if (!rb) return;
|
||||||
|
|
||||||
mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta;
|
mouseRef.current.x += joystickValues.look.x * LOOK_RATE * delta;
|
||||||
|
|
@ -140,18 +132,30 @@ export const PlayerObjectView = observer(function ({ object, objectType }: Playe
|
||||||
|
|
||||||
const t = rb.translation();
|
const t = rb.translation();
|
||||||
_charPos.set(t.x, t.y, t.z);
|
_charPos.set(t.x, t.y, t.z);
|
||||||
_offset.copy(shoulderOffset).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
|
_offset.copy(SHOULDER_OFFSET).applyEuler(_e.set(pitchRef.current, yawRef.current, 0, 'YXZ'));
|
||||||
camera.position.copy(_charPos).add(_offset);
|
camera.position.copy(_charPos).add(_offset);
|
||||||
_lookAt.set(t.x, t.y + lookAtY, t.z);
|
_lookAt.set(t.x, t.y + 1, t.z);
|
||||||
camera.lookAt(_lookAt);
|
camera.lookAt(_lookAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ObjectViewInternal
|
<SyncRigidBody
|
||||||
ref={handleRef}
|
ref={rbRef}
|
||||||
object={object}
|
type="kinematicPosition"
|
||||||
objectType={objectType}
|
colliders={false}
|
||||||
isPlayer
|
position={[pos[0] + 0.5, pos[1] + 0.5, pos[2] + 0.5]}
|
||||||
/>
|
onSync={() => { }}
|
||||||
|
>
|
||||||
|
{/* <BallCollider ref={colliderRef} args={[0.55]} /> */}
|
||||||
|
{/* <CapsuleCollider ref={colliderRef} args={[0.4, 0.5]} /> */}
|
||||||
|
{/* <CuboidCollider ref={colliderRef} args={[0.55, 0.4, 0.55]} /> */}
|
||||||
|
<RoundCuboidCollider ref={colliderRef} args={[0.4, 0.4, 0.4, 0.1]} />
|
||||||
|
<group>
|
||||||
|
<mesh rotation={[-Math.PI / 2, -Math.PI / 4, 0]}>
|
||||||
|
<coneGeometry args={[0.55, 0.8, 4]} />
|
||||||
|
<meshStandardMaterial color="yellow" />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</SyncRigidBody>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,17 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { RuntimeGameObjectInstance } from "../types";
|
import type { ObjectInstance, Runtime } from "../types";
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
import { ObjectViewInternal } from "./ObjectViewInternal";
|
import { ObjectViewInternal } from "./ObjectViewInternal";
|
||||||
import { PlayerObjectView } from "./CharacterView";
|
|
||||||
|
|
||||||
export type GameObjectViewProps = {
|
export type GameObjectViewProps = {
|
||||||
object: RuntimeGameObjectInstance;
|
object: Runtime<ObjectInstance>;
|
||||||
isPlayer: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GameObjectView = observer(function (props: GameObjectViewProps) {
|
export const GameObjectView = observer(function (props: GameObjectViewProps) {
|
||||||
|
|
||||||
const objectType = state.world.getObjectTypeById(props.object.typeId);
|
const objectType = state.world.getObjectTypeById(props.object.typeId);
|
||||||
if (!objectType)
|
if (!objectType)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (props.isPlayer)
|
return <ObjectViewInternal {...props} objectType={objectType} />
|
||||||
return <PlayerObjectView object={props.object as RuntimeGameObjectInstance} objectType={objectType} />;
|
|
||||||
|
|
||||||
function handleClick(): void {
|
|
||||||
state.game?.emitEvent('object_clicked', { object: props.object });
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ObjectViewInternal
|
|
||||||
{...props}
|
|
||||||
objectType={objectType}
|
|
||||||
onClick={handleClick}
|
|
||||||
/>;
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
display: flex;
|
display: block;
|
||||||
padding: 8px 8px;
|
padding: 8px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
@ -28,17 +28,6 @@
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
&>.title {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
& svg {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin: 2px;
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& details {
|
& details {
|
||||||
|
|
|
||||||
|
|
@ -14,30 +14,13 @@ export const MenuNodeView = observer(function ({ node }: { node: MenuNode }) {
|
||||||
ref.current.open = true;
|
ref.current.open = true;
|
||||||
}, [forceOpen]);
|
}, [forceOpen]);
|
||||||
|
|
||||||
const classNames = [];
|
|
||||||
if (node.selected?.())
|
|
||||||
classNames.push('selected');
|
|
||||||
if (node.className !== undefined)
|
|
||||||
classNames.push(node.className);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details ref={ref}>
|
<details ref={ref}>
|
||||||
<summary
|
<summary
|
||||||
className={classNames.join(' ')}
|
className={node.selected?.() ? 'selected' : ''}
|
||||||
onClick={() => node.onClick?.()}
|
onClick={() => node.onClick?.()}
|
||||||
>
|
>
|
||||||
<div className="title">{node.title}</div>
|
{node.title}
|
||||||
{node.actions && <div className="actions">
|
|
||||||
{node.actions.map((action) =>
|
|
||||||
<div
|
|
||||||
key={action.id}
|
|
||||||
title={action.tooltip}
|
|
||||||
onClick={(e) => { e.stopPropagation(); e.preventDefault(); action.onClick(); }}
|
|
||||||
>
|
|
||||||
{action.content}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>}
|
|
||||||
</summary>
|
</summary>
|
||||||
{node.children?.map((child) => <MenuNodeView key={child.id} node={child} />)}
|
{node.children?.map((child) => <MenuNodeView key={child.id} node={child} />)}
|
||||||
</details>
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { ObjectViewInternal, type ObjectViewInternalHandle } from "./ObjectViewI
|
||||||
|
|
||||||
type ObjectEditorViewProps = {
|
type ObjectEditorViewProps = {
|
||||||
object: Runtime<ObjectInstance>;
|
object: Runtime<ObjectInstance>;
|
||||||
isPlayer: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectionOverlayProps = {
|
type SelectionOverlayProps = {
|
||||||
|
|
@ -51,7 +50,7 @@ const SelectionOverlay = observer(function ({ objectId, ref, onTransformEnd }: S
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectEditorViewProps) {
|
export const ObjectEditorView = observer(function ({ object }: ObjectEditorViewProps) {
|
||||||
const dataRef = useRef<ObjectViewInternalHandle>(null);
|
const dataRef = useRef<ObjectViewInternalHandle>(null);
|
||||||
|
|
||||||
// Only observes world object types — not selection state — so won't
|
// Only observes world object types — not selection state — so won't
|
||||||
|
|
@ -60,14 +59,15 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
|
||||||
if (!objectType)
|
if (!objectType)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick(e: ThreeEvent<MouseEvent>) {
|
||||||
if (state.worldEditor.isEnabled) {
|
if (e.delta > 5) return;
|
||||||
// Reading selection state inside an event handler: not tracked by observer.
|
e.stopPropagation();
|
||||||
const currentMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
|
// Reading selection state inside an event handler: not tracked by observer.
|
||||||
? state.worldEditor.selection?.editMode
|
const currentMode = state.worldEditor.isEnabled &&
|
||||||
: undefined;
|
state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
|
||||||
state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) });
|
? state.worldEditor.selection?.editMode
|
||||||
}
|
: undefined;
|
||||||
|
state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransformEnd() {
|
function handleTransformEnd() {
|
||||||
|
|
@ -91,7 +91,6 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
|
||||||
ref={dataRef}
|
ref={dataRef}
|
||||||
object={object}
|
object={object}
|
||||||
isEditor
|
isEditor
|
||||||
isPlayer={isPlayer}
|
|
||||||
objectType={objectType}
|
objectType={objectType}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ type ObjectViewInternalProps = {
|
||||||
object: Omit<RuntimeObjectInstance, 'typeId'>;
|
object: Omit<RuntimeObjectInstance, 'typeId'>;
|
||||||
objectType: ObjectType;
|
objectType: ObjectType;
|
||||||
isEditor?: boolean;
|
isEditor?: boolean;
|
||||||
isPlayer?: boolean;
|
onClick?: (e: ThreeEvent<MouseEvent>) => void;
|
||||||
onClick?: () => void;
|
|
||||||
ref?: Ref<ObjectViewInternalHandle>;
|
ref?: Ref<ObjectViewInternalHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,70 +28,38 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
||||||
useImperativeHandle(ref, () => ({ group: groupRef.current, rb: rbRef.current }));
|
useImperativeHandle(ref, () => ({ group: groupRef.current, rb: rbRef.current }));
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => reaction(
|
||||||
if (props.isPlayer) return;
|
() => 'version' in object ? object.version : 0,
|
||||||
return reaction(
|
() => {
|
||||||
() => 'version' in object ? object.version : 0,
|
if (!rbRef.current)
|
||||||
() => {
|
return;
|
||||||
if (!rbRef.current)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const gameObj = object as RuntimeGameObjectInstance;
|
const gameObj = object as RuntimeGameObjectInstance;
|
||||||
|
|
||||||
rbRef.current.setTranslation(v3toRapier(gameObj.position), true);
|
// position
|
||||||
|
rbRef.current.setTranslation(v3toRapier(gameObj.position), true);
|
||||||
|
|
||||||
const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]);
|
// rotation
|
||||||
const q = new Quaternion().setFromEuler(euler);
|
const euler = new Euler(gameObj.rotation[0], gameObj.rotation[1], gameObj.rotation[2]);
|
||||||
rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
|
const q = new Quaternion().setFromEuler(euler);
|
||||||
|
rbRef.current.setRotation({ x: q.x, y: q.y, z: q.z, w: q.w }, true);
|
||||||
|
|
||||||
groupRef.current?.scale.set(...gameObj.scale);
|
// scale
|
||||||
|
groupRef.current?.scale.set(...gameObj.scale);
|
||||||
|
|
||||||
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
|
rbRef.current.setLinvel(v3toRapier(gameObj.linearVelocity), true);
|
||||||
|
|
||||||
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
|
rbRef.current.setAngvel(v3toRapier(gameObj.angularVelocity), true);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
},
|
|
||||||
[object.id]
|
[object.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
const gameObj = object as RuntimeGameObjectInstance;
|
|
||||||
|
|
||||||
return reaction(
|
|
||||||
() => gameObj.pendingActions.impulse,
|
|
||||||
(impulse) => {
|
|
||||||
if (!impulse) return;
|
|
||||||
if (!rbRef.current) { console.warn('applyImpulse: rbRef is null for', gameObj.id); return; }
|
|
||||||
|
|
||||||
let { direction, amplitude } = impulse;
|
|
||||||
amplitude *= 100;
|
|
||||||
const v = { x: direction[0] * amplitude, y: direction[1] * amplitude, z: direction[2] * amplitude };
|
|
||||||
console.log('applyImpulse', gameObj.id, v, 'bodyType', rbRef.current.bodyType());
|
|
||||||
rbRef.current.applyImpulse(v, true);
|
|
||||||
runInAction(() => {
|
|
||||||
gameObj.pendingActions.impulse = undefined;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[object.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleClick(e: ThreeEvent<MouseEvent>) {
|
|
||||||
if (e.delta > 5)
|
|
||||||
return;
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
props.onClick?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SyncRigidBody
|
<SyncRigidBody
|
||||||
ref={rbRef}
|
ref={rbRef}
|
||||||
colliders={false}
|
colliders={false}
|
||||||
type={props.isPlayer ? 'kinematicPosition' : (object.physics ? 'dynamic' : 'fixed')}
|
type={object.physics ? 'dynamic' : 'fixed'}
|
||||||
gravityScale={object.gravityScale}
|
gravityScale={object.gravityScale}
|
||||||
position={object.position}
|
position={object.position}
|
||||||
rotation={object.rotation}
|
rotation={object.rotation}
|
||||||
|
|
@ -112,7 +79,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
name={`${object.id} (${objectType.id} instance)`}
|
name={`${object.id} (${objectType.id} instance)`}
|
||||||
scale={object.scale}
|
scale={object.scale}
|
||||||
onClick={handleClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
object.cache.voxelGroups.map((vg) =>
|
object.cache.voxelGroups.map((vg) =>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import type { RuntimeScene } from "../types";
|
import type { RuntimeScene } from "../types";
|
||||||
|
import { CharacterView } from "./CharacterView";
|
||||||
import { GameObjectView } from "./GameObjectView";
|
import { GameObjectView } from "./GameObjectView";
|
||||||
import { ObjectEditorView } from "./ObjectEditorView";
|
import { ObjectEditorView } from "./ObjectEditorView";
|
||||||
|
|
||||||
|
|
@ -9,15 +10,30 @@ type SceneViewProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneView = observer(function (props: SceneViewProps) {
|
export const SceneView = observer(function (props: SceneViewProps) {
|
||||||
|
// const rapier = useRapier();
|
||||||
|
|
||||||
|
// useFrame((_, dt) => {
|
||||||
|
// if (props.editMode)
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// const game = state.game;
|
||||||
|
// if (!game || game.isPaused)
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// rapier.step(dt);
|
||||||
|
// })
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<ambientLight intensity={0.5} />
|
<ambientLight intensity={0.5} />
|
||||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||||
{
|
{
|
||||||
Object.values(props.scene.objects).map((obj) => (
|
Object.values(props.scene.objects).map((obj) => (
|
||||||
props.editMode
|
props.editMode
|
||||||
? <ObjectEditorView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
|
? <ObjectEditorView key={obj.id} object={obj} />
|
||||||
: <GameObjectView key={obj.id} object={obj} isPlayer={props.scene.playerObjectId === obj.id} />
|
: <GameObjectView key={obj.id} object={obj} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
{/* {props.editMode && <CharacterView character={props.scene.character} />} */}
|
||||||
|
{<CharacterView character={props.scene.character} editMode={props.editMode} />}
|
||||||
</>);
|
</>);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,6 @@ import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||||
import { KeyboardControls, Stats } from '@react-three/drei';
|
import { KeyboardControls, Stats } from '@react-three/drei';
|
||||||
import { action } from 'mobx';
|
import { action } from 'mobx';
|
||||||
import { chartRef } from './chartRef';
|
import { chartRef } from './chartRef';
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { state } from '../state';
|
|
||||||
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';
|
|
||||||
|
|
||||||
function RenderInfoUpdater() {
|
function RenderInfoUpdater() {
|
||||||
const { gl } = useThree();
|
const { gl } = useThree();
|
||||||
|
|
@ -27,8 +20,19 @@ function RenderInfoUpdater() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { state } from '../state';
|
||||||
|
import { GameView } from './GameView';
|
||||||
|
import { SceneEditorView } from './SceneEditorView';
|
||||||
|
import { JoystickView } from './JoystickView';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
|
||||||
|
const IconStop = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" fill="currentColor" /></svg>;
|
||||||
|
const IconPause = () => <svg viewBox="0 0 14 14"><rect x="2" y="2" width="4" height="10" fill="currentColor" /><rect x="8" y="2" width="4" height="10" fill="currentColor" /></svg >;
|
||||||
|
const IconPlay = () => <svg viewBox="0 0 14 14"><polygon points="3,1 13,7 3,13" fill="currentColor" /></svg>;
|
||||||
|
|
||||||
export const ThreeView = observer(function () {
|
export const ThreeView = observer(function () {
|
||||||
const isGame = !!state.game;
|
const isGame = state.isGamePlaying;
|
||||||
return (
|
return (
|
||||||
<KeyboardControls map={[
|
<KeyboardControls map={[
|
||||||
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
|
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
|
||||||
|
|
@ -47,18 +51,18 @@ export const ThreeView = observer(function () {
|
||||||
{isGame ? <GameView /> : <SceneEditorView />}
|
{isGame ? <GameView /> : <SceneEditorView />}
|
||||||
</Canvas>
|
</Canvas>
|
||||||
{isGame && <JoystickView />}
|
{isGame && <JoystickView />}
|
||||||
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, fontSize: 14 }}>
|
<div style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4 }}>
|
||||||
{
|
{
|
||||||
state.game
|
state.game
|
||||||
? <>
|
? <>
|
||||||
<button onClick={() => state.stopGame()}><IconPlayerStopFilled size="1em" /></button>
|
<button onClick={() => state.stopGame()}><IconStop /></button>
|
||||||
{
|
{
|
||||||
state.game!.isPaused
|
state.game!.isPaused
|
||||||
? <button onClick={() => state.game!.resume()}><IconPlayerPlayFilled size="1em" /></button>
|
? <button onClick={() => state.game!.resume()}><IconPlay /></button>
|
||||||
: <button onClick={() => state.game!.pause()}><IconPlayerPauseFilled size="1em" /></button>
|
: <button onClick={() => state.game!.pause()}><IconPause /></button>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
: <button onClick={() => state.startGame()}><IconPlayerPlayFilled size="1em" /></button>
|
: <button onClick={() => state.startGame()}><IconPlay /></button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
||||||
{ kind: 'block', type: 'game_start_event' },
|
{ kind: 'block', type: 'game_start_event' },
|
||||||
{ kind: 'block', type: 'object_touch_start_event' },
|
{ kind: 'block', type: 'object_touch_start_event' },
|
||||||
{ kind: 'block', type: 'player_touch_start_event' },
|
{ kind: 'block', type: 'player_touch_start_event' },
|
||||||
{ kind: 'block', type: 'object_clicked_event' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -29,13 +28,6 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
||||||
contents: [
|
contents: [
|
||||||
{ kind: 'block', type: 'current_object_value' },
|
{ kind: 'block', type: 'current_object_value' },
|
||||||
{ kind: 'block', type: 'current_object_type_value' },
|
{ kind: 'block', type: 'current_object_type_value' },
|
||||||
{ kind: 'block', type: 'object_type_value' },
|
|
||||||
{ kind: 'block', type: 'any_object_of_type_value' },
|
|
||||||
{ kind: 'block', type: 'position_of_object_value' },
|
|
||||||
{ kind: 'block', type: 'pos_value' },
|
|
||||||
{ kind: 'block', type: 'dpos_value' },
|
|
||||||
{ kind: 'block', type: 'direction_to_position_value' },
|
|
||||||
{ kind: 'block', type: 'object_by_id_value' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -45,7 +37,6 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
|
||||||
contents: [
|
contents: [
|
||||||
{ kind: 'block', type: 'console_log_action' },
|
{ kind: 'block', type: 'console_log_action' },
|
||||||
{ kind: 'block', type: 'text_print' },
|
{ kind: 'block', type: 'text_print' },
|
||||||
{ kind: 'block', type: 'physics_apply_impulse_action' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,13 @@ export class GameFactory {
|
||||||
Object.values(world.objectTypes)
|
Object.values(world.objectTypes)
|
||||||
.filter((ot) => ot.javascript)
|
.filter((ot) => ot.javascript)
|
||||||
.forEach((ot) => {
|
.forEach((ot) => {
|
||||||
const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`);
|
const otCode = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`);
|
||||||
|
const api = new ObjectApi(o, ot, world, scene);
|
||||||
|
|
||||||
Object.values(scene.objects)
|
Object.values(scene.objects)
|
||||||
.filter((o) => o.typeId == ot.id)
|
.filter((o) => o.typeId == ot.id)
|
||||||
.forEach((o) => {
|
.forEach((o) => {
|
||||||
const api = new ObjectApi(o, ot, world, scene);
|
otCode(internalBus, { api, o, ot });
|
||||||
gameScript(internalBus, { api, object: o, objectType: ot });
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,25 @@ function v(x: number, y: number, z: number, color: string): Voxel {
|
||||||
return { typeId: 'stone', position: [x, y, z], color };
|
return { typeId: 'stone', position: [x, y, z], color };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wolf faces -Z (nose at Z=-3). Head spans X:-3..3, Y:0..5, Z:-3..3.
|
// Wolf faces +Z (nose at Z=3). Head spans X:-3..3, Y:0..5, Z:-3..3.
|
||||||
export const wolf: Voxel[] = [
|
export const wolf: Voxel[] = [
|
||||||
// ── Snout tip + nose Z=-3 ────────────────────────────────────────────
|
// ── Back of skull Z=-3 ──────────────────────────────────────────────
|
||||||
v(-1,1,-3,L), v(0,1,-3,L), v(1,1,-3,L),
|
v(-1,2,-3,G), v(0,2,-3,G), v(1,2,-3,G),
|
||||||
v(-1,0,-3,L), v(0,0,-3,L), v(1,0,-3,L),
|
v(-1,3,-3,G), v(0,3,-3,G), v(1,3,-3,G),
|
||||||
v(-1,2,-3,N), v(0,2,-3,N), v(1,2,-3,N), // nose
|
v(-1,4,-3,G), v(0,4,-3,G), v(1,4,-3,G),
|
||||||
|
|
||||||
// ── Front face + pupils + snout Z=-2 ────────────────────────────────
|
// ── Skull Z=-2 ──────────────────────────────────────────────────────
|
||||||
v(-2,2,-2,L), v(-1,2,-2,L), v(0,2,-2,L), v(1,2,-2,L), v(2,2,-2,L),
|
v(-2,2,-2,G), v(-1,2,-2,G), v(0,2,-2,G), v(1,2,-2,G), v(2,2,-2,G),
|
||||||
v(-2,3,-2,G), v(-1,3,-2,N), v(0,3,-2,G), v(1,3,-2,N), v(2,3,-2,G),
|
v(-2,3,-2,G), v(-1,3,-2,G), v(0,3,-2,G), v(1,3,-2,G), v(2,3,-2,G),
|
||||||
v(-2,4,-2,G), v(-1,4,-2,G), v(0,4,-2,G), v(1,4,-2,G), v(2,4,-2,G),
|
v(-2,4,-2,G), v(-1,4,-2,G), v(0,4,-2,G), v(1,4,-2,G), v(2,4,-2,G),
|
||||||
v(-1,0,-2,L), v(0,0,-2,L), v(1,0,-2,L),
|
v(-1,5,-2,G), v(0,5,-2,G), v(1,5,-2,G),
|
||||||
v(-1,1,-2,L), v(0,1,-2,L), v(1,1,-2,L),
|
|
||||||
|
|
||||||
// ── Face + amber eyes + snout Z=-1 ──────────────────────────────────
|
// ── Skull + ear bases Z=-1 ───────────────────────────────────────────
|
||||||
v(-2,2,-1,L), v(-1,2,-1,L), v(0,2,-1,L), v(1,2,-1,L), v(2,2,-1,L),
|
v(-2,2,-1,G), v(-1,2,-1,G), v(0,2,-1,G), v(1,2,-1,G), v(2,2,-1,G),
|
||||||
v(-2,3,-1,G), v(-1,3,-1,E), v(0,3,-1,G), v(1,3,-1,E), v(2,3,-1,G),
|
v(-2,3,-1,G), v(-1,3,-1,G), v(0,3,-1,G), v(1,3,-1,G), v(2,3,-1,G),
|
||||||
v(-2,4,-1,G), v(-1,4,-1,G), v(0,4,-1,G), v(1,4,-1,G), v(2,4,-1,G),
|
v(-3,4,-1,G), v(-2,4,-1,G), v(-1,4,-1,G), v(0,4,-1,G), v(1,4,-1,G), v(2,4,-1,G), v(3,4,-1,G),
|
||||||
v(-1,0,-1,L), v(0,0,-1,L), v(1,0,-1,L),
|
v(-2,5,-1,G), v(-1,5,-1,G), v(0,5,-1,G), v(1,5,-1,G), v(2,5,-1,G),
|
||||||
v(-1,1,-1,L), v(0,1,-1,L), v(1,1,-1,L),
|
v(-3,5,-1,D), v(3,5,-1,D),
|
||||||
|
|
||||||
// ── Face + ears Z=0 ──────────────────────────────────────────────────
|
// ── Face + ears Z=0 ──────────────────────────────────────────────────
|
||||||
v(-2,2,0,L), v(-1,2,0,L), v(0,2,0,L), v(1,2,0,L), v(2,2,0,L),
|
v(-2,2,0,L), v(-1,2,0,L), v(0,2,0,L), v(1,2,0,L), v(2,2,0,L),
|
||||||
|
|
@ -42,21 +41,22 @@ export const wolf: Voxel[] = [
|
||||||
v(-1,0,0,L), v(0,0,0,L), v(1,0,0,L),
|
v(-1,0,0,L), v(0,0,0,L), v(1,0,0,L),
|
||||||
v(-1,1,0,L), v(0,1,0,L), v(1,1,0,L),
|
v(-1,1,0,L), v(0,1,0,L), v(1,1,0,L),
|
||||||
|
|
||||||
// ── Skull + ear bases Z=1 ────────────────────────────────────────────
|
// ── Face + amber eyes + snout Z=1 ────────────────────────────────────
|
||||||
v(-2,2,1,G), v(-1,2,1,G), v(0,2,1,G), v(1,2,1,G), v(2,2,1,G),
|
v(-2,2,1,L), v(-1,2,1,L), v(0,2,1,L), v(1,2,1,L), v(2,2,1,L),
|
||||||
v(-2,3,1,G), v(-1,3,1,G), v(0,3,1,G), v(1,3,1,G), v(2,3,1,G),
|
v(-2,3,1,G), v(-1,3,1,E), v(0,3,1,G), v(1,3,1,E), v(2,3,1,G),
|
||||||
v(-3,4,1,G), v(-2,4,1,G), v(-1,4,1,G), v(0,4,1,G), v(1,4,1,G), v(2,4,1,G), v(3,4,1,G),
|
v(-2,4,1,G), v(-1,4,1,G), v(0,4,1,G), v(1,4,1,G), v(2,4,1,G),
|
||||||
v(-2,5,1,G), v(-1,5,1,G), v(0,5,1,G), v(1,5,1,G), v(2,5,1,G),
|
v(-1,0,1,L), v(0,0,1,L), v(1,0,1,L),
|
||||||
v(-3,5,1,D), v(3,5,1,D),
|
v(-1,1,1,L), v(0,1,1,L), v(1,1,1,L),
|
||||||
|
|
||||||
// ── Skull Z=2 ───────────────────────────────────────────────────────
|
// ── Front face + pupils + snout Z=2 ──────────────────────────────────
|
||||||
v(-2,2,2,G), v(-1,2,2,G), v(0,2,2,G), v(1,2,2,G), v(2,2,2,G),
|
v(-2,2,2,L), v(-1,2,2,L), v(0,2,2,L), v(1,2,2,L), v(2,2,2,L),
|
||||||
v(-2,3,2,G), v(-1,3,2,G), v(0,3,2,G), v(1,3,2,G), v(2,3,2,G),
|
v(-2,3,2,G), v(-1,3,2,N), v(0,3,2,G), v(1,3,2,N), v(2,3,2,G),
|
||||||
v(-2,4,2,G), v(-1,4,2,G), v(0,4,2,G), v(1,4,2,G), v(2,4,2,G),
|
v(-2,4,2,G), v(-1,4,2,G), v(0,4,2,G), v(1,4,2,G), v(2,4,2,G),
|
||||||
v(-1,5,2,G), v(0,5,2,G), v(1,5,2,G),
|
v(-1,0,2,L), v(0,0,2,L), v(1,0,2,L),
|
||||||
|
v(-1,1,2,L), v(0,1,2,L), v(1,1,2,L),
|
||||||
|
|
||||||
// ── Back of skull Z=3 ───────────────────────────────────────────────
|
// ── Snout tip + nose Z=3 ─────────────────────────────────────────────
|
||||||
v(-1,2,3,G), v(0,2,3,G), v(1,2,3,G),
|
v(-1,1,3,L), v(0,1,3,L), v(1,1,3,L),
|
||||||
v(-1,3,3,G), v(0,3,3,G), v(1,3,3,G),
|
v(-1,0,3,L), v(0,0,3,L), v(1,0,3,L),
|
||||||
v(-1,4,3,G), v(0,4,3,G), v(1,4,3,G),
|
v(-1,2,3,N), v(0,2,3,N), v(1,2,3,N), // nose
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@ export class WorldFactory {
|
||||||
objectTypes: {},
|
objectTypes: {},
|
||||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||||
initialScene: {
|
initialScene: {
|
||||||
|
character: {
|
||||||
|
transform: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
look: [0, 0, 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
objects: {},
|
objects: {},
|
||||||
},
|
},
|
||||||
gameRules: {
|
gameRules: {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { makeAutoObservable, reaction, toJS } from "mobx";
|
import { makeAutoObservable, reaction, toJS } from "mobx";
|
||||||
import type { WorldState } from "./worldState";
|
import type { WorldState } from "./worldState";
|
||||||
import type { Game, Pos3, RuntimeGameScene } from "../types";
|
import type { Game, Pos3, V3, R3, 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 type { GameEventContext } from "../types/runtime/gameBus";
|
|
||||||
|
|
||||||
export class GameState {
|
export class GameState {
|
||||||
private readonly world: WorldState;
|
private readonly world: WorldState;
|
||||||
|
|
@ -46,18 +45,18 @@ export class GameState {
|
||||||
|
|
||||||
const eventBus = new GameEventBus();
|
const eventBus = new GameEventBus();
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
GameFactory.evalGameScripts(rawWorld, game.scene, eventBus);
|
||||||
|
|
||||||
this._stopAutoSave = this.startAutoSave();
|
this._stopAutoSave = this.startAutoSave();
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(
|
||||||
|
this,
|
||||||
GameFactory.evalGameScripts(rawWorld, this.scene, eventBus);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public emitEvent(event: string, context: Omit<GameEventContext, 'world' | 'scene'> = {}) {
|
public emitEvent(event: string) {
|
||||||
this.eventBus.emit(
|
this.eventBus.emit(
|
||||||
event,
|
event,
|
||||||
{
|
{
|
||||||
...context,
|
|
||||||
world: this.world.data,
|
world: this.world.data,
|
||||||
scene: this.scene,
|
scene: this.scene,
|
||||||
},
|
},
|
||||||
|
|
@ -84,6 +83,7 @@ export class GameState {
|
||||||
|
|
||||||
public get camera(): Pos3 {
|
public get camera(): Pos3 {
|
||||||
return this.world.data.editorCamera;
|
return this.world.data.editorCamera;
|
||||||
|
return this.scene.character.transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get cameraAsThree(): CameraProps {
|
public get cameraAsThree(): CameraProps {
|
||||||
|
|
@ -122,6 +122,23 @@ export class GameState {
|
||||||
this.isPaused = true;
|
this.isPaused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setCharacterTransform(
|
||||||
|
transform: Pos3,
|
||||||
|
linearVelocity?: V3,
|
||||||
|
angularVelocity?: R3,
|
||||||
|
): void {
|
||||||
|
if (this.isPaused)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.scene.character.transform = transform;
|
||||||
|
if (linearVelocity)
|
||||||
|
this.scene.character.linearVelocity = linearVelocity;
|
||||||
|
if (angularVelocity)
|
||||||
|
this.scene.character.angularVelocity = angularVelocity;
|
||||||
|
|
||||||
|
// console.log(`changed character to ${JSON.stringify(this.scene.character)}`);
|
||||||
|
}
|
||||||
|
|
||||||
public tick(deltaTime: number): void {
|
public tick(deltaTime: number): void {
|
||||||
if (this.isPaused)
|
if (this.isPaused)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
import { state } from "./rootState";
|
||||||
|
|
||||||
|
export type MenuNode = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
selected?: () => boolean;
|
||||||
|
children?: MenuNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MenuState {
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get editorObjectTypesMenu(): MenuNode[] {
|
||||||
|
return Object.values(state.world.data.objectTypes)
|
||||||
|
.map((ot) => ({
|
||||||
|
id: `ot-${ot.id}`,
|
||||||
|
title: ot.name,
|
||||||
|
onClick: () => { state.worldEditor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
|
||||||
|
selected: () => state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection?.id === ot.id,
|
||||||
|
children: Object.values(state.worldEditor.scene.objects)
|
||||||
|
.filter((o) => o.typeId === ot.id)
|
||||||
|
.map((o) => ({
|
||||||
|
id: `o-${o.id}`,
|
||||||
|
title: o.id,
|
||||||
|
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
|
||||||
|
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get editorMenu(): MenuNode[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'editor-objects-menu',
|
||||||
|
title: 'Objects',
|
||||||
|
onClick: () => { state.worldEditor.resetSelection() },
|
||||||
|
selected: () => !state.worldEditor.selection,
|
||||||
|
children: this.editorObjectTypesMenu,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public get nodes(): MenuNode[] {
|
||||||
|
return [
|
||||||
|
...this.editorMenu,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public nodeContainsSelected(node: MenuNode): boolean {
|
||||||
|
return !!(node.selected?.() || node.children?.some((child) => this.nodeContainsSelected(child)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import { makeAutoObservable } from "mobx";
|
|
||||||
import { state } from "./rootState";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { IconRun } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
export type MenuNodeAction = {
|
|
||||||
id: string;
|
|
||||||
content: ReactNode;
|
|
||||||
tooltip: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MenuNode = {
|
|
||||||
id: string;
|
|
||||||
title: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
actions?: MenuNodeAction[];
|
|
||||||
onClick?: () => void;
|
|
||||||
selected?: () => boolean;
|
|
||||||
children?: MenuNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MenuState {
|
|
||||||
constructor() {
|
|
||||||
makeAutoObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get editorObjectTypesMenu(): MenuNode[] {
|
|
||||||
const scene = state.worldEditor.scene;
|
|
||||||
|
|
||||||
return Object.values(state.world.data.objectTypes)
|
|
||||||
.map((ot) => ({
|
|
||||||
id: `ot-${ot.id}`,
|
|
||||||
title: ot.name,
|
|
||||||
onClick: () => { state.worldEditor.setSelectedObjectType({ id: ot.id, editMode: 'scripts' }) },
|
|
||||||
selected: () => state.worldEditor.selection?.type === 'objectType' && state.worldEditor.selection?.id === ot.id,
|
|
||||||
children: Object.values(scene.objects)
|
|
||||||
.filter((o) => o.typeId === ot.id)
|
|
||||||
.map((o) => {
|
|
||||||
const isPlayer = scene.playerObjectId === o.id;
|
|
||||||
|
|
||||||
const actions: MenuNodeAction[] = [];
|
|
||||||
if (!isPlayer)
|
|
||||||
actions.push({
|
|
||||||
id: 'control-by-player',
|
|
||||||
content: <IconRun size="1em" />,
|
|
||||||
tooltip: 'Mark as player',
|
|
||||||
onClick: () => { state.markObjectAsPlayer(o); },
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `o-${o.id}`,
|
|
||||||
title: <>
|
|
||||||
{isPlayer && <IconRun size="1em" />}
|
|
||||||
{o.id}
|
|
||||||
</>,
|
|
||||||
className: isPlayer ? 'player-controlled-object' : undefined,
|
|
||||||
actions,
|
|
||||||
onClick: () => { state.worldEditor.setSelectedObject({ id: o.id, editMode: 'translate' }) },
|
|
||||||
selected: () => state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === o.id,
|
|
||||||
} as MenuNode;
|
|
||||||
})
|
|
||||||
} as MenuNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private get editorMenu(): MenuNode[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'editor-objects-menu',
|
|
||||||
title: 'Objects',
|
|
||||||
onClick: () => { state.worldEditor.resetSelection() },
|
|
||||||
selected: () => !state.worldEditor.selection,
|
|
||||||
children: this.editorObjectTypesMenu,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
public get nodes(): MenuNode[] {
|
|
||||||
return [
|
|
||||||
...this.editorMenu,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
public nodeContainsSelected(node: MenuNode): boolean {
|
|
||||||
return !!(node.selected?.() || node.children?.some((child) => this.nodeContainsSelected(child)));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { WorldState } from "./worldState";
|
||||||
import { WorldEditorState } from "./worldEditorState";
|
import { WorldEditorState } from "./worldEditorState";
|
||||||
import { GameState } from "./gameState";
|
import { GameState } from "./gameState";
|
||||||
import { MenuState } from "./menuState";
|
import { MenuState } from "./menuState";
|
||||||
import type { RuntimeObjectInstance } from "../types";
|
|
||||||
|
|
||||||
export type RenderInfo = {
|
export type RenderInfo = {
|
||||||
calls: number,
|
calls: number,
|
||||||
|
|
@ -32,6 +31,10 @@ export class RootState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get isGamePlaying(): boolean {
|
||||||
|
return this.game !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public startGame(): void {
|
public startGame(): void {
|
||||||
state.worldEditor.resetSelection();
|
state.worldEditor.resetSelection();
|
||||||
if (this.game)
|
if (this.game)
|
||||||
|
|
@ -50,13 +53,6 @@ export class RootState {
|
||||||
public setRenderInfo(value: RenderInfo | undefined) {
|
public setRenderInfo(value: RenderInfo | undefined) {
|
||||||
this.renderInfo = value;
|
this.renderInfo = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public markObjectAsPlayer(object: RuntimeObjectInstance) {
|
|
||||||
if (this.game)
|
|
||||||
this.game.scene.playerObjectId = object.id;
|
|
||||||
else
|
|
||||||
this.world.data.initialScene.playerObjectId = object.id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const state = new RootState();
|
export const state = new RootState();
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export class WorldEditorState {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isEnabled(): boolean {
|
public get isEnabled(): boolean {
|
||||||
return !state.game;
|
return !state.isGamePlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCamera(value: Pos3): void {
|
public setCamera(value: Pos3): void {
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,16 @@ export class WorldState {
|
||||||
},
|
},
|
||||||
voxelTypes: DEFAULT_VOXEL_TYPES,
|
voxelTypes: DEFAULT_VOXEL_TYPES,
|
||||||
editorCamera: {
|
editorCamera: {
|
||||||
position: [14, 17, -25],
|
position: [-9, 11, 30],
|
||||||
look: [-2.5, 0.31, 3],
|
look: [-0.52, -0.35, -0.2],
|
||||||
},
|
},
|
||||||
initialScene: {
|
initialScene: {
|
||||||
|
character: {
|
||||||
|
transform: {
|
||||||
|
position: [0, 5, 20],
|
||||||
|
look: [0, 0, 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
objects: {
|
objects: {
|
||||||
terrain: {
|
terrain: {
|
||||||
id: 'terrain',
|
id: 'terrain',
|
||||||
|
|
@ -84,7 +90,6 @@ export class WorldState {
|
||||||
},
|
},
|
||||||
...objectMap,
|
...objectMap,
|
||||||
},
|
},
|
||||||
playerObjectId: 'obj1',
|
|
||||||
},
|
},
|
||||||
gameRules: {
|
gameRules: {
|
||||||
gravity: true,
|
gravity: true,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Pos3 } from "../3d";
|
||||||
|
import type { GameObjectData } from "./object";
|
||||||
|
|
||||||
|
export type Character = {
|
||||||
|
transform: Pos3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameCharacter = Character & GameObjectData;
|
||||||
|
|
@ -3,5 +3,6 @@ export * from './scene';
|
||||||
export * from './world';
|
export * from './world';
|
||||||
export * from './gameRules';
|
export * from './gameRules';
|
||||||
export * from './game';
|
export * from './game';
|
||||||
|
export * from './character';
|
||||||
export * from './voxel';
|
export * from './voxel';
|
||||||
export * from './runtime';
|
export * from './runtime';
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,9 @@ export type ObjectInstanceRuntimeData = {
|
||||||
cache: {
|
cache: {
|
||||||
voxelGroups: VoxelGroup[];
|
voxelGroups: VoxelGroup[];
|
||||||
colliderMesh: [Float32Array, Uint32Array] | null;
|
colliderMesh: [Float32Array, Uint32Array] | null;
|
||||||
boundingBox: { min: V3; max: V3 };
|
|
||||||
};
|
};
|
||||||
pendingActions: {
|
pendingActions: {
|
||||||
impulse?: { direction: V3, amplitude: number };
|
impulse: { direction: V3, amplitude: number };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
|
import type { Character, GameCharacter } from "./character";
|
||||||
import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object";
|
import type { GameObjectInstance, ObjectInstance, RuntimeGameObjectInstance, RuntimeObjectInstance } from "./object";
|
||||||
|
|
||||||
export type Scene = {
|
export type Scene = {
|
||||||
|
character: Character;
|
||||||
objects: Record<string, ObjectInstance>;
|
objects: Record<string, ObjectInstance>;
|
||||||
playerObjectId: string | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeScene = Scene & {
|
export type RuntimeScene = {
|
||||||
|
character: Character;
|
||||||
objects: Record<string, RuntimeObjectInstance>;
|
objects: Record<string, RuntimeObjectInstance>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameScene = Scene & {
|
export type GameScene = {
|
||||||
|
character: GameCharacter;
|
||||||
objects: Record<string, GameObjectInstance>;
|
objects: Record<string, GameObjectInstance>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeGameScene = Scene & {
|
export type RuntimeGameScene = {
|
||||||
|
character: GameCharacter;
|
||||||
objects: Record<string, RuntimeGameObjectInstance>;
|
objects: Record<string, RuntimeGameObjectInstance>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import type { RuntimeGameObjectInstance, Scene, World } from "../model";
|
import type { ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model";
|
||||||
|
|
||||||
export type GameEventContext = {
|
export type GameEventContext = {
|
||||||
world: World;
|
world: World;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
object?: RuntimeGameObjectInstance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// export type GameObjectEventContext = GameEventContext & {
|
||||||
|
// object: RuntimeGameObjectInstance;
|
||||||
|
// objectType: ObjectType;
|
||||||
|
// }
|
||||||
|
|
||||||
export type GameEventHandler = (context: GameEventContext) => void;
|
export type GameEventHandler = (context: GameEventContext) => void;
|
||||||
|
// export type GameObjectEventHandler = (context: GameObjectEventContext) => void;
|
||||||
|
|
||||||
export type InternalGameEventBus = {
|
export type InternalGameEventBus = {
|
||||||
onGameEvent: (event: string, handler: GameEventHandler) => void;
|
onGameEvent: (event: string, handler: GameEventHandler) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,7 @@
|
||||||
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
import { getObjectVoxelGroups } from "../graphics/voxelGroup";
|
||||||
import type { ObjectInstance, ObjectType, RuntimeObjectInstance, V3, World } from "../../types";
|
import type { ObjectInstance, RuntimeObjectInstance, World } from "../../types";
|
||||||
import { buildObjectTrimesh } from "../graphics/mesh";
|
import { buildObjectTrimesh } from "../graphics/mesh";
|
||||||
|
|
||||||
function computeBoundingBox(objectType: ObjectType): { min: V3; max: V3 } {
|
|
||||||
if (!objectType.voxels.length)
|
|
||||||
return { min: [0, 0, 0], max: [1, 1, 1] };
|
|
||||||
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
||||||
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
||||||
for (const v of objectType.voxels) {
|
|
||||||
minX = Math.min(minX, v.position[0]);
|
|
||||||
minY = Math.min(minY, v.position[1]);
|
|
||||||
minZ = Math.min(minZ, v.position[2]);
|
|
||||||
maxX = Math.max(maxX, v.position[0] + 1);
|
|
||||||
maxY = Math.max(maxY, v.position[1] + 1);
|
|
||||||
maxZ = Math.max(maxZ, v.position[2] + 1);
|
|
||||||
}
|
|
||||||
return { min: [minX, minY, minZ], max: [maxX, maxY, maxZ] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance {
|
export function populateRuntimeObject(object: ObjectInstance, world: World): RuntimeObjectInstance {
|
||||||
const objectType = world.objectTypes[object.typeId];
|
const objectType = world.objectTypes[object.typeId];
|
||||||
|
|
||||||
|
|
@ -26,10 +10,9 @@ export function populateRuntimeObject(object: ObjectInstance, world: World): Run
|
||||||
cache: {
|
cache: {
|
||||||
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
|
voxelGroups: getObjectVoxelGroups(objectType, world.voxelTypes),
|
||||||
colliderMesh: buildObjectTrimesh(objectType, world.voxelTypes),
|
colliderMesh: buildObjectTrimesh(objectType, world.voxelTypes),
|
||||||
boundingBox: computeBoundingBox(objectType),
|
|
||||||
},
|
},
|
||||||
pendingActions: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
export function depopulateRuntimeObject(object: RuntimeObjectInstance, _world: World): ObjectInstance {
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,7 @@ export class ObjectApi {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyImpulse(targetPosition: V3, amplitude: number) {
|
public applyImpulse(direction: V3, amplitude: number) {
|
||||||
const [px, py, pz] = this.object.position;
|
|
||||||
const dx = targetPosition[0] - px;
|
|
||||||
const dy = targetPosition[1] - py;
|
|
||||||
const dz = targetPosition[2] - pz;
|
|
||||||
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
if (len < 1e-6) return;
|
|
||||||
const direction: V3 = [dx / len, dy / len, dz / len];
|
|
||||||
this.object.pendingActions.impulse = { direction, amplitude };
|
this.object.pendingActions.impulse = { direction, amplitude };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue