reldens
Version:
Reldens - MMORPG Platform
462 lines (432 loc) • 18.8 kB
JavaScript
/**
*
* Reldens - Objects Client Plugin
*
*/
const { AnimationEngine } = require('../../objects/client/animation-engine');
const { ObjectsMessageListener } = require('./objects-message-listener');
const { DropsMessageListener } = require('./drops-message-listener');
const Translations = require('./snippets/en_US');
const { TranslationsMapper } = require('../../snippets/client/translations-mapper');
const { UserInterface } = require('../../game/client/user-interface');
const { ObjectsConst } = require('../constants');
const { ActionsConst } = require('../../actions/constants');
const { PluginInterface } = require('../../features/plugin-interface');
const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
class ObjectsPlugin extends PluginInterface
{
setup(props)
{
this.gameManager = sc.get(props, 'gameManager', false);
if(!this.gameManager){
Logger.error('Game Manager undefined in InventoryPlugin.');
}
this.events = sc.get(props, 'events', false);
if(!this.events){
Logger.error('EventsManager undefined in InventoryPlugin.');
}
this.bodyOnAddCallBack = false;
this.bodyOnRemoveCallBack = false;
this.bullets = {};
this.changeBodyVisibilityOnInactiveState = this.gameManager.config.getWithoutLogs(
'client/objects/animations/changeBodyVisibilityOnInactiveState',
true
);
this.missingSpritesTimeOut = this.gameManager.config.getWithoutLogs(
'client/general/animations/missingSpritesTimeOut',
200
);
this.missingSpritesMaxRetries = this.gameManager.config.getWithoutLogs(
'client/general/animations/missingSpritesMaxRetries',
5
);
this.missingSpriteRetry = 0;
this.listenEvents();
this.setTranslations();
this.setListener();
}
setListener()
{
if(!this.gameManager){
return false;
}
this.gameManager.config.client.message.listeners['traderObject'] = new ObjectsMessageListener();
}
setTranslations()
{
if(!this.gameManager){
return false;
}
TranslationsMapper.forConfig(this.gameManager.config.client, Translations, ObjectsConst.MESSAGE.DATA_VALUES);
}
listenEvents()
{
if(!this.events){
return false;
}
// @NOTE: the prepareObjectsUi has to be created before the scenes, so we can use the scenes events before
// the events were called.
this.events.on('reldens.startEngineScene', async (roomEvents) => {
await this.prepareObjectsUi(roomEvents.gameManager, roomEvents.roomData.objectsAnimationsData, roomEvents);
});
this.events.on('reldens.afterSceneDynamicCreate', async (sceneDynamic) => {
await this.createDynamicAnimations(sceneDynamic);
});
this.events.on('reldens.joinedRoom', (room, gameManager) => {
// @TODO - BETA - Refactor.
this.listenMessages(room, gameManager);
DropsMessageListener.listenMessages(room, gameManager);
});
}
listenMessages(room, gameManager)
{
room.onMessage('*', (message) => {
this.startObjectAnimation(message, gameManager);
this.objectBattleEndAnimation(message, gameManager);
});
if(!room.state || !room.state.bodies){
return false;
}
this.setAddBodyCallback(room, gameManager);
this.setRemoveBodyCallback(room);
}
setAddBodyCallback(room, gameManager)
{
// @TODO - BETA - Refactor and extract Colyseus into a driver.
this.bodyOnAddCallBack = room.state.bodies.onAdd((body, key) => {
this.setOnChangeBodyCallback(body, key, room, gameManager);
this.createBulletSprite(key, gameManager, body);
});
}
createBulletSprite(key, gameManager, body)
{
if(-1 === key.indexOf('bullet')){
return false;
}
let currentScene = gameManager.activeRoomEvents.getActiveScene();
let animKey = 'default_bullet';
let skillBullet = (body.key ? body.key + '_' : '') + 'bullet';
if(sc.hasOwn(gameManager.gameEngine.uiScene.directionalAnimations, skillBullet)){
skillBullet = skillBullet + '_' + body.dir;
}
if(sc.hasOwn(currentScene.anims.anims.entries, skillBullet)){
animKey = skillBullet;
}
let bulletSprite = currentScene?.physics?.add?.sprite(body.x, body.y, animKey);
if(!bulletSprite){
Logger.warning('Could not create bullet sprite.', currentScene);
return false;
}
bulletSprite.setDepth(11000);
this.bullets[key] = bulletSprite;
//Logger.debug({createdBulletSprite: skillBullet, shootFrom: body, bulletSprite});
}
setOnChangeBodyCallback(body, key, room, gameManager)
{
// @TODO - BETA - Refactor and extract Colyseus into a driver.
let bodyProperties = Object.keys(body);
for(let propertyKey of bodyProperties){
body.listen(propertyKey, async (newValue) => {
//Logger.debug('Update body property "'+propertyKey+'": '+newValue);
await this.events.emit('reldens.objectBodyChange', {body, key, changes: {[propertyKey]: newValue}});
let currentScene = gameManager.activeRoomEvents.getActiveScene();
this.updateBodyProperties(propertyKey, body, newValue, currentScene, key);
if(!currentScene){
return;
}
let isBullet = -1 !== key.indexOf('bullet');
let currentBody = isBullet ? this.bullets[key] : currentScene?.objectsAnimations[key];
this.setVisibility(currentBody, GameConst.STATUS.ACTIVE === body.inState);
this.logObjectBodyUpdate(key, propertyKey, newValue, currentBody);
let canInterpolate = GameConst.STATUS.AVOID_INTERPOLATION !== body.inState;
if(currentScene?.clientInterpolation && canInterpolate){
currentScene.interpolateObjectsPositions[key] = body;
return;
}
if(isBullet){
return this.updateBulletBodyPosition(key, body);
}
return this.updateObjectsAnimations(key, body, currentScene);
});
}
}
logObjectBodyUpdate(key, propertyKey, newValue, currentBody)
{
let logValues = {key, propertyKey, newValue};
if(('x' === propertyKey || 'y' === propertyKey) && currentBody && currentBody[propertyKey]){
logValues.currentValue = currentBody[propertyKey];
}
//Logger.debug(logValues);
}
setVisibility(currentBody, isActive)
{
if(!currentBody || !currentBody.sceneSprite){
return;
}
currentBody.sceneSprite.setVisible(isActive);
}
updateBodyProperties(bodyProp, body, value, currentScene, key)
{
// @TODO - BETA - Remove hardcoded properties check.
//Logger.debug({update: body, key, animationData: currentScene.objectsAnimationsData[key], bodyProp, value});
if(currentScene.objectsAnimationsData[key] && ('x' === bodyProp || 'y' === bodyProp)){
// @TODO - BETA - Check why bullets keep receiving updates even after the objects animation was removed.
currentScene.objectsAnimationsData[key][bodyProp] = value;
}
body[bodyProp] = value;
}
updateBulletBodyPosition(key, body)
{
if(!this.bullets[key]){
return;
}
this.bullets[key].x = body.x;
this.bullets[key].y = body.y;
this.events.emit('reldens.objectBodyChanged', {body, key});
}
updateObjectsAnimations(key, body, currentScene)
{
let objectAnimation = sc.get(currentScene.objectsAnimations, key);
if(!objectAnimation){
return false;
}
objectAnimation.updateObjectAndSpritePositions(body.x, body.y);
this.events.emit('reldens.objectBodyChanged', {body, key});
let objectNewDepth = objectAnimation.updateObjectDepth();
objectAnimation.inState = body.inState;
let animToPlay = this.fetchAvailableAnimationKey(currentScene, objectAnimation, body);
if('' !== animToPlay){
objectAnimation.sceneSprite.anims.play(animToPlay, true);
}
this.moveSpritesObjects(objectAnimation, body.x, body.y, objectNewDepth);
if(body.mov){
return false;
}
objectAnimation.sceneSprite.anims.stop();
objectAnimation.sceneSprite.mov = body.mov;
if(!objectAnimation.autoStart){
return false;
}
objectAnimation.sceneSprite.anims.play(
this.determineAutoStartAnimation(objectAnimation, animToPlay)
);
return true;
}
determineAutoStartAnimation(objectAnimation, animToPlay)
{
if(true === objectAnimation.autoStart){
return objectAnimation.key;
}
if(objectAnimation.autoStart === ObjectsConst.DYNAMIC_ANIMATION){
return animToPlay;
}
return objectAnimation.autoStart;
}
fetchAvailableAnimationKey(currentScene, objectAnimation, body)
{
return sc.getByPriority(currentScene.anims.anims.entries, [
objectAnimation.key + '_' + body.dir,
objectAnimation.layerName + '_' + objectAnimation.id + '_' + body.dir,
objectAnimation.key
]) || '';
}
setRemoveBodyCallback(room)
{
// @TODO - BETA - Refactor and extract Colyseus into a driver.
this.bodyOnRemoveCallBack = room.state.bodies.onRemove((body, key) => {
if(-1 === key.indexOf('bullet') || !sc.hasOwn(this.bullets, key)){
return false;
}
this.bullets[key].destroy();
delete this.bullets[key];
});
}
objectBattleEndAnimation(message, gameManager)
{
if(message.act !== ActionsConst.BATTLE_ENDED){
return false;
}
// @TODO - BETA - Replace all defaults by constants.
let deathKey = sc.get(gameManager.config.client.skills.animations, message.k + '_death', 'default_death');
let currentScene = gameManager.activeRoomEvents.getActiveScene();
try {
this.playDeathAnimation(deathKey, currentScene, message);
} catch (error) {
Logger.warning('Error on sprite "'+deathKey+'" not available.', error.message);
}
if(!sc.hasOwn(message, ActionsConst.DATA_OBJECT_KEY_TARGET)){
return false;
}
if(message[ActionsConst.DATA_OBJECT_KEY_TARGET] === currentScene.player.currentTarget?.id){
gameManager.gameEngine.clearTarget();
}
let hidePlayerSprite = sc.get(currentScene.player.players, message[ActionsConst.DATA_OBJECT_KEY_TARGET], false);
if(!hidePlayerSprite){
return false;
}
hidePlayerSprite.visible = false;
if(sc.hasOwn(hidePlayerSprite, 'nameSprite') && hidePlayerSprite.nameSprite){
hidePlayerSprite.nameSprite.visible = false;
}
}
playDeathAnimation(deathKey, currentScene, message)
{
if(!currentScene.getAnimationByKey(deathKey)){
if(this.missingSpritesMaxRetries === this.missingSpriteRetry){
Logger.debug('Sprite "'+deathKey+'" not available.', deathKey);
return false;
}
this.missingSpriteRetry++;
setTimeout(
() => {
return this.playDeathAnimation(deathKey, currentScene, message);
},
this.missingSpritesTimeOut
);
return false;
}
let skeletonSprite = currentScene.physics.add.sprite(message.x, message.y, deathKey);
skeletonSprite.setDepth(10500);
skeletonSprite.anims.play(deathKey, true).on('animationcomplete', () => {
skeletonSprite.destroy();
});
return true;
}
startObjectAnimation(message, gameManager)
{
if(message.act !== ObjectsConst.OBJECT_ANIMATION && message.act !== ObjectsConst.TYPE_ANIMATION){
return false;
}
let currentScene = gameManager.activeRoomEvents.getActiveScene();
if(!sc.hasOwn(currentScene.objectsAnimations, message.key)){
return false;
}
currentScene.objectsAnimations[message.key].runAnimation();
}
moveSpritesObjects(currentObj, x, y, objectNewDepth)
{
if(!currentObj.moveSprites){
return;
}
let moveObjectsKeys = Object.keys(currentObj.moveSprites);
if(0 === moveObjectsKeys.length){
return;
}
for(let i of moveObjectsKeys){
let sprite = currentObj.moveSprites[i];
sprite.x = x;
sprite.y = y;
// by default moving sprites will be always below the player:
let depthByPlayer = sc.get(currentObj.animationData, 'depthByPlayer', '');
let spriteDepth = objectNewDepth + ((depthByPlayer === 'above') ? 1 : -0.1);
sprite.setDepth(spriteDepth);
}
}
/**
* The objects UI are the modal dialogs that will be open when you interact with the object.
* To interact with the object you need to be into the object interaction area and click on it.
*
* @param {GameManager} gameManager
* @param {object} objectsAnimationsData
* @param {RoomEvents} roomEvents
* @returns {Promise<void>}
*/
async prepareObjectsUi(gameManager, objectsAnimationsData, roomEvents)
{
if(!objectsAnimationsData){
Logger.info('None objects animations data.');
return;
}
for(let i of Object.keys(objectsAnimationsData)){
let animProps = objectsAnimationsData[i];
if(!sc.hasOwn(animProps, 'ui')){
continue;
}
if(!animProps.id){
Logger.error(['Object ID not specified. Skipping registry:', animProps]);
continue;
}
let template = sc.get(animProps, 'template', '/assets/html/dialog-box.html');
roomEvents.objectsUi[animProps.id] = new UserInterface(gameManager, animProps, template, 'npcDialog');
await gameManager.events.emit('reldens.createdUserInterface', {
gameManager,
id: animProps.id,
userInterface: roomEvents.objectsUi[animProps.id],
ObjectsPlugin: this
});
}
}
async createDynamicAnimations(sceneDynamic)
{
if(!sceneDynamic.objectsAnimationsData){
Logger.info('None animations defined on this scene: '+sceneDynamic.key);
return;
}
await this.events.emit('reldens.createDynamicAnimationsBefore', this, sceneDynamic);
for(let i of Object.keys(sceneDynamic.objectsAnimationsData)){
let animProps = sceneDynamic.objectsAnimationsData[i];
await this.createAnimationFromAnimData(animProps, sceneDynamic);
}
}
async createAnimationFromAnimData(animProps, sceneDynamic)
{
if(!animProps.key){
Logger.error('Animation key not specified. Skipping registry.', animProps);
return false;
}
animProps.frameRate = sceneDynamic.configuredFrameRate;
let activeRoomEvents = sceneDynamic.gameManager.activeRoomEvents;
let existentBody = this.fetchExistentBody(sceneDynamic, activeRoomEvents, animProps);
this.updateAnimationPosition(existentBody, animProps);
await this.events.emit('reldens.createDynamicAnimation_'+animProps.key, this, animProps);
let classDefinition = sceneDynamic.gameManager.config.getWithoutLogs(
'client/customClasses/objects/'+animProps.key,
AnimationEngine
);
let animationEngine = new classDefinition(sceneDynamic.gameManager, animProps, sceneDynamic);
// @NOTE: this will populate the objectsAnimations property in the current scene, see scene-dynamic.
let sprite = animationEngine.createAnimation();
this.updateAnimationVisibility(existentBody, sprite);
return animationEngine;
}
updateAnimationPosition(existentBody, animProps)
{
// Logger.debug('Existent body:', {existentBody});
if(!existentBody){
// expected, not all animation objects may have a body:
return false;
}
// @NOTE: respawn objects would have the animProp position outdated since it comes from the roomData, which
// only contains the objects original initial position.
//Logger.debug('Existent body "'+animProps.key+'" position:', {x: existentBody.x, y: existentBody.y});
//Logger.debug('AnimProps "'+animProps.key+'" position:', {x: animProps.x, y: animProps.y});
if(animProps.x !== existentBody.x){
animProps.x = existentBody.x;
}
if(animProps.y !== existentBody.y){
animProps.y = existentBody.y;
}
}
updateAnimationVisibility(existentBody, sprite)
{
if(!existentBody){
// expected, not all animation objects may have a body:
return false;
}
if(GameConst.STATUS.DEATH !== existentBody.inState && GameConst.STATUS.DISABLED !== existentBody.inState){
return false;
}
sprite.visible = false;
}
fetchExistentBody(sceneDynamic, activeRoomEvents, animProps)
{
//Logger.debug('Scene key vs roomName: '+sceneDynamic.key+' / '+activeRoomEvents.roomName+'.');
if(sceneDynamic.key !== activeRoomEvents.roomName){
Logger.warning('Scene key and roomName miss match: '+sceneDynamic.key+' / '+activeRoomEvents.roomName+'.');
return false;
}
return activeRoomEvents.room.state.bodies.get(animProps?.key);
}
}
module.exports.ObjectsPlugin = ObjectsPlugin;