UNPKG

reldens

Version:
588 lines (552 loc) 21.8 kB
/** * * Reldens - GameManager * * Client-side game manager that orchestrates the game client, handles authentication, * manages rooms, features, and coordinates the Phaser game engine lifecycle. * Initialized on page load, handles user login/registration, joins game rooms, * activates features, and maintains the connection between client and server. * */ const { GameClient } = require('./game-client'); const { GameEngine } = require('./game-engine'); const { RoomEvents } = require('./room-events'); const { ClientStartHandler } = require('./handlers/client-start-handler'); const { FeaturesManager } = require('../../features/client/manager'); const { FirebaseConnector } = require('../../firebase/client/connector'); const { ConfigManager } = require('../../config/client/config-manager'); const { TranslationsMapper } = require('../../snippets/client/translations-mapper'); const Translations = require('./snippets/en_US'); const { GameDom } = require('./game-dom'); const { RoomsConst } = require('../../rooms/constants'); const { GameConst } = require('../constants'); const { ErrorManager, EventsManagerSingleton, Logger, sc } = require('@reldens/utils'); /** * @typedef {import('@reldens/utils').EventsManager} EventsManager * @typedef {import('colyseus.js').Room} Room * @typedef {import('./game-engine').GameEngine} GameEngine * @typedef {import('./room-events').RoomEvents} RoomEvents * @typedef {import('../../users/client/player-engine').PlayerEngine} PlayerEngine * @typedef {import('./scene-dynamic').SceneDynamic} SceneDynamic * @typedef {import('./scene-preloader').ScenePreloader} ScenePreloader * @typedef {object} UserData * @property {number} [forgot] * @property {string} [email] * @property {boolean} [isGuest] * @property {boolean} [isNewUser] * @property {string} [username] * @property {string} [password] * @property {string} [selectedPlayer] * @property {string} [selectedScene] */ class GameManager { constructor() { /** @type {GameEngine|false} */ this.gameEngine = false; /** @type {RoomEvents|null} */ this.activeRoomEvents = null; /** @type {EventsManager} */ this.events = EventsManagerSingleton; /** @type {GameDom} */ this.gameDom = GameDom; /** @type {ConfigManager} */ this.config = new ConfigManager(); let initialConfig = this.gameDom.getWindow()?.reldensInitialConfig || {}; sc.deepMergeProperties(this.config, initialConfig); /** @type {FeaturesManager} */ this.features = new FeaturesManager({gameManager: this, events: this.events}); /** @type {FirebaseConnector} */ this.firebase = new FirebaseConnector(this); /** @type {Object<string, Room>} */ this.joinedRooms = {}; /** @type {UserData} */ this.userData = {}; /** @type {Object<string, object>} */ this.plugins = {}; /** @type {Object<string, object>} */ this.services = {}; /** @type {Object<string, HTMLElement>} */ this.elements = {}; /** @type {object|false} */ this.playerData = false; /** @type {boolean} */ this.gameOver = false; /** @type {boolean} */ this.forcedDisconnection = false; /** @type {boolean} */ this.isChangingScene = false; /** @type {boolean} */ this.canInitEngine = true; /** @type {string} */ this.appServerUrl = ''; /** @type {string} */ this.gameServerUrl = ''; /** @type {string} */ this.locale = ''; TranslationsMapper.forConfig(this.config.client, Translations, GameConst.MESSAGE.DATA_VALUES); /** @type {Object<string, any>} */ this.createdAnimations = {}; } /** * @param {boolean} changingScene */ setChangingScene(changingScene) { this.isChangingScene = changingScene; let rootElement = this.gameDom.getElement('#reldens div'); if(!rootElement){ return; } if(changingScene){ rootElement.classList.add('hidden-forced'); return; } rootElement.classList.remove('hidden-forced'); } /** * @param {string} customPluginKey * @param {Function} customPlugin */ setupCustomClientPlugin(customPluginKey, customPlugin) { this.plugins[customPluginKey] = new customPlugin(); this.plugins[customPluginKey].setup({gameManager: this, events: this.events}); } clientStart() { this.events.emitSync('reldens.clientStartBefore', this); /** @type {ClientStartHandler} */ this.startHandler = new ClientStartHandler(this); this.startHandler.clientStart(); } /** * @param {object} formData * @param {boolean} [isNewUser] * @returns {Promise<boolean>} */ async startGame(formData, isNewUser) { this.events.emitSync('reldens.startGameBefore', this); let gameRoom = await this.joinGame(formData, isNewUser); if(gameRoom){ this.handleLoginSuccess(); return true; } this.handleLoginError(formData); return false; } handleLoginSuccess() { let body = this.gameDom.getElement(GameConst.SELECTORS.BODY); body.classList.add(GameConst.CLASSES.GAME_STARTED); body.classList.remove(GameConst.CLASSES.GAME_ERROR); this.gameDom.getElement(GameConst.SELECTORS.FORMS_CONTAINER).remove(); this.events.emitSync('reldens.startGameAfter', this); } /** * @param {object} formData */ handleLoginError(formData) { let body = this.gameDom.getElement(GameConst.SELECTORS.BODY); body.classList.remove(GameConst.CLASSES.GAME_STARTED); body.classList.add(GameConst.CLASSES.GAME_ERROR); // @NOTE: game room errors should always be because of some wrong login or registration data. For these cases // we will check the isNewUser variable to know where to display the error. this.submitedForm = false; this.events.emitSync('reldens.gameRoomError', this); // @TODO - BETA - Move to firebase plugin with an event subscriber. if(this.firebase && 'firebase-login' === formData.formId){ this.firebase.app.auth().signOut(); } } /** * @param {object} formData * @param {boolean} [isNewUser] * @returns {Promise<Room|boolean>} */ async joinGame(formData, isNewUser = false) { // reset the user data in the object in case another form was used before: this.userData = {}; await this.events.emit('reldens.beforeJoinGame', {gameManager: this, formData, isNewUser}); this.mapFormDataToUserData(formData, isNewUser); // join the initial game room, because we return the promise, we don't need to catch the error here: /** @type {Room|boolean} */ this.gameRoom = await this.gameClient.joinOrCreate(GameConst.ROOM_GAME, this.userData); if(!this.gameRoom){ this.displayFormError('#'+formData.formId, this.gameClient.lastErrorMessage); return false; } await this.events.emit('reldens.beforeJoinGameRoom', this.gameRoom); this.handleGameRoomMessages(); this.activateResponsiveBehavior(); return this.gameRoom; } /** * @param {object} formData * @param {boolean} isNewUser */ mapFormDataToUserData(formData, isNewUser) { if(sc.hasOwn(formData, 'forgot')){ this.userData.forgot = 1; this.userData.email = formData['email']; } this.initializeClient(); if(formData.isGuest){ this.userData.isGuest = true; this.userData.isNewUser = true; } if(isNewUser){ this.userData.isNewUser = true; this.userData.email = formData['email']; } this.userData.username = formData['username']; this.userData.password = formData['password']; } handleGameRoomMessages() { this.gameRoom.onMessage('*', async (message) => { if(message.error){ Logger.error('Game Room message error.', message.message); this.displayFormError(GameConst.SELECTORS.PLAYER_CREATE_FORM, message.message); return false; } if(GameConst.START_GAME === message.act){ this.initialGameData = message; return await this.beforeStartGame(); } if(GameConst.CREATE_PLAYER_RESULT !== message.act){ return false; } this.initialGameData.player = message.player; let playerSelection = this.gameDom.getElement(GameConst.SELECTORS.PLAYER_SELECTION); if(playerSelection){ playerSelection.classList.add('hidden'); } await this.initEngine(); }); } activateResponsiveBehavior() { this.events.on('reldens.afterSceneDynamicCreate', async () => { if(!this.config.getWithoutLogs('client/ui/screen/responsive', true)){ return; } this.gameEngine.updateGameSize(this); this.gameDom.getWindow().addEventListener('resize', () => { this.gameEngine.updateGameSize(this); }); }); } /** * @param {string} formId * @param {string} message * @returns {boolean} */ displayFormError(formId, message) { let errorElement = this.gameDom.getElement(formId+' '+GameConst.SELECTORS.RESPONSE_ERROR); if(!errorElement){ return false; } errorElement.innerHTML = message; let loadingContainer = this.gameDom.getElement(formId+' '+GameConst.SELECTORS.LOADING_CONTAINER); if(loadingContainer){ loadingContainer?.classList.add(GameConst.CLASSES.HIDDEN); } return true; } initializeClient() { this.appServerUrl = this.getAppServerUrl(); this.gameServerUrl = this.getGameServerUrl(); /** @type {GameClient} */ this.gameClient = new GameClient(this.gameServerUrl, this.config); } /** * @returns {Promise<boolean|object>} */ async beforeStartGame() { await this.events.emit('reldens.beforeInitEngineAndStartGame', this.initialGameData, this); if(!sc.hasOwn(this.initialGameData, 'gameConfig')){ ErrorManager.error('Missing game configuration.'); } // apply the initial config to the processor: sc.deepMergeProperties(this.config, (this.initialGameData?.gameConfig || {})); // features list: await this.features.loadFeatures((this.initialGameData?.features || {})); await this.events.emit('reldens.beforeCreateEngine', this.initialGameData, this); if(!this.canInitEngine){ return false; } return await this.initEngine(); } /** * @returns {Promise<Room|void>} */ async initEngine() { // @NOTE we could leave the game room after the game initialized because at that point the user already // joined the scene room and this room doesn't listen for anything, BUT we keep it to track all logged users. // await this.gameRoom.leave(); this.playerData = this.initialGameData?.player || false; if(!this.playerData || !this.playerData.state){ return this.gameDom.alertReload(this.services?.translator.t('game.errors.missingPlayerData')); } this.userData.selectedPlayer = this.playerData.id; let selectedScene = this.initialGameData?.selectedScene || ''; this.userData.selectedScene = selectedScene; let config = this.initialGameData?.gameConfig || {}; this.gameEngine = new GameEngine({config: config.client.gameEngine, events: this.events}); // since the user is now registered: this.userData.isNewUser = false; // for guests use the password from the server: if(this.userData.isGuest){ if(this.initialGameData?.guestPassword){ this.userData.password = this.initialGameData.guestPassword; } if(this.initialGameData?.userName){ this.userData.username = this.initialGameData.userName; } } await this.joinFeaturesRooms(); let useLastLocation = '' !== selectedScene && selectedScene !== RoomsConst.ROOM_LAST_LOCATION_KEY; let playerScene = useLastLocation ? selectedScene : this.playerData.state.scene; this.playerData.state.scene = playerScene; let joinedFirstRoom = await this.gameClient.joinOrCreate(playerScene, this.userData); if(!joinedFirstRoom){ // @NOTE: the errors while trying to join a rooms/scene will always be originated in the // server. For these errors we will alert the user and reload the window automatically. return this.gameDom.alertReload( this.services?.translator.t('game.errors.joiningRoom', {joinRoomName: playerScene}) ); } this.gameDom.getElement(GameConst.SELECTORS.BODY).classList.add(GameConst.CLASSES.GAME_ENGINE_STARTED); this.gameDom.getElement(GameConst.SELECTORS.GAME_CONTAINER).classList.remove(GameConst.CLASSES.HIDDEN); let playerSelection = this.gameDom.getElement(GameConst.SELECTORS.PLAYER_SELECTION); if(playerSelection){ playerSelection.classList.add(GameConst.CLASSES.HIDDEN); } // @NOTE: remove the selected scene after the player used it because the login data will be used again every // time the player changes the scene. delete this.initialGameData['selectedScene']; delete this.userData['selectedScene']; await this.emitJoinedRoom(joinedFirstRoom, playerScene); this.activeRoomEvents = this.createRoomEventsInstance(playerScene); await this.events.emit('reldens.createdRoomsEventsInstance', joinedFirstRoom, this); await this.activeRoomEvents.activateRoom(joinedFirstRoom); await this.emitActivatedRoom(joinedFirstRoom, playerScene); await this.events.emit('reldens.afterInitEngineAndStartGame', this.initialGameData, joinedFirstRoom); return joinedFirstRoom; } /** * @returns {Promise<void>} */ async joinFeaturesRooms() { /** @type {Array<string>} */ let featuresListKeys = Object.keys(this.features.featuresList); if(0 === featuresListKeys.length){ return; } /** @type {Array<string>} */ let featuresRoomsNames = []; for(let i of featuresListKeys){ let feature = this.features.featuresList[i]; if(!sc.hasOwn(feature, 'joinRooms')){ continue; } for(let joinRoomName of feature.joinRooms){ let joinedRoom = await this.gameClient.joinOrCreate(joinRoomName, this.userData); if(!joinedRoom){ // @NOTE: any join room error will always be originated in the server. For these errors we // will alert the user and reload the window automatically. Here the received "data" will // be the actual error message. return this.gameDom.alertReload( this.services.translator.t('game.errors.joiningFeatureRoom', {joinRoomName}) ); } //Logger.debug('Joined room: '+joinRoomName); // after the room was joined added to the joinedRooms list: this.joinedRooms[joinRoomName] = joinedRoom; await this.emitJoinedRoom(joinedRoom, joinRoomName); featuresRoomsNames.push(joinRoomName); } } sc.deepMergeProperties(this.config, {client: {rooms: {featuresRoomsNames}}}); } /** * @param {object} message * @param {Room} previousRoom * @returns {Promise<void>} */ async reconnectGameClient(message, previousRoom) { this.setChangingScene(true); let newRoomEvents = this.createRoomEventsInstance(message.player.state.scene); this.gameClient.joinOrCreate(newRoomEvents.roomName, this.userData).then(async (sceneRoom) => { // leave the old room: previousRoom.leave(); this.activeRoomEvents = newRoomEvents; this.room = sceneRoom; await this.emitJoinedRoom(sceneRoom, message.player.state.scene); // start to listen to the new room events: await newRoomEvents.activateRoom(sceneRoom, message.prev); await this.emitActivatedRoom(sceneRoom, message.player.state.scene); }).catch((error) => { // @NOTE: the errors while trying to reconnect will always be originated in the server. For these errors we // will alert the user and reload the window automatically. Logger.error('Reconnect Game Client error.', {error, message, previousRoom}); this.gameDom.alertReload(this.services.translator.t('game.errors.reconnectClient')); }); } /** * @param {Room} sceneRoom * @param {string} playerScene * @returns {Promise<void>} */ async emitActivatedRoom(sceneRoom, playerScene) { await this.events.emit('reldens.activatedRoom', sceneRoom, this); await this.events.emit('reldens.activatedRoom_'+playerScene, sceneRoom, this); } /** * @param {Room} sceneRoom * @param {string} playerScene * @returns {Promise<void>} */ async emitJoinedRoom(sceneRoom, playerScene) { await this.events.emit('reldens.joinedRoom', sceneRoom, this); await this.events.emit('reldens.joinedRoom_'+playerScene, sceneRoom, this); } /** * @param {string} roomName * @returns {RoomEvents} */ createRoomEventsInstance(roomName) { return new RoomEvents(roomName, this); } /** * @returns {string} */ getAppServerUrl() { if('' === this.appServerUrl){ this.appServerUrl = this.getUrlFromCurrentReferer(); } return this.appServerUrl; } /** * @returns {string} */ getGameServerUrl() { if('' === this.gameServerUrl){ this.gameServerUrl = this.getUrlFromCurrentReferer(true); } return this.gameServerUrl; } /** * @param {boolean} [useWebSocket] * @returns {string} */ getUrlFromCurrentReferer(useWebSocket = false) { let location = this.gameDom.getWindow().location; let protocol = location.protocol; if(useWebSocket){ protocol = 0 === protocol.indexOf('https') ? 'wss:' : 'ws:'; } return protocol + '//'+location.hostname+(location.port ? ':'+location.port : ''); } /** * @returns {SceneDynamic} */ getActiveScene() { return this.activeRoomEvents.getActiveScene(); } /** * @returns {ScenePreloader} */ getActiveScenePreloader() { return this.gameEngine.scene.getScene(GameConst.SCENE_PRELOADER+this.getActiveScene().key); } /** * @returns {PlayerEngine|boolean} */ getCurrentPlayer() { let activeScene = this.getActiveScene(); if(!activeScene){ //Logger.debug('Missing active scene.'); return false; } return activeScene.player; } /** * @returns {string} */ currentPlayerName() { let currentPlayer = this.getCurrentPlayer(); if(!currentPlayer){ return ''; } return currentPlayer.player_id+' - '+currentPlayer.playerName; } /** * @returns {object} */ getCurrentPlayerAnimation() { let current = this.getCurrentPlayer(); return current.players[current.playerId]; } /** * @param {string} uiName * @param {boolean} [logError] * @returns {object|boolean} */ getUiElement(uiName, logError = true) { let uiScene = sc.get(this.gameEngine, 'uiScene', false); if(uiScene){ return uiScene.getUiElement(uiName, logError); } if(logError){ Logger.error('UI Scene not defined.'); } return false; } /** * @param {string} featureKey * @returns {object|boolean} */ getFeature(featureKey) { let featuresList = this.features.featuresList; if(!sc.hasOwn(featuresList, featureKey)){ Logger.error('Feature key not defined.', featureKey); return false; } return featuresList[featureKey]; } /** * @param {string} key * @returns {object} */ getAnimationByKey(key) { return this.getActiveScene().getAnimationByKey(key); } } module.exports.GameManager = GameManager;