diff --git a/GDJS/Runtime/InGameEditor/InGameEditor.ts b/GDJS/Runtime/InGameEditor/InGameEditor.ts index aa61b991f9fe..002023c0fab5 100644 --- a/GDJS/Runtime/InGameEditor/InGameEditor.ts +++ b/GDJS/Runtime/InGameEditor/InGameEditor.ts @@ -1,11 +1,75 @@ namespace gdjs { - const LEFTKEY = 37; - const UPKEY = 38; - const RIGHTKEY = 39; - const DOWNKEY = 40; - const LSHIFTKEY = 1016; - const RSHIFTKEY = 2016; - const SPACEKEY = 32; + const LEFT_KEY = 37; + const UP_KEY = 38; + const RIGHT_KEY = 39; + const DOWN_KEY = 40; + const ALT_KEY = 18; + const LEFT_ALT_KEY = gdjs.InputManager.getLocationAwareKeyCode(ALT_KEY, 1); + const RIGHT_ALT_KEY = gdjs.InputManager.getLocationAwareKeyCode(ALT_KEY, 2); + const SHIFT_KEY = 16; + const LEFT_SHIFT_KEY = gdjs.InputManager.getLocationAwareKeyCode( + SHIFT_KEY, + 1 + ); + const RIGHT_SHIFT_KEY = gdjs.InputManager.getLocationAwareKeyCode( + SHIFT_KEY, + 2 + ); + const SPACE_KEY = 32; + const CTRL_KEY = 17; + const LEFT_CTRL_KEY = gdjs.InputManager.getLocationAwareKeyCode(CTRL_KEY, 1); + const RIGHT_CTRL_KEY = gdjs.InputManager.getLocationAwareKeyCode(CTRL_KEY, 2); + const LEFT_META_KEY = gdjs.InputManager.getLocationAwareKeyCode(91, 1); + const RIGHT_META_KEY = gdjs.InputManager.getLocationAwareKeyCode(93, 2); + + // TODO: factor this? + const isMacLike = + typeof navigator !== 'undefined' && + navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) + ? true + : false; + + const isControlOrCmdPressed = (inputManager: gdjs.InputManager) => { + // On macOS, meta key (Apple/Command key) acts as Control key on Windows/Linux. + return ( + inputManager.isKeyPressed(LEFT_CTRL_KEY) || + inputManager.isKeyPressed(RIGHT_CTRL_KEY) || + inputManager.isKeyPressed(LEFT_META_KEY) || + inputManager.isKeyPressed(RIGHT_META_KEY) + ); + }; + + const isAltPressed = (inputManager: gdjs.InputManager) => { + return ( + inputManager.isKeyPressed(LEFT_ALT_KEY) || + inputManager.isKeyPressed(RIGHT_ALT_KEY) + ); + }; + + const isShiftPressed = (inputManager: gdjs.InputManager) => { + return ( + inputManager.isKeyPressed(LEFT_SHIFT_KEY) || + inputManager.isKeyPressed(RIGHT_SHIFT_KEY) + ); + }; + + const shouldScrollHorizontally = isAltPressed; + + const shouldZoom = (inputManager: gdjs.InputManager) => { + // Browsers trigger a wheel event with ctrlKey or metaKey to true when the user + // does a pinch gesture on a trackpad. If this is the case, we zoom. + // see https://dev.to/danburzo/pinch-me-i-m-zooming-gestures-in-the-dom-a0e + if (isControlOrCmdPressed(inputManager)) return true; + if (isMacLike) { + return isControlOrCmdPressed(inputManager); + } else { + return ( + !isControlOrCmdPressed(inputManager) && + !isAltPressed(inputManager) && + !isShiftPressed(inputManager) + ); + } + }; export class InGameEditor { _runtimeGame: RuntimeGame; @@ -114,8 +178,9 @@ namespace gdjs { // TODO: replace everything by "real 3D movement". // Mouse wheel: forward/backward movement. - const wheelDelta = inputManager.getMouseWheelDelta(); - if (wheelDelta !== 0) { + const wheelDeltaY = inputManager.getMouseWheelDelta(); + const wheelDeltaX = inputManager.getMouseWheelDeltaX(); + if (shouldZoom(inputManager)) { // TODO: factor this? const assumedFovIn2D = 45; const layerRenderer = layer.getRenderer(); @@ -126,26 +191,31 @@ namespace gdjs { : threeCamera.fov : assumedFovIn2D; - layer.setCameraZ(layer.getCameraZ(fov) + wheelDelta, fov); + layer.setCameraZ(layer.getCameraZ(fov) - wheelDeltaY, fov); + } else if (shouldScrollHorizontally(inputManager)) { + layer.setCameraX(layer.getCameraX() + wheelDeltaY / 5); + } else { + layer.setCameraX(layer.getCameraX() + wheelDeltaX / 5); + layer.setCameraY(layer.getCameraY() - wheelDeltaY / 5); } // Movement with the keyboard - if (inputManager.isKeyPressed(LEFTKEY)) { + if (inputManager.isKeyPressed(LEFT_KEY)) { layer.setCameraX(layer.getCameraX() - 5); } - if (inputManager.isKeyPressed(RIGHTKEY)) { + if (inputManager.isKeyPressed(RIGHT_KEY)) { layer.setCameraX(layer.getCameraX() + 5); } - if (inputManager.isKeyPressed(UPKEY)) { + if (inputManager.isKeyPressed(UP_KEY)) { layer.setCameraY(layer.getCameraY() - 5); } - if (inputManager.isKeyPressed(DOWNKEY)) { + if (inputManager.isKeyPressed(DOWN_KEY)) { layer.setCameraY(layer.getCameraY() + 5); } // Space + click: move the camera on its plane. if ( - inputManager.isKeyPressed(SPACEKEY) && + inputManager.isKeyPressed(SPACE_KEY) && inputManager.isMouseButtonPressed(0) ) { const xDelta = this._lastCursorX - inputManager.getCursorX(); @@ -179,21 +249,20 @@ namespace gdjs { } } - reloadInstances(payload: { - layoutName: string; + reloadInstances( instances: Array<{ persistentUuid: string; position: { x: number; y: number; z: number }; - }>; - }) { + }> + ) { const currentScene = this._runtimeGame.getSceneStack().getCurrentScene(); - if (!currentScene || currentScene.getName() !== payload.layoutName) { + if (!currentScene) { return; } // TODO: Might be worth indexing instances data and runtime objects by their // persistentUuid (See HotReloader.indexByPersistentUuid). currentScene.getAdhocListOfAllInstances().forEach((runtimeObject) => { - const instance = payload.instances.find( + const instance = instances.find( (instance) => instance.persistentUuid === runtimeObject.persistentUuid ); if (instance) { diff --git a/GDJS/Runtime/debugger-client/abstract-debugger-client.ts b/GDJS/Runtime/debugger-client/abstract-debugger-client.ts index ded7679f7b99..af06b51d2ffa 100644 --- a/GDJS/Runtime/debugger-client/abstract-debugger-client.ts +++ b/GDJS/Runtime/debugger-client/abstract-debugger-client.ts @@ -263,7 +263,7 @@ namespace gdjs { } else if (data.command === 'profiler.stop') { runtimeGame.stopCurrentSceneProfiler(); } else if (data.command === 'instances.updated') { - runtimeGame._editor.reloadInstances(data.payload); + runtimeGame._editor.reloadInstances(data.payload.instances); } else if (data.command === 'hotReload') { that._hotReloader.hotReload().then((logs) => { that.sendHotReloaderLogs(logs); @@ -276,7 +276,9 @@ namespace gdjs { const sceneName = data.sceneName || null; const externalLayoutName = data.externalLayoutName || null; if (!sceneName) { - logger.warn('No scene name specified, switchForInGameEdition aborted'); + logger.warn( + 'No scene name specified, switchForInGameEdition aborted' + ); return; } @@ -293,14 +295,12 @@ namespace gdjs { } } - runtimeGame - .getSceneStack() - .replace({ - sceneName, - externalLayoutName, - skipCreatingInstancesFromScene: !!externalLayoutName, - clear: true, - }); + runtimeGame.getSceneStack().replace({ + sceneName, + externalLayoutName, + skipCreatingInstancesFromScene: !!externalLayoutName, + clear: true, + }); // Update initialRuntimeGameStatus so that a hard reload // will come back to the same state, and so that we can check later diff --git a/GDJS/Runtime/inputmanager.ts b/GDJS/Runtime/inputmanager.ts index 99e9783eb7c7..124f0b63e38f 100644 --- a/GDJS/Runtime/inputmanager.ts +++ b/GDJS/Runtime/inputmanager.ts @@ -23,33 +23,37 @@ namespace gdjs { * variants and should default to their left variant values * if location is not specified. */ - static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91]; - _pressedKeys: Hashtable; - _releasedKeys: Hashtable; - _lastPressedKey: float = 0; - _pressedMouseButtons: Array; - _releasedMouseButtons: Array; + private static _DEFAULT_LEFT_VARIANT_KEYS: integer[] = [16, 17, 18, 91]; + private _pressedKeys: Hashtable; + private _releasedKeys: Hashtable; + private _lastPressedKey: float = 0; + private _pressedMouseButtons: Array; + private _releasedMouseButtons: Array; /** * The cursor X position (moved by mouse and touch events). */ - _cursorX: float = 0; + private _cursorX: float = 0; /** * The cursor Y position (moved by mouse and touch events). */ - _cursorY: float = 0; + private _cursorY: float = 0; /** * The mouse X position (only moved by mouse events). */ - _mouseX: float = 0; + private _mouseX: float = 0; /** * The mouse Y position (only moved by mouse events). */ - _mouseY: float = 0; - _isMouseInsideCanvas: boolean = true; - _mouseWheelDelta: float = 0; + private _mouseY: float = 0; + private _isMouseInsideCanvas: boolean = true; + private _wheelDeltaX: float = 0; + private _wheelDeltaY: float = 0; + private _wheelDeltaZ: float = 0; + // TODO Remove _touches when there is no longer SpritePanelButton 1.2.0 // extension in the wild. - _touches = { + // @ts-ignore + private _touches = { firstKey: (): string | number | null => { for (const key in this._mouseOrTouches.items) { // Exclude mouse key. @@ -60,22 +64,23 @@ namespace gdjs { return null; }, }; - _mouseOrTouches: Hashtable; + + private _mouseOrTouches: Hashtable; //Identifiers of the touches that started during/before the frame. - _startedTouches: Array = []; + private _startedTouches: Array = []; //Identifiers of the touches that ended during/before the frame. - _endedTouches: Array = []; - _touchSimulateMouse: boolean = true; + private _endedTouches: Array = []; + private _touchSimulateMouse: boolean = true; /** * @deprecated */ - _lastStartedTouchIndex = 0; + private _lastStartedTouchIndex = 0; /** * @deprecated */ - _lastEndedTouchIndex = 0; + private _lastEndedTouchIndex = 0; constructor() { this._pressedKeys = new Hashtable(); @@ -94,7 +99,7 @@ namespace gdjs { * @param keyCode The raw key code * @param location The location */ - _getLocationAwareKeyCode( + static getLocationAwareKeyCode( keyCode: number, location: number | null | undefined ): integer { @@ -119,7 +124,7 @@ namespace gdjs { * @param location The location of the event. */ onKeyPressed(keyCode: number, location?: number): void { - const locationAwareKeyCode = this._getLocationAwareKeyCode( + const locationAwareKeyCode = InputManager.getLocationAwareKeyCode( keyCode, location ); @@ -135,7 +140,7 @@ namespace gdjs { * @param location The location of the event. */ onKeyReleased(keyCode: number, location?: number): void { - const locationAwareKeyCode = this._getLocationAwareKeyCode( + const locationAwareKeyCode = InputManager.getLocationAwareKeyCode( keyCode, location ); @@ -348,17 +353,37 @@ namespace gdjs { /** * Should be called whenever the mouse wheel is used - * @param wheelDelta The mouse wheel delta + * @param wheelDeltaY The mouse wheel delta */ - onMouseWheel(wheelDelta: number): void { - this._mouseWheelDelta = wheelDelta; + onMouseWheel( + wheelDeltaY: number, + wheelDeltaX: number, + wheelDeltaZ: number + ): void { + this._wheelDeltaY = wheelDeltaY; + if (wheelDeltaX !== undefined) this._wheelDeltaX = wheelDeltaX; + if (wheelDeltaZ !== undefined) this._wheelDeltaZ = wheelDeltaZ; } /** - * Return the mouse wheel delta + * Return the mouse wheel delta on Y axis. */ getMouseWheelDelta(): float { - return this._mouseWheelDelta; + return this._wheelDeltaY; + } + + /** + * Return the mouse wheel delta on X axis. + */ + getMouseWheelDeltaX(): float { + return this._wheelDeltaX; + } + + /** + * Return the mouse wheel delta on Z axis. + */ + getMouseWheelDeltaZ(): float { + return this._wheelDeltaZ; } /** @@ -546,7 +571,9 @@ namespace gdjs { this._endedTouches.length = 0; this._releasedKeys.clear(); this._releasedMouseButtons.length = 0; - this._mouseWheelDelta = 0; + this._wheelDeltaX = 0; + this._wheelDeltaY = 0; + this._wheelDeltaZ = 0; this._lastStartedTouchIndex = 0; this._lastEndedTouchIndex = 0; } diff --git a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts index 1868630b63dd..9e673926d796 100644 --- a/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/runtimegame-pixi-renderer.ts @@ -741,7 +741,7 @@ namespace gdjs { }; // @ts-ignore canvas.onwheel = function (event) { - manager.onMouseWheel(-event.deltaY); + manager.onMouseWheel(-event.deltaY, event.deltaX, event.deltaZ); }; // Touches: diff --git a/GDJS/Runtime/scenestack.ts b/GDJS/Runtime/scenestack.ts index 0548e6f58d62..600f28d710ce 100644 --- a/GDJS/Runtime/scenestack.ts +++ b/GDJS/Runtime/scenestack.ts @@ -6,11 +6,11 @@ namespace gdjs { sceneName: string; externalLayoutName?: string; skipCreatingInstancesFromScene?: boolean; - }; + } interface ReplaceSceneOptions extends PushSceneOptions { clear: boolean; - }; + } /** * Hold the stack of scenes ({@link gdjs.RuntimeScene}) being played. @@ -123,7 +123,6 @@ namespace gdjs { } } - /** * Pause the scene currently being played and start the new scene that is specified in `options.sceneName`. * If `options.externalLayoutName` is set, also instantiate the objects from this external layout. @@ -136,12 +135,14 @@ namespace gdjs { deprecatedExternalLayoutName?: string ): gdjs.RuntimeScene | null { this._throwIfDisposed(); - console.log({options, deprecatedExternalLayoutName}) + console.log({ options, deprecatedExternalLayoutName }); const sceneName = typeof options === 'string' ? options : options.sceneName; const skipCreatingInstancesFromScene = - typeof options === 'string' ? false : options.skipCreatingInstancesFromScene; + typeof options === 'string' + ? false + : options.skipCreatingInstancesFromScene; const externalLayoutName = deprecatedExternalLayoutName || (typeof options === 'string' ? undefined : options.externalLayoutName); @@ -214,8 +215,12 @@ namespace gdjs { * @param options Contains the scene name and optional external layout name to instantiate. * @param deprecatedClear Deprecated, use `options.clear` instead. */ - replace(options: ReplaceSceneOptions | string, deprecatedClear?: boolean): gdjs.RuntimeScene | null { - const clear = deprecatedClear || typeof options === 'string' ? false : options.clear; + replace( + options: ReplaceSceneOptions | string, + deprecatedClear?: boolean + ): gdjs.RuntimeScene | null { + const clear = + deprecatedClear || typeof options === 'string' ? false : options.clear; this._throwIfDisposed(); if (!!clear) {