reldens
Version:
Reldens - MMORPG Platform
371 lines (354 loc) • 14.4 kB
JavaScript
/**
*
* Reldens - AnimationEngine
*
* Manages client-side animations for game objects.
*
* Objects flow:
*
* When you create an NpcObject this can/should be set as "interactive", after the validation
* if(this.isInteractive){
* This will activate the onpointerdown event, so when you click on the object, it will send the action
* ObjectsConst.OBJECT_INTERACTION
* Along with its own ID and type.
* The server will pick up this information and validate it on the NpcObject.executeMessageActions method, and return
* a UI message to open a UI dialog box, updated with the information coming in the message.
* See RoomEvents.initUi method.
*
*/
const { Logger, sc } = require('@reldens/utils');
const { ObjectsConst } = require('../constants');
const { GameConst } = require('../../game/constants');
/**
* @typedef {import('../../game/client/game-manager').GameManager} GameManager - CUSTOM DYNAMIC
* @typedef {import('../../game/client/scene-dynamic').SceneDynamic} SceneDynamic
* @typedef {import('../../game/client/scene-preloader').ScenePreloader} ScenePreloader
*
* @typedef {Object} AnimationEngineProps
* @property {boolean} [enabled]
* @property {string} key
* @property {string|number} id
* @property {string} [asset_key]
* @property {string} [assetPath]
* @property {string} [type]
* @property {boolean} [ui]
* @property {string} targetName
* @property {number} [frameRate]
* @property {number} [frameStart]
* @property {number} [frameEnd]
* @property {number} [x]
* @property {number} [y]
* @property {number} [repeat]
* @property {boolean} [hideOnComplete]
* @property {boolean} [destroyOnComplete]
* @property {boolean|string} [autoStart]
* @property {string} [layerName]
* @property {Object} [positionFix]
* @property {number} [zeroPad]
* @property {string} [prefix]
* @property {boolean} [isInteractive]
* @property {boolean} [highlightOnOver]
* @property {string} [highlightColor]
* @property {number} [restartTime]
* @property {Object} [animations]
*/
class AnimationEngine
{
/**
* @param {GameManager} gameManager
* @param {AnimationEngineProps} props
* @param {SceneDynamic|ScenePreloader} currentPreloader
*/
constructor(gameManager, props, currentPreloader)
{
/** @type {SceneDynamic|ScenePreloader} */
this.currentPreloader = currentPreloader;
/** @type {Object|false} */
this.currentAnimation = false;
/** @type {GameManager} */
this.gameManager = gameManager;
/** @type {boolean} */
this.enabled = sc.get(props, 'enabled', false);
/** @type {string} */
this.key = props.key;
/** @type {string|number} */
this.id = props.id;
/** @type {string} */
this.asset_key = sc.get(props, 'asset_key', props.key);
/** @type {string} */
this.assetPath = sc.get(props, 'assetPath', '/assets/custom/sprites/');
/** @type {string|false} */
this.type = sc.get(props, 'type', false);
/** @type {boolean} */
this.ui = sc.get(props, 'ui', false);
/** @type {string} */
this.targetName = props.targetName;
// @TODO - BETA - Refactor to extract the default animation as part of the object animations.
this.frameRate = sc.get(props, 'frameRate', false);
this.frameStart = sc.get(props, 'frameStart', 0);
this.frameEnd = sc.get(props, 'frameEnd', 0);
this.x = sc.get(props, 'x', 0);
this.y = sc.get(props, 'y', 0);
this.repeat = isNaN(props.repeat) ? -1 : props.repeat;
this.hideOnComplete = sc.get(props, 'hideOnComplete', false);
if(!this.gameManager.createdAnimations){
this.gameManager.createdAnimations = {};
}
// @NOTE: you cannot combine destroyOnComplete with repeat = -1, because an animation with infinite
// repetitions will never trigger the complete event.
this.destroyOnComplete = sc.get(props, 'destroyOnComplete', false);
this.autoStart = sc.get(props, 'autoStart', false);
this.layerName = sc.get(props, 'layerName', false);
this.positionFix = sc.get(props, 'positionFix', false);
this.zeroPad = sc.get(props, 'zeroPad', false);
this.prefix = sc.get(props, 'prefix', false);
this.isInteractive = sc.get(props, 'isInteractive', false);
this.highlightOnOver = Boolean(sc.get(
props,
'highlightOnOver',
this.gameManager.config.getWithoutLogs('client/ui/animations/highlightOnOver', true)
));
this.highlightColor = sc.get(
props,
'highlightColor',
this.gameManager.config.getWithoutLogs('client/ui/animations/highlightColor', '0x00ff00')
);
this.restartTime = sc.get(props, 'restartTime', false);
this.calculateAnimPosition();
// @NOTE: having this here we will get the animations generated for each object instance, so normally you would
// get duplicated animations for any respawn "multiple" object, BUT, at the same time, you could have an
// animation for a specific instance ID, we need to keep this here and check if the animation already exists on
// the preloader list to avoid generate it again.
if(sc.hasOwn(props, 'animations')){
this.createObjectAnimations(props.animations);
}
}
/**
* @param {number} x
* @param {number} y
*/
updateObjectAndSpritePositions(x, y)
{
this.sceneSprite.x = x;
this.sceneSprite.y = y;
this.x = x;
this.y = y;
this.calculateAnimPosition();
}
calculateAnimPosition()
{
this.animPos = {x: this.x, y: this.y};
if(!this.positionFix){
return;
}
if(sc.hasOwn(this.positionFix, 'x')){
this.animPos.x = this.x + this.positionFix.x;
}
if(sc.hasOwn(this.positionFix, 'y')){
this.animPos.y = this.y + this.positionFix.y;
}
}
/**
* @returns {number}
*/
updateObjectDepth()
{
let objectNewDepth = this.y + this.sceneSprite.height;
this.sceneSprite.setDepth(objectNewDepth);
return objectNewDepth;
}
/**
* @returns {Object|boolean}
*/
createAnimation()
{
if(!this.enabled){
Logger.error('Animation disabled: '+this.key);
return false;
}
let currentScene = this.gameManager.activeRoomEvents.getActiveScene();
if(!currentScene){
Logger.error('Active scene not found.');
return false;
}
let animationData = {start: this.frameStart, end: this.frameEnd};
if(this.prefix !== false){
animationData.prefix = this.prefix;
}
if(this.zeroPad !== false){
animationData.zeroPad = this.zeroPad;
}
if(!this.currentPreloader.anims.textureManager.list[this.asset_key]){
Logger.warning('Asset not found in preloader.', this.asset_key, animationData);
this.currentPreloader.load.spritesheet(this.asset_key, this.assetPath+this.asset_key, animationData);
this.currentPreloader.load.once('complete', async () => {
this.createAnimation();
});
return false;
}
let frameNumbers = this.currentPreloader.anims.generateFrameNumbers(this.asset_key, animationData);
let createData = {
key: this.key,
frames: frameNumbers,
frameRate: this.frameRate,
repeat: this.repeat,
hideOnComplete: this.hideOnComplete
};
this.currentAnimation = this.gameManager.createdAnimations[this.key];
if(!this.currentAnimation){
Logger.debug('Creating animation: '+this.key);
this.currentAnimation = this.currentPreloader.anims.create(createData);
}
this.currentPreloader.objectsAnimations[this.key] = this.currentAnimation;
this.gameManager.createdAnimations[this.key] = this.currentAnimation;
let spriteX = this.positionFix ? this.animPos.x : this.x;
let spriteY = this.positionFix ? this.animPos.y : this.y;
// this is where the animation is actually created and stored:
this.sceneSprite = currentScene.physics.add.sprite(spriteX, spriteY, this.asset_key);
this.enableInteraction(currentScene);
this.enableAutoRestart();
this.automaticDestroyOnComplete();
// @NOTE: sprites depth will be set according to their Y position, since the same was applied on the
// players sprites and updated as they move the depth is fixed automatically and the objects will get
// above or below the player.
this.sceneSprite.setDepth(this.y + this.sceneSprite.body.height);
currentScene.objectsAnimations[this.key] = this;
this.gameManager.events.emitSync('reldens.createAnimationAfter', {animationEngine: this});
this.autoPlayAnimation(frameNumbers);
return this.sceneSprite;
}
/**
* @param {Array<Object>} frameNumbers
*/
autoPlayAnimation(frameNumbers)
{
if(!this.autoStart || 1 >= frameNumbers.length){
return;
}
// @NOTE: this will play the animation created above for the object using the "client_params" from the storage.
this.sceneSprite.anims.play(this.key, true);
}
automaticDestroyOnComplete()
{
if(!this.destroyOnComplete){
return;
}
this.sceneSprite.on('animationcomplete', () => {
this.currentAnimation?.destroy();
this.sceneSprite.destroy();
}, this);
}
enableAutoRestart()
{
if(!this.restartTime){
return;
}
this.sceneSprite.on('animationcomplete', () => {
setTimeout(() => {
// if the animation was used to change the scene, this won't be available to the user who runs it:
if(!this.sceneSprite.anims){
return;
}
this.sceneSprite.anims.restart();
this.sceneSprite.anims.pause();
}, this.restartTime);
},
this);
}
/**
* @param {SceneDynamic} currentScene
*/
enableInteraction(currentScene)
{
if(!this.isInteractive){
return;
}
this.sceneSprite.setInteractive({useHandCursor: true}).on('pointerdown', (e) => {
// @NOTE: we avoid running the object interactions while any UI element is open, if we click on the UI the
// elements in the background scene should not be executed.
if(GameConst.SELECTORS.CANVAS !== e.downElement.nodeName){
return false;
}
// @TODO - BETA - CHECK - TempId is a temporal fix for multiple objects case.
let tempId = (this.key === this.asset_key) ? this.id : this.key;
let dataSend = {
act: ObjectsConst.OBJECT_INTERACTION,
id: tempId,
type: this.type
};
this.gameManager.activeRoomEvents.send(dataSend);
if(!this.targetName){
return false;
}
let previousTarget = Object.assign({}, currentScene.player.currentTarget);
let thisTarget = {id: tempId, type: ObjectsConst.TYPE_OBJECT};
currentScene.player.currentTarget = thisTarget;
this.gameManager.gameEngine.showTarget(this.targetName, thisTarget, previousTarget);
});
if(this.highlightOnOver){
this.sceneSprite.on('pointerover', () => {
this.sceneSprite.setTint(this.highlightColor);
});
this.sceneSprite.on('pointerout', () => {
this.sceneSprite.clearTint();
});
}
}
runAnimation()
{
if(!this.sceneSprite){
Logger.error('Current animation not found: '+this.key);
return;
}
this.sceneSprite.anims.play(this.key, true);
}
/**
* @returns {Object}
*/
getPosition()
{
// @TODO - BETA - Create position object.
return {x: this.x, y: this.y};
}
/**
* @param {Object} animations
*/
createObjectAnimations(animations)
{
if(!animations){
return;
}
let animationsKeys = Object.keys(animations);
if(0 === animationsKeys.length){
return;
}
for(let i of animationsKeys){
if(this.gameManager.createdAnimations[i]){
this.currentPreloader.objectsAnimations[i] = this.gameManager.createdAnimations[i];
continue;
}
if(sc.hasOwn(this.currentPreloader.objectsAnimations, i)){
// @TODO - BETA - Clean up, can objectsAnimations be removed?
continue;
}
let animData = animations[i];
let frameNumbers = this.currentPreloader.anims.generateFrameNumbers(
(animData['asset_key'] || this.asset_key), {
start: animData['start'] || this.frameStart,
end: animData['end'] || this.frameEnd
}
);
let createData = {
key: i,
frames: frameNumbers,
frameRate: sc.get(animData, 'frameRate', this.frameRate),
repeat: sc.get(animData, 'repeat', this.repeat),
hideOnComplete: sc.get(animData, 'hideOnComplete', this.hideOnComplete),
asset_key: sc.get(animData, 'asset_key', this.asset_key)
};
this.currentPreloader.objectsAnimations[i] = this.currentPreloader.anims.create(createData);
this.gameManager.createdAnimations[i] = this.currentPreloader.objectsAnimations[i];
}
}
}
module.exports.AnimationEngine = AnimationEngine;