UNPKG

reldens

Version:
572 lines (534 loc) 20.9 kB
/** * * Reldens - SceneDynamic * */ const { Scene, Input } = require('phaser'); const { TileSetAnimation } = require('./tileset-animation'); const { Minimap } = require('./minimap'); const { GameConst } = require('../constants'); const { ActionsConst } = require('../../actions/constants'); const { Logger, sc } = require('@reldens/utils'); class SceneDynamic extends Scene { constructor(key, data, gameManager) { super({key}); this.key = key; this.params = data; this.gameManager = gameManager; this.eventsManager = gameManager.events; this.configManager = gameManager.config; this.layers = {}; this.transition = true; this.useTsAnimation = false; this.arrowSprite = false; this.objectsAnimationsData = false; this.objectsAnimations = {}; this.setPropertiesFromConfig(); this.minimap = this.createMinimapInstance(this.minimapConfig); this.player = false; this.interpolatePlayersPosition = {}; this.interpolateObjectsPositions = {}; this.tilesets = []; this.tilesetAnimations = []; this.stopOnDeathOrDisabledSent = false; } setPropertiesFromConfig() { // @TODO - BETA - Move defaults to constants. if(!this.configManager){ this.configuredFrameRate = 10; this.clientInterpolation = true; this.interpolationSpeed = 0.1; this.minimapConfig = {}; return false; } this.configuredFrameRate = this.configManager.getWithoutLogs('client/general/animations/frameRate', 10); this.clientInterpolation = this.configManager.getWithoutLogs('client/general/engine/clientInterpolation', true); this.interpolationSpeed = this.configManager.getWithoutLogs('client/general/engine/interpolationSpeed', 0.1); this.minimapConfig = this.configManager.getWithoutLogs('client/ui/minimap', {}); return true; } createMinimapInstance(config) { if(!this.minimapConfig.enabled){ return false; } return new Minimap({config, events: this.eventsManager}); } init() { this.scene.setVisible(false, this.key); this.input.keyboard.removeAllListeners(); } async create() { this.eventsManager.emitSync('reldens.beforeSceneDynamicCreate', this); this.disableContextMenu(); this.createControllerKeys(); this.setupKeyboardAndPointerEvents(); await this.createSceneMap(); this.cameras.main.on('camerafadeincomplete', () => { this.transition = false; this.gameManager.gameDom.activeElement().blur(); this.minimap.createMap(this, this.gameManager.getCurrentPlayerAnimation()); this.gameManager.isChangingScene = false; }); this.eventsManager.emitSync('reldens.afterSceneDynamicCreate', this); } update(time, delta) { this.interpolatePositions(); this.movePlayerByPressedButtons(); } disableContextMenu() { if(!this.gameManager.config.get('client/ui/controls/disableContextMenu')){ return false; } this.gameManager.gameDom.getDocument().addEventListener('contextmenu', (event) => { event.preventDefault(); event.stopPropagation(); }); } setupKeyboardAndPointerEvents() { this.input.keyboard.on('keydown', (event) => { return this.executeKeyDownBehavior(event); }); this.input.keyboard.on('keyup', (event) => { this.executeKeyUpBehavior(event); }); this.input.on('pointerdown', (pointer, currentlyOver) => { return this.executePointerDownAction(pointer, currentlyOver); }); } async createSceneMap() { this.map = this.make.tilemap({key: this.params.roomName}); for(let imageKey of this.params.sceneImages){ let tileset = this.map.addTilesetImage(this.params.roomName, imageKey); if(!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); this.tilesets.push(tileset); } this.registerLayers(); this.registerTilesetAnimation(); } registerTilesetAnimation() { for(let tileset of this.tilesets){ if(!this.hasTilesetAnimations(tileset)){ continue; } this.useTsAnimation = true; for(let i of Object.keys(this.layers)){ let layer = this.layers[i]; let tilesetAnimation = new TileSetAnimation(); tilesetAnimation.register(layer, tileset); tilesetAnimation.start(); this.tilesetAnimations.push(tilesetAnimation); } } } hasTilesetAnimations(tileset) { let tilesData = tileset?.tileData || {}; let dataKeys = Object.keys(tilesData); if(0 === dataKeys.length){ return false; } for(let i of dataKeys){ if(tilesData[i].animation){ return true; } } return false; } executeKeyDownBehavior(event) { if(this.gameManager.gameDom.insideInput()){ return false; } // @TODO - BETA - Make configurable the keys related to the actions and skills. if(Input.Keyboard.KeyCodes.SPACE === event.keyCode && !this.gameManager.gameDom.insideInput()){ if(!this.player){ return; } this.player.runActions(); } if(Input.Keyboard.KeyCodes.ESC === event.keyCode){ this.gameManager.gameEngine.clearTarget(); } if(Input.Keyboard.KeyCodes.F5 === event.keyCode){ this.gameManager.forcedDisconnection = true; } } executeKeyUpBehavior(event) { if(!this.player){ return; } // stop all directional keys (arrows and wasd): let keys = this.availableControllersKeyCodes(); if(-1 !== keys.indexOf(event.keyCode)){ // @NOTE: all keyup events has to be sent. this.player.stop(); } } createControllerKeys() { // @TODO - BETA - Controllers will be part of the configuration in the database. this.keyLeft = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.LEFT); this.keyA = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.A); this.keyRight = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.RIGHT); this.keyD = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.D); this.keyUp = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.UP); this.keyW = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.W); this.keyDown = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.DOWN); this.keyS = this.input.keyboard.addKey(Input.Keyboard.KeyCodes.S); let keys = this.availableControllersKeyCodes(); let inputElements = this.gameManager.gameDom.getElements('input'); for(let inputElement of inputElements){ this.addAndRemoveCapture(keys, inputElement); } } addAndRemoveCapture(keys, inputElement) { this.loopKeysAddListenerToElement(keys, inputElement, 'focusin', 'removeCapture'); this.loopKeysAddListenerToElement(keys, inputElement, 'click', 'removeCapture'); this.loopKeysAddListenerToElement(keys, inputElement, 'focusout', 'addCapture'); this.loopKeysAddListenerToElement(keys, inputElement, 'blur', 'addCapture'); } availableControllersKeyCodes() { return [ Input.Keyboard.KeyCodes.LEFT, Input.Keyboard.KeyCodes.A, Input.Keyboard.KeyCodes.RIGHT, Input.Keyboard.KeyCodes.D, Input.Keyboard.KeyCodes.UP, Input.Keyboard.KeyCodes.W, Input.Keyboard.KeyCodes.DOWN, Input.Keyboard.KeyCodes.S ]; } executePointerDownAction(pointer, currentlyOver) { if(0 < currentlyOver.length){ return false; } if(!this.gameManager.config.get('client/players/tapMovement/enabled')){ return false; } if(this.gameManager.activeRoomEvents.roomData?.worldConfig?.applyGravity){ return false; } let primaryMove = this.gameManager.config.get('client/ui/controls/primaryMove'); let primaryTouch = this.gameManager.config.get('client/ui/controls/allowPrimaryTouch'); if( (!pointer.wasTouch && !pointer.primaryDown && primaryMove) || (!pointer.wasTouch && pointer.primaryDown && !primaryMove) || (pointer.wasTouch && !pointer.primaryDown && primaryTouch) ){ return false; } // @TODO - BETA - Temporal avoid double actions, if you target something you will not be moved to the // pointer, in a future release this will be configurable, so you can walk to objects and they get // activated, for example: click on and NPC, automatically walk close and automatically get a dialog // opened. if(this.gameManager.gameDom.insideInput()){ this.gameManager.gameDom.activeElement().blur(); } if(!this.appendRowAndColumn(pointer)){ return false; } this.player.moveToPointer(pointer); this.updatePointerObject(pointer); } movePlayerByPressedButtons() { // if player is writing there's no movement: if(this.gameManager.gameDom.insideInput()){ return; } 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(); } if(this.keyLeft.isDown || this.keyA.isDown){ this.player.left(); } if(this.keyDown.isDown || this.keyS.isDown){ this.player.down(); } if(this.keyUp.isDown || this.keyW.isDown){ this.player.up(); } } interpolatePositions() { if(!this.clientInterpolation){ return; } this.processPlayersPositionInterpolation(); this.processObjectsPositionInterpolation(); } processPlayersPositionInterpolation() { let playerKeys = Object.keys(this.interpolatePlayersPosition); if(0 === playerKeys.length){ return; } if(!sc.get(this.player, 'players')){ return; } for(let i of playerKeys){ let entityState = this.interpolatePlayersPosition[i]; if(!entityState){ continue; } let entity = this.player.players[i]; if(!entity){ continue; } if(this.isCurrentPosition(entity, entityState)){ delete this.interpolatePlayersPosition[i]; continue; } 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]; } } } processObjectsPositionInterpolation() { let objectsKeys = Object.keys(this.interpolateObjectsPositions); if(0 === objectsKeys.length){ return; } let objectsPlugin = this.gameManager.getFeature('objects'); for(let i of objectsKeys){ this.interpolateBulletPosition(i, objectsPlugin); this.interpolateObjectAnimationPosition(i, objectsPlugin); } } interpolateBulletPosition(i, objectsPlugin) { if(!this.isBullet(i)){ return; } let entity = sc.get(objectsPlugin.bullets, i); if(!entity){ return; } let entityState = this.interpolateObjectsPositions[i]; if(!entityState){ return; } if(this.isCurrentPosition(entity, entityState)){ delete this.interpolateObjectsPositions[i]; return; } 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]; } } isBullet(objectKey) { return -1 !== objectKey.indexOf('bullet'); } interpolateObjectAnimationPosition(i, objectsPlugin) { let entity = this.objectsAnimations[i]; if(!entity){ return; } let entityState = this.interpolateObjectsPositions[i]; if(!entityState){ return; } if(this.isCurrentPosition(entity, entityState)){ delete this.interpolateObjectsPositions[i]; return; } 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]; } } isCurrentPosition(entity, entityState) { if(!entity || !entityState){ Logger.warning('None entity found to compare current entity position.'); return false; } 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; if(this.useTsAnimation){ for(let tilesetAnimation of this.tilesetAnimations){ tilesetAnimation.destroy(); } } } registerLayers() { if(0 === this.map.layers.length){ return; } let idx = 0; // @TODO - BETA - Use single get(client/map). let depthBelowPlayer = this.configManager.get('client/map/layersDepth/belowPlayer'); let depthForChangePoints = this.configManager.get('client/map/layersDepth/changePoints'); for(let layer of this.map.layers){ this.layers[idx] = this.map.createLayer(layer.name, this.tilesets); if(!this.layers[idx]){ Logger.critical('Map layer could not be created.', layer.name, this.key); continue; } if(-1 !== layer.name.indexOf('below-player')){ this.layers[idx].setDepth(depthBelowPlayer); } if(-1 !== layer.name.indexOf('over-player')){ // we need to set the depth higher than everything else (multiply to get the highest value): this.layers[idx].setDepth(idx * this.map.height * this.map.tileHeight); } if(-1 !== layer.name.indexOf('change-points')){ this.layers[idx].setDepth(depthForChangePoints); } idx++; } } appendRowAndColumn(pointer) { let worldToTileXY = this.map.worldToTileXY(pointer.worldX, pointer.worldY); let playerToTileXY = this.map.worldToTileXY(this.player.state.x, this.player.state.y); if(!worldToTileXY || !playerToTileXY){ Logger.error('Move to pointer error.'); return false; } pointer.worldColumn = worldToTileXY.x; pointer.worldRow = worldToTileXY.y; pointer.playerOriginCol = playerToTileXY.x; pointer.playerOriginRow = playerToTileXY.y; return pointer; } createFloatingText( x, y, message, color, font, fontSize = 14, duration = 600, top = 50, stroke = '#000000', strokeThickness = 4, shadowColor = 'rgba(0,0,0,0.7)' ){ let damageSprite = this.add.text(x, y, message, {fontFamily: font, fontSize: fontSize+'px'}); damageSprite.style.setColor(color); damageSprite.style.setAlign('center'); damageSprite.style.setStroke(stroke, strokeThickness); damageSprite.style.setShadow(5, 5, shadowColor, 5); damageSprite.setDepth(200000); this.add.tween({ targets: damageSprite, duration, ease: 'Exponential.In', y: y - top, onComplete: () => { damageSprite.destroy(); } }); } updatePointerObject(pointer) { if(!this.configManager.get('client/ui/pointer/show')){ return; } if(this.arrowSprite){ this.arrowSprite.destroy(); } let topOffSet = this.configManager.get('client/ui/pointer/topOffSet', 16); this.arrowSprite = this.physics.add.sprite(pointer.worldX, pointer.worldY - topOffSet, GameConst.ARROW_DOWN); this.arrowSprite.setDepth(500000); this.arrowSprite.anims.play(GameConst.ARROW_DOWN, true).on('animationcomplete', () => { this.arrowSprite.destroy(); }); } getAnimationByKey(key) { if(!this.anims || !this.anims?.anims || !this.anims?.anims?.entries){ Logger.error('Animations not loaded.', this.anims); return false; } return sc.get(this.anims.anims.entries, key, false); } getObjectFromExtraData(objKey, extraData, currentPlayer) { // @TODO - BETA - Replace with constants. // objKey = t > target // objKey = o > owner let returnObj = false; let dataTargetType = objKey+'T'; // tT - oT === DATA_TARGET_TYPE - DATA_OWNER_TYPE let dataTargetKey = objKey+'K'; // tK - oK === DATA_TARGET_KEY - DATA_OWNER_KEY let isTargetPlayer = extraData[dataTargetType] === ActionsConst.DATA_TYPE_VALUE_PLAYER; if(!isTargetPlayer && sc.hasOwn(this.objectsAnimations, extraData[dataTargetKey])){ returnObj = this.objectsAnimations[extraData[dataTargetKey]]; } if(isTargetPlayer && sc.hasOwn(currentPlayer.players, extraData[dataTargetKey])){ returnObj = currentPlayer.players[extraData[dataTargetKey]]; } return returnObj; } loopKeysAddListenerToElement(keys, element, eventName, action) { element.addEventListener(eventName, () => { for(let keyCode of keys){ this.input.keyboard[action](keyCode); } }); } } module.exports.SceneDynamic = SceneDynamic;