diff --git a/front/dist/resources/fonts/ka1.ttf b/front/dist/resources/fonts/ka1.ttf new file mode 100644 index 00000000..d1df8523 Binary files /dev/null and b/front/dist/resources/fonts/ka1.ttf differ diff --git a/front/dist/resources/html/gameMenu.html b/front/dist/resources/html/gameMenu.html new file mode 100644 index 00000000..a5190062 --- /dev/null +++ b/front/dist/resources/html/gameMenu.html @@ -0,0 +1,34 @@ + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
diff --git a/front/dist/resources/html/gameMenuIcon.html b/front/dist/resources/html/gameMenuIcon.html new file mode 100644 index 00000000..f7f5e9aa --- /dev/null +++ b/front/dist/resources/html/gameMenuIcon.html @@ -0,0 +1,16 @@ + +
+
+ +
+
\ No newline at end of file diff --git a/front/dist/resources/html/gameQualityMenu.html b/front/dist/resources/html/gameQualityMenu.html new file mode 100644 index 00000000..00599386 --- /dev/null +++ b/front/dist/resources/html/gameQualityMenu.html @@ -0,0 +1,73 @@ + + +
+
+

Game quality

+

(Editing this settings will restart the game)

+ +
+
+

Video quality

+ +
+
+ + +
+
diff --git a/front/dist/resources/objects/talk.png b/front/dist/resources/objects/talk.png new file mode 100644 index 00000000..b9ecdb30 Binary files /dev/null and b/front/dist/resources/objects/talk.png differ diff --git a/front/src/Administration/ConsoleGlobalMessageManager.ts b/front/src/Administration/ConsoleGlobalMessageManager.ts index 03929a46..d437f7c0 100644 --- a/front/src/Administration/ConsoleGlobalMessageManager.ts +++ b/front/src/Administration/ConsoleGlobalMessageManager.ts @@ -3,15 +3,12 @@ import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {RoomConnection} from "../Connexion/RoomConnection"; import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; import {ADMIN_URL} from "../Enum/EnvironmentVariable"; -import {mediaManager} from "../WebRtc/MediaManager"; export const CLASS_CONSOLE_MESSAGE = 'main-console'; export const INPUT_CONSOLE_MESSAGE = 'input-send-text'; export const UPLOAD_CONSOLE_MESSAGE = 'input-upload-music'; export const INPUT_TYPE_CONSOLE = 'input-type'; -export const GAME_QUALITY_SELECT = 'select-game-quality'; export const VIDEO_QUALITY_SELECT = 'select-video-quality'; -export const VIDEO_QUALITY_CONSOLE = 'video-quality'; export const AUDIO_TYPE = 'audio'; export const MESSAGE_TYPE = 'message'; @@ -38,6 +35,7 @@ export class ConsoleGlobalMessageManager { constructor(private Connection: RoomConnection, userInputManager : UserInputManager, private isAdmin: Boolean) { this.buttonMainConsole = document.createElement('div'); this.buttonMainConsole.classList.add('console'); + this.buttonMainConsole.hidden = true; this.divMainConsole = document.createElement('div'); this.divMainConsole.className = CLASS_CONSOLE_MESSAGE; this.divMessageConsole = document.createElement('div'); @@ -49,6 +47,7 @@ export class ConsoleGlobalMessageManager { this.buttonAdminMainConsole = document.createElement('img'); this.userInputManager = userInputManager; this.initialise(); + } initialise() { @@ -118,9 +117,6 @@ export class ConsoleGlobalMessageManager { } }); - /*const buttonText = document.createElement('p'); - buttonText.innerText = 'Console'; - this.buttonMainConsole.appendChild(buttonText);*/ this.divMessageConsole.appendChild(typeConsole); if(this.isAdmin) { @@ -130,7 +126,6 @@ export class ConsoleGlobalMessageManager { this.createUploadAudioPart(); } this.buttonMainConsole.appendChild(this.buttonSettingsMainConsole); - this.createSettings(); this.divMainConsole.appendChild(this.buttonMainConsole); this.divMainConsole.appendChild(this.divMessageConsole); @@ -263,92 +258,6 @@ export class ConsoleGlobalMessageManager { this.divMessageConsole.appendChild(section); } - createSettings(){ - const labelGame = document.createElement('h1'); - labelGame.innerText = "Game quality"; - - const selectGame = document.createElement('select'); - selectGame.id = VIDEO_QUALITY_SELECT; - - const option1 : HTMLOptionElement = document.createElement('option'); - option1.value = '120'; - option1.innerText = 'High video quality (120 fps)'; - selectGame.appendChild(option1); - - const option2 : HTMLOptionElement = document.createElement('option'); - option2.value = '60'; - option2.innerText = 'Medium video quality (60 fps, recommended)'; - selectGame.appendChild(option2); - - const option3 : HTMLOptionElement = document.createElement('option'); - option3.value = '40'; - option3.innerText = 'Minimum video quality (40 fps)'; - selectGame.appendChild(option3); - - const option4 : HTMLOptionElement = document.createElement('option'); - option4.value = '20'; - option4.innerText = 'Small video quality (20 fps)'; - selectGame.appendChild(option4); - - const labelVideo = document.createElement('h1'); - labelVideo.innerText = "Video quality"; - - const selectVideo = document.createElement('select'); - selectVideo.id = GAME_QUALITY_SELECT; - - const optionVideo1 : HTMLOptionElement = document.createElement('option'); - optionVideo1.value = '30'; - optionVideo1.innerText = 'High video quality (30 fps)'; - selectVideo.appendChild(optionVideo1); - - const optionVideo2 : HTMLOptionElement = document.createElement('option'); - optionVideo2.value = '20'; - optionVideo2.innerText = 'Medium video quality (20 fps, recommended)'; - selectVideo.appendChild(optionVideo2); - - const optionVideo3 : HTMLOptionElement = document.createElement('option'); - optionVideo3.value = '10'; - optionVideo3.innerText = 'Minimum video quality (10 fps)'; - selectVideo.appendChild(optionVideo3); - - const optionVideo4 : HTMLOptionElement = document.createElement('option'); - optionVideo4.value = '5'; - optionVideo4.innerText = 'Small video quality (5 fps)'; - selectVideo.appendChild(optionVideo4); - - selectGame.value = '60'; - const localQualityGame = localStorage.getItem(GAME_QUALITY_SELECT); - if(localQualityGame){ - selectGame.value = localQualityGame; - } - - selectVideo.value = '30'; - const localQualityCam = localStorage.getItem(VIDEO_QUALITY_SELECT); - if(localQualityCam){ - selectVideo.value = localQualityCam; - } - - const divButtonAction = document.createElement('div'); - divButtonAction.className = 'btn-action'; - const buttonSave = document.createElement('button'); - buttonSave.innerText = 'Save'; - buttonSave.classList.add('btn'); - buttonSave.addEventListener('click', () => { - this.saveSetting(selectGame.value, selectVideo.value); - this.disabledSettingConsole(); - }); - divButtonAction.appendChild(buttonSave); - - const section = document.createElement('section'); - section.id = this.getSectionId(VIDEO_QUALITY_CONSOLE); - section.appendChild(labelGame); - section.appendChild(selectGame); - section.appendChild(labelVideo); - section.appendChild(selectVideo); - section.appendChild(divButtonAction); - this.divSettingConsole.appendChild(section); - } - private static loadCss(): Promise { return new Promise((resolve, reject) => { if (ConsoleGlobalMessageManager.cssLoaded) { @@ -421,19 +330,6 @@ export class ConsoleGlobalMessageManager { this.Connection.emitGlobalMessage(GlobalMessage); } - private saveSetting(valueGame: string, valueVideo: string){ - const previousGameValue = localStorage.getItem(GAME_QUALITY_SELECT); - if(!previousGameValue || previousGameValue !== valueGame) { - localStorage.setItem(GAME_QUALITY_SELECT, valueGame); - window.location.reload(); - } - const previousVideoValue = localStorage.getItem(VIDEO_QUALITY_SELECT); - if(!previousVideoValue || previousVideoValue !== valueVideo) { - localStorage.setItem(VIDEO_QUALITY_SELECT, valueVideo); - mediaManager.updateCameraQuality(parseInt(valueVideo)); - } - } - active(){ this.userInputManager.clearAllInputKeyboard(); this.divMainConsole.style.top = '0'; @@ -453,12 +349,14 @@ export class ConsoleGlobalMessageManager { } this.active(); this.divMessageConsole.classList.add('active'); + this.buttonMainConsole.hidden = false; this.buttonSendMainConsole.classList.add('active'); } disabledMessageConsole(){ this.activeMessage = false; this.disabled(); + this.buttonMainConsole.hidden = false; this.divMessageConsole.classList.remove('active'); this.buttonSendMainConsole.classList.remove('active'); } diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index b670a388..a113a00c 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -55,8 +55,7 @@ class ConnectionManager { } else { roomId = window.location.pathname + window.location.hash; } - const room = new Room(roomId); - return Promise.resolve(room); + return Promise.resolve(new Room(roomId)); } return Promise.reject('Invalid URL'); diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts index afe01bcd..8ac8c7b2 100644 --- a/front/src/Connexion/LocalUserStore.ts +++ b/front/src/Connexion/LocalUserStore.ts @@ -1,5 +1,9 @@ import {LocalUser} from "./LocalUser"; +const characterLayersKey = 'characterLayers'; +const gameQualityKey = 'gameQuality'; +const videoQualityKey = 'videoQuality'; + //todo: add localstorage fallback class LocalUserStore { @@ -31,6 +35,27 @@ class LocalUserStore { getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null { return JSON.parse(window.localStorage.getItem('customCursorPosition') || "null"); } + + setCharacterLayers(layers: string[]): void { + window.localStorage.setItem(characterLayersKey, JSON.stringify(layers)); + } + getCharacterLayers(): string[]|null { + return JSON.parse(window.localStorage.getItem(characterLayersKey) || "null"); + } + + getGameQualityValue(): number { + return parseInt(window.localStorage.getItem(gameQualityKey) || '') || 60; + } + setGameQualityValue(value: number): void { + localStorage.setItem(gameQualityKey, '' + value); + } + + getVideoQualityValue(): number { + return parseInt(window.localStorage.getItem(videoQualityKey) || '') || 20; + } + setVideoQualityValue(value: number): void { + localStorage.setItem(videoQualityKey, '' + value); + } } export const localUserStore = new LocalUserStore(); \ No newline at end of file diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index cc033974..beb7f9dd 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -583,4 +583,8 @@ export class RoomConnection implements RoomConnection { public hasTag(tag: string): boolean { return this.tags.includes(tag); } + + public isAdmin(): boolean { + return this.hasTag('admin'); + } } diff --git a/front/src/Phaser/Components/ChatModeIcon.ts b/front/src/Phaser/Components/ChatModeIcon.ts new file mode 100644 index 00000000..932a4d88 --- /dev/null +++ b/front/src/Phaser/Components/ChatModeIcon.ts @@ -0,0 +1,11 @@ +export class ChatModeIcon extends Phaser.GameObjects.Sprite { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'layout_modes', 3); + scene.add.existing(this); + this.setScrollFactor(0, 0); + this.setOrigin(0, 1); + this.setInteractive(); + this.setVisible(false); + this.setDepth(99999); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Components/OpenChatIcon.ts b/front/src/Phaser/Components/OpenChatIcon.ts new file mode 100644 index 00000000..bf293bab --- /dev/null +++ b/front/src/Phaser/Components/OpenChatIcon.ts @@ -0,0 +1,18 @@ +import {discussionManager} from "../../WebRtc/DiscussionManager"; + +export const openChatIconName = 'openChatIcon'; +export class OpenChatIcon extends Phaser.GameObjects.Image { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, openChatIconName); + scene.add.existing(this); + this.setScrollFactor(0, 0); + this.setOrigin(0, 1); + this.displayWidth = 30; + this.displayHeight = 30; + this.setInteractive(); + this.setVisible(false) + this.setDepth(99999); + + this.on("pointerup", () => discussionManager.showDiscussionPart()); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Components/PresentationModeIcon.ts b/front/src/Phaser/Components/PresentationModeIcon.ts new file mode 100644 index 00000000..49ff2ea1 --- /dev/null +++ b/front/src/Phaser/Components/PresentationModeIcon.ts @@ -0,0 +1,11 @@ +export class PresentationModeIcon extends Phaser.GameObjects.Sprite { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, x, y, 'layout_modes', 0); + scene.add.existing(this); + this.setScrollFactor(0, 0); + this.setOrigin(0, 1); + this.setInteractive(); + this.setVisible(false); + this.setDepth(99999); + } +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index cc575b6a..88ed1c52 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,6 +1,11 @@ import {GameScene} from "./GameScene"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {Room} from "../../Connexion/Room"; +import {MenuSceneName} from "../Menu/MenuScene"; +import {LoginSceneName} from "../Login/LoginScene"; +import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; +import {EnableCameraSceneName} from "../Login/EnableCameraScene"; +import {localUserStore} from "../../Connexion/LocalUserStore"; export interface HasMovedEvent { direction: string; @@ -9,29 +14,48 @@ export interface HasMovedEvent { y: number; } +/** + * This class should be responsible for any scene starting/stopping + */ export class GameManager { - private playerName!: string; - private characterLayers!: string[]; + private playerName: string|null; + private characterLayers: string[]|null; private startRoom!:Room; + currentSceneName: string|null = null; + + constructor() { + this.playerName = localUserStore.getName(); + this.characterLayers = localUserStore.getCharacterLayers(); + } - public async init(scenePlugin: Phaser.Scenes.ScenePlugin) { + 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) { + return SelectCharacterSceneName; + } else { + return EnableCameraSceneName; + } } public setPlayerName(name: string): void { this.playerName = name; + localUserStore.setName(name); } public setCharacterLayers(layers: string[]): void { this.characterLayers = layers; + localUserStore.setCharacterLayers(layers); } - getPlayerName(): string { + getPlayerName(): string|null { return this.playerName; } - getCharacterSelected(): string[] { + getCharacterLayers(): string[]|null { return this.characterLayers; } @@ -48,7 +72,28 @@ export class GameManager { } public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void { - scenePlugin.start(this.startRoom.id); + console.log('starting '+ (this.currentSceneName || this.startRoom.id)) + scenePlugin.start(this.currentSceneName || this.startRoom.id); + //the menu scene launches faster than the gameScene, so we delay it to not have menu buttons on a black screen + setTimeout(() => scenePlugin.launch(MenuSceneName), 1000); + } + + /** + * Temporary leave a gameScene to go back to the loginScene for example. + * This will close the socket connections and stop the gameScene, but won't remove it. + */ + leaveGame(scene: Phaser.Scene, targetSceneName: string): void { + if (this.currentSceneName === null) throw 'No current scene id set!'; + const gameScene: GameScene = scene.scene.get(this.currentSceneName) as GameScene; + gameScene.cleanupClosingScene(); + scene.scene.stop(this.currentSceneName); + scene.scene.stop(MenuSceneName); + scene.scene.run(targetSceneName); + } + + public getCurrentGameScene(scene: Phaser.Scene): GameScene { + if (this.currentSceneName === null) throw 'No current scene id set!'; + return scene.scene.get(this.currentSceneName) as GameScene } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d7ca5adc..cbfa64d9 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -1,4 +1,4 @@ -import {GameManager, gameManager, HasMovedEvent} from "./GameManager"; +import {gameManager, HasMovedEvent} from "./GameManager"; import { GroupCreatedUpdatedMessageInterface, MessageUserJoined, @@ -60,6 +60,9 @@ import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; import {jitsiFactory} from "../../WebRtc/JitsiFactory"; import {urlManager} from "../../Url/UrlManager"; +import {PresentationModeIcon} from "../Components/PresentationModeIcon"; +import {ChatModeIcon} from "../Components/ChatModeIcon"; +import {OpenChatIcon, openChatIconName} from "../Components/OpenChatIcon"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -99,7 +102,6 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; export class GameScene extends ResizableScene implements CenterListener { - GameManager : GameManager; Terrains : Array; CurrentPlayer!: CurrentGamerInterface; MapPlayers!: Phaser.Physics.Arcade.Group; @@ -116,11 +118,11 @@ export class GameScene extends ResizableScene implements CenterListener { pendingEvents: Queue = new Queue(); private initPosition: PositionInterface|null = null; private playersPositionInterpolator = new PlayersPositionInterpolator(); - private connection!: RoomConnection; + public connection!: RoomConnection; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; private UserMessageManager!: UserMessageManager; - private ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; + public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) @@ -149,16 +151,18 @@ export class GameScene extends ResizableScene implements CenterListener { private userInputManager!: UserInputManager; private isReconnecting: boolean = false; private startLayerName!: string | null; + private openChatIcon!: OpenChatIcon; + private playerName!: string; + private characterLayers!: string[]; constructor(private room: Room, MapUrlFile: string) { super({ key: room.id }); - - this.GameManager = gameManager; this.Terrains = []; this.groups = new Map(); this.instance = room.getInstance(); + this.MapUrlFile = MapUrlFile; this.RoomId = room.id; @@ -173,6 +177,7 @@ export class GameScene extends ResizableScene implements CenterListener { //hook preload scene preload(): void { + this.load.image(openChatIconName, 'resources/objects/talk.png'); this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.scene.start(FourOFourSceneName, { file: file.src @@ -306,9 +311,22 @@ export class GameScene extends ResizableScene implements CenterListener { //hook create scene create(): void { + gameManager.currentSceneName = this.scene.key; urlManager.pushRoomIdToUrl(this.room); this.startLayerName = urlManager.getStartLayerNameFromUrl(); + const playerName = gameManager.getPlayerName(); + if (!playerName) { + throw 'playerName is not set'; + } + this.playerName = playerName; + const characterLayers = gameManager.getCharacterLayers(); + if (!characterLayers) { + throw 'characterLayers are not set'; + } + this.characterLayers = characterLayers; + + //initalise map this.Map = this.add.tilemap(this.MapUrlFile); this.gameMap = new GameMap(this.mapFile); @@ -415,23 +433,14 @@ export class GameScene extends ResizableScene implements CenterListener { this.outlinedItem?.activate(); }); - this.presentationModeSprite = this.add.sprite(2, this.game.renderer.height - 2, 'layout_modes', 0); - this.presentationModeSprite.setScrollFactor(0, 0); - this.presentationModeSprite.setOrigin(0, 1); - this.presentationModeSprite.setInteractive(); - this.presentationModeSprite.setVisible(false); - this.presentationModeSprite.setDepth(99999); + this.presentationModeSprite = new PresentationModeIcon(this, 36, this.game.renderer.height - 2); this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); - this.chatModeSprite = this.add.sprite(36, this.game.renderer.height - 2, 'layout_modes', 3); - this.chatModeSprite.setScrollFactor(0, 0); - this.chatModeSprite.setOrigin(0, 1); - this.chatModeSprite.setInteractive(); - this.chatModeSprite.setVisible(false); - this.chatModeSprite.setDepth(99999); + this.chatModeSprite = new ChatModeIcon(this, 70, this.game.renderer.height - 2); this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); + this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 36) // FIXME: change this to use the UserInputManager class for input - this.input.keyboard.on('keyup-' + 'M', () => { + this.input.keyboard.on('keyup-M', () => { this.switchLayoutMode(); }); @@ -445,8 +454,8 @@ export class GameScene extends ResizableScene implements CenterListener { connectionManager.connectToRoomSocket( this.RoomId, - gameManager.getPlayerName(), - gameManager.getCharacterSelected(), + this.playerName, + this.characterLayers, { x: this.startX, y: this.startY @@ -459,10 +468,6 @@ export class GameScene extends ResizableScene implements CenterListener { }).then((onConnect: OnConnectInterface) => { this.connection = onConnect.connection; - //this.connection.emitPlayerDetailsMessage(gameManager.getPlayerName(), gameManager.getCharacterSelected()) - /*this.connection.onStartRoom((roomJoinedMessage: RoomJoinedMessageInterface) => { - - });*/ this.connection.onUserJoins((message: MessageUserJoined) => { const userMessage: AddPlayerInterface = { userId: message.userId, @@ -493,11 +498,13 @@ export class GameScene extends ResizableScene implements CenterListener { this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { this.shareGroupPosition(groupPositionMessage); + this.openChatIcon.setVisible(true); }) this.connection.onGroupDeleted((groupId: number) => { try { this.deleteGroup(groupId); + this.openChatIcon.setVisible(false); } catch (e) { console.error(e); } @@ -541,7 +548,7 @@ export class GameScene extends ResizableScene implements CenterListener { }); // When connection is performed, let's connect SimplePeer - this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.GameManager.getPlayerName()); + this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); this.GlobalMessageManager = new GlobalMessageManager(this.connection); this.UserMessageManager = new UserMessageManager(this.connection); @@ -569,7 +576,7 @@ export class GameScene extends ResizableScene implements CenterListener { //this.initUsersPosition(roomJoinedMessage.users); this.connectionAnswerPromiseResolve(onConnect.room); // Analyze tags to find if we are admin. If yes, show console. - this.ConsoleGlobalMessageManager = new ConsoleGlobalMessageManager(this.connection, this.userInputManager, this.connection.hasTag('admin')); + this.ConsoleGlobalMessageManager = new ConsoleGlobalMessageManager(this.connection, this.userInputManager, this.connection.isAdmin()); this.scene.wake(); @@ -649,9 +656,7 @@ export class GameScene extends ResizableScene implements CenterListener { if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey); urlManager.pushStartLayerNameToUrl(hash); if (roomId !== this.scene.key) { - // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. - this.connection.closeConnection(); - this.simplePeer.unregister(); + this.cleanupClosingScene(); this.scene.stop(); this.scene.remove(this.scene.key); this.scene.start(roomId); @@ -663,6 +668,12 @@ export class GameScene extends ResizableScene implements CenterListener { } } + public cleanupClosingScene(): void { + // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. + this.connection.closeConnection(); + this.simplePeer.unregister(); + } + private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated if(mediaManager.activatedDiscussion){ @@ -811,8 +822,8 @@ export class GameScene extends ResizableScene implements CenterListener { this, this.startX, this.startY, - this.GameManager.getPlayerName(), - this.GameManager.getCharacterSelected(), + this.playerName, + this.characterLayers, PlayerAnimationNames.WalkDown, false, this.userInputManager @@ -1174,7 +1185,7 @@ export class GameScene extends ResizableScene implements CenterListener { } public startJitsi(roomName: string, jwt?: string): void { - jitsiFactory.start(roomName, gameManager.getPlayerName(), jwt); + jitsiFactory.start(roomName, this.playerName, jwt); this.connection.setSilent(true); mediaManager.hideGameOverlay(); @@ -1209,4 +1220,6 @@ export class GameScene extends ResizableScene implements CenterListener { }); })) } + + } diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index 41ec95c9..d9236c06 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -9,6 +9,8 @@ import {gameManager} from "../Game/GameManager"; import {ResizableScene} from "./ResizableScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; import {PlayerResourceDescriptionInterface} from "../Entity/Character"; +import {SelectCharacterSceneName} from "./SelectCharacterScene"; +import {LoginSceneName} from "./LoginScene"; export const CustomizeSceneName = "CustomizeScene"; @@ -120,7 +122,8 @@ export class CustomizeScene extends ResizableScene { gameManager.setCharacterLayers(layers); - return this.scene.start(EnableCameraSceneName); + this.scene.sleep(CustomizeSceneName) + this.scene.run(EnableCameraSceneName); }); this.input.keyboard.on('keydown-RIGHT', () => this.moveCursorHorizontally(1)); diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 56502704..3cee733e 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -263,6 +263,7 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); + this.scene.sleep(EnableCameraSceneName) gameManager.goToStartingMap(this.scene); } diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index 6a91be1f..c7527349 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -1,13 +1,4 @@ import {gameManager} from "../Game/GameManager"; -import {TextField} from "../Components/TextField"; -import {TextInput} from "../Components/TextInput"; -import {ClickButton} from "../Components/ClickButton"; -import Image = Phaser.GameObjects.Image; -import Rectangle = Phaser.GameObjects.Rectangle; -import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; -import {cypressAsserter} from "../../Cypress/CypressAsserter"; -import {SelectCharacterSceneName} from "./SelectCharacterScene"; -import {ResizableScene} from "./ResizableScene"; import {Scene} from "phaser"; import {LoginSceneName} from "./LoginScene"; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; @@ -25,12 +16,9 @@ export class EntryScene extends Scene { }); } - preload() { - } - create() { - gameManager.init(this.scene).then(() => { - this.scene.start(LoginSceneName); + gameManager.init(this.scene).then((nextSceneName) => { + this.scene.start(nextSceneName); }).catch((err) => { console.error(err) this.scene.start(FourOFourSceneName, { diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 2c5c1882..e828f8cb 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -1,14 +1,11 @@ import {gameManager} from "../Game/GameManager"; import {TextField} from "../Components/TextField"; import {TextInput} from "../Components/TextInput"; -import {ClickButton} from "../Components/ClickButton"; import Image = Phaser.GameObjects.Image; -import Rectangle = Phaser.GameObjects.Rectangle; import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; import {SelectCharacterSceneName} from "./SelectCharacterScene"; import {ResizableScene} from "./ResizableScene"; -import {localUserStore} from "../../Connexion/LocalUserStore"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -29,7 +26,7 @@ export class LoginScene extends ResizableScene { super({ key: LoginSceneName }); - this.name = localUserStore.getName(); + this.name = gameManager.getPlayerName() || ''; } preload() { @@ -55,7 +52,6 @@ export class LoginScene extends ResizableScene { 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, 8, this.name,(text: string) => { this.name = text; - localUserStore.setName(text); }); this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 130, 'Press enter to start'); @@ -87,7 +83,8 @@ export class LoginScene extends ResizableScene { private login(name: string): void { gameManager.setPlayerName(name); - this.scene.start(SelectCharacterSceneName); + this.scene.sleep(LoginSceneName) + this.scene.run(SelectCharacterSceneName); } public onResize(ev: UIEvent): void { diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index 25332b7d..5b4dc6cf 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -116,11 +116,12 @@ export class SelectCharacterScene extends ResizableScene { } private nextScene(): void { + this.scene.sleep(SelectCharacterSceneName); if (this.selectedPlayer !== null) { gameManager.setCharacterLayers([this.selectedPlayer.texture.key]); - this.scene.start(EnableCameraSceneName); + this.scene.run(EnableCameraSceneName); } else { - this.scene.start(CustomizeSceneName); + this.scene.run(CustomizeSceneName); } } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts new file mode 100644 index 00000000..5d796d00 --- /dev/null +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -0,0 +1,189 @@ +import {LoginSceneName} from "../Login/LoginScene"; +import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; +import {gameManager} from "../Game/GameManager"; +import {localUserStore} from "../../Connexion/LocalUserStore"; +import {mediaManager} from "../../WebRtc/MediaManager"; + +export const MenuSceneName = 'MenuScene'; +const gameMenuKey = 'gameMenu'; +const gameMenuIconKey = 'gameMenuIcon'; +const gameSettingsMenuKey = 'gameSettingsMenu'; + +const closedSideMenuX = -200; +const openedSideMenuX = 0; + +/** + * The scene that manages the game menu, rendered using a DOM element. + */ +export class MenuScene extends Phaser.Scene { + private menuElement!: Phaser.GameObjects.DOMElement; + private gameQualityMenuElement!: Phaser.GameObjects.DOMElement; + private sideMenuOpened = false; + private settingsMenuOpened = false; + private gameQualityValue: number; + private videoQualityValue: number; + private menuButton!: Phaser.GameObjects.DOMElement; + + constructor() { + super({key: MenuSceneName}); + + this.gameQualityValue = localUserStore.getGameQualityValue(); + this.videoQualityValue = localUserStore.getVideoQualityValue(); + } + + preload () { + this.load.html(gameMenuKey, 'resources/html/gameMenu.html'); + this.load.html(gameMenuIconKey, 'resources/html/gameMenuIcon.html'); + this.load.html(gameSettingsMenuKey, 'resources/html/gameQualityMenu.html'); + } + + create() { + this.menuElement = this.add.dom(closedSideMenuX, 25).createFromCache(gameMenuKey); + this.menuElement.setOrigin(0); + + this.gameQualityMenuElement = this.add.dom(this.game.renderer.width / 2, -400).createFromCache(gameSettingsMenuKey); + this.gameQualityMenuElement.setOrigin(0.5); + + this.input.keyboard.on('keyup-TAB', () => { + this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); + }); + this.menuButton = this.add.dom(35, 20).createFromCache(gameMenuIconKey); + this.menuButton.addListener('click'); + this.menuButton.on('click', () => { + this.sideMenuOpened ? this.closeSideMenu() : this.openSideMenu(); + }); + } + + openSideMenu() { + if (this.sideMenuOpened) return; + this.sideMenuOpened = true; + this.menuButton.getChildByID('openMenuButton').innerHTML = 'Close' + if (gameManager.getCurrentGameScene(this).connection.isAdmin()) { + const adminSection = this.menuElement.getChildByID('adminConsoleSection') as HTMLElement; + adminSection.hidden = false; + } + this.menuElement.addListener('click'); + this.menuElement.on('click', this.onMenuClick.bind(this)); + this.tweens.add({ + targets: this.menuElement, + x: openedSideMenuX, + duration: 500, + ease: 'Power3' + }); + } + + private closeSideMenu(): void { + if (!this.sideMenuOpened) return; + this.sideMenuOpened = false; + this.closeGameQualityMenu() + this.menuButton.getChildByID('openMenuButton').innerHTML = 'Menu' + this.tweens.add({ + targets: this.menuElement, + x: closedSideMenuX, + duration: 500, + ease: 'Power3' + }); + this.menuElement.removeListener('click'); + } + + + + private openGameSettingsMenu(): void { + if (this.settingsMenuOpened) return; + this.settingsMenuOpened = true; + + const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; + gameQualitySelect.value = ''+this.gameQualityValue; + const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; + videoQualitySelect.value = ''+this.videoQualityValue; + + this.gameQualityMenuElement.addListener('click'); + this.gameQualityMenuElement.on('click', (event:MouseEvent) => { + event.preventDefault(); + if ((event?.target as HTMLInputElement).id === 'gameQualityFormSubmit') { + const gameQualitySelect = this.gameQualityMenuElement.getChildByID('select-game-quality') as HTMLInputElement; + const videoQualitySelect = this.gameQualityMenuElement.getChildByID('select-video-quality') as HTMLInputElement; + this.saveSetting(parseInt(gameQualitySelect.value), parseInt(videoQualitySelect.value)); + } else if((event?.target as HTMLInputElement).id === 'gameQualityFormCancel') { + this.closeGameQualityMenu(); + } + }); + + this.tweens.add({ + targets: this.gameQualityMenuElement, + y: this.game.renderer.height / 2, + duration: 1000, + ease: 'Power3' + }); + } + + private closeGameQualityMenu(): void { + if (!this.settingsMenuOpened) return; + this.settingsMenuOpened = false; + + this.gameQualityMenuElement.removeListener('click'); + this.tweens.add({ + targets: this.gameQualityMenuElement, + y: -400, + duration: 1000, + ease: 'Power3' + }); + } + + + + private onMenuClick(event:MouseEvent) { + event.preventDefault(); + + switch ((event?.target as HTMLInputElement).id) { + case 'changeNameButton': + this.closeSideMenu(); + this.closeGameQualityMenu(); + gameManager.leaveGame(this, LoginSceneName); + break; + case 'sparkButton': + this.goToSpark(); + break; + case 'changeSkinButton': + this.closeSideMenu(); + gameManager.leaveGame(this, SelectCharacterSceneName); + break; + case 'closeButton': + this.closeSideMenu(); + break; + case 'shareButton': + this.shareUrl(); + break; + case 'editGameSettingsButton': + this.openGameSettingsMenu(); + break; + case 'adminConsoleButton': + gameManager.getCurrentGameScene(this).ConsoleGlobalMessageManager.activeMessageConsole(); + break; + } + } + + private async shareUrl() { + await navigator.clipboard.writeText(location.toString()); + alert('URL was copy to your clipboard!'); + } + + private saveSetting(valueGame: number, valueVideo: number){ + if (valueGame !== this.gameQualityValue) { + this.gameQualityValue = valueGame; + localUserStore.setGameQualityValue(valueGame); + window.location.reload(); + } + if (valueVideo !== this.videoQualityValue) { + this.videoQualityValue = valueVideo; + localUserStore.setVideoQualityValue(valueVideo); + mediaManager.updateCameraQuality(valueVideo); + } + this.closeGameQualityMenu(); + } + + private goToSpark() { + const sparkHost = 'https://'+window.location.host.replace('play.', 'admin.')+'/register'; + window.location.assign(sparkHost); + } +} \ No newline at end of file diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts index 053a2f44..583b1384 100644 --- a/front/src/WebRtc/DiscussionManager.ts +++ b/front/src/WebRtc/DiscussionManager.ts @@ -1,5 +1,5 @@ import {HtmlUtils} from "./HtmlUtils"; -import {MediaManager, ReportCallback, UpdatedLocalStreamCallback} from "./MediaManager"; +import {mediaManager, ReportCallback} from "./MediaManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {connectionManager} from "../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../Url/UrlManager"; @@ -13,7 +13,6 @@ export class DiscussionManager { private divParticipants?: HTMLDivElement; private nbpParticipants?: HTMLParagraphElement; private divMessages?: HTMLParagraphElement; - private buttonActiveDiscussion?: HTMLButtonElement; private participants: Map = new Map(); @@ -23,9 +22,9 @@ export class DiscussionManager { private userInputManager?: UserInputManager; - constructor(private mediaManager: MediaManager, name: string) { + constructor() { this.mainContainer = HtmlUtils.getElementByIdOrFail('main-container'); - this.createDiscussPart(name); + this.createDiscussPart(''); //todo: why do we always use empty string? } private createDiscussPart(name: string) { @@ -33,20 +32,12 @@ export class DiscussionManager { this.divDiscuss.classList.add('discussion'); const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button'); - this.buttonActiveDiscussion = document.createElement('button'); buttonCloseDiscussion.classList.add('close-btn'); buttonCloseDiscussion.innerHTML = ``; buttonCloseDiscussion.addEventListener('click', () => { this.hideDiscussion(); - this.showButtonDiscussionBtn(); - }); - this.buttonActiveDiscussion.classList.add('active-btn'); - this.buttonActiveDiscussion.innerHTML = ``; - this.buttonActiveDiscussion.addEventListener('click', () => { - this.showDiscussionPart(); }); this.divDiscuss.appendChild(buttonCloseDiscussion); - this.divDiscuss.appendChild(this.buttonActiveDiscussion); const myName: HTMLParagraphElement = document.createElement('p'); myName.innerText = name.toUpperCase(); @@ -128,7 +119,7 @@ export class DiscussionManager { reportBanUserAction.innerText = 'Report'; reportBanUserAction.addEventListener('click', () => { if(reportCallback) { - this.mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback); + mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback); }else{ console.info('report feature is not activated!'); } @@ -139,7 +130,6 @@ export class DiscussionManager { this.divParticipants?.appendChild(divParticipant); this.participants.set(userId, divParticipant); - this.showButtonDiscussionBtn(); this.updateParticipant(this.participants.size); } @@ -184,9 +174,6 @@ export class DiscussionManager { this.participants.delete(userId); } //if all participant leave, hide discussion button - if(this.participants.size === 1){ - this.hideButtonDiscussionBtn(); - } this.sendMessageCallBack.delete(userId); } @@ -199,14 +186,6 @@ export class DiscussionManager { return this.activeDiscussion; } - private showButtonDiscussionBtn(){ - //if it's first participant, show discussion button - if(this.activatedDiscussion || this.participants.size === 1) { - return; - } - this.buttonActiveDiscussion?.classList.add('active'); - } - private showDiscussion(){ this.activeDiscussion = true; if(this.userInputManager) { @@ -222,17 +201,14 @@ export class DiscussionManager { } this.divDiscuss?.classList.remove('active'); } - - private hideButtonDiscussionBtn(){ - this.buttonActiveDiscussion?.classList.remove('active'); - } - + public setUserInputManager(userInputManager : UserInputManager){ this.userInputManager = userInputManager; } public showDiscussionPart(){ this.showDiscussion(); - this.hideButtonDiscussionBtn(); } -} \ No newline at end of file +} + +export const discussionManager = new DiscussionManager(); \ No newline at end of file diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 7fced45c..4aa240cd 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -1,6 +1,6 @@ import {DivImportance, layoutManager} from "./LayoutManager"; import {HtmlUtils} from "./HtmlUtils"; -import {DiscussionManager, SendMessageCallback} from "./DiscussionManager"; +import {discussionManager, SendMessageCallback} from "./DiscussionManager"; import {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {VIDEO_QUALITY_SELECT} from "../Administration/ConsoleGlobalMessageManager"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -56,8 +56,6 @@ export class MediaManager { private lastUpdateScene : Date = new Date(); private setTimeOutlastUpdateScene? : NodeJS.Timeout; - private discussionManager: DiscussionManager; - private hasCamera = true; private triggerCloseJistiFrame : Map = new Map(); @@ -120,8 +118,6 @@ export class MediaManager { this.pingCameraStatus(); this.checkActiveUser(); //todo: desactivated in case of bug - - this.discussionManager = new DiscussionManager(this,''); } public setLastUpdateScene(){ @@ -687,11 +683,11 @@ export class MediaManager { } public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){ - this.discussionManager.addParticipant(userId, name, img, false, reportCallBack); + discussionManager.addParticipant(userId, name, img, false, reportCallBack); } public removeParticipant(userId: number|string){ - this.discussionManager.removeParticipant(userId); + discussionManager.removeParticipant(userId); } public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ this.triggerCloseJistiFrame.set(id, Function); @@ -718,24 +714,24 @@ export class MediaManager { } public addNewMessage(name: string, message: string, isMe: boolean = false){ - this.discussionManager.addMessage(name, message, isMe); + discussionManager.addMessage(name, message, isMe); //when there are new message, show discussion - if(!this.discussionManager.activatedDiscussion) { - this.discussionManager.showDiscussionPart(); + if(!discussionManager.activatedDiscussion) { + discussionManager.showDiscussionPart(); } } public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){ - this.discussionManager.onSendMessageCallback(userId, callback); + discussionManager.onSendMessageCallback(userId, callback); } get activatedDiscussion(){ - return this.discussionManager.activatedDiscussion; + return discussionManager.activatedDiscussion; } public setUserInputManager(userInputManager : UserInputManager){ - this.discussionManager.setUserInputManager(userInputManager); + discussionManager.setUserInputManager(userInputManager); } //check if user is active private checkActiveUser(){ diff --git a/front/src/index.ts b/front/src/index.ts index 640b04f9..c8783b90 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -13,7 +13,8 @@ import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; import {EntryScene} from "./Phaser/Login/EntryScene"; import {coWebsiteManager} from "./WebRtc/CoWebsiteManager"; -import {GAME_QUALITY_SELECT} from "./Administration/ConsoleGlobalMessageManager"; +import {MenuScene} from "./Phaser/Menu/MenuScene"; +import {localUserStore} from "./Connexion/LocalUserStore"; // Load Jitsi if the environment variable is set. if (JITSI_URL) { @@ -24,15 +25,7 @@ if (JITSI_URL) { const {width, height} = coWebsiteManager.getGameSize(); -let valueGameQuality : number = 60 -const localGameQuality = localStorage.getItem(GAME_QUALITY_SELECT); -if(localGameQuality){ - try { - valueGameQuality = parseInt(localGameQuality); - }catch (err){ - console.error(err); - } -} +const valueGameQuality = localUserStore.getGameQualityValue(); const fps : Phaser.Types.Core.FPSConfig = { /** * The minimum acceptable rendering rate, in frames per second. @@ -66,9 +59,12 @@ const config: GameConfig = { width: width / RESOLUTION, height: height / RESOLUTION, parent: "game", - scene: [EntryScene, LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], + scene: [EntryScene, LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene, MenuScene], zoom: RESOLUTION, fps: fps, + dom: { + createContainer: true + }, physics: { default: "arcade", arcade: {