reldens
Version:
Reldens - MMORPG Platform
588 lines (552 loc) • 21.8 kB
JavaScript
/**
*
* 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;