diff --git a/README.md b/README.md
index 71a0e086a..e4d8f2862 100644
--- a/README.md
+++ b/README.md
@@ -39,26 +39,30 @@ While the current stage of the platform is tailored for developers, ongoing impr
As for the latest version released the platform will provide you with the following features.
- Installation GUI: Easy to install through the web.
-- Administration Panel: Manage every single aspect of your game through the panel (WIP, temporally disabled).
+- Administration Panel: Manage every single aspect of your game through the panel.
+- Automatic data generators and import commands for attributes, levels, maps, objects, etc.
- Trade System: Between Players (trade) and NPCs (buy and sell).
- Full In-game Chat: Global, by room, and private messages between users with chat types and split in tabs.
- Player Stats: Fully configurable stats! HP? MP? Magic? Stamina, you can set up as much as you need for your game logic.
-- Items System: Based on @reldens/items-system, create all kinds of items (usable and equipment).
-- Attacks, PVP, and PVE: Based on @reldens/skills, you will be able to create all kind of skills, attacks, bullets type (can be dodged), target type (can't be dodged), fight with other players or just with NPCs.
+- Items System: create all kinds of items (usable and equipment).
+- Attacks, PVP, and PVE: create all kind of skills, attacks, bullets type (can be dodged), target type (can't be dodged), fight with other players or just with NPCs.
- NPC's, Enemies, and Respawn Areas: Setup NPC's with different options to interact and create respawn areas.
- Teams and Clans System: Create, join, dismantle teams and clans, bonus based on clan level, and more.
-- Drops and Rewards: NPCs can drop rewards, and it can be configured to be split among team members.
+- Drops and Rewards: NPCs can drop any kind of items (rewards), and it can be configured to be split among team members.
+- Game rewards: you can configure rewards based on game events like daily or weekly login, it's events managed.
+- Game scores: configurable games scores, for example keep track on monsters kills and get a global scores table view.
- Physics Engine and Pathfinder: Authoritative server with a physics engine and pathfinder.
- Gravity World: Setup your rooms with or without gravity to get totally different kinds of gameplay.
- Sounds System: Configurable multiple sounds categories for any animation or scene.
- In-game Ads: Integrated with CrazyGames and GameMonetize to show ads in-game.
- Minimap: Optional configurable minimap.
-- Configurable Visible Players Name and Life-bars.
+- Configurable Players Name and Life-bars visibility.
- Terms and Conditions: Ready to be set up as you need.
- Guest Users.
- Users Registration: Continue playing later and double login invalidation.
- Multiple Players Creation.
- Registration and Login Integrated with Firebase.
+- Multiple servers switch support, from room-A in server-A, to room-B in server-B without notice.
- Database Ready: With multiple drivers for different storage or using MySQL by default all you need will be saved.
---
@@ -103,6 +107,13 @@ If you like to contribute in any way or donate to support the project please als
Public URL (useful if you have a reverse proxy)
@@ -244,7 +248,8 @@
diff --git a/install/index.js b/install/index.js
index 81c2f1d83..4e1ed7429 100644
--- a/install/index.js
+++ b/install/index.js
@@ -46,4 +46,16 @@ window.addEventListener('load', () => {
document.querySelector('.'+errorCode).style.display = 'block';
}
+ document.getElementById('install-form').addEventListener('submit', (event) => {
+ let loadingImage = document.querySelector('.install-loading');
+ if(loadingImage){
+ loadingImage.classList.remove('hidden');
+ }
+ let installButton = document.getElementById('install-submit-button');
+ if(installButton){
+ installButton.classList.add('disabled');
+ installButton.disabled = true;
+ }
+ });
+
});
\ No newline at end of file
diff --git a/lib/actions/client/player-selector.js b/lib/actions/client/player-selector.js
index caf3268e6..558688c3c 100644
--- a/lib/actions/client/player-selector.js
+++ b/lib/actions/client/player-selector.js
@@ -12,11 +12,6 @@
* - appendAvatarOnSelector: appends the avatar of the selected class to the selector and handles the avatar change
* when the user selects a different class.
*
- * Fields:
- * - gameManager: an instance of the GameManager class.
- * - events: an instance of the EventsManager class.
- * - gameDom: an instance of the GameDom class.
- *
*/
const { ActionsConst } = require('../constants');
@@ -28,15 +23,17 @@ class PlayerSelector
constructor(props)
{
- // @TODO - BETA - Refactor use DI or something to avoid all props assigned and validations.
+ /** @type {GameManager} **/
this.gameManager = sc.get(props, 'gameManager', false);
if(!this.gameManager){
Logger.error('Game Manager undefined in ActionsPlugin PlayerSelector.');
}
+ /** @type EventsManager **/
this.events = sc.get(props, 'events', false);
if(!this.events){
Logger.error('EventsManager undefined in ActionsPlugin PlayerSelector.');
}
+ /** @type {GameDom} **/
this.gameDom = this.gameManager.gameDom;
}
diff --git a/lib/actions/factories/skill-data-factory.js b/lib/actions/factories/skill-data-factory.js
index 7298c73b1..9382153e4 100644
--- a/lib/actions/factories/skill-data-factory.js
+++ b/lib/actions/factories/skill-data-factory.js
@@ -112,14 +112,14 @@ class SkillDataFactory
{
let animations = sc.get(data, 'animations', {});
let animationKeys = Object.keys(animations);
- if (0 === animationKeys.length) {
+ if(0 === animationKeys.length){
return;
}
let animationsDefaults = sc.get(defaults, 'animations', {});
let animationDefaultData = animationsDefaults.defaults;
- for (let i of animationKeys) {
+ for(let i of animationKeys){
let animationData = Object.assign({}, animationDefaultData, animationsDefaults[i], animations[i]);
- if (animationsDefaults.appendSkillKeyOnAnimationImage || animations[i].appendSkillKeyOnAnimationImage){
+ if(animationsDefaults.appendSkillKeyOnAnimationImage || animations[i].appendSkillKeyOnAnimationImage){
animationData.img = key + animationData.img;
}
this.animations.push({key: i, animationData});
@@ -129,10 +129,10 @@ class SkillDataFactory
mapOwnerConditions(data, defaults)
{
let ownerConditions = sc.get(data, 'ownerConditions', []);
- if (0 === ownerConditions.length) {
+ if(0 === ownerConditions.length){
return;
}
- for (let ownerCondition of ownerConditions) {
+ for(let ownerCondition of ownerConditions){
this.ownerConditions.push(
Object.assign(
{},
@@ -147,10 +147,10 @@ class SkillDataFactory
mapOwnerEffects(data, defaults)
{
let ownerEffects = sc.get(data, 'ownerEffects', []);
- if (0 === ownerEffects.length) {
+ if(0 === ownerEffects.length){
return;
}
- for (let ownerEffect of ownerEffects) {
+ for(let ownerEffect of ownerEffects){
this.ownerEffects.push(
Object.assign(
{},
@@ -165,10 +165,10 @@ class SkillDataFactory
mapTargetEffects(data, defaults)
{
let targetEffects = sc.get(data, 'targetEffects', []);
- if (0 === targetEffects.length) {
+ if(0 === targetEffects.length){
return;
}
- for (let targetEffect of targetEffects) {
+ for(let targetEffect of targetEffects){
this.targetEffects.push(
Object.assign(
{},
diff --git a/lib/actions/server/battle.js b/lib/actions/server/battle.js
index c9e7824cc..fb332604b 100644
--- a/lib/actions/server/battle.js
+++ b/lib/actions/server/battle.js
@@ -5,6 +5,7 @@
*/
const { BattleEndAction } = require('./battle-end-action');
+const { PlayerDeathEvent } = require('./events/player-death-event');
const { ActionsConst } = require('../constants');
const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
@@ -20,32 +21,35 @@ class Battle
// specific behaviors.
this.battleTimeOff = props.battleTimeOff || false;
this.battleTimer = false;
+ this.playerReviveTimer = false;
this.timerType = props.timerType || ActionsConst.BATTLE_TYPE_PER_TARGET;
this.lastAttack = false;
this.lastAttackKey = false;
this.events = sc.get(props, 'events', false);
if(!this.events){
- Logger.error('EventsManager undefined in Battle.');
+ Logger.error('EventsManager undefined in Battle "'+this.constructor.name+'".');
}
}
async runBattle(playerSchema, target)
{
- if(GameConst.STATUS.ACTIVE !== playerSchema.inState){
- Logger.error('Battle inactive player with ID "'+playerSchema.player_id+'".', playerSchema.inState);
+ let playerState = playerSchema?.state?.inState;
+ if(GameConst.STATUS.ACTIVE !== playerState){
+ Logger.error('Battle inactive player with ID "'+playerSchema.player_id+'".', playerState);
+ delete this.inBattleWith[target.id];
return false;
}
- if(target.inState && GameConst.STATUS.ACTIVE.toString() !== target.inState.toString()){
- Logger.error(
- 'Inactive target ID "'+(target.uid || target.player_id)
- +'" in state "'+target.inState+'"/"'+GameConst.STATUS.ACTIVE+'".'
- );
+ let targetState = target?.state?.inState;
+ if(targetState && GameConst.STATUS.ACTIVE.toString() !== targetState.toString()){
+ //Logger.debug('Inactive target ID "'+(target.uid || target.player_id)+'" in state "'+targetState+'".');
+ delete this.inBattleWith[target.id];
return false;
}
// @NOTE: each attack will have different properties to validate like range, delay, etc.
let currentAction = this.getCurrentAction(playerSchema);
if(!currentAction){
Logger.error('Actions not defined for this player with ID "'+playerSchema.player_id+'".');
+ delete this.inBattleWith[target.id];
return false;
}
currentAction.currentBattle = this;
@@ -54,7 +58,7 @@ class Battle
// include the target in the battle list:
this.lastAttack = Date.now();
this.inBattleWith[target.id] = {target: target, time: this.lastAttack, battleTimer: false};
- let useTimerObj = this; // ActionsConst.BATTLE_TYPE_GENERAL
+ let useTimerObj = this;
if(this.timerType === ActionsConst.BATTLE_TYPE_PER_TARGET){
useTimerObj = this.inBattleWith[target.id];
}
@@ -77,6 +81,7 @@ class Battle
{
if(useTimerObj.battleTimer){
clearTimeout(useTimerObj.battleTimer);
+ delete this.inBattleWith[target.id];
}
if(this.battleTimeOff){
useTimerObj.battleTimer = setTimeout(() => {
@@ -85,21 +90,39 @@ class Battle
}
}
- async updateTargetClient(targetClient, targetSchema, attackerId, room)
+ async updateTargetClient(targetClient, targetSchema, attackerId, room, attackerPlayer)
{
+ if(!targetSchema.stats){
+ Logger.warning('Target schema for player (ID: '+targetSchema?.player_id+') with undefined stats.');
+ return false;
+ }
let affectedProperty = room.config.get('client/actions/skills/affectedProperty');
- return 0 === targetSchema.stats[affectedProperty]
- ? await this.clientDeathUpdate(targetSchema, room, targetClient, affectedProperty)
- : await room.savePlayerStats(targetSchema, targetClient);
+ if(0 === targetSchema.stats[affectedProperty]){
+ return await this.clientDeathUpdate(
+ targetSchema,
+ room,
+ targetClient,
+ affectedProperty,
+ attackerId,
+ attackerPlayer
+ );
+ }
+ return await room.savePlayerStats(targetSchema, targetClient);
}
- async clientDeathUpdate(targetSchema, room, targetClient, affectedProperty)
+ async clientDeathUpdate(targetSchema, room, targetClient, affectedProperty, attackerId, attackerPlayer)
{
if(!targetSchema.player_id){
Logger.error('Target is not a player.', targetSchema.player_id);
return false;
}
- targetSchema.inState = GameConst.STATUS.DEATH;
+ if(targetSchema.isDeath() || targetSchema.isDisabled()){
+ // Logger.debug('Target is already death.', targetSchema.player_id);
+ // expected when multiple enemies get the player life = 0 as response from the last hit in battle:
+ return false;
+ }
+ //Logger.debug('Player with ID "'+targetSchema.player_id+'" is death.');
+ room.deactivatePlayer(targetSchema, GameConst.STATUS.DEATH);
let actionData = new BattleEndAction(
targetSchema.state.x,
targetSchema.state.y,
@@ -114,19 +137,48 @@ class Battle
await room.savePlayerStats(targetSchema, targetClient);
targetSchema.setPrivate(
'playerDeathTimer',
- setTimeout(
+ this.playerReviveTimer = setTimeout(
async () => {
- room.roomWorld.addBody(body);
- targetSchema.inState = GameConst.STATUS.ACTIVE;
- // player is dead! reinitialize the stats using its base value:
- targetSchema.stats[affectedProperty] = targetSchema.statsBase[affectedProperty];
- await room.savePlayerStats(targetSchema, targetClient);
- room.broadcast('*', {act: GameConst.REVIVED, t: targetSchema.sessionId});
+ return await this.revivePlayer(room, body, targetSchema, affectedProperty, targetClient);
},
(room.config.get('server/players/gameOver/timeOut') || 1)
)
);
- room.events.emit('reldens.playerDeath', {targetSchema, room, targetClient, affectedProperty});
+ room.events.emit('reldens.playerDeath', new PlayerDeathEvent({
+ targetSchema,
+ room,
+ targetClient,
+ affectedProperty,
+ attackerPlayer
+ }));
+ return false;
+ }
+
+ async revivePlayer(room, body, targetSchema, affectedProperty, targetClient)
+ {
+ if(!room.roomWorld){
+ Logger.critical('Room world not available to set player death timer.');
+ return false;
+ }
+ let sessionId = targetSchema.sessionId;
+ if(!room.activePlayerBySessionId(sessionId, room.roomId)){
+ // @NOTE: expected if player disconnected while is death and timer keep running after disconnection.
+ return false;
+ }
+ try {
+ if(sc.isFunction(room.roomWorld.addBody)){
+ room.roomWorld.addBody(body);
+ }
+ room.activatePlayer(targetSchema, GameConst.STATUS.ACTIVE);
+ // player is dead! reinitialize the stats using its base value:
+ targetSchema.stats[affectedProperty] = targetSchema.statsBase[affectedProperty];
+ await room.savePlayerStats(targetSchema, targetClient);
+ room.broadcast('*', {act: GameConst.REVIVED, t: sessionId});
+ //Logger.debug('Revived: '+targetSchema.player_id+' ('+ sessionId+' / '+targetSchema.state.inState+')');
+ return true;
+ } catch (error) {
+ Logger.error('There was an error on setting player death timer. ' + error.message);
+ }
return false;
}
}
diff --git a/lib/actions/server/events/battle-ended-event.js b/lib/actions/server/events/battle-ended-event.js
new file mode 100644
index 000000000..23749f9df
--- /dev/null
+++ b/lib/actions/server/events/battle-ended-event.js
@@ -0,0 +1,20 @@
+/**
+ *
+ * Reldens - BattleEndedEvent
+ *
+ */
+
+class BattleEndedEvent
+{
+
+ constructor(props)
+ {
+ this.playerSchema = props.playerSchema;
+ this.pve = props.pve
+ this.actionData = props.actionData;
+ this.room = props.room;
+ }
+
+}
+
+module.exports.BattleEndedEvent = BattleEndedEvent;
diff --git a/lib/actions/server/events/player-death-event.js b/lib/actions/server/events/player-death-event.js
new file mode 100644
index 000000000..c66fe9c28
--- /dev/null
+++ b/lib/actions/server/events/player-death-event.js
@@ -0,0 +1,21 @@
+/**
+ *
+ * Reldens - PlayerDeathEvent
+ *
+ */
+
+class PlayerDeathEvent
+{
+
+ constructor(props)
+ {
+ this.targetSchema = props.targetSchema;
+ this.room = props.room;
+ this.targetClient = props.targetClient
+ this.affectedProperty = props.affectedProperty;
+ this.attackerPlayer = props.attackerPlayer;
+ }
+
+}
+
+module.exports.PlayerDeathEvent = PlayerDeathEvent;
diff --git a/lib/actions/server/initial-game-data-enricher.js b/lib/actions/server/initial-game-data-enricher.js
index 0480a9481..d65e106fa 100644
--- a/lib/actions/server/initial-game-data-enricher.js
+++ b/lib/actions/server/initial-game-data-enricher.js
@@ -7,17 +7,25 @@
class InitialGameDataEnricher
{
- static async withClassPathLabels(roomGame, superInitialGameData)
+ constructor()
+ {
+ this.classesData = false;
+ }
+
+ async withClassPathLabels(roomGame, superInitialGameData)
{
if(!roomGame.config.skills.classPaths.classPathsByKey){
return;
}
- let classPathsLabelsByKey = {};
- for(let i of Object.keys(roomGame.config.skills.classPaths.classPathsByKey)){
- let classPath = roomGame.config.skills.classPaths.classPathsByKey[i];
- classPathsLabelsByKey[classPath.data.id] = {key: i, label: classPath.data.label};
+ if(!this.classesData){
+ let classPathsLabelsByKey = {};
+ for(let i of Object.keys(roomGame.config.skills.classPaths.classPathsByKey)){
+ let classPath = roomGame.config.skills.classPaths.classPathsByKey[i];
+ classPathsLabelsByKey[classPath.data.id] = {key: i, label: classPath.data.label};
+ }
+ this.classesData = classPathsLabelsByKey;
}
- superInitialGameData.classesData = classPathsLabelsByKey;
+ superInitialGameData.classesData = this.classesData;
}
}
diff --git a/lib/actions/server/message-actions.js b/lib/actions/server/message-actions.js
index 393213cbd..e6452ae1a 100644
--- a/lib/actions/server/message-actions.js
+++ b/lib/actions/server/message-actions.js
@@ -19,6 +19,9 @@ class ActionsMessageActions
// if body is blocked do NOTHING! it could be because a scene change, or a skill activation or an item
return false;
}
+ if(playerSchema.isDeath() || playerSchema.isDisabled()){
+ return false;
+ }
if(ActionsConst.ACTION !== data.act || !data.target){
return false;
}
@@ -34,9 +37,8 @@ class ActionsMessageActions
if(!currentAction.validateRange(validTarget) || !currentAction.validate()){
return false;
}
- // set the room:
currentAction.room = room;
- if(data.target.type === GameConst.TYPE_PLAYER){
+ if(data.target.type === GameConst.TYPE_PLAYER && playerSchema.actions['pvp']){
await playerSchema.actions['pvp'].runBattle(playerSchema, validTarget, room);
}
if(data.target.type === ObjectsConst.TYPE_OBJECT && sc.hasOwn(validTarget, 'battle')){
diff --git a/lib/actions/server/player-enricher.js b/lib/actions/server/player-enricher.js
index 206c17833..237ffad44 100644
--- a/lib/actions/server/player-enricher.js
+++ b/lib/actions/server/player-enricher.js
@@ -5,7 +5,7 @@
*/
const { Pvp } = require('./pvp');
-const { SkillsExtraData } = require('./skills/skills-extra-data');
+const { SkillsExtraDataMapper } = require('./skills-extra-data-mapper');
const { ClientWrapper } = require('../../game/server/client-wrapper');
const SkillsServer = require('@reldens/skills/lib/server');
const { SkillConst } = require('@reldens/skills');
@@ -14,18 +14,26 @@ const { Logger, sc } = require('@reldens/utils');
class PlayerEnricher
{
- static async withClassPath(roomGame, superInitialGameData, dataServer)
+ constructor(props)
{
+ this.config = props.config;
+ this.events = props.events;
+ this.skillsModelsManager = props.skillsModelsManager;
+ this.pvpEnabled = this.config.getWithoutLogs('server/actions/pvp/enabled', true);
+ this.pvpConfig = Object.assign({events: this.events}, this.config.get('server/actions/pvp'));
+ this.affectedProperty = this.config.get('client/actions/skills/affectedProperty');
+ this.skillsExtraDataMapper = new SkillsExtraDataMapper();
+ }
+
+ async withClassPath(roomGame, superInitialGameData)
+ {
+ // Logger.debug('Players Models:', superInitialGameData.players);
if(!superInitialGameData.players){
return;
}
for(let i of Object.keys(superInitialGameData.players)){
let player = superInitialGameData.players[i];
- let classPath = await dataServer.getEntity('ownersClassPath').loadOneByWithRelations(
- 'owner_id',
- player.id,
- 'owner_full_class_path'
- );
+ let classPath = await this.skillsModelsManager.loadOwnerClassPath(player.id);
if(!classPath){
continue;
}
@@ -35,15 +43,14 @@ class PlayerEnricher
}
}
- static async withActions(currentPlayer, room, events)
+ async withActions(currentPlayer, room)
{
currentPlayer.actions = {};
- let pvpConfig = Object.assign({events: events}, room.config.get('server/actions/pvp'));
- if(pvpConfig){
- currentPlayer.actions['pvp'] = new Pvp(pvpConfig);
+ if(this.pvpEnabled && false !== sc.get(room.customData, 'pvpEnabled', true)){
+ currentPlayer.actions['pvp'] = new Pvp(this.pvpConfig);
}
currentPlayer.getSkillExtraData = (params) => {
- return SkillsExtraData.extractSkillExtraData(params);
+ return this.skillsExtraDataMapper.extractSkillExtraData(params);
};
currentPlayer.executePhysicalSkill = this.playerExecutePhysicalSkillCallback(
currentPlayer,
@@ -51,8 +58,9 @@ class PlayerEnricher
);
}
- static playerExecutePhysicalSkillCallback(currentPlayer, skillsAnimationsData)
+ playerExecutePhysicalSkillCallback(currentPlayer, skillsAnimationsData)
{
+ // @TODO - BETA - Replace with bind.
return async (target, executedSkill) => {
let messageData = Object.assign({skillKey: executedSkill.key}, executedSkill.owner.getPosition());
if(sc.isObjectFunction(executedSkill.owner, 'getSkillExtraData')){
@@ -85,9 +93,10 @@ class PlayerEnricher
};
}
- static async withSkillsServerAndClassPath(props)
+ async withSkillsServerAndClassPath(props)
{
let {client, currentPlayer, room, skillsModelsManager, dataServer, events} = props;
+ // @TODO - BETA - Improve prepareClassPathData to avoid loadOwnerClassPath double queries on each room.
let classPathData = await skillsModelsManager.prepareClassPathData(
currentPlayer,
'player_id',
@@ -101,7 +110,7 @@ class PlayerEnricher
events: events,
persistence: true,
dataServer: dataServer,
- affectedProperty: room.config.get('client/actions/skills/affectedProperty'),
+ affectedProperty: this.affectedProperty,
client: new ClientWrapper({client, room})
});
currentPlayer.skillsServer = new SkillsServer(classPathData);
diff --git a/lib/actions/server/plugin.js b/lib/actions/server/plugin.js
index 3b6b77cde..b810f4ed5 100644
--- a/lib/actions/server/plugin.js
+++ b/lib/actions/server/plugin.js
@@ -27,7 +27,17 @@ class ActionsPlugin extends PluginInterface
if(!this.dataServer){
Logger.error('Data Server undefined in ActionsPlugin.');
}
+ this.config = sc.get(props, 'config', false);
+ if(!this.config){
+ Logger.error('Config undefined in ActionsPlugin.');
+ }
this.skillsModelsManager = new ModelsManager({events: this.events, dataServer: this.dataServer});
+ this.playerEnricher = new PlayerEnricher({
+ events: this.events,
+ config: this.config,
+ skillsModelsManager: this.skillsModelsManager
+ });
+ this.initialGameDataEnricher = new InitialGameDataEnricher();
this.listenEvents();
}
@@ -50,8 +60,8 @@ class ActionsPlugin extends PluginInterface
async enrichInitialGameDataWithClassPathData(superInitialGameData, roomGame)
{
- await InitialGameDataEnricher.withClassPathLabels(roomGame, superInitialGameData);
- await PlayerEnricher.withClassPath(roomGame, superInitialGameData, this.dataServer);
+ await this.initialGameDataEnricher.withClassPathLabels(roomGame, superInitialGameData);
+ await this.playerEnricher.withClassPath(roomGame, superInitialGameData);
}
async appendRoomActions(roomMessageActions)
@@ -61,8 +71,9 @@ class ActionsPlugin extends PluginInterface
async enrichPlayerWithSkillsAndActions(client, userModel, currentPlayer, room)
{
- await PlayerEnricher.withActions(currentPlayer, room, this.events);
- await PlayerEnricher.withSkillsServerAndClassPath({
+ await this.playerEnricher.withActions(currentPlayer, room, this.events);
+ // @TODO - BETA - Improve login performance.
+ await this.playerEnricher.withSkillsServerAndClassPath({
client,
room,
skillsModelsManager: this.skillsModelsManager,
diff --git a/lib/actions/server/pve.js b/lib/actions/server/pve.js
index 86df01e94..334dd9f9c 100644
--- a/lib/actions/server/pve.js
+++ b/lib/actions/server/pve.js
@@ -6,6 +6,7 @@
const { Battle } = require('./battle');
const { BattleEndAction } = require('./battle-end-action');
+const { BattleEndedEvent } = require('./events/battle-ended-event');
const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
@@ -16,7 +17,9 @@ class Pve extends Battle
{
super(props);
this.chaseMultiple = sc.get(props, 'chaseMultiple', false);
- this.inBattleWithPlayer = [];
+ this.inBattleWithPlayers = {};
+ this.isBattleEndProcessing = false;
+ this.uid = sc.randomChars(8)+'-'+(new Date()).getTime();
}
setTargetObject(targetObject)
@@ -26,8 +29,10 @@ class Pve extends Battle
async runBattle(playerSchema, target, roomScene)
{
- if(GameConst.STATUS.ACTIVE !== playerSchema.inState){
- Logger.info('PvE inactive player.', playerSchema.inState);
+ //Logger.debug('Running Battle between "'+playerSchema.sessionId+'" and "'+target.id+'".', this.uid);
+ if(GameConst.STATUS.ACTIVE !== playerSchema.state.inState){
+ Logger.info('PvE inactive player.', playerSchema.state.inState);
+ delete this.inBattleWith[target.id];
return false;
}
// @TODO - BETA - Make PvP available by configuration.
@@ -42,7 +47,17 @@ class Pve extends Battle
return false;
}
// @TODO - BETA - Target affected property could be passed on the target object.
- if(0 < target.stats[roomScene.config.get('client/actions/skills/affectedProperty')]){
+ let affectedProperty = roomScene.config.get('client/actions/skills/affectedProperty');
+ if(!affectedProperty){
+ Logger.error('Affected property configuration is missing');
+ return false;
+ }
+ if(!sc.hasOwn(target.stats, affectedProperty)){
+ Logger.error('Affected property is not present on target stats.', Object.keys(target.stats));
+ return false;
+ }
+ let affectedPropertyTargetValue = Number(sc.get(target.stats, affectedProperty, 0));
+ if(0 < affectedPropertyTargetValue){
return await this.startBattleWith(playerSchema, roomScene);
}
// physical attacks or effects will run the battleEnded, normal attacks or effects will hit this case:
@@ -52,15 +67,21 @@ class Pve extends Battle
async startBattleWith(playerSchema, room)
{
if(!this.targetObject){
- Logger.critical('Target Object reference removed.');
+ // @NOTE: this will be the expected case when the player was killed in between different NPCs attacks.
+ // Logger.debug('Target Object reference removed.');
return false;
}
- let thisWorldKey = this.targetObject?.objectBody?.world?.worldKey;
- let targetWorldKey = playerSchema?.physicalBody?.world?.worldKey;
- if(!thisWorldKey || !targetWorldKey || thisWorldKey !== targetWorldKey){
- if(targetWorldKey){
- Logger.debug('World keys check failed.', {thisWorldKey, targetWorldKey});
+ let targetObjectWorld = this.targetObject.objectBody?.world;
+ let objectWorldKey = targetObjectWorld?.worldKey;
+ let playerWorld = playerSchema?.physicalBody?.world;
+ let playerWorldKey = playerWorld?.worldKey;
+ if(!objectWorldKey || !playerWorldKey || objectWorldKey !== playerWorldKey){
+ if(playerWorldKey){
+ Logger.debug('World keys check failed:', playerWorld.sceneName, {objectWorldKey, playerWorldKey});
}
+ // playerWorldKey will be null while the player is dead because the removeBody
+ // Logger.debug('Leaving battle, world keys check failed.', playerSchema.player_id);
+ this.leaveBattle(playerSchema);
return false;
}
if(
@@ -69,11 +90,16 @@ class Pve extends Battle
|| !playerSchema
|| !room.playerBySessionIdFromState(playerSchema.sessionId)
){
- Logger.debug('Room or player missing references.');
+ //Logger.debug('Room or player missing references.');
// @NOTE: leaveBattle is used for when the player can't be reached anymore or disconnected.
this.leaveBattle(playerSchema);
return false;
}
+ if(!playerSchema.stats){
+ Logger.debug('Player not ready yet to be attacked.');
+ this.leaveBattle(playerSchema);
+ return false;
+ }
// @NOTE: in PVE we will have this additional method startBattleWith which is when the environment attacks the
// player.
if(!this.targetObject){
@@ -82,74 +108,86 @@ class Pve extends Battle
return false;
}
if(0 === (this.targetObject.actionsKeys?.length ?? 0)){
- Logger.error('Target Object does not have any actions assigned.');
+ Logger.warning('Target Object does not have any actions assigned.');
this.leaveBattle(playerSchema);
return false;
}
- // the enemy died:
if(0 >= this.targetObject.stats[room.config.get('client/actions/skills/affectedProperty')]){
- // battle ended checkpoint:
+ // Logger.debug('Target object affected property is zero.');
return false;
}
// if target (npc) is already in battle with another player then ignore the current attack:
- if(
- !this.chaseMultiple
- && 1 <= this.inBattleWithPlayer.length
- && -1 === this.inBattleWithPlayer.indexOf(playerSchema.player_id)
- ){
- this.leaveBattle(playerSchema);
+ let inBattleWithPlayersIds = Object.keys(this.inBattleWithPlayers);
+ let inBattleWithCurrentPlayer = this.inBattleWithPlayers[playerSchema.player_id];
+ if(!this.chaseMultiple && 1 <= inBattleWithPlayersIds.length && !inBattleWithCurrentPlayer){
+ Logger.debug('Object already in battle with player "'+playerSchema.player_id+'".');
return false;
}
- if(-1 === this.inBattleWithPlayer.indexOf(playerSchema.player_id)){
- this.inBattleWithPlayer.push(playerSchema.player_id);
- }
- let objActionIdx = Math.floor(Math.random() * this.targetObject.actionsKeys.length);
- let objectActionKey = this.targetObject.actionsKeys[objActionIdx];
- let objectAction = this.targetObject.actions[objectActionKey];
+ this.inBattleWithPlayers[playerSchema.player_id] = true;
+ let objectAction = this.pickRandomActionFromObject();
objectAction.room = room;
objectAction.currentBattle = this;
if(!objectAction.validate()){
- this.leaveBattle(playerSchema);
- return false;
+ // @NOTE: none logs here because it will create a (useless) log entry everytime an NPC tries to attack.
+ // expected when for example the action is out of range or has a skill delay, then we restart the battle:
+ return this.chasePlayer(playerSchema, room, objectAction);
}
let ownerPos = {x: this.targetObject.state.x, y: this.targetObject.state.y};
let targetPos = {x: playerSchema.state.x, y: playerSchema.state.y};
let inRange = objectAction.isInRange(ownerPos, targetPos);
if(inRange){
+ this.isBattleEndProcessing = false;
return await this.attackInRange(objectAction, playerSchema, room);
}
return this.chasePlayer(playerSchema, room, objectAction);
}
+ pickRandomActionFromObject()
+ {
+ let objActionIdx = Math.floor(Math.random() * this.targetObject.actionsKeys.length);
+ let objectActionKey = this.targetObject.actionsKeys[objActionIdx];
+ return this.targetObject.actions[objectActionKey];
+ }
+
chasePlayer(playerSchema, room, objectAction)
{
- // @TODO - BETA - Fix chase behavior when a bullet attack is available on enemies.
let chaseResult = this.targetObject.chaseBody(playerSchema.physicalBody);
- if(0 < chaseResult.length){
+ if(chaseResult && 0 < chaseResult.length){
return this.startBattleWithDelay(playerSchema, room, objectAction);
}
+ Logger.debug('Leave battle, chase result failed.');
return this.leaveBattle(playerSchema);
}
async attackInRange(objectAction, playerSchema, room)
{
- // reset the pathfinder in case the object was moving:
- this.targetObject.objectBody.resetAuto();
- this.targetObject.objectBody.velocity = [0, 0];
+ if(this.targetObject.objectBody){
+ // reset the pathfinder in case the object was moving:
+ this.targetObject.objectBody.resetAuto();
+ this.targetObject.objectBody.velocity = [0, 0];
+ }
// execute and apply the attack:
await objectAction.execute(playerSchema);
+ Logger.debug('Executed action "'+objectAction.key+'" on player "'+playerSchema.player_id+'".');
let targetClient = room.getClientById(playerSchema.sessionId);
if(!targetClient){
- return false;
+ Logger.debug('Leave battle, missing target client.');
+ return this.leaveBattle(playerSchema);
}
- let update = await this.updateTargetClient(targetClient, playerSchema, this.targetObject.key, room)
- .catch((err) => {
- Logger.error(err);
- return false;
+ let targetObjectId = this.targetObject?.id;
+ if(!targetObjectId){
+ Logger.debug('Leave battle, missing target object ID.');
+ return this.leaveBattle(playerSchema);
+ }
+ let update = await this.updateTargetClient(targetClient, playerSchema, targetObjectId, room)
+ .catch((error) => {
+ Logger.error('Leave battle, update target client catch error.', error);
+ return this.leaveBattle(playerSchema);
});
if(update){
return await this.startBattleWithDelay(playerSchema, room, objectAction);
}
+ Logger.debug('Leave battle, target client update failed.');
return this.leaveBattle(playerSchema);
}
@@ -157,6 +195,9 @@ class Pve extends Battle
{
if(0 < objectAction.skillDelay){
setTimeout(async () => {
+ if(!this.targetObject){
+ return false;
+ }
await this.startBattleWith(playerSchema, room);
}, objectAction.skillDelay);
return;
@@ -166,19 +207,47 @@ class Pve extends Battle
leaveBattle(playerSchema)
{
- this.removeInBattlePlayer(playerSchema);
+ Logger.debug('Leaving battle.', {player: playerSchema?.player_id, object: this.targetObject?.uid});
+ if(playerSchema?.player_id){
+ this.removeInBattlePlayer(playerSchema);
+ }
+ return this.moveObjectToOriginPoints();
+ }
+
+ moveObjectToOriginPoints()
+ {
if(!this.targetObject){
- Logger.critical('Target Object reference not found.');
+ // expected on client disconnection:
+ // Logger.debug('Target Object reference not found.');
return false;
}
+ if(GameConst.STATUS.ACTIVE !== this.targetObject.objectBody.bodyState.inState){
+ return false;
+ }
+ Logger.debug('Move back to origin.', {
+ uid: this.uid,
+ object: this.targetObject.uid,
+ state: this.targetObject.objectBody.bodyState.inState,
+ column: this.targetObject.objectBody.originalCol,
+ row: this.targetObject.objectBody.originalRow
+ });
this.targetObject.objectBody.moveToOriginalPoint();
+ return true;
}
async battleEnded(playerSchema, room)
{
+ if(this.isBattleEndProcessing){
+ Logger.debug('Battle end in progress.', this.uid);
+ return false;
+ }
+ this.isBattleEndProcessing = true;
// @TODO - BETA - Implement battle end in both PvE and PvP.
this.targetObject.objectBody.bodyState.inState = GameConst.STATUS.DEATH;
- Logger.debug('Battle ended, set target with ID "'+this.targetObject.uid+'" state to "DEATH".');
+ Logger.debug(
+ 'Battle end, player ID "'+playerSchema?.player_id+'", target ID "'+this.targetObject.uid+'".',
+ this.uid
+ );
this.removeInBattlePlayer(playerSchema);
let actionData = new BattleEndAction(
this.targetObject.objectBody.position[0],
@@ -191,8 +260,9 @@ class Pve extends Battle
await this.targetObject.respawn(room);
}
this.sendBattleEndedActionData(room, playerSchema, actionData);
- await this.events.emit(this.targetObject.getBattleEndEvent(), playerSchema, this, actionData);
- await this.events.emit('reldens.battleEnded', {playerSchema, pve: this, actionData, room});
+ let event = new BattleEndedEvent({playerSchema, pve: this, actionData, room});
+ await this.events.emit(this.targetObject.getBattleEndEvent(), event);
+ await this.events.emit('reldens.battleEnded', event);
}
sendBattleEndedActionData(room, playerSchema, actionData)
@@ -207,10 +277,13 @@ class Pve extends Battle
removeInBattlePlayer(playerSchema)
{
- let playerIndex = this.inBattleWithPlayer.indexOf(playerSchema.player_id);
- if(-1 !== playerIndex){
- this.inBattleWithPlayer.splice(playerIndex, 1);
+ if(!playerSchema?.player_id){
+ return false;
+ }
+ if(this.inBattleWithPlayers[playerSchema.player_id]){
+ delete this.inBattleWithPlayers[playerSchema.player_id];
}
+ return true;
}
}
diff --git a/lib/actions/server/pvp.js b/lib/actions/server/pvp.js
index 8d91b30d8..892907fe2 100644
--- a/lib/actions/server/pvp.js
+++ b/lib/actions/server/pvp.js
@@ -13,12 +13,13 @@ class Pvp extends Battle
async runBattle(playerSchema, target, room)
{
- if(GameConst.STATUS.ACTIVE !== playerSchema.inState){
- Logger.info('PvP inactive player.', playerSchema.inState);
+ // @TODO - BETA - Implement battle end for PvP.
+ if(GameConst.STATUS.ACTIVE !== playerSchema.state.inState){
+ Logger.info('PvP inactive player.', playerSchema.state.inState);
return false;
}
- if(GameConst.STATUS.ACTIVE !== target.inState){
- Logger.info('PvP inactive target.', target.inState);
+ if(GameConst.STATUS.ACTIVE !== target.state.inState){
+ Logger.info('PvP inactive target.', target.state.inState);
return false;
}
// @TODO - BETA - Make PvP available by configuration.
@@ -34,7 +35,7 @@ class Pvp extends Battle
}
let targetClient = room.getClientById(target.sessionId);
if(targetClient){
- await this.updateTargetClient(targetClient, target, playerSchema.sessionId, room);
+ await this.updateTargetClient(targetClient, target, playerSchema.sessionId, room, playerSchema);
}
return true;
}
diff --git a/lib/actions/server/skills/skills-extra-data.js b/lib/actions/server/skills-extra-data-mapper.js
similarity index 66%
rename from lib/actions/server/skills/skills-extra-data.js
rename to lib/actions/server/skills-extra-data-mapper.js
index f0452f13f..472fe0bde 100644
--- a/lib/actions/server/skills/skills-extra-data.js
+++ b/lib/actions/server/skills-extra-data-mapper.js
@@ -1,36 +1,43 @@
/**
*
- * Reldens - SkillsExtraData
+ * Reldens - SkillsExtraDataMapper
*
*/
-const { ActionsConst } = require('../../constants');
+const { ActionsConst } = require('../constants');
+const { TypeDeterminer } = require('../../game/type-determiner');
const { sc } = require('@reldens/utils');
-class SkillsExtraData
+class SkillsExtraDataMapper
{
- static extractSkillExtraData(params)
+ constructor()
{
+ this.typeDeterminer = new TypeDeterminer();
+ }
+
+ extractSkillExtraData(params)
+ {
+ // @TODO - BETA - Refactor conditions.
let extraData = {};
let target = sc.get(params, 'target');
if(target){
- if(sc.hasOwn(target, 'key')){
+ if(this.typeDeterminer.isObject(target)){
extraData[ActionsConst.DATA_TARGET_TYPE] = ActionsConst.DATA_TYPE_VALUE_ENEMY;
extraData[ActionsConst.DATA_TARGET_KEY] = target.key;
}
- if(sc.hasOwn(target, 'sessionId')){
+ if(this.typeDeterminer.isPlayer(target)){
extraData[ActionsConst.DATA_TARGET_TYPE] = ActionsConst.DATA_TYPE_VALUE_PLAYER;
extraData[ActionsConst.DATA_TARGET_KEY] = target.sessionId;
}
}
let skill = sc.get(params, 'skill');
if(skill){
- if(sc.hasOwn(skill.owner, 'key')){
+ if(this.typeDeterminer.isObject(skill.owner)){
extraData[ActionsConst.DATA_OWNER_TYPE] = ActionsConst.DATA_TYPE_VALUE_ENEMY;
extraData[ActionsConst.DATA_OWNER_KEY] = skill.owner.key;
}
- if(sc.hasOwn(skill.owner, 'sessionId')){
+ if(this.typeDeterminer.isPlayer(skill.owner)){
extraData[ActionsConst.DATA_OWNER_TYPE] = ActionsConst.DATA_TYPE_VALUE_PLAYER;
extraData[ActionsConst.DATA_OWNER_KEY] = skill.owner.sessionId;
}
@@ -41,6 +48,7 @@ class SkillsExtraData
}
return extraData;
}
+
}
-module.exports.SkillsExtraData = SkillsExtraData;
+module.exports.SkillsExtraDataMapper = SkillsExtraDataMapper;
diff --git a/lib/actions/server/skills/type-physical-attack.js b/lib/actions/server/skills/type-physical-attack.js
index 90c0a935a..8d47ed81f 100644
--- a/lib/actions/server/skills/type-physical-attack.js
+++ b/lib/actions/server/skills/type-physical-attack.js
@@ -73,7 +73,8 @@ class TypePhysicalAttack extends PhysicalAttack
targetClient,
validDefender,
this.owner.sessionId,
- this.room
+ this.room,
+ this.owner
);
}
@@ -128,8 +129,9 @@ class TypePhysicalAttack extends PhysicalAttack
removeBullet(body)
{
- body.world.removeBodies.push(body);
- // @TODO - BETA - Refactor and extract Colyseus into a driver. Check is been used?
+ if(body.world){
+ body.world.removeBodies.push(body);
+ }
this.room.state.removeBody(this.key+'_bullet_'+body.id);
}
diff --git a/lib/actions/server/skills/type-physical-effect.js b/lib/actions/server/skills/type-physical-effect.js
index 21516a70d..5e0493486 100644
--- a/lib/actions/server/skills/type-physical-effect.js
+++ b/lib/actions/server/skills/type-physical-effect.js
@@ -63,15 +63,21 @@ class TypePhysicalEffect extends PhysicalEffect
if(!targetClient){
return false;
}
- await this.currentBattle.updateTargetClient(targetClient, validDefender, this.owner.sessionId, this.room);
+ await this.currentBattle.updateTargetClient(
+ targetClient,
+ validDefender,
+ this.owner.sessionId,
+ this.room,
+ this.owner
+ );
}
async startPvE(validDefender)
{
- // re-run the process if pve:
- 0 < validDefender.stats[this.room.config.get('client/actions/skills/affectedProperty')]
- ? await this.restartBattle(validDefender)
- : await this.currentBattle.battleEnded(this.owner, this.room);
+ if(0 < validDefender.stats[this.room.config.get('client/actions/skills/affectedProperty')]){
+ return await this.restartBattle(validDefender);
+ }
+ return await this.currentBattle.battleEnded(this.owner, this.room);
}
async restartBattle(validDefender)
diff --git a/lib/admin/server/admin-manager-config.js b/lib/admin/server/admin-manager-config.js
index a323f827a..8788ad4e0 100644
--- a/lib/admin/server/admin-manager-config.js
+++ b/lib/admin/server/admin-manager-config.js
@@ -4,6 +4,7 @@
*
*/
+const { MapsImporter } = require('../../import/server/maps-importer');
const { PropertiesHandler } = require('../../game/properties-handler');
class AdminManagerConfig extends PropertiesHandler
@@ -23,6 +24,7 @@ class AdminManagerConfig extends PropertiesHandler
this.app = serverManager?.app;
this.applicationFramework = serverManager?.appServerFactory?.applicationFramework;
this.fileStorageManager = serverManager?.appServerFactory?.fileStorageManager;
+ this.mapsImporter = new MapsImporter(serverManager);
this.bodyParser = serverManager?.appServerFactory?.bodyParser;
this.session = serverManager?.appServerFactory?.session;
this.broadcastCallback = (props) => {
diff --git a/lib/admin/server/admin-manager.js b/lib/admin/server/admin-manager.js
index 48f3da529..f57f5f569 100644
--- a/lib/admin/server/admin-manager.js
+++ b/lib/admin/server/admin-manager.js
@@ -10,8 +10,17 @@ const { AdminEntitiesGenerator } = require('./admin-entities-generator');
const { AdminManagerConfig } = require('./admin-manager-config');
const { AdminDistHelper } = require('./admin-dist-helper');
const { FileHandler } = require('../../game/server/file-handler');
+const { AllowedFileTypes } = require('../../game/allowed-file-types');
+const { PageRangeProvider } = require('../../game/page-range-provider');
const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
+const {
+ RandomMapGenerator,
+ LayerElementsObjectLoader,
+ LayerElementsCompositeLoader,
+ MultipleByLoaderGenerator,
+ MultipleWithAssociationsByLoaderGenerator
+} = require('@reldens/tile-map-generator');
class AdminManager
{
@@ -23,6 +32,7 @@ class AdminManager
app = null;
applicationFramework = null;
fileStorageManager = null;
+ mapsImporter = null;
bodyParser = null;
session = null;
broadcastCallback = null;
@@ -33,7 +43,7 @@ class AdminManager
secret = '';
useSecureLogin = false;
rootPath = '';
- adminRoleId = null;
+ adminRoleId = 0;
buildAdminScriptsOnActivation = null;
buildAdminCssOnActivation = null;
buckets = null;
@@ -60,8 +70,15 @@ class AdminManager
this.savePath = '/save';
this.deletePath = '/delete';
this.managementPath = '/management';
+ this.mapsWizardPath = '/maps-wizard';
this.adminEntitiesGenerator = new AdminEntitiesGenerator();
this.uploaderFactory = new UploaderFactory();
+ this.mapsWizardHandlers = {
+ 'elements-object-loader': LayerElementsObjectLoader,
+ 'elements-composite-loader': LayerElementsCompositeLoader,
+ 'multiple-by-loader': MultipleByLoaderGenerator,
+ 'multiple-with-association-by-loader': MultipleWithAssociationsByLoaderGenerator
+ };
}
async setupAdmin()
@@ -139,6 +156,7 @@ class AdminManager
this.adminContents.login = await this.buildLogin();
this.adminContents.dashboard = await this.buildDashboard();
this.adminContents.management = await this.buildManagement();
+ this.adminContents.mapsWizard = await this.buildMapsWizard();
this.adminContents.entities = await this.buildEntitiesContents();
}
@@ -160,7 +178,14 @@ class AdminManager
async buildSideBar()
{
- let navigationContents = {};
+ let navigationContents = {
+ 'Wizards': {
+ [this.translations.labels['mapsWizard']]: await this.render(
+ this.adminFilesContents.sideBarItem,
+ {name: this.translations.labels['mapsWizard'], path: this.rootPath+this.mapsWizardPath}
+ )
+ }
+ };
for(let driverResource of this.resources){
let navigation = driverResource.options?.navigation;
let name = this.translations.labels[driverResource.id()] || this.translations.labels[driverResource.entityKey];
@@ -180,7 +205,7 @@ class AdminManager
{name, path}
);
}
- navigationContents['Server'] = {'Maintenance': await this.render(
+ navigationContents['Server'] = {'Management': await this.render(
this.adminFilesContents.sideBarItem,
{name: this.translations.labels['management'], path: this.rootPath+this.managementPath}
)};
@@ -211,32 +236,12 @@ class AdminManager
async buildLogin()
{
- return await this.render(
- this.adminContents.layout,
- {
- sideBar: '',
- pageContent: this.adminFilesContents.login,
- stylesFilePath: this.stylesFilePath,
- scriptsFilePath: this.scriptsFilePath,
- brandingCompanyName: this.branding.companyName,
- copyRight: this.branding.copyRight
- }
- );
+ return await this.renderRoute(this.adminFilesContents.login, '');
}
async buildDashboard()
{
- return await this.render(
- this.adminContents.layout,
- {
- sideBar: this.adminContents.sideBar,
- pageContent: this.adminFilesContents.dashboard,
- stylesFilePath: this.stylesFilePath,
- scriptsFilePath: this.scriptsFilePath,
- brandingCompanyName: this.branding.companyName,
- copyRight: this.branding.copyRight
- }
- );
+ return await this.renderRoute(this.adminFilesContents.dashboard, this.adminContents.sideBar);
}
async buildManagement()
@@ -252,17 +257,18 @@ class AdminManager
submitType: '{{&submitType}}'
}
);
- return await this.render(
- this.adminContents.layout,
+ return await this.renderRoute(pageContent, this.adminContents.sideBar);
+ }
+
+ async buildMapsWizard()
+ {
+ let pageContent = await this.render(
+ this.adminFilesContents.mapsWizard,
{
- sideBar: this.adminContents.sideBar,
- pageContent,
- stylesFilePath: this.stylesFilePath,
- scriptsFilePath: this.scriptsFilePath,
- brandingCompanyName: this.branding.companyName,
- copyRight: this.branding.copyRight
+ actionPath: this.rootPath+this.mapsWizardPath
}
);
+ return await this.renderRoute(pageContent, this.adminContents.sideBar);
}
async buildEntitiesContents()
@@ -485,11 +491,7 @@ class AdminManager
{
// apply session middleware only to /admin routes:
this.adminRouter = this.applicationFramework.Router();
- this.adminRouter.use(this.session({
- secret: this.secret,
- resave: false,
- saveUninitialized: true
- }));
+ this.adminRouter.use(this.session({secret: this.secret, resave: false, saveUninitialized: true}));
this.adminRouter.use(this.bodyParser.json());
// route for the login page:
this.adminRouter.get(this.loginPath, async (req, res) => {
@@ -503,86 +505,288 @@ class AdminManager
req.session.user = loginResult;
return res.redirect(this.rootPath);
}
- return res.redirect(this.rootPath+this.loginPath);
+ return res.redirect(this.rootPath+this.loginPath+'?login-error=true');
});
// route for the admin panel dashboard:
this.adminRouter.get('/', this.isAuthenticated.bind(this), async (req, res) => {
return res.send(this.adminContents.dashboard);
});
- // route for logging out:
+ // route for logout:
this.adminRouter.get(this.logoutPath, (req, res) => {
req.session.destroy();
res.redirect(this.rootPath+this.loginPath);
});
// management routes:
- this.adminRouter.get(
- this.managementPath,
- this.isAuthenticated.bind(this),
- async (req, res) => {
- let management = this.adminContents.management;
- let rendererContent = await this.render(management, this.fetchShuttingDownData());
- return res.send(rendererContent);
+ this.adminRouter.get(this.managementPath, this.isAuthenticated.bind(this), async (req, res) => {
+ let management = this.adminContents.management;
+ let rendererContent = await this.render(management, this.fetchShuttingDownData());
+ return res.send(rendererContent);
+ });
+ this.adminRouter.post(this.managementPath, this.isAuthenticated.bind(this), async (req, res) => {
+ this.shutdownTime = req.body['shutdown-time'];
+ let redirectManagementPath = this.rootPath+this.managementPath;
+ if(!this.shutdownTime){
+ return res.redirect(redirectManagementPath+'?result=shutdownError');
+ }
+ if(0 < this.shuttingDownIn){
+ clearInterval(this.shutdownInterval);
+ clearTimeout(this.shutdownTimeout);
+ this.shuttingDownIn = 0;
+ return res.redirect(redirectManagementPath+'?result=success');
}
+ await this.broadcastShutdownMessage();
+ this.shutdownTimeout = setTimeout(
+ async () => {
+ Logger.info('Server is shutting down by request on the administration panel.', sc.getTime());
+ if(this.broadcastCallback && sc.isFunction(this.broadcastCallback)){
+ await this.broadcastCallback({message: 'Server Offline.'});
+ }
+ throw new Error('Server shutdown by request on the administration panel.');
+ },
+ this.shutdownTime * 1000
+ );
+ this.shutdownInterval = setInterval(
+ async () => {
+ this.shuttingDownIn--;
+ Logger.info('Server is shutting down in '+this.shuttingDownIn+' seconds.');
+ if(
+ 0 < this.shuttingDownIn
+ && (this.shuttingDownIn <= 5 || Math.ceil(this.shutdownTime / 2) === this.shuttingDownIn)
+ ){
+ await this.broadcastShutdownMessage();
+ }
+ if(0 === this.shuttingDownIn){
+ Logger.info('Server OFF at: '+ sc.getTime());
+ clearInterval(this.shutdownInterval);
+ }
+ },
+ 1000
+ );
+ this.shuttingDownIn = this.shutdownTime;
+ return res.redirect(redirectManagementPath+'?result=success');
+ });
+ this.setupMapsWizardRoutes();
+ // apply the adminRouter to the /admin path:
+ this.app.use(this.rootPath, this.adminRouter);
+ }
+
+ setupMapsWizardRoutes()
+ {
+ // set generated paths to be available in the admin
+ this.adminRouter.use(
+ '/generate-data',
+ this.isAuthenticated.bind(this),
+ this.applicationFramework.static(this.themeManager.projectGenerateDataPath)
+ );
+ this.adminRouter.use(
+ '/generated',
+ this.isAuthenticated.bind(this),
+ this.applicationFramework.static(this.themeManager.projectGeneratedDataPath)
+ );
+ Logger.info(
+ 'Included administration panel static routes.',
+ this.themeManager.projectGenerateDataPath,
+ this.themeManager.projectGeneratedDataPath
);
+ // step-1, initial wizard options:
+ this.adminRouter.get(this.mapsWizardPath, this.isAuthenticated.bind(this), async (req, res) => {
+ let rendererContent = await this.render(this.adminContents.mapsWizard, this.fetchShuttingDownData());
+ return res.send(rendererContent);
+ });
+ let fields = [
+ {name: 'generatorImages'},
+ {name: 'generatorJsonFiles'}
+ ];
+ let buckets = {
+ generatorImages: this.themeManager.projectGenerateDataPath,
+ generatorJsonFiles: this.themeManager.projectGenerateDataPath
+ };
+ let allowedFileTypes = {
+ generatorImages: AllowedFileTypes.IMAGE,
+ generatorJsonFiles: AllowedFileTypes.TEXT
+ };
this.adminRouter.post(
- this.managementPath,
+ this.mapsWizardPath,
this.isAuthenticated.bind(this),
+ this.uploaderFactory.createUploader(fields, buckets, allowedFileTypes),
async (req, res) => {
- this.shutdownTime = req.body['shutdown-time'];
- let redirectManagementPath = this.rootPath+this.managementPath;
- if(!this.shutdownTime){
- return res.redirect(redirectManagementPath+'?result=shutdownError');
+ // step-2, upload and maps generation:
+ if('generate' === req?.body?.mainAction){
+ return await this.generateMaps(req, res);
}
- if(0 < this.shuttingDownIn){
- clearInterval(this.shutdownInterval);
- clearTimeout(this.shutdownTimeout);
- this.shuttingDownIn = 0;
- return res.redirect(redirectManagementPath+'?result=success');
+ // step-3, maps selection and import:
+ if('import' === req?.body?.mainAction){
+ return res.redirect(await this.importSelectedMaps(req));
}
- await this.broadcastShutdownMessage();
- this.shutdownTimeout = setTimeout(
- async () => {
- Logger.info('Server is shutting down by request on the administration panel.', sc.getTime());
- if(this.broadcastCallback && sc.isFunction(this.broadcastCallback)){
- await this.broadcastCallback({message: 'Server Offline.'});
- }
- throw new Error('Server shutdown by request on the administration panel.');
- },
- this.shutdownTime * 1000
- );
- this.shutdownInterval = setInterval(
- async () => {
- this.shuttingDownIn--;
- Logger.info('Server is shutting down in '+this.shuttingDownIn+' seconds.');
- if(
- 0 < this.shuttingDownIn
- && (this.shuttingDownIn <= 5 || Math.ceil(this.shutdownTime / 2) === this.shuttingDownIn)
- ){
- await this.broadcastShutdownMessage();
- }
- if(0 === this.shuttingDownIn){
- Logger.info('Server OFF at: '+ sc.getTime());
- clearInterval(this.shutdownInterval);
- }
- },
- 1000
- );
- this.shuttingDownIn = this.shutdownTime;
- return res.redirect(redirectManagementPath+'?result=success');
}
);
- // apply the adminRouter to the /admin path:
- this.app.use(this.rootPath, this.adminRouter);
}
- async broadcastShutdownMessage()
+ async generateMaps(req, res)
{
- if(this.broadcastCallback && sc.isFunction(this.broadcastCallback)){
- let shuttingDownTime = 0 === this.shuttingDownIn ? this.shutdownTime : this.shuttingDownIn;
- await this.broadcastCallback({
- message: 'Server is shutting down in ' + shuttingDownTime + ' seconds.'
+ let selectedHandler = req?.body?.mapsWizardAction;
+ if(!selectedHandler){
+ return this.mapsWizardRedirect(res, 'mapsWizardMissingActionError');
+ }
+ let generatorData = req?.body?.generatorData;
+ if(!generatorData){
+ return this.mapsWizardRedirect(res, 'mapsWizardMissingDataError');
+ }
+ let mapData = sc.toJson(generatorData);
+ if(!mapData){
+ return this.mapsWizardRedirect(res, 'mapsWizardWrongJsonDataError');
+ }
+ let handler = this.mapsWizardHandlers[selectedHandler];
+ if(!handler){
+ return this.mapsWizardRedirect(res, 'mapsWizardMissingHandlerError');
+ }
+ let generatorWithData = false;
+ let generatedMap = false;
+ try {
+ let handlerParams = {mapData, rootFolder: this.themeManager.projectGenerateDataPath};
+ if('elements-object-loader' === selectedHandler){
+ let loader = new handler(handlerParams);
+ await loader.load();
+ let generator = new RandomMapGenerator(loader.mapData);
+ generatedMap = await generator.generate();
+ generatorWithData = generator;
+ }
+ if('elements-composite-loader' === selectedHandler){
+ let loader = new handler(handlerParams);
+ await loader.load();
+ let generator = new RandomMapGenerator();
+ await generator.fromElementsProvider(loader.mapData);
+ generatedMap = await generator.generate();
+ generatorWithData = generator;
+ }
+ if('multiple-by-loader' === selectedHandler){
+ let generator = new MultipleByLoaderGenerator({loaderData: handlerParams});
+ await generator.generate();
+ generatorWithData = generator;
+ }
+ if('multiple-with-association-by-loader' === selectedHandler){
+ let generator = new MultipleWithAssociationsByLoaderGenerator({loaderData: handlerParams});
+ await generator.generate();
+ generatorWithData = generator;
+ }
+ } catch (error) {
+ Logger.error('Maps generator error.', selectedHandler, generatorData, error);
+ return this.mapsWizardRedirect(res, 'mapsWizardGeneratorError');
+ }
+ if(!generatorWithData){
+ Logger.error('Maps not generated, incompatible selected handler.', selectedHandler, generatorData);
+ return this.mapsWizardRedirect(res, 'mapsWizardSelectedHandlerError');
+ }
+ let mapsData = {
+ maps: [],
+ actionPath: this.rootPath+this.mapsWizardPath,
+ generatedMapsHandler: selectedHandler,
+ importAssociationsForChangePoints: Number(mapData.importAssociationsForChangePoints || 0),
+ importAssociationsRecursively: Number(mapData.importAssociationsRecursively || 0),
+ verifyTilesetImage: Number(mapData.verifyTilesetImage || 1),
+ automaticallyExtrudeMaps: Number(mapData.automaticallyExtrudeMaps || 1)
+ };
+ if(generatedMap){
+ let tileWidth = generatedMap.tilewidth;
+ let tileHeight = generatedMap.tileheight;
+ let mapFileName = generatorWithData.mapFileName;
+ if(-1 === mapFileName.indexOf('json')){
+ mapFileName = mapFileName+'.json';
+ }
+ mapsData.maps.push({
+ key: generatorWithData.mapName,
+ mapWidth: generatedMap.width * tileWidth,
+ mapHeight: generatedMap.height * tileHeight,
+ tileWidth,
+ tileHeight,
+ mapImage: this.rootPath+'/generated/'+generatorWithData.tileSheetName,
+ mapJson: this.rootPath+'/generated/'+mapFileName
});
}
+ if(generatorWithData.generators && generatorWithData.generatedMaps){
+ for(let i of Object.keys(generatorWithData.generators)){
+ let generator = generatorWithData.generators[i];
+ let generatedMap = generatorWithData.generatedMaps[generator.mapName];
+ let tileWidth = generatedMap.tilewidth;
+ let tileHeight = generatedMap.tileheight;
+ let mapFileName = generator.mapFileName;
+ if(-1 === mapFileName.indexOf('json')){
+ mapFileName = mapFileName+'.json';
+ }
+ mapsData.maps.push({
+ key: generator.mapName,
+ mapWidth: generatedMap.width * tileWidth,
+ mapHeight: generatedMap.height * tileHeight,
+ tileWidth,
+ tileHeight,
+ mapImage: this.rootPath+'/generated/'+generator.tileSheetName,
+ mapJson: this.rootPath+'/generated/'+mapFileName
+ });
+ }
+ }
+ if(0 === mapsData.maps.length){
+ return this.mapsWizardRedirect(res, 'mapsWizardMapsNotGeneratedError');
+ }
+ return this.mapsWizardMapsSelection(res, mapsData);
+ }
+
+ mapsWizardRedirect(res, result)
+ {
+ return res.redirect(this.rootPath + this.mapsWizardPath + '?result='+result);
+ }
+
+ async mapsWizardMapsSelection(res, data)
+ {
+ let renderedView = await this.render(this.adminFilesContents.mapsWizardMapsSelection, data);
+ return res.send(await this.renderRoute(renderedView, this.adminContents.sideBar));
+ }
+
+ async importSelectedMaps(req)
+ {
+ let generatedMapData = this.mapGeneratedMapsDataForImport(req.body);
+ if(!generatedMapData){
+ return this.rootPath+this.mapsWizardPath+'?result=mapsWizardImportDataError';
+ }
+ let importResult = await this.mapsImporter.import(generatedMapData);
+ if(!importResult){
+ let errorCode = this.mapsImporter.errorCode || 'mapsWizardImportError'
+ return this.rootPath+this.mapsWizardPath+'?result='+errorCode;
+ }
+ return this.rootPath+this.mapsWizardPath+'?result=success';
+ }
+
+ mapGeneratedMapsDataForImport(data)
+ {
+ if(!data.selectedMaps){
+ return false;
+ }
+ let importAssociations = 'multiple-with-association-by-loader' === data.generatedMapsHandler;
+ let mappedData = {
+ importAssociationsForChangePoints: importAssociations,
+ importAssociationsRecursively: importAssociations,
+ automaticallyExtrudeMaps: data.automaticallyExtrudeMaps,
+ verifyTilesetImage: data.verifyTilesetImage,
+ relativeGeneratedDataPath: 'generate-data/generated',
+ maps: {}
+ };
+ for(let mapKey of data.selectedMaps){
+ mappedData.maps[data['map-title-'+mapKey]] = mapKey; // for example: {'Town 1': 'town-001'}
+ }
+ return mappedData;
+ }
+
+ async broadcastShutdownMessage()
+ {
+ let shuttingDownTime = 0 === this.shuttingDownIn ? this.shutdownTime : this.shuttingDownIn;
+ await this.broadcastSystemMessage('Server is shutting down in ' + shuttingDownTime + ' seconds.');
+ }
+
+ async broadcastSystemMessage(message)
+ {
+ if(!this.broadcastCallback || !sc.isFunction(this.broadcastCallback)){
+ return;
+ }
+ await this.broadcastCallback({message});
}
fetchShuttingDownData()
@@ -611,53 +815,33 @@ class AdminManager
for(let driverResource of this.resources){
let entityPath = driverResource.entityPath;
let entityRoute = '/'+entityPath;
- this.adminRouter.get(
- entityRoute,
- this.isAuthenticated.bind(this),
- async (req, res) => {
- let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
- return res.send(routeContents);
- }
- );
- this.adminRouter.post(
- entityRoute,
- this.isAuthenticated.bind(this),
- async (req, res) => {
- let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
- return res.send(routeContents);
- }
- );
- this.adminRouter.get(
- entityRoute+this.viewPath,
- this.isAuthenticated.bind(this),
- async (req, res) => {
- let routeContents = await this.generateViewRouteContent(req, driverResource, entityPath);
- if('' === routeContents){
- return res.redirect(this.rootPath+'/'+entityPath+'?result=errorView');
- }
- return res.send(routeContents);
+ this.adminRouter.get(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
+ let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
+ return res.send(routeContents);
+ });
+ this.adminRouter.post(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
+ let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
+ return res.send(routeContents);
+ });
+ this.adminRouter.get(entityRoute+this.viewPath, this.isAuthenticated.bind(this), async (req, res) => {
+ let routeContents = await this.generateViewRouteContent(req, driverResource, entityPath);
+ if('' === routeContents){
+ return res.redirect(this.rootPath+'/'+entityPath+'?result=errorView');
}
- );
- this.adminRouter.get(
- entityRoute+this.editPath,
- this.isAuthenticated.bind(this),
- async (req, res) => {
- let routeContents = await this.generateEditRouteContent(req, driverResource, entityPath);
- if('' === routeContents){
- return res.redirect(this.rootPath+'/'+entityPath+'?result=errorEdit');
- }
- return res.send(routeContents);
+ return res.send(routeContents);
+ });
+ this.adminRouter.get(entityRoute+this.editPath, this.isAuthenticated.bind(this), async (req, res) => {
+ let routeContents = await this.generateEditRouteContent(req, driverResource, entityPath);
+ if('' === routeContents){
+ return res.redirect(this.rootPath+'/'+entityPath+'?result=errorEdit');
}
- );
+ return res.send(routeContents);
+ });
this.setupSavePath(entityRoute, driverResource, entityPath);
- this.adminRouter.post(
- entityRoute+this.deletePath,
- this.isAuthenticated.bind(this),
- async (req, res) => {
- let redirectResult = await this.processDeleteEntities(req, res, driverResource, entityPath);
- return res.redirect(redirectResult);
- }
- );
+ this.adminRouter.post(entityRoute+this.deletePath, this.isAuthenticated.bind(this), async (req, res) => {
+ let redirectResult = await this.processDeleteEntities(req, res, driverResource, entityPath);
+ return res.redirect(redirectResult);
+ });
}
}
@@ -770,7 +954,7 @@ class AdminManager
if(this.autoSyncDist){
let uploadProperties = this.fetchUploadProperties(driverResource);
if(0 < Object.keys(uploadProperties).length){
- for (let uploadPropertyKey of Object.keys(uploadProperties)){
+ for(let uploadPropertyKey of Object.keys(uploadProperties)){
let property = uploadProperties[uploadPropertyKey];
await AdminDistHelper.copyBucketFilesToDist(
property.bucket,
@@ -971,19 +1155,17 @@ class AdminManager
let entitiesRows = await this.loadEntitiesForList(driverResource, pageSize, page, req, filters);
let listRawContent = this.adminContents.entities[entityPath].list.toString();
let totalPages = Math.ceil(await this.countTotalEntities(driverResource) / pageSize);
- let pages = this.getPageRange(page, totalPages);
+ let pages = PageRangeProvider.fetch(page, totalPages);
let renderedPagination = '';
- if(0 < pages.length){
- for (let page of pages){
- renderedPagination += await this.render(
- this.adminFilesContents.fields.view['link'],
- {
- fieldName: page.label,
- fieldValue: this.rootPath+'/'+driverResource.entityPath+'?page='+ page.value,
- fieldOriginalValue: page.value,
- }
- );
- }
+ for(let page of pages){
+ renderedPagination += await this.render(
+ this.adminFilesContents.fields.view['link'],
+ {
+ fieldName: page.label,
+ fieldValue: this.rootPath+'/'+driverResource.entityPath+'?page='+ page.value,
+ fieldOriginalValue: page.value,
+ }
+ );
}
let listVars = {
deletePath: this.rootPath + '/' + driverResource.entityPath + this.deletePath,
@@ -1001,37 +1183,7 @@ class AdminManager
listRawContent,
Object.assign({list, pagination: renderedPagination}, ...mappedFiltersValues)
);
- return await this.renderRoute(
- entitiesListView,
- this.adminContents.sideBar
- );
- }
-
- getPageRange(page, totalPages)
- {
- let totalDisplayedPages = 5;
- let half = Math.floor(totalDisplayedPages / 2);
- let start = page - half;
- let end = page + half;
- start = Math.max(1, start);
- end = Math.min(totalPages, end);
- if(end - start + 1 < totalDisplayedPages){
- if(start === 1){
- end = Math.min(totalPages, start + totalDisplayedPages - 1);
- }
- start = Math.max(1, end - totalDisplayedPages + 1);
- }
- let range = [];
- if(1 < start){
- range.push({label: 'first', value: 1});
- }
- for(let i = start; i <= end; i++){
- range.push({label: i, value: i});
- }
- if(end < totalPages){
- range.push({label: 'last', value: totalPages - 1});
- }
- return range;
+ return await this.renderRoute(entitiesListView, this.adminContents.sideBar);
}
async countTotalEntities(driverResource)
@@ -1174,7 +1326,7 @@ class AdminManager
{fieldName: propertyKey, fieldValue}
);
}
-
+
async fetchRelationOptions(relationDriverResource)
{
let relationEntityRepository = this.dataServer.getEntity(relationDriverResource.entityKey);
@@ -1223,15 +1375,15 @@ class AdminManager
return allowContinue.callback(event);
}
let user = req.session?.user;
- if(user){
- let userBlackList = this.blackList[user.role_id] || [];
- if(-1 !== userBlackList.indexOf(req.path)){
- let referrer = String(req.headers?.referer || '');
- return res.redirect('' !== referrer ? referrer : returnPath);
- }
- return next();
+ if(!user){
+ return res.redirect(returnPath);
+ }
+ let userBlackList = this.blackList[user.role_id] || [];
+ if(-1 !== userBlackList.indexOf(req.path)){
+ let referrer = String(req.headers?.referer || '');
+ return res.redirect('' !== referrer ? referrer : returnPath);
}
- res.redirect(returnPath);
+ return next();
}
prepareFilters(filtersList, driverResource)
@@ -1243,7 +1395,7 @@ class AdminManager
let filters = {};
for(let i of filtersKeys){
let filter = filtersList[i];
- if ('' === filter){
+ if('' === filter){
continue;
}
let rawConfigFilterProperties = driverResource.options.properties[i];
diff --git a/lib/admin/server/admin-translations.js b/lib/admin/server/admin-translations.js
index 134a15128..3274befa6 100644
--- a/lib/admin/server/admin-translations.js
+++ b/lib/admin/server/admin-translations.js
@@ -29,6 +29,7 @@ class AdminTranslations
loginWelcome: 'Reldens',
pages: 'Server Management',
management: 'Management',
+ mapsWizard: 'Maps Generation and Import',
shuttingDown: 'Server is shutting down in:',
submitShutdownLabel: 'Shutdown Server',
submitCancelLabel: 'Cancel Server Shutdown',
diff --git a/lib/admin/server/templates-list.js b/lib/admin/server/templates-list.js
index 175924190..95bf29af6 100644
--- a/lib/admin/server/templates-list.js
+++ b/lib/admin/server/templates-list.js
@@ -8,6 +8,8 @@ module.exports.TemplatesList = {
login: 'login.html',
dashboard: 'dashboard.html',
management: 'management.html',
+ mapsWizard: 'maps-wizard.html',
+ mapsWizardMapsSelection: 'maps-wizard-maps-selection.html',
list: 'list.html',
listContent: 'list-content.html',
view: 'view.html',
diff --git a/lib/ads/client/plugin.js b/lib/ads/client/plugin.js
index 266f3c41e..a30e6dfc6 100644
--- a/lib/ads/client/plugin.js
+++ b/lib/ads/client/plugin.js
@@ -52,13 +52,13 @@ class AdsPlugin extends PluginInterface
let providers = sc.get(this.config, 'providers', {});
let providersKeys = Object.keys(providers);
if(0 === providersKeys.length){
- Logger.debug('None ads providers configured.', this.config);
+ //Logger.debug('None ads providers configured.', this.config);
return false;
}
for(let i of providersKeys){
let provider = providers[i];
if(!provider.enabled){
- Logger.debug({'Provider disabled': providers});
+ //Logger.debug({'Provider disabled': providers});
continue;
}
provider.classDefinition = sc.get(ProvidersList, i, false);
diff --git a/lib/ads/client/providers/crazy-games.js b/lib/ads/client/providers/crazy-games.js
index a4c2164ba..4ed8ef94b 100644
--- a/lib/ads/client/providers/crazy-games.js
+++ b/lib/ads/client/providers/crazy-games.js
@@ -100,8 +100,8 @@ class CrazyGames
Logger.critical('Adblock detected, please disable.');
}
return result;
- } catch (e) {
- Logger.info('SDK detected error.', e);
+ } catch (error) {
+ Logger.info('SDK detected error.', error);
}
return false;
}
diff --git a/lib/ads/client/providers/crazy-games/banners-handler.js b/lib/ads/client/providers/crazy-games/banners-handler.js
index 742619454..9a95df49d 100644
--- a/lib/ads/client/providers/crazy-games/banners-handler.js
+++ b/lib/ads/client/providers/crazy-games/banners-handler.js
@@ -155,8 +155,8 @@ class BannersHandler
this.gameDom.setElementStyles(div, styles);
div.classList.add('ads-banner-container');
return div;
- } catch (err) {
- Logger.critical('CrazyGames - Error on banner request.', err);
+ } catch (error) {
+ Logger.critical('CrazyGames - Error on banner request.', error);
return false;
}
}
@@ -222,8 +222,8 @@ class BannersHandler
}
div.classList.add('ads-banner-container');
return div;
- } catch (err) {
- Logger.critical('CrazyGames - Error on banner request.', err);
+ } catch (error) {
+ Logger.critical('CrazyGames - Error on banner request.', error);
return false;
}
}
diff --git a/lib/ads/client/sdk-handler.js b/lib/ads/client/sdk-handler.js
index 3f701b089..9d69bffb9 100644
--- a/lib/ads/client/sdk-handler.js
+++ b/lib/ads/client/sdk-handler.js
@@ -21,12 +21,12 @@ class SdkHandler
return false;
}
if(!sc.isObject(providers)){
- Logger.debug('Providers not available.');
+ //Logger.debug('Providers not available.');
return false;
}
let keys = Object.keys(providers);
if(0 === keys.length){
- Logger.debug('Providers not found.');
+ //Logger.debug('Providers not found.');
return false;
}
for(let i of keys){
@@ -41,7 +41,7 @@ class SdkHandler
{
let sdkUrl = sc.get(provider, 'sdkUrl', '');
if('' === sdkUrl){
- Logger.debug('Provider does not have an SDK URL.', provider);
+ //Logger.debug('Provider does not have an SDK URL.', provider);
return false;
}
let body = this.gameDom.getElement('body');
diff --git a/lib/ads/client/snippets/en_US.js b/lib/ads/client/snippets/en_US.js
index 411f32382..bb108c777 100644
--- a/lib/ads/client/snippets/en_US.js
+++ b/lib/ads/client/snippets/en_US.js
@@ -5,12 +5,5 @@
*/
module.exports = {
- actions: {
- selectClassPath: 'Select Your Class-Path',
- currentLevel: 'Level %currentLevel',
- experience: '%experience',
- experienceLabel: 'XP',
- classPathLabel: '%classPathLabel',
- nextLevelExperience: '%nextLevelExperience'
- }
+ ads: {}
}
diff --git a/lib/ads/server/event-handlers/create-player-ads-handler.js b/lib/ads/server/event-handlers/create-player-ads-handler.js
index 47a61bc7b..53103482c 100644
--- a/lib/ads/server/event-handlers/create-player-ads-handler.js
+++ b/lib/ads/server/event-handlers/create-player-ads-handler.js
@@ -5,23 +5,38 @@
*/
const { AdsConst } = require('../../constants');
+const { Logger } = require('@reldens/utils');
class CreatePlayerAdsHandler
{
- static async enrichPlayedWithPlayedAds(client, userModel, playerSchema, roomScene, adsPlugin)
+ constructor(adsPlugin)
{
- let playedAdsModels = await adsPlugin.dataServer.getEntity('adsPlayed').loadByWithRelations(
- 'player_id',
- playerSchema.player_id,
- ['parent_player']
- );
- if(!playedAdsModels || 0 === playedAdsModels.length){
- await client.send('*', {act: AdsConst.ACTIONS.ADS_PLAYED, playedAdsModels: []});
+ this.adsPlayedRepository = adsPlugin.dataServer.getEntity('adsPlayed');
+ this.adsByPlayerId = {};
+ }
+
+ async enrichPlayedWithPlayedAds(playerSchema, client)
+ {
+ if(!this.adsPlayedRepository){
+ Logger.error('Missing adsPlayedRepository in "CreatePlayerAdsHandler".');
+ return false;
+ }
+ if(!this.adsByPlayerId[playerSchema.player_id]){
+ this.adsByPlayerId[playerSchema.player_id] = await this.adsPlayedRepository.loadByWithRelations(
+ 'player_id',
+ playerSchema.player_id,
+ ['parent_player']
+ );
+ }
+ if(!this.adsByPlayerId[playerSchema.player_id] || 0 === this.adsByPlayerId[playerSchema.player_id].length){
return false;
}
- playerSchema.setCustom('playedAds', playedAdsModels);
- await client.send('*', {act: AdsConst.ACTIONS.ADS_PLAYED, playedAdsModels});
+ playerSchema.setCustom('playedAds', this.adsByPlayerId[playerSchema.player_id]);
+ await client.send('*', {
+ act: AdsConst.ACTIONS.ADS_PLAYED,
+ playedAdsModels: this.adsByPlayerId[playerSchema.player_id]
+ });
}
}
diff --git a/lib/ads/server/message-actions.js b/lib/ads/server/message-actions.js
index a94f9f4bc..f6aa85739 100644
--- a/lib/ads/server/message-actions.js
+++ b/lib/ads/server/message-actions.js
@@ -4,7 +4,9 @@
*
*/
+const { GiveRewardAction } = require('../../rewards/server/actions/give-reward-action');
const { AdsConst } = require('../constants');
+const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
class AdsMessageActions
@@ -20,6 +22,7 @@ class AdsMessageActions
if(!this.dataServer){
Logger.error('AdsPlugin undefined in AdsMessageActions.');
}
+ this.giveRewardAction = new GiveRewardAction();
this.setRepository();
}
@@ -51,11 +54,16 @@ class AdsMessageActions
if(null !== endedAt){
newAdData['ended_at'] = endedAt;
}
- let playedAdModel = await this.loadPlayedAd(playerId, adId);
- if(!playedAdModel){
- return this.adsPlayedRepository.create(newAdData);
+ try {
+ let playedAdModel = await this.loadPlayedAd(playerId, adId);
+ if(!playedAdModel){
+ return this.adsPlayedRepository.create(newAdData);
+ }
+ return this.adsPlayedRepository.updateById(playedAdModel.id, newAdData);
+ } catch (error) {
+ Logger.error(error.message);
+ return false;
}
- return this.adsPlayedRepository.updateById(playedAdModel.id, newAdData);
}
async executeMessageActions(client, data, room, playerSchema)
@@ -73,7 +81,7 @@ class AdsMessageActions
if(data.act !== AdsConst.ACTIONS.AD_STARTED){
return false;
}
- room.deactivatePlayer(playerSchema, room);
+ room.deactivatePlayer(playerSchema, room, GameConst.STATUS.DISABLED);
let saveResult = await this.upsertPlayedAd(playerSchema.player_id, data.ads_id, sc.getCurrentDate(), null);
if(!saveResult){
Logger.critical('Ad started save error.', data, playerSchema.player_id);
@@ -86,7 +94,7 @@ class AdsMessageActions
if(data.act !== AdsConst.ACTIONS.AD_ENDED){
return false;
}
- room.activatePlayer(playerSchema);
+ room.activatePlayer(playerSchema, GameConst.STATUS.ACTIVE);
let saveResult = await this.upsertPlayedAd(playerSchema.player_id, data.ads_id, null, sc.getCurrentDate());
if(!saveResult){
Logger.critical('Ad ended save error.', data, playerSchema.player_id);
@@ -114,12 +122,9 @@ class AdsMessageActions
async giveRewardItem(playerSchema, playedAd)
{
- let rewardItem = playerSchema.inventory.manager.createItemInstance(
- playedAd.rewardItemKey,
- playedAd.rewardItemQty
- );
- await playerSchema.inventory.manager.addItem(rewardItem);
+ return this.giveRewardAction.execute(playerSchema, playedAd.rewardItemKey, playedAd.rewardItemQty);
}
+
}
module.exports.AdsMessageActions = AdsMessageActions;
diff --git a/lib/ads/server/plugin.js b/lib/ads/server/plugin.js
index 8e4b3cf4a..920667b14 100644
--- a/lib/ads/server/plugin.js
+++ b/lib/ads/server/plugin.js
@@ -24,6 +24,7 @@ class AdsPlugin extends PluginInterface
Logger.error('DataServer undefined in AdsPlugin.');
}
this.adsStartHandler = new AdsStartHandler();
+ this.createPlayerAdsHandler = new CreatePlayerAdsHandler(this);
this.listenEvents();
}
@@ -42,8 +43,8 @@ class AdsPlugin extends PluginInterface
this.events.on('reldens.roomsMessageActionsGlobal', (roomMessageActions) => {
roomMessageActions.ads = new AdsMessageActions({dataServer: this.dataServer, adsPlugin: this});
});
- this.events.on('reldens.createPlayerAfter', async (client, userModel, playerSchema, roomScene) => {
- await CreatePlayerAdsHandler.enrichPlayedWithPlayedAds(client, userModel, playerSchema, roomScene, this);
+ this.events.on('reldens.createPlayerAfter', async (client, userModel, playerSchema) => {
+ await this.createPlayerAdsHandler.enrichPlayedWithPlayedAds(playerSchema, client);
});
}
}
diff --git a/lib/audio/client/manager.js b/lib/audio/client/manager.js
index e0224ef14..fd52431a3 100644
--- a/lib/audio/client/manager.js
+++ b/lib/audio/client/manager.js
@@ -14,12 +14,13 @@ class AudioManager
{
this.events = sc.get(props, 'events', false);
if(!this.events){
- Logger.error('EventsManager undefined in ChatPlugin.');
+ Logger.error('EventsManager undefined in AudioManager.');
}
this.globalAudios = sc.get(props, 'globalAudios', {});
this.roomsAudios = sc.get(props, 'roomsAudios', {});
this.categories = sc.get(props, 'categories', {});
this.playerConfig = sc.get(props, 'playerConfig', {});
+ this.currentPlayerData = sc.get(props, 'currentPlayerData', {});
// @NOTE: it's important to add any been played audios here for the "changeMuteState" method.
this.playing = {};
this.currentMuteState = false;
@@ -79,8 +80,8 @@ class AudioManager
try {
playingElement[playOrStop]();
playingElement.mute = !enabled;
- } catch (err) {
- Logger.error('PlayingElement error.', {audioKey, playOrStop, playingElement, err});
+ } catch (error) {
+ Logger.error('PlayingElement error.', {audioKey, playOrStop, playingElement, error});
}
return true;
}
diff --git a/lib/audio/client/plugin.js b/lib/audio/client/plugin.js
index 3dd61a318..4dcb06178 100644
--- a/lib/audio/client/plugin.js
+++ b/lib/audio/client/plugin.js
@@ -49,7 +49,10 @@ class AudioPlugin extends PluginInterface
}
this.events.on('reldens.beforeCreateEngine', (initialGameData, gameManager) => {
if(!gameManager.audioManager){
- gameManager.audioManager = new AudioManager({events: this.events});
+ gameManager.audioManager = new AudioManager({
+ events: this.events,
+ currentPlayerData: initialGameData.player
+ });
this.initialAudiosData = sc.get(initialGameData, 'audio', {});
}
});
diff --git a/lib/audio/client/scene-audio-player.js b/lib/audio/client/scene-audio-player.js
index 65df2c8cd..0809034d8 100644
--- a/lib/audio/client/scene-audio-player.js
+++ b/lib/audio/client/scene-audio-player.js
@@ -21,7 +21,7 @@ class SceneAudioPlayer
}
sceneDynamic['associatedAudio'] = audioManager.findAudio(sceneDynamic.key, sceneDynamic.key);
if(sceneDynamic['associatedAudio']){
- this.playSpriteAudio(sceneDynamic['associatedAudio'], sceneDynamic.key, audioManager);
+ this.playSpriteAudio(sceneDynamic['associatedAudio'], sceneDynamic, false, audioManager);
return sceneDynamic['associatedAudio'];
}
return false;
@@ -41,35 +41,35 @@ class SceneAudioPlayer
let animationKey = AudioConst.AUDIO_ANIMATION_KEY_START+event.key;
let associatedAudio = this.attachAudioToSprite(sprite, animationKey, audioManager, sceneDynamic);
if(false !== associatedAudio){
- this.playSpriteAudio(associatedAudio, sceneDynamic.key, audioManager);
+ this.playSpriteAudio(associatedAudio, sceneDynamic, sprite, audioManager);
}
});
sprite.on('animationupdate', (event) => {
let animationKey = AudioConst.AUDIO_ANIMATION_KEY_UPDATE+event.key;
let associatedAudio = this.attachAudioToSprite(sprite, animationKey, audioManager, sceneDynamic);
if(false !== associatedAudio){
- this.playSpriteAudio(associatedAudio, sceneDynamic.key, audioManager);
+ this.playSpriteAudio(associatedAudio, sceneDynamic, sprite, audioManager);
}
});
sprite.on('animationcomplete', (event) => {
let animationKey = AudioConst.AUDIO_ANIMATION_KEY_COMPLETE+event.key;
let associatedAudio = this.attachAudioToSprite(sprite, animationKey, audioManager, sceneDynamic);
if(false !== associatedAudio){
- this.playSpriteAudio(associatedAudio, sceneDynamic.key, audioManager);
+ this.playSpriteAudio(associatedAudio, sceneDynamic, sprite, audioManager);
}
});
sprite.on('animationrepeat', (event) => {
let animationKey = AudioConst.AUDIO_ANIMATION_KEY_REPEAT+event.key;
let associatedAudio = this.attachAudioToSprite(sprite, animationKey, audioManager, sceneDynamic);
if(false !== associatedAudio){
- this.playSpriteAudio(associatedAudio, sceneDynamic.key, audioManager);
+ this.playSpriteAudio(associatedAudio, sceneDynamic, sprite, audioManager);
}
});
sprite.on('animationstop', (event) => {
let animationKey = AudioConst.AUDIO_ANIMATION_KEY_STOP+event.key;
let associatedAudio = this.attachAudioToSprite(sprite, animationKey, audioManager, sceneDynamic);
if(false !== associatedAudio){
- this.playSpriteAudio(associatedAudio, sceneDynamic.key, audioManager);
+ this.playSpriteAudio(associatedAudio, sceneDynamic, sprite, audioManager);
}
});
}
@@ -78,17 +78,33 @@ class SceneAudioPlayer
attachAudioToSprite(sprite, animationAudioKey, audioManager, sceneDynamic)
{
+ if(sc.hasOwn(sprite.associatedAudio, animationAudioKey)){
+ return sprite.associatedAudio[animationAudioKey];
+ }
if(!sc.hasOwn(sprite, 'associatedAudio')){
- sprite['associatedAudio'] = {};
+ sprite.associatedAudio = {};
}
- if(!sc.hasOwn(sprite['associatedAudio'], animationAudioKey)){
- sprite['associatedAudio'][animationAudioKey] = audioManager.findAudio(animationAudioKey, sceneDynamic.key);
+ if(!sc.hasOwn(sprite.associatedAudio, animationAudioKey)){
+ sprite.associatedAudio[animationAudioKey] = audioManager.findAudio(animationAudioKey, sceneDynamic.key);
}
- return sprite['associatedAudio'][animationAudioKey];
+ return sprite.associatedAudio[animationAudioKey];
}
- playSpriteAudio(associatedAudio, sceneKey, audioManager)
+ playSpriteAudio(associatedAudio, sceneDynamic, sprite, audioManager)
{
+ let currentPlayerId = Number(audioManager.currentPlayerData.id);
+ let spritePlayerId = Number(sc.get(sprite, 'player_id'));
+ //Logger.debug('Play sprite audio.', associatedAudio, sceneDynamic.key, sprite, currentPlayerId);
+ let isCurrentPlayerSprite = this.isCurrentPlayerSprite(spritePlayerId, currentPlayerId);
+ if(associatedAudio.audio.data.config?.onlyCurrentPlayer && !isCurrentPlayerSprite){
+ //Logger.debug('Play sprite audio avoided for current player.', associatedAudio, sceneKey);
+ return false;
+ }
+ let currentPlayer = sceneDynamic.player;
+ if(isCurrentPlayerSprite && currentPlayer && (currentPlayer.isDisabled() || currentPlayer.isDeath())){
+ //Logger.debug('Play sprite audio avoided for current dead player.', associatedAudio, sceneKey);
+ return false;
+ }
// @NOTE:
// - We need the status update from the actual category in the audio manager the category associated to the
// audio here is just the storage reference.
@@ -149,6 +165,11 @@ class SceneAudioPlayer
}
}
+ isCurrentPlayerSprite(spritePlayerId, currentPlayerId)
+ {
+ return spritePlayerId && spritePlayerId === currentPlayerId;
+ }
+
isMutedState(audioManager, mutedKey, audioInstance)
{
if(false === audioManager.currentMuteState){
@@ -158,6 +179,7 @@ class SceneAudioPlayer
audioManager.changedMutedState[mutedKey] = audioManager.currentMuteState;
return true;
}
+
}
module.exports.SceneAudioPlayer = new SceneAudioPlayer();
diff --git a/lib/audio/server/manager.js b/lib/audio/server/manager.js
index e7e771e1b..565e9bd97 100644
--- a/lib/audio/server/manager.js
+++ b/lib/audio/server/manager.js
@@ -5,7 +5,7 @@
*/
const { AudioConst } = require('../constants');
-const { sc } = require('@reldens/utils');
+const { sc, Logger} = require('@reldens/utils');
class AudioManager
{
@@ -17,46 +17,89 @@ class AudioManager
this.roomsAudios = {}; // each room id will have its own array of audios
this.roomsManager = props.roomsManager;
this.dataServer = props.dataServer;
+ this.setRepositories();
+ }
+
+ setRepositories()
+ {
+ if(!this.dataServer){
+ Logger.error('DataServer undefined in AudioManager.');
+ return false;
+ }
+ this.audioRepository = this.dataServer.getEntity('audio');
+ this.audioPlayerConfigModelRepository = this.dataServer.getEntity('audioPlayerConfigModel');
+ this.audioCategoriesRepository = this.dataServer.getEntity('audioCategories');
}
async loadAudioCategories()
{
- this.categories = await this.dataServer.getEntity('audioCategories').loadBy('enabled', 1);
+ // @TODO - BETA - Include config fields in audio categories table.
+ this.categories = await this.audioCategoriesRepository.loadBy('enabled', 1);
}
async loadGlobalAudios()
{
if(0 === Object.keys(this.globalAudios).length){
- let loadedGlobalAudios = await this.dataServer.getEntity('audio').loadWithRelations(
+ let loadedGlobalAudios = await this.audioRepository.loadWithRelations(
{room_id: null, enabled: 1},
['category', 'markers']
);
- this.globalAudios = sc.convertObjectsArrayToObjectByKeys(loadedGlobalAudios, 'audio_key');
+ this.globalAudios = sc.convertObjectsArrayToObjectByKeys(
+ this.convertAudiosConfigJsonToObjects(loadedGlobalAudios),
+ 'audio_key'
+ );
}
-
return this.globalAudios;
}
+ convertAudiosConfigJsonToObjects(loadedGlobalAudios)
+ {
+ for(let audio of loadedGlobalAudios){
+ if(audio.config){
+ let convertedData = sc.toJson(audio.config);
+ if(convertedData){
+ audio.config = convertedData;
+ }
+ }
+ if(audio.markers){
+ for(let marker of audio.markers){
+ if(marker.config){
+ let convertedData = sc.toJson(marker.config);
+ if(convertedData){
+ marker.config = convertedData;
+ }
+ }
+ }
+ }
+ }
+ return loadedGlobalAudios;
+ }
+
async loadRoomAudios(roomId)
{
if(!sc.hasOwn(this.roomsAudios, roomId)){
- let loadedRoomAudios = await this.dataServer.getEntity('audio').loadWithRelations(
+ let loadedRoomAudios = await this.audioRepository.loadWithRelations(
{room_id: roomId, enabled: 1},
['parent_room', 'category', 'markers']
);
- this.roomsAudios[roomId] = sc.convertObjectsArrayToObjectByKeys(loadedRoomAudios, 'audio_key');
+ this.roomsAudios[roomId] = sc.convertObjectsArrayToObjectByKeys(
+ this.convertAudiosConfigJsonToObjects(loadedRoomAudios),
+ 'audio_key'
+ );
}
return this.roomsAudios[roomId];
}
async loadAudioPlayerConfig(playerId)
{
- let configModels = await this.dataServer.getEntity('audioPlayerConfigModel').loadBy('player_id', playerId);
+ // @TODO - BETA - Improve login performance, avoid query by getting config from existent player schema.
+ let configModels = await this.audioPlayerConfigModelRepository.loadBy('player_id', playerId);
+ if(0 === configModels.length){
+ return {};
+ }
let playerConfig = {};
- if(configModels.length > 0){
- for(let config of configModels){
- playerConfig[config['category_id']] = config['enabled'];
- }
+ for(let config of configModels) {
+ playerConfig[config['category_id']] = config['enabled'];
}
return playerConfig;
}
@@ -67,29 +110,31 @@ class AudioManager
return false;
}
let currentPlayer = room.playerBySessionIdFromState(client.sessionId);
- let audioCategory = await this.dataServer.getEntity('audioCategories').loadOneBy(
+ let audioCategory = await this.audioCategoriesRepository.loadOneBy(
'category_key',
message[AudioConst.MESSAGE.DATA.UPDATE_TYPE]
);
if(!currentPlayer || currentPlayer.playerId || !audioCategory){
return false;
}
- let audioPlayerConfigModel = this.dataServer.getEntity('audioPlayerConfigModel');
let filters = {
player_id: currentPlayer.player_id,
category_id: audioCategory.id
};
- let playerConfig = await audioPlayerConfigModel.loadOne(filters);
+ let playerConfig = await this.audioPlayerConfigModelRepository.loadOne(filters);
let updatePatch = {enabled: (message[AudioConst.MESSAGE.DATA.UPDATE_VALUE] ? 1 : 0)};
- playerConfig
- ? await audioPlayerConfigModel.update(filters, updatePatch)
- : await audioPlayerConfigModel.createWithRelations(Object.assign(updatePatch, filters));
+ if(playerConfig){
+ return await this.audioPlayerConfigModelRepository.update(filters, updatePatch);
+ }
+ return await this.audioPlayerConfigModelRepository.createWithRelations(Object.assign(updatePatch, filters));
}
hotPlugAudio(options)
{
- let { newAudioModel } = options;
- !newAudioModel.room_id ? this.hotPlugGlobalAudio(newAudioModel) : this.hotPlugRoomAudio(newAudioModel);
+ if(options?.newAudioModel?.room_id){
+ return this.hotPlugGlobalAudio(options?.newAudioModel);
+ }
+ return this.hotPlugRoomAudio(options?.newAudioModel);
}
hotPlugRoomAudio(newAudioModel)
@@ -105,12 +150,7 @@ class AudioManager
// the new audio, it will be loaded automatically when the room is created.
return true;
}
- let data = {
- act: AudioConst.AUDIO_UPDATE,
- roomId,
- audios: [newAudioModel]
- };
- roomInstance.broadcast('*', data);
+ roomInstance.broadcast('*', {act: AudioConst.AUDIO_UPDATE, roomId, audios: [newAudioModel]});
}
hotPlugGlobalAudio(newAudioModel)
@@ -135,7 +175,10 @@ class AudioManager
hotUnplugAudio(props)
{
let {newAudioModel, id} = props;
- newAudioModel.room_id ? this.hotUnplugRoomAudio(newAudioModel, id) : this.hotUnplugGlobalAudio(newAudioModel, id);
+ if (newAudioModel.room_id){
+ return this.hotUnplugRoomAudio(newAudioModel, id);
+ }
+ return this.hotUnplugGlobalAudio(newAudioModel, id);
}
hotUnplugRoomAudio(newAudioModel, id)
diff --git a/lib/bundlers/drivers/parcel-config.json b/lib/bundlers/drivers/parcel-config.json
index 0bffc8fae..4e5fd9945 100644
--- a/lib/bundlers/drivers/parcel-config.json
+++ b/lib/bundlers/drivers/parcel-config.json
@@ -32,7 +32,6 @@
],
"*.pug": ["@parcel/transformer-pug"],
"*.coffee": ["@parcel/transformer-coffeescript"],
- "*.elm": ["@parcel/transformer-elm"],
"*.vue": ["@parcel/transformer-vue"],
"template:*.vue": ["@parcel/transformer-vue"],
"script:*.vue": ["@parcel/transformer-vue"],
diff --git a/lib/chat/client/chat-ui.js b/lib/chat/client/chat-ui.js
index de364e555..c8540e381 100644
--- a/lib/chat/client/chat-ui.js
+++ b/lib/chat/client/chat-ui.js
@@ -316,8 +316,8 @@ class ChatUi
attachNewMessage(message)
{
if(!this.gameManager.gameEngine.uiScene.cache){
- // expected when the same player login with the same player that's logged in another session:
- Logger.info('Missing uiScene cache on chat message.', message);
+ // expected while uiScene is being created:
+ // Logger.info('Missing uiScene cache on chat message.', message);
return;
}
let messageTemplate = this.gameManager.gameEngine.uiScene.cache.html.get('chatMessage');
@@ -413,13 +413,13 @@ class ChatUi
if(!messageData){
return this.t(message[ChatConst.MESSAGE.KEY]);
}
- if(messageData.modifiers){
+ if(messageData[ChatConst.MESSAGE.DATA.MODIFIERS]){
let translatedConcat = '';
let targetLabel = messageData[ChatConst.MESSAGE.DATA.TARGET_LABEL];
- let propertyKeys = Object.keys(messageData.modifiers);
+ let propertyKeys = Object.keys(messageData[ChatConst.MESSAGE.DATA.MODIFIERS]);
for(let propertyKey of propertyKeys){
let propertyLabel = this.t(propertyKey);
- let propertyValue = propertyKeys[propertyKey];
+ let propertyValue = messageData[ChatConst.MESSAGE.DATA.MODIFIERS][propertyKey];
translatedConcat += this.t(message[ChatConst.MESSAGE.KEY], {propertyValue, propertyLabel, targetLabel});
}
return translatedConcat;
diff --git a/lib/chat/server/event-listener/guest-invalid-change-point.js b/lib/chat/server/event-listener/guest-invalid-change-point.js
index e31f0d487..990b52ffd 100644
--- a/lib/chat/server/event-listener/guest-invalid-change-point.js
+++ b/lib/chat/server/event-listener/guest-invalid-change-point.js
@@ -22,8 +22,8 @@ class GuestInvalidChangePoint
event.playerSchema.state.room_id,
false,
ChatConst.TYPES.ERROR
- ).catch((err) => {
- Logger.error('Save chat message error on player damage callback.', err);
+ ).catch((error) => {
+ Logger.error('Save chat message error on player damage callback.', error);
});
}
diff --git a/lib/chat/server/manager.js b/lib/chat/server/manager.js
index 0fc0f9cb1..d31b8f285 100644
--- a/lib/chat/server/manager.js
+++ b/lib/chat/server/manager.js
@@ -13,6 +13,7 @@ class ChatManager
constructor(props)
{
this.dataServer = sc.get(props, 'dataServer', false);
+ this.chatRepository = this.dataServer?.getEntity('chat');
}
async saveMessage(message, playerId, roomId, clientToPlayerSchema, messageType)
@@ -32,9 +33,14 @@ class ChatManager
if(clientToPlayerSchema && sc.hasOwn(clientToPlayerSchema, 'id')){
entryData.private_player_id = clientToPlayerSchema.state.player_id;
}
- let insertResult = await this.dataServer.getEntity('chat').create(entryData);
- if(!insertResult){
- Logger.critical('Chat message save error.', entryData);
+ try {
+ let insertResult = await this.chatRepository.create(entryData);
+ if(!insertResult){
+ Logger.critical('Chat message insert error.', entryData);
+ return false;
+ }
+ } catch (error) {
+ Logger.critical('Chat message save error.', entryData, error.message);
return false;
}
return true;
diff --git a/lib/chat/server/message-actions.js b/lib/chat/server/message-actions.js
index ff96cb955..8fb7217a7 100644
--- a/lib/chat/server/message-actions.js
+++ b/lib/chat/server/message-actions.js
@@ -32,6 +32,9 @@ class ChatMessageActions
async clientJoinAction(data, room, playerSchema)
{
+ if(!data || !room || !playerSchema){
+ return false;
+ }
if(data.act !== GameConst.CLIENT_JOINED || !room.config.get('server/chat/messages/broadcast_join')){
return false;
}
@@ -62,6 +65,9 @@ class ChatMessageActions
async chatAction(data, room, playerSchema)
{
+ if(!data || !room || !playerSchema){
+ return false;
+ }
if(!MessagesGuard.validate(data)){
return false;
}
diff --git a/lib/chat/server/messages/message-data-mapper.js b/lib/chat/server/messages/message-data-mapper.js
new file mode 100644
index 000000000..6f674d043
--- /dev/null
+++ b/lib/chat/server/messages/message-data-mapper.js
@@ -0,0 +1,37 @@
+/**
+ *
+ * Reldens - MessageDataMapper
+ *
+ */
+
+const { ChatConst } = require('../../constants');
+const { sc } = require('@reldens/utils');
+
+class MessageDataMapper
+{
+
+ static mapMessageWithData(skill)
+ {
+ let lastAppliedModifiers = sc.get(skill, 'lastAppliedModifiers', {});
+ let appliedModifiersKeys = Object.keys(lastAppliedModifiers);
+ if(0 === appliedModifiersKeys.length){
+ return false;
+ }
+ let isObjectTarget = sc.hasOwn(skill.target, 'key');
+ let targetLabel = isObjectTarget ? skill.target.title : skill.target.playerName;
+ let message = ChatConst.SNIPPETS.MODIFIERS_APPLY;
+ let messageData = {
+ [ChatConst.MESSAGE.DATA.TARGET_LABEL]: targetLabel,
+ [ChatConst.MESSAGE.DATA.MODIFIERS]: {}
+ };
+ for(let i of appliedModifiersKeys){
+ let value = lastAppliedModifiers[i];
+ let property = i.split('/').pop();
+ messageData[ChatConst.MESSAGE.DATA.MODIFIERS][property] = value;
+ }
+ return {message, messageData};
+ }
+
+}
+
+module.exports.MessageDataMapper = MessageDataMapper;
diff --git a/lib/chat/server/messages/npc-modifiers-callback.js b/lib/chat/server/messages/npc-modifiers-callback.js
index f0ed0c1a2..af060620f 100644
--- a/lib/chat/server/messages/npc-modifiers-callback.js
+++ b/lib/chat/server/messages/npc-modifiers-callback.js
@@ -5,13 +5,14 @@
*/
const { MessageFactory } = require('../../message-factory');
+const { MessageDataMapper } = require('./message-data-mapper');
const { Validator } = require('./validator');
const { ChatConst } = require('../../constants');
const { Logger, sc } = require('@reldens/utils');
class NpcModifiersCallback
{
-
+
static async sendMessage(props)
{
if(!Validator.validateMessage(props, ['skill', 'chatManager'])){
@@ -24,23 +25,11 @@ class NpcModifiersCallback
Logger.info('Client not defined on NpcModifiersCallback.', skill);
return false;
}
- let lastAppliedModifiers = sc.get(skill, 'lastAppliedModifiers', {});
- let appliedModifiersKeys = Object.keys(lastAppliedModifiers);
- if(0 === appliedModifiersKeys.length){
+ let messageWithData = MessageDataMapper.mapMessageWithData(skill);
+ if(!messageWithData){
return false;
}
- let isObjectTarget = sc.hasOwn(skill.target, 'key');
- let targetLabel = isObjectTarget ? skill.target.title : skill.target.playerName;
- let message = ChatConst.SNIPPETS.MODIFIERS_APPLY;
- let messageData = {
- [ChatConst.MESSAGE.DATA.TARGET_LABEL]: targetLabel,
- [ChatConst.MESSAGE.DATA.MODIFIERS]: []
- };
- for(let i of appliedModifiersKeys){
- let value = lastAppliedModifiers[i];
- let property = i.split('/').pop();
- messageData[property] = value;
- }
+ let {message, messageData} = messageWithData;
let isObjectOwner = sc.hasOwn(skill.owner, 'key');
let from = isObjectOwner ? skill.owner.title : skill.owner.playerName;
let messageObject = MessageFactory.create(ChatConst.TYPES.SKILL, message, messageData, from);
@@ -55,10 +44,10 @@ class NpcModifiersCallback
ChatConst.TYPES.SKILL
);
if(!saveResult){
- Logger.error('Save chat message error on npc modifiers callback.', messageObject, playerId, roomId);
+ Logger.error('Save chat message error on modifiers callback.', messageObject, playerId, roomId);
}
}
-
+
}
module.exports.NpcModifiersCallback = NpcModifiersCallback;
diff --git a/lib/chat/server/messages/player-damage-callback.js b/lib/chat/server/messages/player-damage-callback.js
index cb9f8242a..3a55a89aa 100644
--- a/lib/chat/server/messages/player-damage-callback.js
+++ b/lib/chat/server/messages/player-damage-callback.js
@@ -19,6 +19,10 @@ class PlayerDamageCallback
return false;
}
let {skill, target, damage, client, chatManager} = props;
+ if(!client){
+ Logger.info('Client not defined on PlayerDamageCallback.', skill);
+ return false;
+ }
let isObjectTarget = sc.hasOwn(target, 'key');
let targetLabel = isObjectTarget ? target.title : target.playerName;
let message = ChatConst.SNIPPETS.PLAYER.DAMAGE;
@@ -29,18 +33,21 @@ class PlayerDamageCallback
let messageObject = MessageFactory.create(ChatConst.TYPES.DAMAGE, message, messageData, skill.owner.playerName);
client.send(messageObject);
let targetClient = skill.target?.skillsServer?.client?.client || null;
- if(!isObjectTarget && targetClient){
+ if(!isObjectTarget && targetClient && targetClient !== client){
targetClient.send(messageObject);
}
- await chatManager.saveMessage(
+ let playerId = skill.owner?.player_id || null;
+ let roomId = skill.owner?.state?.room_id || null;
+ let saveResult = await chatManager.saveMessage(
MessageFactory.withDataToJson(message, messageData),
- skill.owner.player_id,
- skill.owner.state.room_id,
+ playerId,
+ roomId,
false,
ChatConst.TYPES.DAMAGE
- ).catch((err) => {
- Logger.error('Save chat message error on player damage callback.', err);
- });
+ );
+ if(!saveResult){
+ Logger.error('Save chat message error on player damage callback.', messageObject, playerId, roomId);
+ }
}
}
diff --git a/lib/chat/server/messages/player-dodge-callback.js b/lib/chat/server/messages/player-dodge-callback.js
index 70f279056..46bf28701 100644
--- a/lib/chat/server/messages/player-dodge-callback.js
+++ b/lib/chat/server/messages/player-dodge-callback.js
@@ -19,6 +19,10 @@ class PlayerDodgeCallback
return false;
}
let {skill, client, chatManager} = props;
+ if(!client){
+ Logger.info('Client not defined on PlayerDamageCallback.', skill);
+ return false;
+ }
let isObjectTarget = sc.hasOwn(skill.target, 'key');
let targetLabel = isObjectTarget ? skill.target.title : skill.target.playerName;
let message = ChatConst.SNIPPETS.PLAYER.DODGED_SKILL;
@@ -29,7 +33,7 @@ class PlayerDodgeCallback
let messageObject = MessageFactory.create(ChatConst.TYPES.SKILL, message, messageData, skill.owner.playerName);
client.send(messageObject);
let targetClient = skill.target?.skillsServer?.client?.client || null;
- if(!isObjectTarget && targetClient){
+ if(!isObjectTarget && targetClient && targetClient !== client){
targetClient.send(messageObject);
}
let playerId = skill.owner?.player_id || null;
diff --git a/lib/chat/server/messages/player-modifiers-callback.js b/lib/chat/server/messages/player-modifiers-callback.js
index b0339c321..e23b7e8e1 100644
--- a/lib/chat/server/messages/player-modifiers-callback.js
+++ b/lib/chat/server/messages/player-modifiers-callback.js
@@ -5,13 +5,14 @@
*/
const { MessageFactory } = require('../../message-factory');
+const { MessageDataMapper } = require('./message-data-mapper');
const { Validator } = require('./validator');
const { ChatConst } = require('../../constants');
-const { Logger, sc } = require('@reldens/utils');
+const { Logger, sc} = require('@reldens/utils');
class PlayerModifiersCallback
{
-
+
static async sendMessage(props)
{
if(!Validator.validateMessage(props, ['skill', 'client', 'chatManager'])){
@@ -19,27 +20,20 @@ class PlayerModifiersCallback
return false;
}
let {skill, client, chatManager} = props;
- let isObjectTarget = sc.hasOwn(skill.target, 'key');
- let lastAppliedModifiers = sc.get(skill, 'lastAppliedModifiers', {});
- let appliedModifiersKeys = Object.keys(lastAppliedModifiers);
- if(0 === appliedModifiersKeys.length){
+ if(!client){
+ Logger.info('Client not defined on PlayerModifiersCallback.', skill);
return false;
}
- let targetLabel = isObjectTarget ? skill.target.title : skill.target.playerName;
- let message = ChatConst.SNIPPETS.MODIFIERS_APPLY;
- let messageData = {
- [ChatConst.MESSAGE.DATA.TARGET_LABEL]: targetLabel,
- [ChatConst.MESSAGE.DATA.MODIFIERS]: []
- };
- for(let i of appliedModifiersKeys){
- let value = lastAppliedModifiers[i];
- let property = i.split('/').pop();
- messageData[property] = value;
+ let messageWithData = MessageDataMapper.mapMessageWithData(skill);
+ if(!messageWithData){
+ return false;
}
+ let {message, messageData} = messageWithData;
let messageObject = MessageFactory.create(ChatConst.TYPES.SKILL, message, messageData, skill.owner.playerName);
client.send(messageObject);
let targetClient = skill.target?.skillsServer?.client?.client || null;
- if(!isObjectTarget && targetClient){
+ let isObjectTarget = sc.hasOwn(skill.target, 'key');
+ if(!isObjectTarget && targetClient && targetClient !== client){
targetClient.send(messageObject);
}
let playerId = skill.owner.player_id;
@@ -52,7 +46,7 @@ class PlayerModifiersCallback
ChatConst.TYPES.SKILL
);
if(!saveResult){
- Logger.error('Save chat message error on player modifiers callback.', messageObject, playerId, roomId);
+ Logger.error('Save chat message error on modifiers callback.', messageObject, playerId, roomId);
}
}
diff --git a/lib/chat/server/room-chat.js b/lib/chat/server/room-chat.js
index cfa004721..11dc2a71f 100644
--- a/lib/chat/server/room-chat.js
+++ b/lib/chat/server/room-chat.js
@@ -18,27 +18,19 @@ class RoomChat extends RoomLogin
onCreate(props)
{
super.onCreate(props);
+ Logger.info('Created RoomChat: '+this.roomName+' ('+this.roomId+').');
this.roomType = ChatConst.ROOM_TYPE_CHAT;
- Logger.info('Created RoomChat: '+this.roomName+' - ID: '+this.roomId+' - Type: '+ChatConst.ROOM_TYPE_CHAT);
let dataServer = sc.get(this, 'dataServer', false);
if(!dataServer){
Logger.error('DataServer undefined in RoomChat.');
}
this.chatManager = new ChatManager({dataServer: this.dataServer});
- this.activePlayers = {};
+ delete props.roomsManager.creatingInstances[this.roomName];
}
onJoin(client, props, userModel)
{
- // we do not need to create a player entity since we only need the name for the chat:
- this.activePlayers[client.sessionId] = {
- id: userModel.id,
- sessionId: client.sessionId,
- playerName: userModel.player.name,
- role_id: userModel.role_id,
- playerData: userModel.player,
- client: client
- };
+ this.loginManager.activePlayers.add(userModel, client, this);
}
async handleReceivedMessage(client, data)
@@ -58,7 +50,7 @@ class RoomChat extends RoomLogin
// do nothing if text is shorter than 3 characters (including @ and #):
return;
}
- let activePlayer = this.activePlayers[client.sessionId];
+ let activePlayer = this.activePlayerBySessionId(client.sessionId, this.roomId);
if(!activePlayer){
Logger.warning('Current Active Player not found: '+client.sessionId);
return;
@@ -77,8 +69,8 @@ class RoomChat extends RoomLogin
Logger.info('Missing player recipient.');
return false;
}
- let clientTo = this.getActivePlayerByName(toPlayer);
- if(!clientTo){
+ let activePlayerTo = this.activePlayerByPlayerName(toPlayer, this.roomId);
+ if(!activePlayerTo){
let message = ChatConst.SNIPPETS.PRIVATE_MESSAGE_PLAYER_NOT_FOUND;
let messageData = {
[ChatConst.MESSAGE.DATA.PLAYER_NAME]: toPlayer
@@ -91,9 +83,9 @@ class RoomChat extends RoomLogin
client.send('*', messageObject);
let saveResult = await this.chatManager.saveMessage(
MessageFactory.withDataToJson(message, messageData),
- activePlayer.playerData.id,
- activePlayer.playerData.state.room_id,
- clientTo?.playerData,
+ activePlayer.playerId,
+ activePlayer?.playerData?.state?.room_id,
+ activePlayerTo?.playerData,
ChatConst.TYPES.ERROR
);
if(!saveResult){
@@ -108,12 +100,12 @@ class RoomChat extends RoomLogin
activePlayer.playerName
);
client.send('*', messageObject);
- clientTo.client.send('*', messageObject);
+ activePlayerTo?.client.send('*', messageObject);
let saveResult = await this.chatManager.saveMessage(
messageObject[ChatConst.MESSAGE.KEY],
- activePlayer.playerData.id,
- activePlayer.playerData.state.room_id,
- clientTo?.playerData,
+ activePlayer.playerId,
+ activePlayer?.playerData?.state?.room_id,
+ activePlayerTo?.playerData,
ChatConst.TYPES.PRIVATE
);
if(!saveResult){
@@ -130,7 +122,7 @@ class RoomChat extends RoomLogin
));
}
let globalAllowedRoles = this.config.get('server/chat/messages/global_allowed_roles').split(',').map(Number);
- if(-1 === globalAllowedRoles.indexOf(activePlayer.role_id)){
+ if(-1 === globalAllowedRoles.indexOf(activePlayer.roleId)){
return client.send('*', MessageFactory.create(
ChatConst.TYPES.ERROR,
ChatConst.SNIPPETS.GLOBAL_MESSAGE_PERMISSION_DENIED,
@@ -145,8 +137,8 @@ class RoomChat extends RoomLogin
this.broadcast('*', messageObject);
let saveResult = await this.chatManager.saveMessage(
messageObject[ChatConst.MESSAGE.KEY],
- activePlayer.playerData.id,
- activePlayer.playerData.state.room_id,
+ activePlayer.playerId,
+ activePlayer?.playerData?.state?.room_id,
false,
ChatConst.TYPES.GLOBAL
);
@@ -157,38 +149,30 @@ class RoomChat extends RoomLogin
async onLeave(client, consented)
{
- let activePlayer = this.activePlayers[client.sessionId];
- if(!activePlayer){
- return false;
- }
- if(this.config.getWithoutLogs('server/chat/messages/broadcast_leave', false)){
- let message = ChatConst.SNIPPETS.LEFT_ROOM;
- let messageData = {
- [ChatConst.MESSAGE.DATA.PLAYER_NAME]: activePlayer.playerName
- };
- let messageObject = MessageFactory.create(
- ChatConst.TYPES.SYSTEM,
- message,
- messageData
- );
- this.broadcast('*', messageObject);
- }
- delete this.activePlayers[client.sessionId];
+ this.broadcastLeaveMessage(client.sessionId);
+ this.loginManager.activePlayers.removeByRoomAndSessionId(client.sessionId, this.roomId);
}
- getActivePlayerByName(playerName)
+ broadcastLeaveMessage(sessionId)
{
- let clientTo = false;
- for(let i of Object.keys(this.activePlayers)){
- let client = this.activePlayers[i];
- if(client.playerName === playerName){
- clientTo = client;
- break;
- }
+ let activePlayer = this.activePlayerBySessionId(sessionId, this.roomId);
+ if(!activePlayer){
+ return false;
+ }
+ if(!this.config.getWithoutLogs('server/chat/messages/broadcast_leave', false)){
+ return false;
}
- return clientTo;
+ let message = ChatConst.SNIPPETS.LEFT_ROOM;
+ let messageData = {
+ [ChatConst.MESSAGE.DATA.PLAYER_NAME]: activePlayer.playerName
+ };
+ let messageObject = MessageFactory.create(
+ ChatConst.TYPES.SYSTEM,
+ message,
+ messageData
+ );
+ this.broadcast('*', messageObject);
}
-
}
module.exports.RoomChat = RoomChat;
diff --git a/lib/features/client/config-client.js b/lib/features/client/config-client.js
index a5643858a..e9bdf73b9 100644
--- a/lib/features/client/config-client.js
+++ b/lib/features/client/config-client.js
@@ -20,6 +20,8 @@ const { TeamsPlugin } = require('../../teams/client/plugin');
const { SnippetsPlugin } = require('../../snippets/client/plugin');
const { AdsPlugin } = require('../../ads/client/plugin');
const { WorldPlugin } = require('../../world/client/plugin');
+const { ScoresPlugin } = require('../../scores/client/plugin');
+const { RewardsPlugin } = require('../../rewards/client/plugin');
module.exports.ClientCoreFeatures = {
chat: ChatPlugin,
@@ -33,5 +35,7 @@ module.exports.ClientCoreFeatures = {
teams: TeamsPlugin,
snippets: SnippetsPlugin,
ads: AdsPlugin,
- world: WorldPlugin
+ world: WorldPlugin,
+ scores: ScoresPlugin,
+ rewards: RewardsPlugin
};
diff --git a/lib/features/plugin-interface.js b/lib/features/plugin-interface.js
index 1c9a5726c..d65b25a41 100644
--- a/lib/features/plugin-interface.js
+++ b/lib/features/plugin-interface.js
@@ -9,7 +9,17 @@ const { Logger } = require('@reldens/utils');
class PluginInterface
{
- setup(props)
+ /**
+ * {
+ * requiredProperties,
+ * events,
+ * dataServer,
+ * config,
+ * featuresManager,
+ * themeManager
+ * }
+ */
+ async setup(props)
{
Logger.error('Setup plugin not implemented.', props);
}
diff --git a/lib/features/server/config-server.js b/lib/features/server/config-server.js
index 414e64048..d5715b606 100644
--- a/lib/features/server/config-server.js
+++ b/lib/features/server/config-server.js
@@ -22,6 +22,7 @@ const { RewardsPlugin } = require('../../rewards/server/plugin');
const { SnippetsPlugin } = require('../../snippets/server/plugin');
const { ObjectsPlugin } = require('../../objects/server/plugin');
const { AdsPlugin } = require('../../ads/server/plugin');
+const { ScoresPlugin } = require('../../scores/server/plugin');
module.exports.ServerCoreFeatures = {
chat: ChatPlugin,
@@ -37,5 +38,6 @@ module.exports.ServerCoreFeatures = {
rewards: RewardsPlugin,
snippets: SnippetsPlugin,
objects: ObjectsPlugin,
- ads: AdsPlugin
+ ads: AdsPlugin,
+ scores: ScoresPlugin
};
diff --git a/lib/features/server/manager.js b/lib/features/server/manager.js
index 3f122c69f..33a96616c 100644
--- a/lib/features/server/manager.js
+++ b/lib/features/server/manager.js
@@ -19,6 +19,7 @@ class FeaturesManager
this.featuresCodeList = [];
this.events = sc.get(props, 'events', false);
this.dataServer = sc.get(props, 'dataServer', false);
+ this.themeManager = sc.get(props, 'themeManager', false);
this.config = sc.get(props, 'config', {});
}
@@ -37,7 +38,8 @@ class FeaturesManager
events: this.events,
dataServer: this.dataServer,
config: this.config,
- featuresManager: this,
+ themeManager: this.themeManager,
+ featuresManager: this
});
if(!setupServerProperties.validate()){
return false;
diff --git a/lib/features/server/setup-server-properties.js b/lib/features/server/setup-server-properties.js
index f1df7463e..45de2f16f 100644
--- a/lib/features/server/setup-server-properties.js
+++ b/lib/features/server/setup-server-properties.js
@@ -17,6 +17,7 @@ class SetupServerProperties extends PropertiesHandler
this.dataServer = sc.get(props, 'dataServer', false);
this.config = sc.get(props, 'config', {});
this.featuresManager = sc.get(props, 'featuresManager', false);
+ this.themeManager = sc.get(props, 'themeManager', false);
this.requiredProperties = Object.keys(this);
}
diff --git a/lib/firebase/server/plugin.js b/lib/firebase/server/plugin.js
index c7f70228e..6f47d884c 100644
--- a/lib/firebase/server/plugin.js
+++ b/lib/firebase/server/plugin.js
@@ -5,6 +5,7 @@
*/
const { PluginInterface } = require('../../features/plugin-interface');
+const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
class FirebasePlugin extends PluginInterface
@@ -23,7 +24,7 @@ class FirebasePlugin extends PluginInterface
declareFirebaseConfigRequestHandler(app)
{
- app.get('/reldens-firebase', (req, res) => {
+ app.get(GameConst.ROUTE_PATHS.FIREBASE, (req, res) => {
res.json(this.firebaseConfig());
});
}
diff --git a/lib/game/client/game-client.js b/lib/game/client/game-client.js
index a12ced167..48d67d930 100644
--- a/lib/game/client/game-client.js
+++ b/lib/game/client/game-client.js
@@ -5,13 +5,106 @@
*/
const { Client } = require('colyseus.js');
+const { RoomsConst } = require('../../rooms/constants');
+const { GameConst } = require('../constants');
+const { Logger } = require('@reldens/utils');
-class GameClient extends Client
+class GameClient
{
- constructor(serverUrl)
+ constructor(serverUrl, config)
{
- super(serverUrl);
+ this.serverUrl = serverUrl;
+ this.config = config;
+ this.autoConnectServerGameRoom = this.config.getWithoutLogs(
+ 'client/rooms/autoConnectServerGameRoom',
+ true
+ );
+ this.autoConnectServerFeatureRooms = this.config.getWithoutLogs(
+ 'client/rooms/autoConnectServerFeatureRooms',
+ true
+ );
+ this.roomsUrls = {};
+ this.roomClients = {};
+ this.gameRoomsByServer = {};
+ this.featuresByServerFlag = {};
+ this.featuresRoomsByServer = {};
+ }
+
+ async joinOrCreate(roomName, options)
+ {
+ try {
+ let client = this.roomClient(roomName);
+ if(!client){
+ Logger.error('Client not found for room name "'+roomName+'".');
+ return false;
+ }
+ let roomUrl = this.roomsUrls[roomName];
+ await this.connectToGlobalGameRoom(roomUrl, client, options);
+ await this.connectToGlobalFeaturesRooms(roomUrl, client, options);
+ return await client.joinOrCreate(roomName, options);
+ } catch (error) {
+ if(RoomsConst.ERRORS.CREATING_ROOM_AWAIT === error.message){
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return await this.joinOrCreate(roomName, options);
+ }
+ // any connection errors will be handled in the higher level class, see snippet "game.errors.joiningRoom"
+ Logger.error('Joining room error: '+error.message);
+ }
+ }
+
+ async connectToGlobalGameRoom(roomUrl, client, options)
+ {
+ if(!this.autoConnectServerGameRoom){
+ return;
+ }
+ if('' === roomUrl || this.serverUrl === roomUrl){
+ Logger.debug('Avoid connect to global game room.', this.serverUrl, roomUrl);
+ return;
+ }
+ if(this.gameRoomsByServer[roomUrl]){
+ return;
+ }
+ Logger.debug('Registering GameRoom for server: '+roomUrl);
+ this.gameRoomsByServer[roomUrl] = await client.joinOrCreate(GameConst.ROOM_GAME, options);
+ // required to avoid unregistered messages warning:
+ this.gameRoomsByServer[roomUrl].onMessage('*', () => {});
+ }
+
+ async connectToGlobalFeaturesRooms(roomUrl, client, options)
+ {
+ if(!this.autoConnectServerFeatureRooms){
+ return;
+ }
+ if('' === roomUrl || this.serverUrl === roomUrl){
+ Logger.debug('Avoid connect to features rooms.', this.serverUrl, roomUrl);
+ return;
+ }
+ if(this.featuresByServerFlag[roomUrl]){
+ return;
+ }
+ Logger.debug('Registering features rooms for server: '+roomUrl);
+ this.featuresByServerFlag[roomUrl] = true;
+ let featuresRoomsNames = this.config.getWithoutLogs('client/rooms/featuresRoomsNames', []);
+ if(0 < featuresRoomsNames.length){
+ return;
+ }
+ this.featuresRoomsByServer[roomUrl] = {};
+ for(let featureRoomName of featuresRoomsNames){
+ this.featuresRoomsByServer[roomUrl][featureRoomName] = await client.joinOrCreate(featureRoomName, options);
+ }
+ }
+
+ roomClient(roomName)
+ {
+ if(!this.roomClients[roomName]){
+ this.roomsUrls[roomName] = this.config.getWithoutLogs('client/rooms/servers/'+roomName, this.serverUrl);
+ Logger.debug('Creating client with URL "'+this.roomsUrls[roomName]+'" for room "'+roomName+'".');
+ this.roomClients[roomName] = new Client(
+ this.roomsUrls[roomName]
+ );
+ }
+ return this.roomClients[roomName];
}
}
diff --git a/lib/game/client/game-dom.js b/lib/game/client/game-dom.js
index b47dcf42b..9e44fecff 100644
--- a/lib/game/client/game-dom.js
+++ b/lib/game/client/game-dom.js
@@ -137,6 +137,7 @@ class GameDom
{
alert(message);
this.getWindow().location.reload();
+ return false;
}
}
diff --git a/lib/game/client/game-engine.js b/lib/game/client/game-engine.js
index 1a1ed314e..a80137959 100644
--- a/lib/game/client/game-engine.js
+++ b/lib/game/client/game-engine.js
@@ -9,7 +9,7 @@ const { Game, Input } = require('phaser');
const { FPSCounter } = require('./fps-counter');
const { GameConst } = require('../constants');
const { ObjectsConst } = require('../../objects/constants');
-const { sc } = require('@reldens/utils');
+const { Logger, sc } = require('@reldens/utils');
class GameEngine extends Game
{
@@ -17,6 +17,7 @@ class GameEngine extends Game
constructor(props)
{
super(props.config);
+ Logger.debug('Game Engine configuration.', props.config);
// @TODO - BETA - Refactor the entire class:
// - Extract all Phaser methods into the engine driver class and implement the engine on the GameManager.
// - Extract the template parsing into a new "template" or "elements" domain driver.
diff --git a/lib/game/client/game-manager.js b/lib/game/client/game-manager.js
index cc9ce3580..1e52d60f2 100644
--- a/lib/game/client/game-manager.js
+++ b/lib/game/client/game-manager.js
@@ -26,10 +26,12 @@ class GameManager
// @NOTE: the game engine will be initialized after the user logged in the game that way we will get the full
// game configuration from the server when the game starts.
this.gameEngine = false;
- // active room is the currently connected server room:
- this.activeRoomEvents = false;
+ /** @type {?RoomEvents} */
+ this.activeRoomEvents = null; // active room is the currently connected server room
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);
@@ -117,16 +119,15 @@ class GameManager
this.userData.password = formData['password'];
// join initial game room, since we return the promise we don't need to catch the error here:
this.gameRoom = await this.gameClient.joinOrCreate(GameConst.ROOM_GAME, this.userData);
+ if(!this.gameRoom){
+ this.displayPlayerCreateError('GameRoom not available, try again later please.');
+ return false;
+ }
await this.events.emit('reldens.beforeJoinGameRoom', this.gameRoom);
this.gameRoom.onMessage('*', async (message) => {
if(message.error){
- let errorElement = this.gameDom.getElement(
- GameConst.SELECTORS.PLAYER_CREATE_FORM+' '+GameConst.SELECTORS.RESPONSE_ERROR
- );
- if(errorElement){
- errorElement.innerHTML = message.message;
- }
- return false;
+ this.displayPlayerCreateError(message);
+ return false;
}
// only the current client will get this message:
if(GameConst.START_GAME === message.act){
@@ -154,11 +155,23 @@ class GameManager
return this.gameRoom;
}
+ displayPlayerCreateError(message)
+ {
+ let errorElement = this.gameDom.getElement(
+ GameConst.SELECTORS.PLAYER_CREATE_FORM+' '+GameConst.SELECTORS.RESPONSE_ERROR
+ );
+ if(!errorElement){
+ return false;
+ }
+ errorElement.innerHTML = message.message;
+ return true;
+ }
+
initializeClient()
{
this.appServerUrl = this.getAppServerUrl();
this.gameServerUrl = this.getGameServerUrl();
- this.gameClient = new GameClient(this.gameServerUrl);
+ this.gameClient = new GameClient(this.gameServerUrl, this.config);
}
async beforeStartGame()
@@ -211,7 +224,9 @@ class GameManager
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', {playerScene}));
+ 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);
// @NOTE: remove the selected scene after the player used it because the login data will be used again every
@@ -233,6 +248,7 @@ class GameManager
if(0 === featuresListKeys.length){
return;
}
+ let featuresRoomsNames = [];
for(let i of featuresListKeys){
let feature = this.features.featuresList[i];
if(!sc.hasOwn(feature, 'joinRooms')){
@@ -244,22 +260,24 @@ class GameManager
// @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.
- this.gameDom.alertReload(
+ return this.gameDom.alertReload(
this.services.translator.t('game.errors.joiningFeatureRoom', {joinRoomName})
);
}
- Logger.debug('Joined room: '+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}}});
}
async reconnectGameClient(message, previousRoom)
{
- let newRoomEvents = this.createRoomEventsInstance(message.player.state.scene);
this.isChangingScene = true;
+ let newRoomEvents = this.createRoomEventsInstance(message.player.state.scene);
this.gameClient.joinOrCreate(newRoomEvents.roomName, this.userData).then(async (sceneRoom) => {
// leave old room:
previousRoom.leave();
@@ -269,10 +287,10 @@ class GameManager
// start listen to the new room events:
await newRoomEvents.activateRoom(sceneRoom, message.prev);
await this.emitActivatedRoom(sceneRoom, message.player.state.scene);
- }).catch((err) => {
+ }).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.', {err, message, previousRoom});
+ Logger.error('Reconnect Game Client error.', {error, message, previousRoom});
this.gameDom.alertReload(this.services.translator.t('game.errors.reconnectClient'));
});
}
@@ -289,6 +307,10 @@ class GameManager
await this.events.emit('reldens.joinedRoom_'+playerScene, sceneRoom, this);
}
+ /**
+ * @param roomName
+ * @returns {RoomEvents}
+ */
createRoomEventsInstance(roomName)
{
return new RoomEvents(roomName, this);
@@ -312,14 +334,12 @@ class GameManager
getUrlFromCurrentReferer(useWebSocket = false)
{
- let protocol = this.gameDom.getWindow().location.protocol;
+ let location = this.gameDom.getWindow().location;
+ let protocol = location.protocol;
if(useWebSocket){
- protocol = protocol.indexOf('https') === 0 ? 'wss:' : 'ws:';
+ protocol = 0 === protocol.indexOf('https') ? 'wss:' : 'ws:';
}
- protocol = protocol + '//';
- let host = this.gameDom.getWindow().location.hostname;
- let port = (this.gameDom.getWindow().location.port ? ':'+this.gameDom.getWindow().location.port : '');
- return protocol+host+port;
+ return protocol + '//'+location.hostname+(location.port ? ':'+location.port : '');
}
getActiveScene()
@@ -333,10 +353,14 @@ class GameManager
return this.gameEngine.scene.getScene('ScenePreloader'+activeSceneKey);
}
+ /**
+ * @returns {PlayerEngine|boolean}
+ */
getCurrentPlayer()
{
let activeScene = this.getActiveScene();
if(!activeScene){
+ //Logger.debug('Missing active scene.');
return false;
}
return activeScene.player;
diff --git a/lib/game/client/handlers/forgot-password-form-handler.js b/lib/game/client/handlers/forgot-password-form-handler.js
index 9f49a08ef..a18da8fa4 100644
--- a/lib/game/client/handlers/forgot-password-form-handler.js
+++ b/lib/game/client/handlers/forgot-password-form-handler.js
@@ -39,7 +39,7 @@ class ForgotPasswordFormHandler
forgot: true,
email: this.form.querySelector(GameConst.SELECTORS.FORGOT_PASSWORD.EMAIL).value
};
- reldens.startGame(formData, false);
+ this.gameManager.startGame(formData, false);
});
}
diff --git a/lib/game/client/joystick.js b/lib/game/client/joystick.js
new file mode 100644
index 000000000..e769356d6
--- /dev/null
+++ b/lib/game/client/joystick.js
@@ -0,0 +1,155 @@
+/**
+ *
+ * Reldens - Joystick
+ *
+ */
+
+const { GameConst } = require('../constants');
+const { Logger } = require('@reldens/utils');
+
+class Joystick
+{
+
+ constructor(props)
+ {
+ this.gameManager = props?.scenePreloader?.gameManager;
+ this.scenePreloader = props?.scenePreloader;
+ this.gameDom = this.gameManager?.gameDom;
+ this.isDragging = false;
+ this.centerX = false;
+ this.centerY = false;
+ this.threshold = this.gameManager.config.getWithoutLogs('client/ui/controls/joystickThreshold', 20);
+ this.joystickLeft = this.gameManager.config.getWithoutLogs('client/ui/controls/joystickLeft', 25);
+ this.joystickTop = this.gameManager.config.getWithoutLogs('client/ui/controls/joystickTop', 25);
+ this.positionSufix = 'px';
+ }
+
+ registerJoystickController()
+ {
+ if(!this.gameManager){
+ Logger.error('GameManager undefined on Joystick.');
+ return false;
+ }
+ this.joystick = this.gameDom.getElement('#joystick');
+ this.joystickThumb = this.gameDom.getElement('#joystick-thumb');
+ this.joystickThumb.addEventListener('mousedown', (event) => {
+ this.applyMovement(event.clientX, event.clientY);
+ });
+ this.joystickThumb.addEventListener('touchstart', (event) => {
+ event.preventDefault();
+ let touch = event.touches?.shift();
+ this.applyMovement(touch.clientX, touch.clientY);
+ });
+ this.gameDom.getDocument().addEventListener('mousemove', this.handleMouseMove.bind(this));
+ this.gameDom.getDocument().addEventListener('mouseup', this.handleStop.bind(this));
+ this.gameDom.getDocument().addEventListener('touchmove', this.handleTouchMove.bind(this));
+ this.gameDom.getDocument().addEventListener('touchend', this.handleStop.bind(this));
+ }
+
+ position(value)
+ {
+ return value+this.positionSufix;
+ }
+
+ applyMovement(clientX, clientY)
+ {
+ this.isDragging = true;
+ let rect = this.joystick.getBoundingClientRect();
+ this.centerX = rect.width / 2;
+ this.centerY = rect.height / 2;
+ this.updateThumbPosition(clientX - rect.left, clientY - rect.top);
+ }
+
+ handleStop()
+ {
+ this.isDragging = false;
+ this.joystickThumb.style.left = this.position(this.joystickLeft);
+ this.joystickThumb.style.top = this.position(this.joystickTop);
+ this.gameManager.getCurrentPlayer().stop();
+ }
+
+ updateDirection(x, y)
+ {
+ let dx = x - this.centerX;
+ let dy = y - this.centerY;
+ let direction = GameConst.STOP;
+ if(Math.abs(dx) > Math.abs(dy)){
+ if(Math.abs(dx) > this.threshold){
+ direction = dx > 0
+ ? (Math.abs(dy) > this.threshold ? (dy > 0 ? 'right-down' : 'right-up') : 'right')
+ : (Math.abs(dy) > this.threshold ? (dy > 0 ? 'left-down' : 'left-up') : 'left');
+ for(let dir of direction.split('-')){
+ try {
+ this.gameManager.getCurrentPlayer()[dir]();
+ } catch (error) {
+ //Logger.debug('Unknown direction on PlayerEngine.', dir, error);
+ }
+ }
+ return direction;
+ }
+ }
+ if(Math.abs(dy) > this.threshold){
+ direction = dy > 0
+ ? (Math.abs(dx) > this.threshold ? (dx > 0 ? 'down-right' : 'down-left') : 'down')
+ : (Math.abs(dx) > this.threshold ? (dx > 0 ? 'up-right' : 'up-left') : 'up');
+ for(let dir of direction.split('-')){
+ try {
+ this.gameManager.getCurrentPlayer()[dir]();
+ } catch (error) {
+ //Logger.debug('Unknown direction on PlayerEngine.', dir, error);
+ }
+ }
+ return direction;
+ }
+ this.gameManager.getCurrentPlayer().stop();
+ return direction;
+ }
+
+ updateThumbPosition(x, y)
+ {
+ let dx = x - this.centerX;
+ let dy = y - this.centerY;
+ let distance = Math.sqrt(dx * dx + dy * dy);
+ let maxDistance = Math.min(this.centerX, this.centerY);
+ if(distance > maxDistance){
+ let angle = Math.atan2(dy, dx);
+ let joystickLeft = Math.cos(angle) * maxDistance + this.centerX - this.joystickThumb.offsetWidth / 2;
+ this.joystickThumb.style.left = this.position(joystickLeft);
+ let joystickTop = Math.sin(angle) * maxDistance + this.centerY - this.joystickThumb.offsetHeight / 2;
+ this.joystickThumb.style.top = this.position(joystickTop);
+ return;
+ }
+ let joystickLeft = x - this.joystickThumb.offsetWidth / 2;
+ this.joystickThumb.style.left = this.position(joystickLeft);
+ let joystickTop = y - this.joystickThumb.offsetHeight / 2;
+ this.joystickThumb.style.top = this.position(joystickTop);
+ }
+
+ handleMouseMove(event)
+ {
+ if(!this.isDragging){
+ return;
+ }
+ let rect = this.joystick.getBoundingClientRect();
+ let x = event.clientX - rect.left;
+ let y = event.clientY - rect.top;
+ this.updateThumbPosition(x, y);
+ this.updateDirection(x, y);
+ }
+
+ handleTouchMove(event)
+ {
+ if(!this.isDragging){
+ return;
+ }
+ let touch = event.touches?.shift();
+ let rect = this.joystick.getBoundingClientRect();
+ let x = touch.clientX - rect.left;
+ let y = touch.clientY - rect.top;
+ this.updateThumbPosition(x, y);
+ this.updateDirection(x, y);
+ }
+
+}
+
+module.exports.Joystick = Joystick;
diff --git a/lib/game/client/minimap-ui.js b/lib/game/client/minimap-ui.js
index 11bf2c1e4..266eb8e38 100644
--- a/lib/game/client/minimap-ui.js
+++ b/lib/game/client/minimap-ui.js
@@ -4,6 +4,8 @@
*
*/
+const { sc } = require('@reldens/utils');
+
class MinimapUi
{
@@ -14,36 +16,68 @@ class MinimapUi
.createFromCache('minimap');
let openButton = scenePreloader.elementsUi['minimap'].getChildByProperty('id', 'minimap-open');
let closeButton = scenePreloader.elementsUi['minimap'].getChildByProperty('id', 'minimap-close');
- closeButton?.addEventListener('click', () => {
+ openButton?.addEventListener('click', () => {
+ let box = scenePreloader.elementsUi['minimap'].getChildByProperty('id', 'minimap-ui');
+ box.classList.remove('hidden');
+ openButton.classList.add('hidden');
let minimap = scenePreloader.gameManager.getActiveScene().minimap;
- if(!minimap.minimapCamera){
- return false;
+ if(!minimap){
+ return;
}
+ this.showMap(minimap, scenePreloader, openButton, closeButton, box);
+ });
+ closeButton?.addEventListener('click', () => {
let box = scenePreloader.elementsUi['minimap'].getChildByProperty('id', 'minimap-ui');
box.classList.add('hidden');
if(openButton){
openButton.classList.remove('hidden');
}
- minimap.minimapCamera.setVisible(false);
- if(minimap.circle){
- minimap.circle.setVisible(false);
- }
- });
- openButton?.addEventListener('click', () => {
let minimap = scenePreloader.gameManager.getActiveScene().minimap;
- if(!minimap.minimapCamera){
- return false;
- }
- let box = scenePreloader.elementsUi['minimap'].getChildByProperty('id', 'minimap-ui');
- box.classList.remove('hidden');
- openButton.classList.add('hidden');
- minimap.minimapCamera.setVisible(true);
- if(minimap.circle){
- minimap.circle.setVisible(true);
+ if(!minimap){
+ return;
}
+ this.hideMap(minimap, scenePreloader, closeButton, box);
});
}
+ showMap(minimap, scenePreloader, openButton, closeButton, box)
+ {
+ if(this.awaitForCamera(minimap)){
+ setTimeout(() => {
+ this.showMap(minimap, scenePreloader, openButton, closeButton, box);
+ }, minimap.awaitOnCamera);
+ return;
+ }
+ minimap.minimapCamera.setVisible(true);
+ if(minimap.circle){
+ minimap.circle.setVisible(true);
+ }
+ scenePreloader.gameManager.events.emit('reldens.openUI', {ui: this, openButton, minimap, box});
+ }
+
+ hideMap(minimap, scenePreloader, closeButton, box)
+ {
+ if(this.awaitForCamera(minimap)){
+ setTimeout(() => {
+ this.hideMap(minimap, scenePreloader, closeButton, box);
+ }, minimap.awaitOnCamera);
+ return;
+ }
+ minimap.minimapCamera.setVisible(false);
+ if (minimap.circle) {
+ minimap.circle.setVisible(false);
+ }
+ scenePreloader.gameManager.events.emit(
+ 'reldens.closeUI',
+ {ui: this, closeButton, minimap, box}
+ );
+ }
+
+ awaitForCamera(minimap)
+ {
+ return 0 < minimap.awaitOnCamera && (!minimap.minimapCamera || !sc.isFunction(minimap.minimapCamera.setVisible));
+ }
+
}
module.exports.MinimapUi = MinimapUi;
diff --git a/lib/game/client/minimap.js b/lib/game/client/minimap.js
index 56d99f655..220740b63 100644
--- a/lib/game/client/minimap.js
+++ b/lib/game/client/minimap.js
@@ -18,6 +18,10 @@ class Minimap
createMap(scene, playerSprite)
{
// @TODO - BETA - Improve camera.
+ this.minimapCamera = false;
+ this.circle = false;
+ this.scope = false;
+ this.awaitOnCamera = sc.get(this.config, 'awaitOnCamera', 400);
this.autoWidth = scene.map.widthInPixels / sc.get(this.config, 'mapWidthDivisor', 1);
this.camWidth = sc.get(this.config, 'fixedWidth', this.autoWidth);
this.autoHeight = scene.map.heightInPixels / sc.get(this.config, 'mapHeightDivisor', 1);
@@ -26,6 +30,15 @@ class Minimap
this.camY = sc.get(this.config, 'camY', 0);
this.camBackgroundColor = sc.get(this.config, 'camBackgroundColor', 'rgba(0,0,0,0.6)');
this.camZoom = sc.get(this.config, 'camZoom', 0.15);
+ this.roundMap = sc.get(this.config, 'roundMap', false);
+ this.addCircle = sc.get(this.config, 'addCircle', false);
+ this.createMinimapCamera(scene, playerSprite);
+ this.createRoundMap(scene);
+ this.events.emitSync('reldens.createdMinimap', this);
+ }
+
+ createMinimapCamera(scene, playerSprite)
+ {
this.minimapCamera = scene.cameras.add(this.camX, this.camY, this.camWidth, this.camHeight)
.setName('minimap')
.setBackgroundColor(this.camBackgroundColor)
@@ -42,24 +55,21 @@ class Minimap
sc.get(this.config, 'mapCameraOriginX', 0.18),
sc.get(this.config, 'mapCameraOriginY', 0.18)
);
- this.roundMap = sc.get(this.config, 'roundMap', false);
- if(this.roundMap){
- // @NOTE: because of the camara zoom the circle size append to the preload scene is different from the map
- // size.
- this.addCircle = sc.get(this.config, 'addCircle', false);
- if(this.addCircle){
- this.addMinimapCircle(scene);
- }
- this.createRoundCamera(scene);
+ }
+
+ createRoundMap(scene)
+ {
+ if(!this.roundMap){
+ return false;
}
- this.events.emitSync('reldens.createdMinimap', this);
+ if(this.addCircle){
+ this.addMinimapCircle(scene);
+ }
+ this.createRoundCamera(scene);
}
addMinimapCircle(scene)
{
- if(this.circle){
- return true;
- }
let activeScenePreloader = scene.gameManager.getActiveScenePreloader();
this.circle = activeScenePreloader.add.circle(
sc.get(this.config, 'circleX', 220),
@@ -81,15 +91,20 @@ class Minimap
createRoundCamera(scene)
{
- if(!this.scope){
- this.scope = scene.add.graphics();
- this.scope.fillStyle(0x000000, 0).fillCircle(
- sc.get(this.config, 'circleX', 220),
- sc.get(this.config, 'circleY', 88),
- sc.get(this.config, 'circleRadio', 80.35)
- );
- this.minimapCamera.setMask(this.scope.createGeometryMask());
- }
+ this.scope = scene.add.graphics();
+ this.scope.fillStyle(0x000000, 0).fillCircle(
+ sc.get(this.config, 'circleX', 220),
+ sc.get(this.config, 'circleY', 88),
+ sc.get(this.config, 'circleRadio', 80.35)
+ );
+ this.minimapCamera.setMask(this.scope.createGeometryMask());
+ }
+
+ destroyMap()
+ {
+ delete this.minimapCamera;
+ delete this.circle;
+ delete this.scope;
}
}
diff --git a/lib/game/client/room-events.js b/lib/game/client/room-events.js
index 24120ad1b..55da27a79 100644
--- a/lib/game/client/room-events.js
+++ b/lib/game/client/room-events.js
@@ -8,7 +8,7 @@ const { PlayerEngine } = require('../../users/client/player-engine');
const { SceneDynamic } = require('./scene-dynamic');
const { ScenePreloader } = require('./scene-preloader');
const { GameConst } = require('../constants');
-const { Logger, sc } = require('@reldens/utils');
+const { ErrorManager, Logger, sc } = require('@reldens/utils');
class RoomEvents
{
@@ -28,6 +28,13 @@ class RoomEvents
// @TODO - BETA - Move the following inside a single property called "metadata" and set each on their plugins.
this.objectsUi = {};
this.tradeUi = {};
+ this.gameOverRetries = 0;
+ this.gameOverMaxRetries = 0;
+ this.gameOverRetryTime = 200;
+ this.automaticallyCloseAllDialogsOnSceneChange = gameManager.config.getWithoutLogs(
+ 'client/rooms/automaticallyCloseAllDialogsOnSceneChange',
+ true
+ );
}
async activateRoom(room, previousScene = false)
@@ -53,34 +60,30 @@ class RoomEvents
listenPlayerAndStateChanges(player, key)
{
+ // @TODO - BETA - Remove hardcoded "state" property and "inState" sub-property.
+ let currentPlayerId = this.gameManager.getCurrentPlayer().player_id;
let playerProps = Object.keys(player);
let stateProps = Object.keys(player.state);
for(let prop of playerProps){
player.listen(prop, (value) => {
- this.updatePlayerProperties(prop, stateProps, player, value);
this.playersOnChange(player, key, 'playerChange');
});
}
for(let prop of stateProps){
player.state.listen(prop, (value) => {
+ //Logger.debug('Updating state.', {prop, value});
player.state[prop] = value;
this.playersOnChange(player, key, 'playerChange');
+ if('inState' === prop && player.player_id === currentPlayerId){
+ if(GameConst.STATUS.DEATH === value){
+ return this.showGameOverBox();
+ }
+ this.hideGameOverBox();
+ }
});
}
}
- updatePlayerProperties(prop, stateProps, player, value)
- {
- // @TODO - BETA - Refactor, remove hardcoded "state", use a new method like hasSubProperties().
- if(prop === 'state'){
- for(let stateProp of stateProps){
- player.state[stateProp] = value[stateProp];
- }
- return;
- }
- player[prop] = value;
- }
-
checkAndCreateScene()
{
if(!this.room.state || this.room.state.sceneData === this.sceneData){
@@ -160,13 +163,22 @@ class RoomEvents
// do not move the player if it is changing the scene:
if(player.state.scene !== this.roomName){
if(player.player_id === this.gameManager.getCurrentPlayer().player_id && !this.gameManager.isChangingScene){
- Logger.info('Player scene miss match.', {playerScene: player.state.scene, currentScene: this.roomName});
+ Logger.info(
+ 'Player scene miss match.',
+ {
+ currentScene: this.roomName,
+ playerSceneOnState: player?.state.scene,
+ player: player?.sessionId,
+ currentPlayer: this.gameManager.getCurrentPlayer()?.playerId,
+ isChangingScene: this.gameManager.isChangingScene
+ }
+ );
}
return;
}
let currentScene = this.getActiveScene();
if(!this.playerExists(currentScene, key)){
- /* @NOTE: this is expected to happen when the player is created in the scene, so we don't log the error.
+ /* @NOTE: this is expected to happen when the player is being created in the scene.
Logger.info('Player not found in current scene.', {
player: currentScene?.player,
currentKeys: Object.keys(currentScene?.player?.players || {}),
@@ -254,6 +266,7 @@ class RoomEvents
{
let listenerKey = sc.get(message, 'listener', '');
if('' === listenerKey){
+ //Logger.debug('ListenerKey undefined for message in room events.', message);
return false;
}
let defaultListeners = this.gameManager.config.get('client/message/listeners', {});
@@ -270,6 +283,7 @@ class RoomEvents
Logger.error('Listener is missing "executeClientMessageActions" method.', listener);
return false;
}
+ //Logger.debug({executeClientMessageActions: message});
listener['executeClientMessageActions']({message, roomEvents: this});
}
@@ -281,7 +295,7 @@ class RoomEvents
// @NOTE: now this method will update the stats every time the stats action is received but the UI will be
// created only once in the preloader.
await this.events.emit('reldens.playerStatsUpdateBefore', message, this);
- await this.updatePlayerStats(message);
+ return await this.updatePlayerStats(message);
}
async runReconnect(message)
@@ -297,6 +311,8 @@ class RoomEvents
async runChangeScene(message)
{
if(message.act === GameConst.CHANGING_SCENE && this.room.sessionId === message.id){
+ this.gameManager.isChangingScene = true;
+ this.closeAllActiveDialogs();
this.gameManager.getActiveScene().scene.setVisible(false);
}
if(
@@ -307,7 +323,6 @@ class RoomEvents
return false;
}
await this.events.emit('reldens.startChangedScene', {message, roomEvents: this});
- this.closeAllActiveDialogs();
let currentScene = this.getActiveScene();
// if other users enter the current scene we need to add them:
let {id, x, y, dir, playerName, playedTime, avatarKey, player_id} = message;
@@ -315,12 +330,16 @@ class RoomEvents
let leftOff = this.gameManager.config.get('client/players/size/leftOffset');
let addPlayerData = {x: (x - leftOff), y: (y - topOff), dir, playerName, playedTime, avatarKey, player_id};
currentScene.player.addPlayer(id, addPlayerData);
+ this.gameManager.isChangingScene = false;
await this.events.emit('reldens.endChangedScene', {message, roomEvents: this});
}
closeAllActiveDialogs()
{
- let closeButtons = this.gameManager.gameDom.getElements('.ui-box .box-close');
+ if(!this.automaticallyCloseAllDialogsOnSceneChange){
+ return;
+ }
+ let closeButtons = this.gameManager.gameDom.getElements('.box-close');
if(0 === closeButtons.length){
return;
}
@@ -344,6 +363,7 @@ class RoomEvents
if(sc.hasOwn(showSprite, 'nameSprite') && showSprite.nameSprite){
showSprite.nameSprite.visible = true;
}
+ this.getActiveScene().stopOnDeathOrDisabledSent = false;
}
async runGameOver(message)
@@ -351,17 +371,56 @@ class RoomEvents
if(message.act !== GameConst.GAME_OVER){
return false;
}
- let defaultBehavior = true;
- await this.events.emit('reldens.runGameOver', {message, defaultBehavior, roomEvents: this});
- if(!defaultBehavior){
+ try {
+ let defaultBehavior = true;
+ await this.events.emit('reldens.runGameOver', {message, defaultBehavior, roomEvents: this});
+ if(!defaultBehavior){
+ return false;
+ }
+ await this.events.emit('reldens.gameOver', message, this);
+ this.gameManager.gameOver = true;
+ let currentPlayer = this.gameManager.getCurrentPlayer();
+ if(!currentPlayer){
+ if(this.gameOverRetries < this.gameOverMaxRetries){
+ setTimeout(() => this.runGameOver(message), this.gameOverRetryTime);
+ this.gameOverRetries++;
+ }
+ return false;
+ }
+ let currentPlayerSprite = currentPlayer.players[currentPlayer.playerId];
+ currentPlayerSprite.visible = false;
+ this.showGameOverBox();
+ } catch (error) {
+ setTimeout(() => this.runGameOver(message), 200);
+ this.gameOverRetries++;
return false;
}
- await this.events.emit('reldens.gameOver', message, this);
- this.gameManager.gameOver = true;
- let currentPlayer = this.gameManager.getCurrentPlayer();
- let currentPlayerSprite = currentPlayer.players[currentPlayer.playerId];
- currentPlayerSprite.visible = false;
- this.gameManager.gameDom.getElement('#game-over').classList.remove('hidden');
+ }
+
+ showGameOverBox()
+ {
+ return this.displayGameOverBox(true);
+ }
+
+ hideGameOverBox()
+ {
+ return this.displayGameOverBox(false);
+ }
+
+ displayGameOverBox(display)
+ {
+ Logger.debug('Display game over box: '+(display ? 'yes' : 'no')+'.');
+ let gameOverElement = this.gameManager.gameDom.getElement('#game-over');
+ if(!gameOverElement){
+ Logger.debug('GameOver box element not found.');
+ return false;
+ }
+ if(display){
+ gameOverElement.classList.remove('hidden');
+ return true;
+ }
+ gameOverElement.classList.add('hidden');
+ return false;
}
async roomOnLeave(code)
@@ -397,8 +456,9 @@ class RoomEvents
}
let currentScene = this.getActiveScene();
if(!currentScene.player || !sc.hasOwn(currentScene.player.players, this.room.sessionId)){
- // @TODO - BETA - Refactor and fix cases.
- Logger.error('For some reason you hit this case which should not happen.', this.room, currentScene);
+ // @NOTE: this can happen when you get killed and logout, then on login you will have 0 life points and
+ // if you get killed automatically again you will hit a player stats update before the player gets ready.
+ Logger.error('Player not available.', this.room, currentScene);
return false;
}
let playerSprite = currentScene.player.players[this.room.sessionId];
@@ -449,6 +509,12 @@ class RoomEvents
}
}
+ uiSetTitleAndContent(uiBox, props, uiScene)
+ {
+ this.uiSetTitle(uiBox, props);
+ this.uiSetContent(uiBox, props, uiScene);
+ }
+
uiSetTitle(uiBox, props)
{
let newTitle = sc.get(props, 'title', false);
@@ -661,7 +727,6 @@ class RoomEvents
Logger.warning('Missing previous scene instance.', previousSceneInstance);
return false;
}
- // previousSceneInstance.scene.setVisible(false);
await previousSceneInstance.changeScene();
this.gameEngine.scene.stop(previousScene);
this.gameEngine.scene.start(player.state.scene);
@@ -704,14 +769,21 @@ class RoomEvents
send(data, key)
{
try {
+ if(
+ this.room.connection.transport.ws.readyState === this.room.connection.transport.ws.CLOSED
+ || this.room.connection.transport.ws.readyState === this.room.connection.transport.ws.CLOSING
+ ){
+ ErrorManager.error('Connection lost.');
+ }
if(!key){
key = '*';
}
this.room.send(key, data);
+ return true;
} catch (error) {
- Logger.critical(error, data);
- this.gameManager.gameDom.alertReload(this.gameManager.services.translator.t('game.errors.connectionLost'));
+ Logger.critical(error.message, data);
}
+ this.gameManager.gameDom.alertReload(this.gameManager.services.translator.t('game.errors.connectionLost'));
}
}
diff --git a/lib/game/client/scene-dynamic.js b/lib/game/client/scene-dynamic.js
index 5c0447810..02cb0ab20 100644
--- a/lib/game/client/scene-dynamic.js
+++ b/lib/game/client/scene-dynamic.js
@@ -29,12 +29,13 @@ class SceneDynamic extends Scene
this.objectsAnimationsData = false;
this.objectsAnimations = {};
this.setPropertiesFromConfig();
- this.minimap = this.minimapConfig.enabled ? this.createMinimapInstance(this.minimapConfig) : false;
+ this.minimap = this.createMinimapInstance(this.minimapConfig);
this.player = false;
this.interpolatePlayersPosition = {};
this.interpolateObjectsPositions = {};
this.tilesets = [];
this.tilesetAnimations = [];
+ this.stopOnDeathOrDisabledSent = false;
}
setPropertiesFromConfig()
@@ -56,6 +57,9 @@ class SceneDynamic extends Scene
createMinimapInstance(config)
{
+ if(!this.minimapConfig.enabled){
+ return false;
+ }
return new Minimap({config, events: this.eventsManager});
}
@@ -74,9 +78,9 @@ class SceneDynamic extends Scene
await this.createSceneMap();
this.cameras.main.on('camerafadeincomplete', () => {
this.transition = false;
- this.gameManager.isChangingScene = false;
this.gameManager.gameDom.activeElement().blur();
this.minimap.createMap(this, this.gameManager.getCurrentPlayerAnimation());
+ this.gameManager.isChangingScene = false;
});
this.eventsManager.emitSync('reldens.afterSceneDynamicCreate', this);
}
@@ -117,13 +121,16 @@ class SceneDynamic extends Scene
for(let imageKey of this.params.sceneImages){
let tileset = this.map.addTilesetImage(this.params.roomName, imageKey);
if(!tileset){
- Logger.critical('Tileset could not be created.', {
- roomName: this.params.roomName,
- imageKeys: this.params.sceneImages,
- createdTileset: tileset
- });
+ Logger.critical(
+ 'Tileset creation error. Check if the tileset name equals the imageKey without the extension.',
+ {
+ roomName: this.params.roomName,
+ imageKeys: this.params.sceneImages,
+ createdTileset: tileset
+ }
+ );
}
- Logger.debug('Created tileset.', imageKey, this.params.roomName);
+ //Logger.debug('Created tileset.', imageKey, this.params.roomName);
this.tilesets.push(tileset);
}
this.registerLayers();
@@ -278,6 +285,13 @@ class SceneDynamic extends Scene
if(this.transition || this.gameManager.isChangingScene){
return;
}
+ if(this.player.isDeath() || this.player.isDisabled()){
+ if(!this.stopOnDeathOrDisabledSent){
+ this.player.fullStop();
+ }
+ this.stopOnDeathOrDisabledSent = true;
+ return;
+ }
// @TODO - BETA - Controllers will be part of the configuration in the database.
if(this.keyRight.isDown || this.keyD.isDown){
this.player.right();
@@ -324,8 +338,15 @@ class SceneDynamic extends Scene
delete this.interpolatePlayersPosition[i];
continue;
}
- let newX = Phaser.Math.Linear(entity.x, (entityState.x - this.player.leftOff), this.interpolationSpeed);
- let newY = Phaser.Math.Linear(entity.y, (entityState.y - this.player.topOff), this.interpolationSpeed);
+ let newX = sc.roundToPrecision(
+ Phaser.Math.Linear(entity.x, (entityState.x - this.player.leftOff), this.interpolationSpeed),
+ 2
+ );
+ let newY = sc.roundToPrecision(
+ Phaser.Math.Linear(entity.y, (entityState.y - this.player.topOff), this.interpolationSpeed),
+ 2
+ );
+ //Logger.debug('Player interpolation update.', newX, newY);
this.player.processPlayerPositionAnimationUpdate(entity, entityState, i, newX, newY);
if(!entityState.mov){
delete this.interpolatePlayersPosition[i];
@@ -363,10 +384,9 @@ class SceneDynamic extends Scene
delete this.interpolateObjectsPositions[i];
return;
}
- let bodyData = {
- x: Phaser.Math.Linear(entity.x, entityState.x, this.interpolationSpeed),
- y: Phaser.Math.Linear(entity.y, entityState.y, this.interpolationSpeed)
- };
+ let x = sc.roundToPrecision(Phaser.Math.Linear(entity.x, entityState.x, this.interpolationSpeed), 0);
+ let y = sc.roundToPrecision(Phaser.Math.Linear(entity.y, entityState.y, this.interpolationSpeed), 0);
+ let bodyData = {x, y};
objectsPlugin.updateBulletBodyPosition(i, bodyData);
if(!entityState.mov){
delete this.interpolateObjectsPositions[i];
@@ -392,12 +412,9 @@ class SceneDynamic extends Scene
delete this.interpolateObjectsPositions[i];
return;
}
- let bodyData = {
- x: Phaser.Math.Linear(entity.x, entityState.x, this.interpolationSpeed),
- y: Phaser.Math.Linear(entity.y, entityState.y, this.interpolationSpeed),
- inState: entityState.inState,
- mov: entityState.mov
- };
+ let x = sc.roundToPrecision(Phaser.Math.Linear(entity.x, entityState.x, this.interpolationSpeed), 0);
+ let y = sc.roundToPrecision(Phaser.Math.Linear(entity.y, entityState.y, this.interpolationSpeed), 0);
+ let bodyData = {x, y, inState: entityState.inState, mov: entityState.mov};
objectsPlugin.updateObjectsAnimations(i, bodyData, this);
if(!entityState.mov){
delete this.interpolateObjectsPositions[i];
@@ -410,11 +427,12 @@ class SceneDynamic extends Scene
Logger.warning('None entity found to compare current entity position.');
return false;
}
- return entity.x === entityState.x && entity.y === entityState.y;
+ return Math.round(entity.x) === Math.round(entityState.x) && Math.round(entity.y) === Math.round(entityState.y);
}
async changeScene()
{
+ this.minimap?.destroyMap();
this.eventsManager.emitSync('reldens.changeSceneDestroyPrevious', this);
this.objectsAnimations = {};
this.objectsAnimationsData = false;
diff --git a/lib/game/client/scene-preloader.js b/lib/game/client/scene-preloader.js
index 47a7aa29d..7bed84125 100644
--- a/lib/game/client/scene-preloader.js
+++ b/lib/game/client/scene-preloader.js
@@ -8,6 +8,7 @@ const { Scene, Geom } = require('phaser');
const { MinimapUi } = require('./minimap-ui');
const { InstructionsUi } = require('./instructions-ui');
const { SettingsUi } = require('./settings-ui');
+const { Joystick } = require('./joystick');
const { GameConst } = require('../constants');
const { ActionsConst } = require('../../actions/constants');
const { Logger, sc } = require('@reldens/utils');
@@ -18,7 +19,6 @@ class ScenePreloader extends Scene
constructor(props)
{
super({key: props.name});
- this.holdTimer = null;
this.progressBar = null;
this.progressCompleteRect = null;
this.progressRect = null;
@@ -41,6 +41,8 @@ class ScenePreloader extends Scene
frameWidth: this.gameManager.config.get('client/players/size/width', 52),
frameHeight: this.gameManager.config.get('client/players/size/height', 71)
};
+ this.useJoystick = this.gameManager.config.getWithoutLogs('client/ui/controls/useJoystick', false);
+ this.joystick = new Joystick({scenePreloader: this});
}
preload()
@@ -105,6 +107,9 @@ class ScenePreloader extends Scene
if(this.gameManager.config.get('client/ui/controls/enabled')){
this.load.html('controls', '/assets/html/ui-controls.html');
}
+ if(this.useJoystick){
+ this.load.html('joystick', '/assets/html/ui-joystick.html');
+ }
if(this.gameManager.config.get('client/ui/sceneLabel/enabled')){
this.load.html('sceneLabel', '/assets/html/ui-scene-label.html');
}
@@ -118,11 +123,17 @@ class ScenePreloader extends Scene
this.load.html('settings', '/assets/html/ui-settings.html');
this.load.html('settings-content', '/assets/html/ui-settings-content.html');
}
- this.load.html('uiTarget', '/assets/html/ui-target.html');
- this.load.html('uiOptionButton', '/assets/html/ui-option-button.html');
- this.load.html('uiOptionIcon', '/assets/html/ui-option-icon.html');
- this.load.html('uiOptionsContainer', '/assets/html/ui-options-container.html');
- this.load.html('uiLoading', '/assets/html/ui-loading.html');
+ if(this.gameManager.config.getWithoutLogs('client/ui/preloadTarget/enabled', true)){
+ this.load.html('uiTarget', '/assets/html/ui-target.html');
+ }
+ if(this.gameManager.config.getWithoutLogs('client/ui/preloadOptionsTemplates/enabled', true)){
+ this.load.html('uiOptionButton', '/assets/html/ui-option-button.html');
+ this.load.html('uiOptionIcon', '/assets/html/ui-option-icon.html');
+ this.load.html('uiOptionsContainer', '/assets/html/ui-options-container.html');
+ }
+ if(this.gameManager.config.getWithoutLogs('client/ui/preloadLoading/enabled', true)){
+ this.load.html('uiLoading', '/assets/html/ui-loading.html');
+ }
this.eventsManager.emitSync('reldens.preloadUiScene', this);
}
@@ -133,7 +144,7 @@ class ScenePreloader extends Scene
}
for(let imageFile of this.preloadImages){
this.load.image(imageFile, `/assets/maps/${imageFile}`);
- Logger.debug('Preload map image: "'+imageFile+'".');
+ //Logger.debug('Preload map image: "'+imageFile+'".');
}
}
@@ -222,8 +233,22 @@ class ScenePreloader extends Scene
if(!controlsUi.enabled){
return;
}
- this.elementsUi['controls'] = this.add.dom(controlsUi.uiX, controlsUi.uiY).createFromCache('controls');
- this.registerControllers(this.elementsUi['controls']);
+ if(this.useJoystick){
+ this.elementsUi['controls'] = this.createUi('joystick', controlsUi);
+ return this.joystick.registerJoystickController();
+ }
+ this.elementsUi['controls'] = this.createUi('controls', controlsUi);
+ return this.registerControllers(this.elementsUi['controls']);
+ }
+
+ createUi(key, uiConfig)
+ {
+ return this.createContent(key, uiConfig.uiX, uiConfig.uiY);
+ }
+
+ createContent(key, x, y)
+ {
+ return this.add.dom(x, y).createFromCache(key);
}
createSceneLabelBox()
@@ -232,7 +257,7 @@ class ScenePreloader extends Scene
if(!sceneLabelUi.enabled){
return;
}
- this.elementsUi['sceneLabel'] = this.add.dom(sceneLabelUi.uiX, sceneLabelUi.uiY).createFromCache('sceneLabel');
+ this.elementsUi['sceneLabel'] = this.createUi('sceneLabel', sceneLabelUi);
}
createTargetUi()
@@ -241,7 +266,7 @@ class ScenePreloader extends Scene
if(!targetUi.enabled){
return;
}
- this.uiTarget = this.add.dom(targetUi.uiX, targetUi.uiY).createFromCache('uiTarget');
+ this.uiTarget = this.createUi('uiTarget', targetUi);
let closeButton = this.uiTarget.getChildByProperty('className', 'close-target');
closeButton.addEventListener('click', () => {
this.gameManager.gameEngine.clearTarget();
@@ -254,7 +279,7 @@ class ScenePreloader extends Scene
if(!playerBox.enabled){
return;
}
- this.elementsUi['playerBox'] = this.add.dom(playerBox.uiX, playerBox.uiY).createFromCache('playerBox');
+ this.elementsUi['playerBox'] = this.createUi('playerBox', playerBox);
let logoutButton = this.elementsUi['playerBox'].getChildByProperty('id', 'logout');
logoutButton?.addEventListener('click', () => {
this.gameManager.forcedDisconnection = true;
@@ -304,33 +329,36 @@ class ScenePreloader extends Scene
createPlayerAnimations(avatarKey)
{
- let defaultFrames = this.gameManager.config.get('client/players/animations/defaultFrames');
+ let avatarFrames = this.gameManager.config.getWithoutLogs(
+ 'client/players/animations/'+avatarKey+'Frames',
+ this.gameManager.config.get('client/players/animations/defaultFrames')
+ );
let availableAnimations = [{
k: avatarKey + '_' + GameConst.LEFT,
img: avatarKey,
- start: defaultFrames.left.start || 3,
- end: defaultFrames.left.end || 5,
+ start: avatarFrames.left.start || 3,
+ end: avatarFrames.left.end || 5,
repeat: -1,
hide: false
}, {
k: avatarKey + '_' + GameConst.RIGHT,
img: avatarKey,
- start: defaultFrames.right.start || 6,
- end: defaultFrames.right.end || 8,
+ start: avatarFrames.right.start || 6,
+ end: avatarFrames.right.end || 8,
repeat: -1,
hide: false
}, {
k: avatarKey + '_' + GameConst.UP,
img: avatarKey,
- start: defaultFrames.up.start || 9,
- end: defaultFrames.up.end || 11,
+ start: avatarFrames.up.start || 9,
+ end: avatarFrames.up.end || 11,
repeat: -1,
hide: false
}, {
k: avatarKey + '_' + GameConst.DOWN,
img: avatarKey,
- start: defaultFrames.down.start || 0,
- end: defaultFrames.down.end || 2,
+ start: avatarFrames.down.start || 0,
+ end: avatarFrames.down.end || 2,
repeat: -1,
hide: false
}
@@ -369,7 +397,7 @@ class ScenePreloader extends Scene
repeat: anim.repeat,
hideOnComplete: sc.get(anim, 'hide', true),
};
- Logger.debug('Creating animation: '+anim.k, animationConfig);
+ //Logger.debug('Creating animation: '+anim.k, animationConfig);
this.gameManager.createdAnimations[anim.k] = this.anims.create(animationConfig);
return this.gameManager.createdAnimations[anim.k];
}
@@ -478,43 +506,51 @@ class ScenePreloader extends Scene
if(this.gameManager.config.get('client/ui/controls/opacityEffect')){
button.classList.remove('button-opacity-off');
}
- clearTimeout(this.holdTimer);
this.gameManager.activeRoomEvents.send({act: GameConst.STOP});
}
createProgressBar()
{
+ if(!this.gameManager.config.getWithoutLogs('client/ui/loading/show', true)){
+ return;
+ }
let Rectangle = Geom.Rectangle;
let main = Rectangle.Clone(this.cameras.main);
this.progressRect = new Rectangle(0, 0, main.width / 2, 50);
Rectangle.CenterOn(this.progressRect, main.centerX, main.centerY);
this.progressCompleteRect = Geom.Rectangle.Clone(this.progressRect);
- this.progressBar = this.add.graphics();
+ this.progressBar = this.createGraphics();
let width = this.cameras.main.width;
let height = this.cameras.main.height;
- let loadingFont = this.gameManager.config.get('client/ui/loading/font');
+ let fontFamily = this.gameManager.config.get('client/ui/loading/font');
let loadingFontSize = this.gameManager.config.get('client/ui/loading/fontSize');
let loadingAssetsSize = this.gameManager.config.get('client/ui/loading/assetsSize');
- this.loadingText = this.add.text(width / 2, height / 2 - 50, 'Loading...', {
- fontFamily: loadingFont,
- fontSize: loadingFontSize
- });
+ this.loadingText = this.createText(
+ width / 2,
+ height / 2 - 50,
+ 'Loading...',
+ {fontFamily, fontSize: loadingFontSize}
+ );
this.loadingText.setOrigin(0.5, 0.5);
this.loadingText.setFill(this.gameManager.config.get('client/ui/loading/loadingColor'));
- this.percentText = this.add.text(width / 2, height / 2 - 5, '0%', {
- fontFamily: loadingFont,
- fontSize: loadingAssetsSize
- });
+ this.percentText = this.createText(width / 2, height / 2 - 5, '0%', {fontFamily, fontSize: loadingAssetsSize});
this.percentText.setOrigin(0.5, 0.5);
this.percentText.setFill(this.gameManager.config.get('client/ui/loading/percentColor'));
- this.assetText = this.add.text(width / 2, height / 2 + 50, '', {
- fontFamily: loadingFont,
- fontSize: loadingAssetsSize
- });
+ this.assetText = this.createText(width / 2, height / 2 + 50, '', {fontFamily, fontSize: loadingAssetsSize});
this.assetText.setFill(this.gameManager.config.get('client/ui/loading/assetsColor'));
this.assetText.setOrigin(0.5, 0.5);
}
+ createText(width, height, text, styles)
+ {
+ return this.add.text(width, height, text, styles);
+ }
+
+ createGraphics()
+ {
+ return this.add.graphics();
+ }
+
onLoadComplete()
{
for(let child of this.children.list){
diff --git a/lib/game/client/snippets/en_US.js b/lib/game/client/snippets/en_US.js
index 3e6a403e8..10901cd7e 100644
--- a/lib/game/client/snippets/en_US.js
+++ b/lib/game/client/snippets/en_US.js
@@ -12,7 +12,7 @@ module.exports = {
errors: {
missingClasses: 'None configured classes available.',
missingPlayerData: 'Missing player data.',
- joiningRoom: 'There was an error while joining the room "%joinRoomName".',
+ joiningRoom: 'There was an error while joining the room "%joinRoomName", please try again later.',
joiningFeatureRoom: 'There was an error while joining the feature room "%joinRoomName".',
reconnectClient: 'Reconnect Game Client error.',
sessionEnded: 'Your session ended, please login again.',
diff --git a/lib/game/client/user-interface.js b/lib/game/client/user-interface.js
index 8ef02ca81..6320627c5 100644
--- a/lib/game/client/user-interface.js
+++ b/lib/game/client/user-interface.js
@@ -13,12 +13,15 @@ class UserInterface
constructor(gameManager, animProps, template = '/assets/html/dialog-box.html', uiPositionKey)
{
this.events = gameManager.events;
+ this.gameDom = gameManager.gameDom;
this.initialTitle = '';
this.initialContent = '';
this.id = animProps.id;
this.animProps = animProps;
this.template = template;
this.uiPositionKey = uiPositionKey || 'default';
+ this.openButton = null;
+ this.closeButton = null;
this.listenEvents();
}
@@ -49,13 +52,12 @@ class UserInterface
templateKey = this.id;
}
let objectElementId = 'box-'+this.id;
- let gameDom = uiScene.gameManager.gameDom;
if(sc.get(uiScene.elementsUi, this.id)){
return this;
}
let dialogBox = this.createDialogBox(uiScene, templateKey);
this.createBoxContent(uiScene, templateKey, dialogBox);
- let dialogContainer = gameDom.getElement('.ui-box.ui-dialog-box', dialogBox.node);
+ let dialogContainer = this.gameDom.getElement('.ui-box.ui-dialog-box', dialogBox.node);
if(!dialogContainer){
Logger.critical('Missing dialog container for template key: "'+templateKey+'".', {
dialogBox,
@@ -66,8 +68,8 @@ class UserInterface
}
dialogContainer.id = objectElementId;
dialogContainer.classList.add('type-'+(this.animProps?.type || 'dialog-box'));
- let openButton = this.activateOpenButton(dialogBox, dialogContainer, uiScene, gameDom);
- this.activateCloseButton(dialogBox, dialogContainer, openButton, uiScene, gameDom);
+ this.activateOpenButton(dialogBox, dialogContainer, uiScene);
+ this.activateCloseButton(dialogBox, dialogContainer, uiScene);
uiScene.userInterfaces[this.id] = this;
uiScene.elementsUi[this.id] = dialogBox;
// @TODO - BETA - refactor to return the created dialog box.
@@ -90,19 +92,19 @@ class UserInterface
});
}
- activateOpenButton(dialogBox, dialogContainer, uiScene, gameDom)
+ activateOpenButton(dialogBox, dialogContainer, uiScene)
{
// @TODO - BETA - Extract into a new service.
- let openButton = gameDom.getElement('.'+GameConst.UI_BOX + GameConst.UI_OPEN, dialogBox.node);
- if(!openButton){
+ this.openButton = this.gameDom.getElement('.'+GameConst.UI_BOX + GameConst.UI_OPEN, dialogBox.node);
+ if(!this.openButton){
return false;
}
- openButton.id = GameConst.UI_BOX + GameConst.UI_OPEN + '-' + this.id
- openButton.addEventListener('click', () => {
+ this.openButton.id = GameConst.UI_BOX + GameConst.UI_OPEN + '-' + this.id
+ this.openButton.addEventListener('click', () => {
// @TODO - BETA - Replace styles classes.
if(sc.get(this.animProps, 'defaultOpen', true)){
dialogContainer.style.display = 'block';
- openButton.style.display = 'none';
+ this.openButton.style.display = 'none';
if(false !== sc.get(this.animProps, 'depth', false)){
dialogBox.setDepth(this.animProps.depth);
}
@@ -110,28 +112,31 @@ class UserInterface
if(sc.isFunction(this.animProps['openCallBack'])){
this.animProps['openCallBack']();
}
- this.events.emit('reldens.openUI', {ui: this, openButton, dialogBox, dialogContainer, uiScene});
+ this.events.emit(
+ 'reldens.openUI',
+ {ui: this, openButton: this.openButton, dialogBox, dialogContainer, uiScene}
+ );
});
- return openButton;
+ return this.openButton;
}
- activateCloseButton(dialogBox, dialogContainer, openButton, uiScene, gameDom)
+ activateCloseButton(dialogBox, dialogContainer, uiScene)
{
// @TODO - BETA - Extract into a new service.
- let closeButton = gameDom.getElement('.'+GameConst.UI_BOX + GameConst.UI_CLOSE, dialogBox.node);
- if(!closeButton){
+ this.closeButton = this.gameDom.getElement('.'+GameConst.UI_BOX + GameConst.UI_CLOSE, dialogBox.node);
+ if(!this.closeButton){
return false;
}
- closeButton.id = GameConst.UI_BOX + GameConst.UI_CLOSE + '-' + this.id;
- closeButton.addEventListener('click', () => {
+ this.closeButton.id = GameConst.UI_BOX + GameConst.UI_CLOSE + '-' + this.id;
+ this.closeButton.addEventListener('click', () => {
if(!sc.hasOwn(this.animProps, 'sendCloseMessage') || false === this.animProps['sendCloseMessage']){
uiScene.gameManager.activeRoomEvents.send({act: GameConst.CLOSE_UI_ACTION, id: this.id});
}
// @TODO - BETA - Replace styles classes.
if(sc.get(this.animProps, 'defaultClose', true)){
dialogContainer.style.display = 'none';
- if(openButton){
- openButton.style.display = 'block';
+ if(this.openButton){
+ this.openButton.style.display = 'block';
}
if(false !== sc.get(this.animProps, 'depth', false)){
dialogBox.setDepth(1);
@@ -140,10 +145,14 @@ class UserInterface
if(sc.isFunction(this.animProps['closeCallback'])){
this.animProps['closeCallback']();
}
- this.events.emit(
- 'reldens.closeUI',
- {ui: this, closeButton, openButton, dialogBox, dialogContainer, uiScene}
- );
+ this.events.emit('reldens.closeUI', {
+ ui: this,
+ closeButton: this.closeButton,
+ openButton: this.openButton,
+ dialogBox,
+ dialogContainer,
+ uiScene
+ });
});
}
diff --git a/lib/game/constants.js b/lib/game/constants.js
index e8cc0ede5..f3504ce93 100644
--- a/lib/game/constants.js
+++ b/lib/game/constants.js
@@ -5,6 +5,7 @@
*/
module.exports.GameConst = {
+ // @TODO - BETA - Move all the actions like "START_GAME" into ACTIONS property.
START_GAME: 's',
ACTION_KEY: 'act',
CREATE_PLAYER: 'cp',
@@ -12,6 +13,7 @@ module.exports.GameConst = {
CHANGING_SCENE: 'cgs',
CHANGED_SCENE: 'cs',
RECONNECT: 'r',
+ // @TODO - BETA - Move into "rooms" plugin.
ROOM_GAME: 'room_game',
ROOM_NAME_MAP: 'Map',
SCENE_PRELOADER: 'ScenePreloader',
@@ -27,6 +29,7 @@ module.exports.GameConst = {
UI_BOX: 'box',
UI_CLOSE: '-close',
UI_OPEN: '-open',
+ // @TODO - BETA - Move into DIRECTIONS property.
UP: 'up',
LEFT: 'left',
DOWN: 'down',
@@ -35,6 +38,9 @@ module.exports.GameConst = {
POINTER: 'mp',
ARROW_DOWN: 'ard',
IMAGE_PLAYER: 'player',
+ ACTIONS: {
+ LOGIN_UPDATE_ERROR: 'luer'
+ },
STATUS: {
ACTIVE: 1,
DISABLED: 2,
@@ -60,9 +66,10 @@ module.exports.GameConst = {
ADMIN_SCSS_FILE: 'reldens-admin-client.scss',
ADMIN_CSS_FILE: 'reldens-admin-client.css',
INSTALLER_FOLDER: 'install',
- INSTALL_LOCK: 'install.lock',
+ INSTALL_LOCK: 'install.lock'
},
ROUTE_PATHS: {
+ DISCONNECT_USER: '/reldens-disconnect-user',
TERMS_AND_CONDITIONS: '/terms-and-conditions',
MAILER: '/reldens-mailer-enabled',
FIREBASE: '/reldens-firebase'
@@ -142,6 +149,6 @@ module.exports.GameConst = {
SHOW_PLAYER_TIME: {
NONE: -1,
ONLY_OWN_PLAYER: 0,
- ALL_PLAYERS: 2,
+ ALL_PLAYERS: 2
}
};
diff --git a/lib/game/page-range-provider.js b/lib/game/page-range-provider.js
new file mode 100644
index 000000000..7800fedcc
--- /dev/null
+++ b/lib/game/page-range-provider.js
@@ -0,0 +1,38 @@
+/**
+ *
+ * Reldens - PageRangeProvider
+ *
+ */
+
+class PageRangeProvider
+{
+
+ fetch(page, totalPages, totalDisplayedPages = 5, firstLabel = 'first', lastLabel = 'last')
+ {
+ let half = Math.floor(totalDisplayedPages / 2);
+ let start = page - half;
+ let end = page + half;
+ start = Math.max(1, start);
+ end = Math.min(totalPages, end);
+ if(end - start + 1 < totalDisplayedPages){
+ if(start === 1){
+ end = Math.min(totalPages, start + totalDisplayedPages - 1);
+ }
+ start = Math.max(1, end - totalDisplayedPages + 1);
+ }
+ let range = [];
+ if(1 < start){
+ range.push({label: firstLabel, value: 1});
+ }
+ for(let i = start; i <= end; i++){
+ range.push({label: i, value: i});
+ }
+ if(end < totalPages){
+ range.push({label: lastLabel, value: totalPages - 1});
+ }
+ return range;
+ }
+
+}
+
+module.exports.PageRangeProvider = new PageRangeProvider();
diff --git a/lib/game/server/app-server-factory.js b/lib/game/server/app-server-factory.js
index e4a888edf..6d1bf4108 100644
--- a/lib/game/server/app-server-factory.js
+++ b/lib/game/server/app-server-factory.js
@@ -27,21 +27,31 @@ class AppServerFactory
this.fileStorageManager = multer;
this.appServer = false;
this.app = express();
- this.encoding = (process.env.RELDENS_DEFAULT_ENCODING || 'utf-8').toString();
+ this.encoding = String(process.env.RELDENS_DEFAULT_ENCODING || 'utf-8');
this.useHttps = 1 === Number(process.env.RELDENS_EXPRESS_USE_HTTPS || 0);
- this.passphrase = (process.env.RELDENS_EXPRESS_HTTPS_PASSPHRASE || '').toString();
- this.httpsChain = (process.env.RELDENS_EXPRESS_HTTPS_CHAIN || '').toString();
- this.keyPath = (process.env.RELDENS_EXPRESS_HTTPS_PRIVATE_KEY || '').toString();
- this.certPath = (process.env.RELDENS_EXPRESS_HTTPS_CERT || '').toString();
+ this.passphrase = String(process.env.RELDENS_EXPRESS_HTTPS_PASSPHRASE || '');
+ this.httpsChain = String(process.env.RELDENS_EXPRESS_HTTPS_CHAIN || '');
+ this.keyPath = String(process.env.RELDENS_EXPRESS_HTTPS_PRIVATE_KEY || '');
+ this.certPath = String(process.env.RELDENS_EXPRESS_HTTPS_CERT || '');
+ this.trustedProxy = String(process.env.RELDENS_EXPRESS_TRUSTED_PROXY || '');
this.windowMs = Number(process.env.RELDENS_EXPRESS_RATE_LIMIT_MS || 60000);
this.maxRequests = Number(process.env.RELDENS_EXPRESS_RATE_LIMIT_MAX_REQUESTS || 30);
+ this.applyKeyGenerator = 1 === Number(process.env.RELDENS_EXPRESS_RATE_LIMIT_APPLY_KEY_GENERATOR || 0);
}
- createAppServer()
+ createAppServer(appServerConfig)
{
+ if(appServerConfig){
+ Object.assign(this, appServerConfig);
+ Logger.debug('Setting server config:', appServerConfig);
+ }
this.app.use(cors());
this.app.use(express.json());
this.app.use(bodyParser.urlencoded({extended: true}));
+ if('' !== this.trustedProxy){
+ this.app.enable('trust proxy', this.trustedProxy);
+ Logger.debug('Enabled trusted proxy:', this.trustedProxy);
+ }
this.appServer = this.createServer();
// @TODO - BETA - Rename the class and use it as wrapper for the server and the associated services.
return {app: this.app, appServer: this.appServer, express};
@@ -78,12 +88,19 @@ class AppServerFactory
enableServeHome(app, distPath, initialConfiguration)
{
- let limiter = rateLimit({
+ let limiterParams = {
// default 60000 = 1 minute:
windowMs: this.windowMs,
// limit each IP to 30 requests per windowMs:
max: this.maxRequests,
- });
+ };
+ if(this.applyKeyGenerator){
+ limiterParams.keyGenerator = function (req) {
+ return req.ip;
+ };
+ Logger.debug('Applied key generator.');
+ }
+ let limiter = rateLimit(limiterParams);
app.post('/', limiter);
app.get('/', limiter);
app.get('/', async (req, res, next) => {
@@ -101,4 +118,4 @@ class AppServerFactory
}
-module.exports.AppServerFactory = new AppServerFactory();
+module.exports.AppServerFactory = AppServerFactory;
diff --git a/lib/game/server/file-handler.js b/lib/game/server/file-handler.js
index 013023021..1a3d41045 100644
--- a/lib/game/server/file-handler.js
+++ b/lib/game/server/file-handler.js
@@ -32,8 +32,10 @@ class FileHandler
try {
let deleteFile = sc.isArray(filePath) ? this.joinPaths(...filePath) : filePath
await fs.promises.unlink(deleteFile);
- } catch (err) {
- // Logger.error(err);
+ return true;
+ } catch (error) {
+ // Logger.error(error);
+ return false;
}
}
@@ -101,7 +103,8 @@ class FileHandler
let origin = sc.isArray(from) ? this.joinPaths(...from) : from;
let dest = sc.isArray(to) ? this.joinPaths(...to) : to;
try {
- return await fs.promises.copyFile(origin, dest);
+ await fs.promises.copyFile(origin, dest);
+ return true;
} catch (error) {
Logger.error(error.message);
return false;
diff --git a/lib/game/server/forgot-password.js b/lib/game/server/forgot-password.js
index a4016f1b4..ee8dc250b 100644
--- a/lib/game/server/forgot-password.js
+++ b/lib/game/server/forgot-password.js
@@ -4,6 +4,7 @@
*
*/
+const { FileHandler } = require('./file-handler');
const { sc } = require('@reldens/utils');
class ForgotPassword
@@ -15,28 +16,37 @@ class ForgotPassword
let rEmail = req.query.email;
let rId = req.query.id;
let user = false;
- let resetResult = '';
if(rEmail && rId){
user = await serverManager.usersManager.loadUserByEmail(rEmail);
}
- resetResult = await this.resetResultContent(user, rId, serverManager, rEmail);
- let resetPath = serverManager.themeManager.assetPath('email', 'reset.html');
- let content = await serverManager.themeManager.loadAndRenderTemplate(resetPath, {resetResult});
- res.send(content);
+ let content = await this.resetResultContent(user, rId, serverManager, rEmail);
+ res.send(
+ await serverManager.themeManager.templateEngine.render(
+ FileHandler.fetchFileContents(FileHandler.joinPaths(
+ serverManager.themeManager.projectAssetsPath,
+ 'html',
+ 'layout.html'
+ )),
+ {content, contentKey: 'forgot-password-content'}
+ )
+ );
});
}
static async resetResultContent(user, rId, serverManager, rEmail)
{
if(!user || user.password !== rId){
- let resetErrorPath = serverManager.themeManager.assetPath('email', 'reset-error.html');
- return await serverManager.themeManager.loadAndRenderTemplate(resetErrorPath);
+ return await serverManager.themeManager.loadAndRenderTemplate(
+ serverManager.themeManager.assetPath('email', 'reset-error.html')
+ );
}
let newPass = sc.randomCharsWithSymbols(12);
let newPassHash = serverManager.loginManager.passwordManager.encryptPassword(newPass);
await serverManager.usersManager.updateUserByEmail(rEmail, {password: newPassHash});
- let resetSuccessPath = serverManager.themeManager.assetPath('email', 'reset-success.html');
- return await serverManager.themeManager.loadAndRenderTemplate(resetSuccessPath, {newPass});
+ return await serverManager.themeManager.loadAndRenderTemplate(
+ serverManager.themeManager.assetPath('email', 'reset-success.html'),
+ {userName: user.username, newPass}
+ );
}
}
diff --git a/lib/game/server/game-server.js b/lib/game/server/game-server.js
index 26e49b990..5d72b7a37 100644
--- a/lib/game/server/game-server.js
+++ b/lib/game/server/game-server.js
@@ -6,7 +6,7 @@
const Monitor = require('@colyseus/monitor');
const basicAuth = require('express-basic-auth');
-const { Server } = require('colyseus');
+const { Server } = require('@colyseus/core');
const { Logger } = require('@reldens/utils');
class GameServer extends Server
diff --git a/lib/game/server/homepage-loader.js b/lib/game/server/homepage-loader.js
index 7bc867664..f9b84c8da 100644
--- a/lib/game/server/homepage-loader.js
+++ b/lib/game/server/homepage-loader.js
@@ -14,8 +14,8 @@ class HomepageLoader
static async loadContents(requestLanguage, distPath, initialConfiguration)
{
let languageParam = (requestLanguage || '').toString();
- if ('' !== languageParam) {
- if (!sc.isValidIsoCode(languageParam)) {
+ if('' !== languageParam){
+ if(!sc.isValidIsoCode(languageParam)){
Logger.error('Invalid selected language ISO code.');
languageParam = '';
}
@@ -24,7 +24,7 @@ class HomepageLoader
let indexPath = FileHandler.joinPaths(distPath, languageParam + '-' + GameConst.STRUCTURE.INDEX);
let defaultIndexPath = FileHandler.joinPaths(distPath, GameConst.STRUCTURE.INDEX);
let filePath = '' !== languageParam && FileHandler.exists(indexPath) ? indexPath : defaultIndexPath;
- Logger.info('Loading index: ' + filePath);
+ Logger.info('Loading index: '+filePath);
let html = FileHandler.readFile(filePath);
let configScriptContents = '
\ No newline at end of file
diff --git a/theme/default/assets/custom/rewards/default-reward.png b/theme/default/assets/custom/rewards/default-reward.png
new file mode 100644
index 000000000..40a73be5e
Binary files /dev/null and b/theme/default/assets/custom/rewards/default-reward.png differ
diff --git a/theme/default/assets/email/reset-success.html b/theme/default/assets/email/reset-success.html
index 22a602b3d..dc886b063 100644
--- a/theme/default/assets/email/reset-success.html
+++ b/theme/default/assets/email/reset-success.html
@@ -1,2 +1,5 @@
-
Your new password is: {{&newPass}}
+
diff --git a/theme/default/assets/email/reset.html b/theme/default/assets/email/reset.html
deleted file mode 100644
index 639fc82b7..000000000
--- a/theme/default/assets/email/reset.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
DwDeveloper - Reldens | MMORPG Platform
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/theme/default/assets/features/chat/templates/ui-chat.html b/theme/default/assets/features/chat/templates/ui-chat.html
index 4bd46e865..bfc538d59 100644
--- a/theme/default/assets/features/chat/templates/ui-chat.html
+++ b/theme/default/assets/features/chat/templates/ui-chat.html
@@ -6,6 +6,6 @@