diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html index d5e9ad7e..ce740ec5 100644 --- a/front/dist/resources/html/gameMenu.html +++ b/front/dist/resources/html/gameMenu.html @@ -36,6 +36,9 @@
+
+ +
diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index ba5b6c07..5544ca47 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -151,6 +151,7 @@ video#myCamVideo{ .btn-cam-action { + pointer-events: all; position: absolute; bottom: 0px; right: 0px; @@ -186,18 +187,26 @@ video#myCamVideo{ transition: 280ms; } .btn-micro{ + pointer-events: auto; transition: all .3s; right: 44px; } .btn-video{ + pointer-events: auto; transition: all .25s; right: 134px; } .btn-monitor{ + pointer-events: auto; transition: all .2s; right: 224px; } - +.btn-copy{ + pointer-events: auto; + transition: all .3s; + right: 44px; + opacity: 1; +} .btn-cam-action div img{ height: 22px; width: 30px; @@ -502,6 +511,7 @@ input[type=range]:focus::-ms-fill-upper { position: absolute; width: 100%; height: 100%; + pointer-events: none; /* TODO: DO WE NEED FLEX HERE???? WE WANT A SIDEBAR OF EXACTLY 25% (note: flex useful for direction!!!) */ } @@ -537,6 +547,7 @@ input[type=range]:focus::-ms-fill-upper { .sidebar { flex: 0 0 25%; display: flex; + pointer-events: none; } .sidebar > div { diff --git a/front/package.json b/front/package.json index 1ba7f3c2..6189ff4c 100644 --- a/front/package.json +++ b/front/package.json @@ -31,6 +31,7 @@ "generic-type-guard": "^3.2.0", "google-protobuf": "^3.13.0", "phaser": "3.24.1", + "phaser3-rex-plugins": "^1.1.42", "queue-typescript": "^1.0.1", "quill": "^1.3.7", "rxjs": "^6.6.3", diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index 4741fc8c..ace7b17e 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -9,7 +9,8 @@ const gameQualityKey = 'gameQuality'; const videoQualityKey = 'videoQuality'; const audioPlayerVolumeKey = 'audioVolume'; const audioPlayerMuteKey = 'audioMute'; -const helpCameraSettingsShown = 'helpCameraSettingsShown'; +const helpCameraSettingsShown = 'helpCameraSettingsShown'; +const fullscreenKey = 'fullscreen'; class LocalUserStore { saveUser(localUser: LocalUser) { @@ -100,6 +101,13 @@ class LocalUserStore { getHelpCameraSettingsShown(): boolean { return localStorage.getItem(helpCameraSettingsShown) === '1'; } + + setFullscreen(value: boolean): void { + localStorage.setItem(fullscreenKey, value.toString()); + } + getFullscreen(): boolean { + return localStorage.getItem(fullscreenKey) === 'true'; + } } export const localUserStore = new LocalUserStore(); diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts index 1e01029b..a8ea772f 100644 --- a/front/src/Phaser/Components/TextInput.ts +++ b/front/src/Phaser/Components/TextInput.ts @@ -1,46 +1,68 @@ +const IGNORED_KEYS = new Set([ + 'Esc', + 'Escape', + 'Alt', + 'Meta', + 'Control', + 'Ctrl', + 'Space', + 'Backspace' +]) + export class TextInput extends Phaser.GameObjects.BitmapText { private minUnderLineLength = 4; private underLine: Phaser.GameObjects.Text; + private domInput = document.createElement('input'); - constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, onChange: (text: string) => void) { + constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, + onChange: (text: string) => void) { super(scene, x, y, 'main_font', text, 32); - this.setOrigin(0.5).setCenterAlign() + this.setOrigin(0.5).setCenterAlign(); this.scene.add.existing(this); - this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) - this.underLine.setOrigin(0.5) + const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}; + this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style); + this.underLine.setOrigin(0.5); + this.domInput.maxLength = maxLength; + this.domInput.style.opacity = "0"; + if (text) { + this.domInput.value = text; + } - this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => { - if (event.keyCode === 8 && this.text.length > 0) { - this.deleteLetter(); - } else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) { - this.addLetter(event.key); + this.domInput.addEventListener('keydown', event => { + if (IGNORED_KEYS.has(event.key)) { + return; } + + if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) { + event.preventDefault(); + } + }); + + this.domInput.addEventListener('input', (event) => { + if (event.defaultPrevented) { + return; + } + this.text = this.domInput.value; this.underLine.text = this.getUnderLineBody(this.text.length); onChange(this.text); }); + + document.body.append(this.domInput); + this.focus(); } - + private getUnderLineBody(textLength:number): string { if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength; let text = '_______'; for (let i = this.minUnderLineLength; i < textLength; i++) { - text += '__' + text += '__'; } return text; } - private deleteLetter() { - this.text = this.text.substr(0, this.text.length - 1); - } - - - private addLetter(letter: string) { - this.text += letter; - } - getText(): string { return this.text; } @@ -56,4 +78,13 @@ export class TextInput extends Phaser.GameObjects.BitmapText { this.underLine.y = y+1; return this; } + + focus() { + this.domInput.focus(); + } + + destroy(): void { + super.destroy(); + this.domInput.remove(); + } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index a9ba86a6..294174a2 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -51,6 +51,10 @@ 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"; @@ -164,6 +168,7 @@ 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({ @@ -399,9 +404,30 @@ 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 - this.userInputManager = new UserInputManager(this); 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; + }); + + if (localUserStore.getFullscreen()) { + document.querySelector('body')?.requestFullscreen(); + } //notify game manager can to create currentUser in map this.createCurrentPlayer(); @@ -668,7 +694,7 @@ export class GameScene extends ResizableScene implements CenterListener { if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(WEBSITE_MESSAGE_PROPERTIES); if(message === undefined){ - message = 'Press on SPACE to open the web site'; + message = 'Press SPACE or touch here to open web site'; } layoutManager.addActionButton('openWebsite', message.toString(), () => { openWebsiteFunction(); @@ -700,7 +726,7 @@ export class GameScene extends ResizableScene implements CenterListener { if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) { let message = allProps.get(JITSI_MESSAGE_PROPERTIES); if (message === undefined) { - message = 'Press on SPACE to enter in jitsi meet room'; + message = 'Press SPACE or touch here to enter Jitsi Meet room'; } layoutManager.addActionButton('jitsiRoom', message.toString(), () => { openJitsiRoomFunction(); diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index 6cf13a86..3b27f01e 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -35,6 +35,13 @@ export class CustomizeScene extends AbstractCharacterScene { private Rectangle!: Rectangle; + private mobileTapUP!: Rectangle; + private mobileTapDOWN!: Rectangle; + private mobileTapLEFT!: Rectangle; + private mobileTapRIGHT!: Rectangle; + + private mobileTapENTER!: Rectangle; + private logo!: Image; private selectedLayers: number[] = [0]; @@ -70,7 +77,7 @@ export class CustomizeScene extends AbstractCharacterScene { create() { this.textField = new TextField(this, this.game.renderer.width / 2, 30, 'Customize your own Avatar!'); - this.enterField = new TextField(this, this.game.renderer.width / 2, 40, 'you can start the game by pressing SPACE..'); + this.enterField = new TextField(this, this.game.renderer.width / 2, 60, 'Start the game by pressing ENTER\n\n or touching the center rectangle'); this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, CustomizeTextures.icon); this.add.existing(this.logo); @@ -78,22 +85,88 @@ export class CustomizeScene extends AbstractCharacterScene { this.arrowRight = new Image(this, this.game.renderer.width*0.9, this.game.renderer.height/2, CustomizeTextures.arrowRight); this.add.existing(this.arrowRight); + this.mobileTapRIGHT = this.add + .rectangle( + this.game.renderer.width*0.9, + this.game.renderer.height/2, + 32, + 32, + ) + .setInteractive() + .on("pointerdown", () => { + this.moveCursorHorizontally(1); + }); this.arrowLeft = new Image(this, this.game.renderer.width/9, this.game.renderer.height/2, CustomizeTextures.arrowRight); this.arrowLeft.flipX = true; this.add.existing(this.arrowLeft); - + this.mobileTapLEFT = this.add + .rectangle( + this.game.renderer.width/9, + this.game.renderer.height/2, + 32, + 32, + ) + .setInteractive() + .on("pointerdown", () => { + this.moveCursorHorizontally(-1); + }); this.Rectangle = this.add.rectangle(this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.y + this.cameras.main.height / 2, 32, 33) this.Rectangle.setStrokeStyle(2, 0xFFFFFF); this.add.existing(this.Rectangle); + this.mobileTapENTER = this.add + .rectangle( + this.cameras.main.worldView.x + this.cameras.main.width / 2, + this.cameras.main.worldView.y + this.cameras.main.height / 2, + 32, + 32, + ) + .setInteractive() + .on("pointerdown", () => { + const layers: string[] = []; + let i = 0; + for (const layerItem of this.selectedLayers) { + if (layerItem !== undefined) { + layers.push(this.layers[i][layerItem].name); + } + i++; + } + + gameManager.setCharacterLayers(layers); + + this.scene.sleep(CustomizeSceneName); + gameManager.tryResumingGame(this, EnableCameraSceneName); + }); this.arrowDown = new Image(this, this.game.renderer.width - 30, 100, CustomizeTextures.arrowUp); this.arrowDown.flipY = true; this.add.existing(this.arrowDown); + this.mobileTapDOWN = this.add + .rectangle( + this.game.renderer.width - 30, + 100, + 32, + 32, + ) + .setInteractive() + .on("pointerdown", () => { + this.moveCursorVertically(1); + }); this.arrowUp = new Image(this, this.game.renderer.width - 30, 60, CustomizeTextures.arrowUp); this.add.existing(this.arrowUp); + this.mobileTapUP = this.add + .rectangle( + this.game.renderer.width - 30, + 60, + 32, + 32, + ) + .setInteractive() + .on("pointerdown", () => { + this.moveCursorVertically(-1); + }); this.createCustomizeLayer(0, 0, 0); this.createCustomizeLayer(0, 0, 1); @@ -268,7 +341,9 @@ export class CustomizeScene extends AbstractCharacterScene { this.moveLayers(); this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; + this.mobileTapENTER.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 2; + this.mobileTapENTER.y = this.cameras.main.worldView.y + this.cameras.main.height / 2; this.textField.x = this.game.renderer.width/2; @@ -276,15 +351,25 @@ export class CustomizeScene extends AbstractCharacterScene { this.logo.y = this.game.renderer.height - 20; this.arrowUp.x = this.game.renderer.width - 30; + this.mobileTapUP.x = this.game.renderer.width - 30; this.arrowUp.y = 60; + this.mobileTapUP.y = 60; this.arrowDown.x = this.game.renderer.width - 30; + this.mobileTapDOWN.x = this.game.renderer.width - 30; this.arrowDown.y = 100; + this.mobileTapDOWN.y = 100; this.arrowLeft.x = this.game.renderer.width/9; + this.mobileTapLEFT.x = this.game.renderer.width/9; this.arrowLeft.y = this.game.renderer.height/2; + this.mobileTapLEFT.y = this.game.renderer.height/2; this.arrowRight.x = this.game.renderer.width*0.9; + this.mobileTapRIGHT.x = this.game.renderer.width*0.9; this.arrowRight.y = this.game.renderer.height/2; + this.mobileTapRIGHT.y = this.game.renderer.height/2; + + } } diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index a3ca5cf1..c3aa5077 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -1,6 +1,7 @@ import {gameManager} from "../Game/GameManager"; import {TextField} from "../Components/TextField"; import Image = Phaser.GameObjects.Image; +import Rectangle = Phaser.GameObjects.Rectangle; import {mediaManager} from "../../WebRtc/MediaManager"; import {RESOLUTION} from "../../Enum/EnvironmentVariable"; import {SoundMeter} from "../Components/SoundMeter"; @@ -35,6 +36,7 @@ export class EnableCameraScene extends Phaser.Scene { private microphoneNameField!: TextField; private repositionCallback!: (this: Window, ev: UIEvent) => void; + private mobileTapRectangle!: Rectangle; constructor() { super({ key: EnableCameraSceneName @@ -54,7 +56,19 @@ export class EnableCameraScene extends Phaser.Scene { create() { 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, 'Press enter to start'); + 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'); + // For mobile purposes - we need a big enough touchable area. + this.mobileTapRectangle = this.add + .rectangle( + this.game.renderer.width / 2, + this.game.renderer.height - 30, + 200, + 50, + ) + .setInteractive() + .on("pointerdown", () => { + this.login(); + }); this.cameraNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 60, ''); @@ -195,6 +209,7 @@ export class EnableCameraScene extends Phaser.Scene { } this.textField.x = this.game.renderer.width / 2; + this.mobileTapRectangle.x = this.game.renderer.width / 2; this.cameraNameField.x = this.game.renderer.width / 2; this.microphoneNameField.x = this.game.renderer.width / 2; this.pressReturnField.x = this.game.renderer.width / 2; diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index ffdf1367..4ff582b4 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -5,6 +5,8 @@ import Image = Phaser.GameObjects.Image; import {SelectCharacterSceneName} from "./SelectCharacterScene"; import {ResizableScene} from "./ResizableScene"; import {isUserNameValid, maxUserNameLength} from "../../Connexion/LocalUser"; +import { localUserStore } from "../../Connexion/LocalUserStore"; +import Rectangle = Phaser.GameObjects.Rectangle; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -20,6 +22,7 @@ export class LoginScene extends ResizableScene { private pressReturnField!: TextField; private logo!: Image; private name: string = ''; + private mobileTapRectangle!: Rectangle; constructor() { super({ @@ -37,17 +40,36 @@ export class LoginScene extends ResizableScene { create() { - this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:'); this.nameInput = new TextInput(this, this.game.renderer.width / 2, 70, maxUserNameLength, this.name,(text: string) => { this.name = text; - }); + localUserStore.setName(text); + }) + .setInteractive() + .on('pointerdown', () => { + this.nameInput.focus(); + }) - this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 130, 'Press enter to start'); + this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:') + .setInteractive() + .on('pointerdown', () => { + this.nameInput.focus(); + }) + // For mobile purposes - we need a big enough touchable area. + this.mobileTapRectangle = this.add.rectangle( + this.game.renderer.width / 2, + 130, + this.game.renderer.width / 2, + 60, + ).setInteractive() + .on('pointerdown', () => { + this.login(this.name) + }) + this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 130, 'Touch here\n\n or \n\nPress enter to start') this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); this.add.existing(this.logo); - const infoText = "Commands: \n - Arrows or Z,Q,S,D to move\n - SHIFT to run"; + const infoText = "Commands: \n - Arrows or W, A, S, D to move\n - SHIFT to run"; this.infoTextField = new TextField(this, 10, this.game.renderer.height - 35, infoText, false); this.input.keyboard.on('keyup-ENTER', () => { @@ -66,6 +88,7 @@ export class LoginScene extends ResizableScene { } private login(name: string): void { + if (this.name === '') return gameManager.setPlayerName(name); this.scene.stop(LoginSceneName) @@ -77,6 +100,7 @@ export class LoginScene extends ResizableScene { this.textField.x = this.game.renderer.width / 2; this.nameInput.setX(this.game.renderer.width / 2 - 64); this.pressReturnField.x = this.game.renderer.width / 2; + this.mobileTapRectangle.x = this.game.renderer.width / 2; this.logo.x = this.game.renderer.width - 30; this.logo.y = this.game.renderer.height - 20; this.infoTextField.y = this.game.renderer.height - 35; diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index a1f3fd81..edd09a91 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -36,6 +36,7 @@ export class SelectCharacterScene extends AbstractCharacterScene { private selectedRectangleYPos = 0; // Number of the character selected in the columns private selectedPlayer!: Phaser.Physics.Arcade.Sprite|null; // null if we are selecting the "customize" option private players: Array = new Array(); + private mobileTapRectangle!: Rectangle; private playerModels!: BodyResourceDescriptionInterface[]; constructor() { @@ -69,8 +70,20 @@ export class SelectCharacterScene extends AbstractCharacterScene { this.pressReturnField = new TextField( this, this.game.renderer.width / 2, - 90 + 32 * Math.ceil( this.playerModels.length / this.nbCharactersPerRow) + 40, - 'Press enter to start'); + 90 + 32 * Math.ceil( this.playerModels.length / this.nbCharactersPerRow) + 60, + 'Touch here\n\n or \n\nPress enter to start'); + // For mobile purposes - we need a big enough touchable area. + this.mobileTapRectangle = this.add + .rectangle( + this.game.renderer.width / 2, + 275, + this.game.renderer.width / 2, + 50, + ) + .setInteractive() + .on("pointerdown", () => { + this.nextScene(); + }); const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; @@ -193,6 +206,10 @@ export class SelectCharacterScene extends AbstractCharacterScene { this.customizeButton.setInteractive().on("pointerdown", () => { this.selectedRectangleYPos = Math.ceil(this.playerModels.length / this.nbCharactersPerRow); this.updateSelectedPlayer(); + this.nextScene(); + }); + this.customizeButtonSelected.setInteractive().on("pointerdown", () => { + this.nextScene(); }); this.selectedPlayer = this.players[0]; diff --git a/front/src/Phaser/Login/SelectCompanionScene.ts b/front/src/Phaser/Login/SelectCompanionScene.ts index 9b5c38fb..c7ee56f1 100644 --- a/front/src/Phaser/Login/SelectCompanionScene.ts +++ b/front/src/Phaser/Login/SelectCompanionScene.ts @@ -29,6 +29,8 @@ export class SelectCompanionScene extends ResizableScene { private companions: Array = new Array(); private companionModels: Array = [null]; + private confirmTouchArea!: Rectangle; + constructor() { super({ key: SelectCompanionSceneName @@ -54,12 +56,17 @@ export class SelectCompanionScene extends ResizableScene { create() { 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); this.pressReturnField = new TextField( this, this.game.renderer.width / 2, - 90 + 32 * Math.ceil(this.companionModels.length / this.nbCharactersPerRow), - 'Press enter to start' + confirmTouchAreaY, + 'Touch here\n\n or \n\n press enter to start' ); + this.confirmTouchArea = this.add + .rectangle(this.game.renderer.width / 2, confirmTouchAreaY, 200, 50) + .setInteractive() + .on("pointerdown", this.nextScene.bind(this)); const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index f29fd39d..b42b575f 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -291,6 +291,9 @@ export class MenuScene extends Phaser.Scene { case 'editGameSettingsButton': this.openGameSettingsMenu(); break; + case 'toggleFullscreen': + this.toggleFullscreen(); + break; case 'adminConsoleButton': gameManager.getCurrentGameScene(this).ConsoleGlobalMessageManager.activeMessageConsole(); break; @@ -328,4 +331,15 @@ export class MenuScene extends Phaser.Scene { this.closeGameShare(); this.gameReportElement.close(); } + + private toggleFullscreen() { + const body = document.querySelector('body') + if (body) { + if (document.fullscreenElement ?? document.fullscreen) { + document.exitFullscreen() + } else { + body.requestFullscreen(); + } + } + } } diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index bb961115..438f1228 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -47,7 +47,7 @@ export class Player extends Character implements CurrentGamerInterface { let x = 0; let y = 0; if (activeEvents.get(UserInputEvent.MoveUp)) { - y = - moveAmount; + y = -moveAmount; direction = PlayerAnimationDirections.Up; moving = true; } else if (activeEvents.get(UserInputEvent.MoveDown)) { @@ -64,16 +64,18 @@ export class Player extends Character implements CurrentGamerInterface { direction = PlayerAnimationDirections.Right; moving = true; } + moving = moving || activeEvents.get(UserInputEvent.JoystickMove); if (x !== 0 || y !== 0) { this.move(x, y); this.emit(hasMovedEventName, {moving, direction, x: this.x, y: this.y}); - } else { - if (this.wasMoving) { - //direction = PlayerAnimationNames.None; - this.stop(); - this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); - } + } else if (this.wasMoving && moving) { + // slow joystick movement + this.move(0, 0); + this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); + } else if (this.wasMoving && !moving) { + this.stop(); + this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y}); } if (direction !== null) { diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index 2e686d8e..c3f2c0bb 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -1,4 +1,8 @@ +import { Direction, IVirtualJoystick } from "../../types"; import {GameScene} from "../Game/GameScene"; +const { + default: VirtualJoystick, +} = require("phaser3-rex-plugins/plugins/virtualjoystick.js"); interface UserInputManagerDatum { keyInstance: Phaser.Input.Keyboard.Key; @@ -13,17 +17,24 @@ export enum UserInputEvent { SpeedUp, Interact, Shout, + JoystickMove, } -//we cannot the map structure so we have to create a replacment +//we cannot use a map structure so we have to create a replacment export class ActiveEventList { - private KeysCode : Map = new Map(); + private eventMap : Map = new Map(); get(event: UserInputEvent): boolean { - return this.KeysCode.get(event) || false; + return this.eventMap.get(event) || false; } set(event: UserInputEvent, value: boolean): void { - this.KeysCode.set(event, value); + this.eventMap.set(event, value); + } + forEach(callback: (value: boolean, key: UserInputEvent) => void): void { + this.eventMap.forEach(callback); + } + any(): boolean { + return Array.from(this.eventMap.values()).reduce((accu, curr) => accu || curr, false); } } @@ -32,10 +43,40 @@ export class UserInputManager { private KeysCode!: UserInputManagerDatum[]; private Scene: GameScene; private isInputDisabled : boolean; - constructor(Scene : GameScene) { + + private joystick : IVirtualJoystick; + private joystickEvents = new ActiveEventList(); + private joystickForceThreshold = 60; + private joystickForceAccuX = 0; + private joystickForceAccuY = 0; + + constructor(Scene: GameScene, virtualJoystick: IVirtualJoystick) { this.Scene = Scene; - this.initKeyBoardEvent(); this.isInputDisabled = false; + this.initKeyBoardEvent(); + this.joystick = virtualJoystick; + this.joystick.on("update", () => { + this.joystickForceAccuX = this.joystick.forceX ? this.joystickForceAccuX : 0; + this.joystickForceAccuY = this.joystick.forceY ? this.joystickForceAccuY : 0; + const cursorKeys = this.joystick.createCursorKeys(); + 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; + } + } + }); } initKeyBoardEvent(){ @@ -78,11 +119,33 @@ export class UserInputManager { if (this.isInputDisabled) { return eventsMap; } + this.joystickEvents.forEach((value, key) => { + if (value) { + switch (key) { + case UserInputEvent.MoveUp: + case UserInputEvent.MoveDown: + this.joystickForceAccuY += this.joystick.forceY; + if (Math.abs(this.joystickForceAccuY) > this.joystickForceThreshold) { + eventsMap.set(key, value); + this.joystickForceAccuY = 0; + } + break; + case UserInputEvent.MoveLeft: + case UserInputEvent.MoveRight: + this.joystickForceAccuX += this.joystick.forceX; + if (Math.abs(this.joystickForceAccuX) > this.joystickForceThreshold) { + eventsMap.set(key, value); + this.joystickForceAccuX = 0; + } + break; + } + } + }); + eventsMap.set(UserInputEvent.JoystickMove, this.joystickEvents.any()); this.KeysCode.forEach(d => { - if (d. keyInstance.isDown) { + if (d.keyInstance.isDown) { eventsMap.set(d.event, true); } - }); return eventsMap; } diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 233b5327..a0b805d4 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -340,14 +340,10 @@ class LayoutManager { const mainContainer = HtmlUtils.getElementByIdOrFail('main-container'); mainContainer.appendChild(div); - const callBackFunctionTrigger = (() => { - console.log('user click on space => ', id); - callBack(); - }); - //add trigger action - this.actionButtonTrigger.set(id, callBackFunctionTrigger); - userInputManager.addSpaceEventListner(callBackFunctionTrigger); + div.onpointerdown = () => callBack(); + this.actionButtonTrigger.set(id, callBack); + userInputManager.addSpaceEventListner(callBack); } public removeActionButton(id: string, userInputManager: UserInputManager){ diff --git a/front/src/types.ts b/front/src/types.ts new file mode 100644 index 00000000..6b99434d --- /dev/null +++ b/front/src/types.ts @@ -0,0 +1,24 @@ +import Phaser from "phaser"; + +export type CursorKey = { + isDown: boolean +} + +export type Direction = 'left' | 'right' | 'up' | 'down' + +export interface CursorKeys extends Record { + left: CursorKey; + right: CursorKey; + up: CursorKey; + down: CursorKey; +} + +export interface IVirtualJoystick extends Phaser.GameObjects.GameObject { + y: number; + x: number; + forceX: number; + forceY: number; + visible: boolean; + createCursorKeys: () => CursorKeys; +} + diff --git a/front/tsconfig.json b/front/tsconfig.json index b5c8c74d..fea8fd54 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -9,17 +9,18 @@ "downlevelIteration": true, "jsx": "react", "allowJs": true, + "esModuleInterop": true, - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "strictNullChecks": true, /* Enable strict null checks. */ - "strictFunctionTypes": true, /* Enable strict checking of function types. */ - "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */ } } diff --git a/front/yarn.lock b/front/yarn.lock index eac99e4e..26573239 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1819,6 +1819,11 @@ eventemitter3@^2.0.3: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba" integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= +eventemitter3@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -3064,6 +3069,11 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ== +lokijs@^1.5.11: + version "1.5.11" + resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.11.tgz#2b2ea82ec66050e4b112c6cfc588dac22d362b13" + integrity sha512-YYyuBPxMn/oS0tFznQDbIX5XL1ltMcwFqCboDr8voYE4VCDzR5vAsrvQDhlnua4lBeqMqHmLvUXRTmRUzUKH1Q== + lower-case@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7" @@ -3625,6 +3635,11 @@ pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +papaparse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.0.tgz#ab1702feb96e79ab4309652f36db9536563ad05a" + integrity sha512-Lb7jN/4bTpiuGPrYy4tkKoUS8sTki8zacB5ke1p5zolhcSE4TlWgrlsxjrDTbG/dFVh07ck7X36hUf/b5V68pg== + parallel-transform@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" @@ -3752,6 +3767,16 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +phaser3-rex-plugins@^1.1.42: + version "1.1.44" + resolved "https://registry.yarnpkg.com/phaser3-rex-plugins/-/phaser3-rex-plugins-1.1.44.tgz#83588ab2801c5b3a80a18be4f0ae37f1f32096b0" + integrity sha512-JPr3+UQv4+uv4etZr80aFhYxw2dk4UYG9/gQ4uMSSXQuc8gXQkXCr3J3zm8LJqH+1FXeK8nncpC7TKa4uKLgQw== + dependencies: + eventemitter3 "^3.1.2" + lokijs "^1.5.11" + papaparse "^5.3.0" + webfontloader "^1.6.28" + phaser@3.24.1: version "3.24.1" resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.24.1.tgz#376e0c965d2a35af37c06ee78627dafbde5be017" @@ -5144,6 +5169,11 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +webfontloader@^1.6.28: + version "1.6.28" + resolved "https://registry.yarnpkg.com/webfontloader/-/webfontloader-1.6.28.tgz#db786129253cb6e8eae54c2fb05f870af6675bae" + integrity sha1-23hhKSU8tujq5UwvsF+HCvZnW64= + webpack-cli@^3.3.11: version "3.3.12" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a"