diff --git a/src/blockly/actions/consoleLog.ts b/src/blockly/actions/consoleLog.ts index 0ca6088..806a302 100644 --- a/src/blockly/actions/consoleLog.ts +++ b/src/blockly/actions/consoleLog.ts @@ -5,6 +5,7 @@ Blockly.Blocks['console_log_action'] = { init(this: Blockly.Block) { this.appendValueInput('VALUE') .setAlign(Blockly.inputs.Align.RIGHT) + .setCheck('ObjectType') .appendField('print'); this.setInputsInline(false) this.setPreviousStatement(true, null); diff --git a/src/blockly/actions/index.ts b/src/blockly/actions/index.ts index 6f414f9..2d359d1 100644 --- a/src/blockly/actions/index.ts +++ b/src/blockly/actions/index.ts @@ -1 +1,2 @@ -export * from './consoleLog'; \ No newline at end of file +export * from './consoleLog'; +export * from './physics'; diff --git a/src/blockly/actions/physics/applyImpulse.ts b/src/blockly/actions/physics/applyImpulse.ts new file mode 100644 index 0000000..1166fc5 --- /dev/null +++ b/src/blockly/actions/physics/applyImpulse.ts @@ -0,0 +1,29 @@ +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 to'); + this.appendValueInput('POSITION') + .setCheck('Pos'); + this.appendDummyInput('') + .appendField('with force') + .appendField(new Blockly.FieldNumber(1, -10, 10, 2), '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 { }; diff --git a/src/blockly/actions/physics/index.ts b/src/blockly/actions/physics/index.ts new file mode 100644 index 0000000..5e0541a --- /dev/null +++ b/src/blockly/actions/physics/index.ts @@ -0,0 +1 @@ +export * from './applyImpulse'; \ No newline at end of file diff --git a/src/blockly/events/imClicked.ts b/src/blockly/events/imClicked.ts new file mode 100644 index 0000000..e6aade8 --- /dev/null +++ b/src/blockly/events/imClicked.ts @@ -0,0 +1,18 @@ +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 { }; diff --git a/src/blockly/events/index.ts b/src/blockly/events/index.ts index 96ec612..bafa550 100644 --- a/src/blockly/events/index.ts +++ b/src/blockly/events/index.ts @@ -1,3 +1,4 @@ export * from './gameStart'; export * from './objectTouchedMe'; export * from './playerTouchedMe'; +export * from './imClicked'; diff --git a/src/blockly/events/objectTouchedMe.ts b/src/blockly/events/objectTouchedMe.ts index f38056d..125f899 100644 --- a/src/blockly/events/objectTouchedMe.ts +++ b/src/blockly/events/objectTouchedMe.ts @@ -1,15 +1,13 @@ import * as Blockly from "blockly"; import { javascriptGenerator } from "blockly/javascript"; -import { getObjectTypeDropDownOptions } from "../fieldTypes/objectType"; Blockly.Blocks['object_touch_start_event'] = { init(this: Blockly.Block) { - this.appendDummyInput() - .appendField('When') - .appendField( - new Blockly.FieldDropdown(getObjectTypeDropDownOptions), - 'OBJECT_TYPE', - ) + this.appendDummyInput('NAME') + .appendField('When'); + this.appendValueInput('NAME') + .setCheck('Object'); + this.appendDummyInput('NAME') .appendField('touches me'); this.setStyle('event_hat'); this.setNextStatement(true); diff --git a/src/blockly/fieldTypes/index.ts b/src/blockly/fieldTypes/index.ts new file mode 100644 index 0000000..76640da --- /dev/null +++ b/src/blockly/fieldTypes/index.ts @@ -0,0 +1 @@ +export * from './objectType'; diff --git a/src/blockly/fieldTypes/objectType.ts b/src/blockly/fieldTypes/objectType.ts index 428b2dd..1e57905 100644 --- a/src/blockly/fieldTypes/objectType.ts +++ b/src/blockly/fieldTypes/objectType.ts @@ -1,11 +1,11 @@ -import { state } from "../../state/rootState"; -import { toJS } from "mobx"; +import * as Blockly from "blockly"; -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; +export class ObjecTypeField extends Blockly.Field { + constructor(value: any, validator: any) { + super(value, validator); + + this.SERIALIZABLE = true; + } } + +Blockly.fieldRegistry.register('field_object_type', ObjecTypeField); \ No newline at end of file diff --git a/src/blockly/generateCode.ts b/src/blockly/generateCode.ts index aea1591..0cb552e 100644 --- a/src/blockly/generateCode.ts +++ b/src/blockly/generateCode.ts @@ -21,7 +21,16 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string { //TODO use args - result += `onGameEvent('${block.type.replace(/_event$/, '')}', function(context) {\n${body}});\n`; + result += `onGameEvent( +'${block.type.replace(/_event$/, '')}', +function(context) { + // console.dir({context, api, object, objectType}); + if (context.object && (context.object.id !== object.id)) + return; + ${body} +} +); +`; } } return result; diff --git a/src/blockly/index.ts b/src/blockly/index.ts index 773874b..66ab015 100644 --- a/src/blockly/index.ts +++ b/src/blockly/index.ts @@ -3,3 +3,4 @@ export * from './values'; export * from './actions'; export * from './theme'; export * from './generateCode'; +export * from './fieldTypes'; diff --git a/src/blockly/values/anyObjectOfType.ts b/src/blockly/values/anyObjectOfType.ts new file mode 100644 index 0000000..ba361d3 --- /dev/null +++ b/src/blockly/values/anyObjectOfType.ts @@ -0,0 +1,21 @@ +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 { }; diff --git a/src/blockly/values/currentObject.ts b/src/blockly/values/currentObject.ts index 9cdc4ca..9310863 100644 --- a/src/blockly/values/currentObject.ts +++ b/src/blockly/values/currentObject.ts @@ -4,16 +4,16 @@ import { javascriptGenerator, Order } from "blockly/javascript"; Blockly.Blocks['current_object_value'] = { init(this: Blockly.Block) { this.appendEndRowInput('NAME') - .appendField('Current object'); + .appendField('Me'); this.setInputsInline(false) - this.setOutput(true, 'String'); - this.setTooltip('Returns id of current object instance'); + this.setOutput(true, 'Object'); + this.setTooltip('Returns current object instance'); this.setColour(315); } }; javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) { - return ['object.id', Order.NONE]; + return ['object', Order.NONE]; }; export { }; diff --git a/src/blockly/values/currentObjectType.ts b/src/blockly/values/currentObjectType.ts index fe31879..8b72ddd 100644 --- a/src/blockly/values/currentObjectType.ts +++ b/src/blockly/values/currentObjectType.ts @@ -4,16 +4,16 @@ import { javascriptGenerator, Order } from "blockly/javascript"; Blockly.Blocks['current_object_type_value'] = { init(this: Blockly.Block) { this.appendEndRowInput('NAME') - .appendField('Current object type'); + .appendField('My type'); this.setInputsInline(false) - this.setOutput(true, 'String'); - this.setTooltip('Returns id of current object type'); + this.setOutput(true, 'ObjectType'); + this.setTooltip('Returns current object type'); this.setColour(315); } }; javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) { - return ['objectType.id', Order.NONE]; + return ['objectType', Order.NONE]; }; export { }; diff --git a/src/blockly/values/index.ts b/src/blockly/values/index.ts index d3da9f4..a243c2d 100644 --- a/src/blockly/values/index.ts +++ b/src/blockly/values/index.ts @@ -1,2 +1,6 @@ export * from './currentObject'; export * from './currentObjectType'; +export * from './anyObjectOfType'; +export * from './objectType'; +export * from './positionOfObject'; +export * from './position'; diff --git a/src/blockly/values/objectType.ts b/src/blockly/values/objectType.ts new file mode 100644 index 0000000..1b17af3 --- /dev/null +++ b/src/blockly/values/objectType.ts @@ -0,0 +1,33 @@ +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 { }; diff --git a/src/blockly/values/position.ts b/src/blockly/values/position.ts new file mode 100644 index 0000000..21eb151 --- /dev/null +++ b/src/blockly/values/position.ts @@ -0,0 +1,29 @@ +import * as Blockly from "blockly"; +import { javascriptGenerator, Order } from "blockly/javascript"; + +Blockly.Blocks['pos_value'] = { + init(this: Blockly.Block) { + this.appendDummyInput('POSITION') + .appendField('[') + .appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 2), 'X') + .appendField(',') + .appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 2), 'Y') + .appendField(',') + .appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 2), '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 { }; diff --git a/src/blockly/values/positionOfObject.ts b/src/blockly/values/positionOfObject.ts new file mode 100644 index 0000000..c3ad230 --- /dev/null +++ b/src/blockly/values/positionOfObject.ts @@ -0,0 +1,21 @@ +import * as Blockly from "blockly"; +import { javascriptGenerator, Order } from "blockly/javascript"; + +Blockly.Blocks['position_of_object_value'] = { + init(this: Blockly.Block) { + this.appendDummyInput('NAME') + .appendField('Position of'); + this.appendValueInput('NAME') + .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) { + return ['123', Order.NONE]; +}; + +export { }; diff --git a/src/components/GameObjectView.tsx b/src/components/GameObjectView.tsx index 3744bc0..16b5f16 100644 --- a/src/components/GameObjectView.tsx +++ b/src/components/GameObjectView.tsx @@ -1,11 +1,11 @@ import { observer } from "mobx-react-lite"; -import type { ObjectInstance, Runtime, RuntimeGameObjectInstance } from "../types"; +import type { RuntimeGameObjectInstance } from "../types"; import { state } from "../state"; import { ObjectViewInternal } from "./ObjectViewInternal"; import { PlayerObjectView } from "./CharacterView"; export type GameObjectViewProps = { - object: Runtime; + object: RuntimeGameObjectInstance; isPlayer: boolean; } @@ -17,5 +17,13 @@ export const GameObjectView = observer(function (props: GameObjectViewProps) { if (props.isPlayer) return ; - return ; + function handleClick(): void { + state.game?.emitEvent('object_clicked', { object: props.object }); + } + + return ; }); diff --git a/src/components/ObjectEditorView.tsx b/src/components/ObjectEditorView.tsx index 1f3daf7..9905033 100644 --- a/src/components/ObjectEditorView.tsx +++ b/src/components/ObjectEditorView.tsx @@ -60,15 +60,14 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE if (!objectType) return null; - function handleClick(e: ThreeEvent) { - if (e.delta > 5) return; - e.stopPropagation(); - // Reading selection state inside an event handler: not tracked by observer. - const currentMode = state.worldEditor.isEnabled && - state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id - ? state.worldEditor.selection?.editMode - : undefined; - state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) }); + function handleClick() { + if (state.worldEditor.isEnabled) { + // Reading selection state inside an event handler: not tracked by observer. + const currentMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id + ? state.worldEditor.selection?.editMode + : undefined; + state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) }); + } } function handleTransformEnd() { diff --git a/src/components/ObjectViewInternal.tsx b/src/components/ObjectViewInternal.tsx index 8b62d2f..0e990e8 100644 --- a/src/components/ObjectViewInternal.tsx +++ b/src/components/ObjectViewInternal.tsx @@ -18,7 +18,7 @@ type ObjectViewInternalProps = { objectType: ObjectType; isEditor?: boolean; isPlayer?: boolean; - onClick?: (e: ThreeEvent) => void; + onClick?: () => void; ref?: Ref; } @@ -56,6 +56,37 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props [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; } + + const { direction, amplitude } = impulse; + 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) { + if (e.delta > 5) + return; + e.stopPropagation(); + + props.onClick?.(); + } + return ( { object.cache.voxelGroups.map((vg) => diff --git a/src/components/scriptEditor/ScriptEditorView.tsx b/src/components/scriptEditor/ScriptEditorView.tsx index 815510f..10f1d76 100644 --- a/src/components/scriptEditor/ScriptEditorView.tsx +++ b/src/components/scriptEditor/ScriptEditorView.tsx @@ -19,6 +19,7 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { { kind: 'block', type: 'game_start_event' }, { kind: 'block', type: 'object_touch_start_event' }, { kind: 'block', type: 'player_touch_start_event' }, + { kind: 'block', type: 'object_clicked_event' }, ], }, { @@ -28,6 +29,10 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { contents: [ { kind: 'block', type: 'current_object_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' }, ], }, { @@ -37,6 +42,7 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = { contents: [ { kind: 'block', type: 'console_log_action' }, { kind: 'block', type: 'text_print' }, + { kind: 'block', type: 'physics_apply_impulse_action' }, ], }, { diff --git a/src/model/gameFactory.ts b/src/model/gameFactory.ts index 08d6b3b..6f8a2dc 100644 --- a/src/model/gameFactory.ts +++ b/src/model/gameFactory.ts @@ -79,13 +79,13 @@ export class GameFactory { Object.values(world.objectTypes) .filter((ot) => ot.javascript) .forEach((ot) => { - const otCode = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`); + const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`); Object.values(scene.objects) .filter((o) => o.typeId == ot.id) .forEach((o) => { const api = new ObjectApi(o, ot, world, scene); - otCode(internalBus, { api, o, ot }); + gameScript(internalBus, { api, object: o, objectType: ot }); }) }); } diff --git a/src/state/gameState.ts b/src/state/gameState.ts index ee95518..edd6791 100644 --- a/src/state/gameState.ts +++ b/src/state/gameState.ts @@ -3,6 +3,7 @@ import type { WorldState } from "./worldState"; import type { Game, Pos3, RuntimeGameScene } from "../types"; import type { CameraProps } from "@react-three/fiber"; import { GameEventBus, GameFactory } from "../model/gameFactory"; +import type { GameEventContext } from "../types/runtime/gameBus"; export class GameState { private readonly world: WorldState; @@ -45,18 +46,18 @@ export class GameState { const eventBus = new GameEventBus(); this.eventBus = eventBus; - GameFactory.evalGameScripts(rawWorld, game.scene, eventBus); this._stopAutoSave = this.startAutoSave(); - makeAutoObservable( - this, - ); + makeAutoObservable(this); + + GameFactory.evalGameScripts(rawWorld, this.scene, eventBus); } - public emitEvent(event: string) { + public emitEvent(event: string, context: Omit = {}) { this.eventBus.emit( event, { + ...context, world: this.world.data, scene: this.scene, }, diff --git a/src/types/runtime/gameBus.ts b/src/types/runtime/gameBus.ts index aa2a631..fcad4df 100644 --- a/src/types/runtime/gameBus.ts +++ b/src/types/runtime/gameBus.ts @@ -1,17 +1,12 @@ -import type { ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model"; +import type { RuntimeGameObjectInstance, Scene, World } from "../model"; export type GameEventContext = { world: World; scene: Scene; + object?: RuntimeGameObjectInstance; } -// export type GameObjectEventContext = GameEventContext & { -// object: RuntimeGameObjectInstance; -// objectType: ObjectType; -// } - export type GameEventHandler = (context: GameEventContext) => void; -// export type GameObjectEventHandler = (context: GameObjectEventContext) => void; export type InternalGameEventBus = { onGameEvent: (event: string, handler: GameEventHandler) => void; diff --git a/src/utils/runtime/objectApi.ts b/src/utils/runtime/objectApi.ts index d47b2c3..109ed13 100644 --- a/src/utils/runtime/objectApi.ts +++ b/src/utils/runtime/objectApi.ts @@ -18,7 +18,14 @@ export class ObjectApi { this.scene = scene; } - public applyImpulse(direction: V3, amplitude: number) { + public applyImpulse(targetPosition: 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 }; } }