diff --git a/front/dist/resources/objects/joystickSplitted.png b/front/dist/resources/objects/joystickSplitted.png new file mode 100644 index 00000000..04ae67a3 Binary files /dev/null and b/front/dist/resources/objects/joystickSplitted.png differ diff --git a/front/dist/resources/objects/smallHandleFilledGrey.png b/front/dist/resources/objects/smallHandleFilledGrey.png new file mode 100644 index 00000000..40e968fb Binary files /dev/null and b/front/dist/resources/objects/smallHandleFilledGrey.png differ diff --git a/front/src/Phaser/Components/MobileJoystick.ts b/front/src/Phaser/Components/MobileJoystick.ts new file mode 100644 index 00000000..1ace529c --- /dev/null +++ b/front/src/Phaser/Components/MobileJoystick.ts @@ -0,0 +1,35 @@ +import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js'; + +const outOfScreenX = -1000; +const outOfScreenY = -1000; + + +//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free +export const joystickBaseKey = 'joystickBase'; +export const joystickBaseImg = 'resources/objects/joystickSplitted.png'; +export const joystickThumbKey = 'joystickThumb'; +export const joystickThumbImg = 'resources/objects/smallHandleFilledGrey.png'; + +export class MobileJoystick extends VirtualJoystick { + + constructor(scene: Phaser.Scene) { + super(scene, { + x: outOfScreenX, + y: outOfScreenY, + radius: 20, + base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(60, 60).setDepth(99999), + thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(30, 30).setDepth(99999), + enable: true, + dir: "8dir", + }); + + this.scene.input.on('pointerdown', (pointer: { x: number; y: number; }) => { + this.x = pointer.x; + this.y = pointer.y; + }); + this.scene.input.on('pointerup', () => { + this.x = outOfScreenX; + this.y = outOfScreenY; + }); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index c146c06d..da10a8ca 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -34,7 +34,7 @@ export class GameManager { public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise { this.startRoom = await connectionManager.initGameConnexion(); await this.loadMap(this.startRoom, scenePlugin); - + if (!this.playerName) { return LoginSceneName; } else if (!this.characterLayers || !this.characterLayers.length) { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index f07e936b..990f702c 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -58,10 +58,6 @@ import {Room} from "../../Connexion/Room"; import {jitsiFactory} from "../../WebRtc/JitsiFactory"; import {urlManager} from "../../Url/UrlManager"; import {audioManager} from "../../WebRtc/AudioManager"; -import {IVirtualJoystick} from "../../types"; -const { - default: VirtualJoystick, -} = require("phaser3-rex-plugins/plugins/virtualjoystick.js"); import {PresentationModeIcon} from "../Components/PresentationModeIcon"; import {ChatModeIcon} from "../Components/ChatModeIcon"; import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; @@ -81,6 +77,9 @@ import DOMElement = Phaser.GameObjects.DOMElement; import {Subscription} from "rxjs"; import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream"; import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager"; +import {touchScreenManager} from "../../Touch/TouchScreenManager"; +import {PinchManager} from "../UserInput/PinchManager"; +import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -175,7 +174,6 @@ export class GameScene extends ResizableScene implements CenterListener { private messageSubscription: Subscription|null = null; private popUpElements : Map = new Map(); private originalMapUrl: string|undefined; - public virtualJoystick!: IVirtualJoystick; constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) { super({ @@ -199,6 +197,7 @@ export class GameScene extends ResizableScene implements CenterListener { //hook preload scene preload(): void { + addLoader(this); const localUser = localUserStore.getLocalUser(); const textures = localUser?.textures; if (textures) { @@ -208,6 +207,10 @@ export class GameScene extends ResizableScene implements CenterListener { } this.load.image(openChatIconName, 'resources/objects/talk.png'); + if (touchScreenManager.supportTouchScreen) { + this.load.image(joystickBaseKey, joystickBaseImg); + this.load.image(joystickThumbKey, joystickThumbImg); + } this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { @@ -254,8 +257,6 @@ export class GameScene extends ResizableScene implements CenterListener { this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32}); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); - - addLoader(this); } // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. @@ -358,6 +359,10 @@ export class GameScene extends ResizableScene implements CenterListener { urlManager.pushRoomIdToUrl(this.room); this.startLayerName = urlManager.getStartLayerNameFromUrl(); + if (touchScreenManager.supportTouchScreen) { + new PinchManager(this); + } + this.messageSubscription = worldFullMessageStream.stream.subscribe((message) => this.showWorldFullError()) const playerName = gameManager.getPlayerName(); @@ -411,26 +416,10 @@ export class GameScene extends ResizableScene implements CenterListener { //initialise list of other player this.MapPlayers = this.physics.add.group({immovable: true}); - this.virtualJoystick = new VirtualJoystick(this, { - x: this.game.renderer.width / 2, - y: this.game.renderer.height / 2, - radius: 20, - base: this.add.circle(0, 0, 20), - thumb: this.add.circle(0, 0, 10), - enable: true, - dir: "8dir", - }); - this.virtualJoystick.visible = true; + //create input to move mediaManager.setUserInputManager(this.userInputManager); - this.userInputManager = new UserInputManager(this, this.virtualJoystick); - - // Listener event to reposition virtual joystick - // whatever place you click in game area - this.input.on('pointerdown', (pointer: { x: number; y: number; }) => { - this.virtualJoystick.x = pointer.x; - this.virtualJoystick.y = pointer.y; - }); + this.userInputManager = new UserInputManager(this); if (localUserStore.getFullscreen()) { document.querySelector('body')?.requestFullscreen(); diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index c3aa5077..6a91fc34 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -7,6 +7,8 @@ import {RESOLUTION} from "../../Enum/EnvironmentVariable"; import {SoundMeter} from "../Components/SoundMeter"; import {SoundMeterSprite} from "../Components/SoundMeterSprite"; import {HtmlUtils} from "../../WebRtc/HtmlUtils"; +import {touchScreenManager} from "../../Touch/TouchScreenManager"; +import {PinchManager} from "../UserInput/PinchManager"; export const EnableCameraSceneName = "EnableCameraScene"; enum LoginTextures { @@ -54,6 +56,10 @@ export class EnableCameraScene extends Phaser.Scene { } create() { + if (touchScreenManager.supportTouchScreen) { + new PinchManager(this); + } + this.textField = new TextField(this, this.game.renderer.width / 2, 20, 'Turn on your camera and microphone'); this.pressReturnField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 30, 'Touch here\n\n or \n\nPress enter to start'); diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 4ff582b4..057cb6ae 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -7,6 +7,8 @@ import {ResizableScene} from "./ResizableScene"; import {isUserNameValid, maxUserNameLength} from "../../Connexion/LocalUser"; import { localUserStore } from "../../Connexion/LocalUserStore"; import Rectangle = Phaser.GameObjects.Rectangle; +import {touchScreenManager} from "../../Touch/TouchScreenManager"; +import {PinchManager} from "../UserInput/PinchManager"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -39,6 +41,9 @@ export class LoginScene extends ResizableScene { } create() { + if (touchScreenManager.supportTouchScreen) { + new PinchManager(this); + } this.nameInput = new TextInput(this, this.game.renderer.width / 2, 70, maxUserNameLength, this.name,(text: string) => { this.name = text; diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index edd09a91..3c8d0281 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -4,13 +4,14 @@ import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; import {EnableCameraSceneName} from "./EnableCameraScene"; import {CustomizeSceneName} from "./CustomizeScene"; -import {ResizableScene} from "./ResizableScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {loadAllDefaultModels, loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager"; +import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager"; import {addLoader} from "../Components/Loader"; import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures"; import {AbstractCharacterScene} from "./AbstractCharacterScene"; import {areCharacterLayersValid} from "../../Connexion/LocalUser"; +import {touchScreenManager} from "../../Touch/TouchScreenManager"; +import {PinchManager} from "../UserInput/PinchManager"; //todo: put this constants in a dedicated file @@ -66,6 +67,9 @@ export class SelectCharacterScene extends AbstractCharacterScene { } create() { + if (touchScreenManager.supportTouchScreen) { + new PinchManager(this); + } this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Select your character'); this.pressReturnField = new TextField( this, diff --git a/front/src/Phaser/Login/SelectCompanionScene.ts b/front/src/Phaser/Login/SelectCompanionScene.ts index c7ee56f1..aeacd0c2 100644 --- a/front/src/Phaser/Login/SelectCompanionScene.ts +++ b/front/src/Phaser/Login/SelectCompanionScene.ts @@ -8,6 +8,8 @@ import { EnableCameraSceneName } from "./EnableCameraScene"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures"; import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingManager"; +import {touchScreenManager} from "../../Touch/TouchScreenManager"; +import {PinchManager} from "../UserInput/PinchManager"; export const SelectCompanionSceneName = "SelectCompanionScene"; @@ -54,6 +56,10 @@ export class SelectCompanionScene extends ResizableScene { } create() { + if (touchScreenManager.supportTouchScreen) { + new PinchManager(this); + } + this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Select your companion'); const confirmTouchAreaY = 115 + 32 * Math.ceil(this.companionModels.length / this.nbCharactersPerRow); diff --git a/front/src/Phaser/UserInput/PinchManager.ts b/front/src/Phaser/UserInput/PinchManager.ts new file mode 100644 index 00000000..f7c445fa --- /dev/null +++ b/front/src/Phaser/UserInput/PinchManager.ts @@ -0,0 +1,22 @@ +import {Pinch} from "phaser3-rex-plugins/plugins/gestures.js"; + +export class PinchManager { + private scene: Phaser.Scene; + private pinch!: any; // eslint-disable-line + + constructor(scene: Phaser.Scene) { + this.scene = scene; + this.pinch = new Pinch(scene); + + this.pinch.on('pinch', (pinch:any) => { // eslint-disable-line + let newZoom = this.scene.cameras.main.zoom * pinch.scaleFactor; + if (newZoom < 0.25) { + newZoom = 0.25; + } else if(newZoom > 2) { + newZoom = 2; + } + this.scene.cameras.main.setZoom(newZoom); + }); + } + +} \ No newline at end of file diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index c3f2c0bb..2f14672b 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -1,8 +1,7 @@ -import { Direction, IVirtualJoystick } from "../../types"; +import { Direction } from "../../types"; import {GameScene} from "../Game/GameScene"; -const { - default: VirtualJoystick, -} = require("phaser3-rex-plugins/plugins/virtualjoystick.js"); +import {touchScreenManager} from "../../Touch/TouchScreenManager"; +import {MobileJoystick} from "../Components/MobileJoystick"; interface UserInputManagerDatum { keyInstance: Phaser.Input.Keyboard.Key; @@ -20,6 +19,7 @@ export enum UserInputEvent { JoystickMove, } + //we cannot use a map structure so we have to create a replacment export class ActiveEventList { private eventMap : Map = new Map(); @@ -44,17 +44,23 @@ export class UserInputManager { private Scene: GameScene; private isInputDisabled : boolean; - private joystick : IVirtualJoystick; + private joystick!: MobileJoystick; private joystickEvents = new ActiveEventList(); private joystickForceThreshold = 60; private joystickForceAccuX = 0; private joystickForceAccuY = 0; - constructor(Scene: GameScene, virtualJoystick: IVirtualJoystick) { + constructor(Scene: GameScene) { this.Scene = Scene; this.isInputDisabled = false; this.initKeyBoardEvent(); - this.joystick = virtualJoystick; + if (touchScreenManager.supportTouchScreen) { + this.initVirtualJoystick(); + } + } + + initVirtualJoystick() { + this.joystick = new MobileJoystick(this.Scene); this.joystick.on("update", () => { this.joystickForceAccuX = this.joystick.forceX ? this.joystickForceAccuX : 0; this.joystickForceAccuY = this.joystick.forceY ? this.joystickForceAccuY : 0; @@ -62,18 +68,18 @@ export class UserInputManager { for (const name in cursorKeys) { const key = cursorKeys[name as Direction]; switch (name) { - case "up": - this.joystickEvents.set(UserInputEvent.MoveUp, key.isDown); - break; - case "left": - this.joystickEvents.set(UserInputEvent.MoveLeft, key.isDown); - break; - case "down": - this.joystickEvents.set(UserInputEvent.MoveDown, key.isDown); - break; - case "right": - this.joystickEvents.set(UserInputEvent.MoveRight, key.isDown); - break; + case "up": + this.joystickEvents.set(UserInputEvent.MoveUp, key.isDown); + break; + case "left": + this.joystickEvents.set(UserInputEvent.MoveLeft, key.isDown); + break; + case "down": + this.joystickEvents.set(UserInputEvent.MoveDown, key.isDown); + break; + case "right": + this.joystickEvents.set(UserInputEvent.MoveRight, key.isDown); + break; } } }); @@ -105,6 +111,7 @@ export class UserInputManager { this.Scene.input.keyboard.removeAllListeners(); } + //todo: should we also disable the joystick? disableControls(){ this.Scene.input.keyboard.removeAllKeys(); this.isInputDisabled = true; diff --git a/front/src/Touch/TouchScreenManager.ts b/front/src/Touch/TouchScreenManager.ts new file mode 100644 index 00000000..dcb56ded --- /dev/null +++ b/front/src/Touch/TouchScreenManager.ts @@ -0,0 +1,16 @@ + +class TouchScreenManager { + + readonly supportTouchScreen:boolean; + + constructor() { + this.supportTouchScreen = this.detectTouchscreen(); + } + + //found here: https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript#4819886 + detectTouchscreen(): boolean { + return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)); + } +} + +export const touchScreenManager = new TouchScreenManager(); \ No newline at end of file diff --git a/front/src/index.ts b/front/src/index.ts index 89582cd4..268f857d 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -2,7 +2,7 @@ import 'phaser'; import GameConfig = Phaser.Types.Core.GameConfig; import "../dist/resources/style/index.scss"; -import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable"; +import {DEBUG_MODE, RESOLUTION} from "./Enum/EnvironmentVariable"; import {LoginScene} from "./Phaser/Login/LoginScene"; import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene"; @@ -17,7 +17,6 @@ import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene"; import {localUserStore} from "./Connexion/LocalUserStore"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; import {iframeListener} from "./Api/IframeListener"; -import {discussionManager} from "./WebRtc/DiscussionManager"; const {width, height} = coWebsiteManager.getGameSize(); diff --git a/front/src/rex-plugins.d.ts b/front/src/rex-plugins.d.ts new file mode 100644 index 00000000..7ba8f65b --- /dev/null +++ b/front/src/rex-plugins.d.ts @@ -0,0 +1,12 @@ + +declare module 'phaser3-rex-plugins/plugins/virtualjoystick.js' { + const content: any; // eslint-disable-line + export default content; +} +declare module 'phaser3-rex-plugins/plugins/gestures-plugin.js' { + const content: any; // eslint-disable-line + export default content; +} +declare module 'phaser3-rex-plugins/plugins/gestures.js' { + export const Pinch: any; // eslint-disable-line +} \ No newline at end of file