UNPKG

reldens

Version:
451 lines (422 loc) 17.2 kB
/** * * Reldens - CollisionsManager * */ const { PhysicalBody } = require('./physical-body'); const { ErrorManager, Logger, sc } = require('@reldens/utils'); class CollisionsManager { constructor(room) { this.activateCollisions(room); } activateCollisions(room) { if(!room){ return; } this.room = room; if(!sc.hasOwn(this.room, 'roomWorld')){ ErrorManager.error('Room world not found.'); } this.guestEmailDomain = this.room.config.getWithoutLogs('server/players/guestsUser/emailDomain'); // @TODO - BETA - Refactor to extract p2js as driver. // @NOTE: postBroadphase will be used to check pairs and test overlap instead of collision, for example, a spell // will overlap the player but not collide with it, if the spell collides with the player it will push it in // the opposite direction because the physics engine. this.room.roomWorld.on('postBroadphase', this.onWorldStepStart.bind(this)); this.room.roomWorld.on('preSolve', this.beforeResolveCollision.bind(this)); this.room.roomWorld.on('beginContact', this.onCollisionsBegin.bind(this)); // @NOTE: "endContact" will trigger when the contact ends and not when the collision step ends. this.room.roomWorld.on('endContact', this.onCollisionsEnd.bind(this)); } onWorldStepStart(evt) { let { pairs } = evt; // @NOTE: pairs can be a lot more than 2, these are not related to collisions pairs. if(1 >= pairs.length){ return; } let bulletA = false; let bulletB = false; let player = false; let roomObject = false; for(let body of pairs){ if(body.playerId && body.pStop){ body.stopFull(); } if(body.roomObject && body.pStop){ body.stopFull(); } if(body.playerId){ player = body; } if(body.isBullet){ if(bulletA && !bulletB){ bulletB = body; } if(!bulletA){ bulletA = body; } body.removeInvalidStateBulletBody(); } if(body.isRoomObject && !body.isBullet){ roomObject = body; } } if(this.room.roomWorld.bulletsStopOnPlayer && player && bulletA){ player.stopFull(); } if(this.room.roomWorld.bulletsStopOnObject && roomObject && bulletA){ roomObject.stopFull(); } this.removeIdleBullets(); } removeIdleBullets() { if(0 === this.room.roomWorld.removeBulletsStateIds.length){ return; } for(let stateId of this.room.roomWorld.removeBulletsStateIds){ this.room.state.removeBody(stateId); this.room.roomWorld.removeBulletsStateIds.splice( this.room.roomWorld.removeBulletsStateIds.indexOf(stateId), 1 ); } } beforeResolveCollision(evt) { if(!this.room.roomWorld.allowPassWallsFromBelow){ return; } for(let contact of evt.contactEquations){ let playerBody = this.getPlayerBody(contact); let wallBody = this.getWallBody(contact); if(!playerBody || !wallBody || wallBody.isWorldWall){ return; } if(playerBody.position[1] > wallBody.position[1]){ contact.enabled = false; } } } /** * Collision cases: * - player hit a player * - player hit an object (any type, animations, NPC, etc.) * - player hit change point * - player hit wall * - object hit object (bullets will hit objects) * - object hit wall * @param evt * @returns {{continue: boolean, wall, objectBody}|boolean|void} */ onCollisionsBegin(evt) { let bodyA = evt.bodyA, bodyB = evt.bodyB, playerBody = false, otherBody = false, roomObjectBody = false; if(bodyA.playerId && bodyB.playerId){ return this.playerHitPlayerBegin(bodyA, bodyB); } if(bodyA.playerId){ playerBody = bodyA; otherBody = bodyB; } if(bodyB.playerId){ playerBody = bodyB; otherBody = bodyA; } if(playerBody && otherBody.isRoomObject){ return this.playerHitObjectBegin(playerBody, otherBody); } if(playerBody && otherBody.changeScenePoint){ return this.playerHitChangePointBegin(playerBody, otherBody); } if(playerBody && otherBody.isWall){ return this.playerHitWallBegin(playerBody, otherBody); } if(bodyA.isRoomObject && bodyB.isRoomObject){ return this.objectHitObjectBegin(bodyA, bodyB); } if(bodyA.isRoomObject){ roomObjectBody = bodyA; otherBody = bodyB; } if(bodyB.isRoomObject){ roomObjectBody = bodyB; otherBody = bodyA; } if(roomObjectBody && otherBody.isWall){ this.objectHitWallBegin(roomObjectBody, otherBody); } } onCollisionsEnd(evt) { let bodyA = evt.bodyA, bodyB = evt.bodyB, playerBody = false, otherBody = false, roomObjectBody = false; if(evt.bodyA.playerId && evt.bodyB.playerId){ this.playerHitPlayerEnd(evt.bodyA, evt.bodyB); } if(bodyA.playerId){ playerBody = bodyA; otherBody = bodyB; } if(bodyB.playerId){ playerBody = bodyB; otherBody = bodyA; } if(playerBody && otherBody.isRoomObject){ return this.playerHitObjectEnd(playerBody, otherBody); } if(playerBody && otherBody.isWall){ return this.playerHitWallEnd(playerBody, otherBody); } if(bodyA.isRoomObject && bodyB.isRoomObject){ this.objectHitObjectEnd(bodyA, bodyB); } if(bodyA.isRoomObject){ roomObjectBody = bodyA; otherBody = bodyB; } if(bodyB.isRoomObject){ roomObjectBody = bodyB; otherBody = bodyA; } if(roomObjectBody && otherBody.isWall){ return this.objectHitWallEnd(roomObjectBody, otherBody); } } playerHitPlayerBegin(bodyA, bodyB) { // @NOTE: we could run specific events when a player collides with another player. // Logger.debug('Player hit player begin.', bodyA.playerId, bodyB.playerId); this.room.events.emit('reldens.playerHitPlayer', {bodyA, bodyB}); } playerHitPlayerEnd(bodyA, bodyB) { // Logger.debug('Player hit player end.', bodyA.playerId, bodyB.playerId); // player stops pushing a player: bodyA.stopFull(); bodyB.stopFull(); this.room.events.emit('reldens.playerHitPlayerEnd', {bodyA, bodyB}); } playerHitObjectBegin(playerBody, otherBody) { // Logger.debug('Player hit object being.', playerBody.playerId, otherBody.bodyState?.key); this.room.events.emit('reldens.startPlayerHitObjectBegin', {playerBody, otherBody}); // if the player collides with something we need to restart the pathfinder if it was active: this.findAlternativePath(playerBody); // now the collision manager only run the object hit action: if(otherBody.roomObject && sc.isFunction(otherBody.roomObject.onHit)){ otherBody.roomObject.onHit({bodyA: playerBody, bodyB: otherBody, room: this.room}); } this.room.events.emit('reldens.endPlayerHitObjectBegin', {playerBody, otherBody}); } playerHitObjectEnd(playerBody, otherBody) { // Logger.debug('Player hit object end.', playerBody.playerId, otherBody.bodyState?.key); let result = {stopFull: true, continue: true}; this.room.events.emit('reldens.playerHitObjectEnd', {playerBody, result}); if(!result.continue){ return false; } playerBody.stopFull(result.stopFull); } playerHitWallBegin(playerBody, wallBody) { // Logger.debug('Player hit wall being.', playerBody.playerId); this.room.events.emit('reldens.playerHitWallBegin', {playerBody, wallBody}); } playerHitWallEnd(playerBody, wallBody) { // Logger.debug('Player hit wall end.', playerBody.playerId); this.room.events.emit('reldens.startPlayerHitWallEnd', {playerBody, wallBody}); // @NOTE: we can use wall.material to trigger an action over the player, like: // wall.material = lava > reduce player.hp in every step // if the player collides with something we need to restart the pathfinder if it was active: if(playerBody.autoMoving && 1 < playerBody.autoMoving.length){ let destPoint = playerBody.autoMoving.pop(); playerBody.moveToPoint({column: destPoint[0], row: destPoint[1]}); return; } if(playerBody.world && !playerBody.world.applyGravity){ playerBody.stopFull(true); } this.room.events.emit('reldens.endPlayerHitWallEnd', {playerBody, wallBody}); } playerHitChangePointBegin(playerBody, changePoint) { // Logger.debug('Player hit change point begin.', playerBody.playerId, changePoint.changeScenePoint); this.room.events.emit('reldens.startPlayerHitChangePoint', {collisionsManager: this, playerBody, changePoint}); playerBody.resetAuto(); // check if the player is not changing scenes already: let isChangingScene = sc.get(playerBody, 'isChangingScene', false); if(isChangingScene){ // @NOTE: if the player is already changing scene do nothing. Logger.info('Player is busy for a change point: '+playerBody.playerId); return false; } let playerSchema = this.room.playerBySessionIdFromState(playerBody.playerId); let contactClient = this.room.getClientById(playerBody.playerId); let isGuest = -1 !== contactClient.auth.email?.indexOf(this.guestEmailDomain); if(!this.room.validateRoom(changePoint.changeScenePoint, isGuest, true)){ Logger.info('Guest Player hit change point but is not allowed to the room: '+playerSchema.state.scene); this.room.events.emit('reldens.guestInvalidChangePoint', { collisionsManager: this, playerBody, changePoint, playerSchema, contactClient, isGuest }); return false; } let playerPosition = {x: playerBody.position[0], y: playerBody.position[1]}; this.room.state.positionPlayer(playerBody.playerId, playerPosition); let changeData = {prev: playerSchema.state.scene, next: changePoint.changeScenePoint}; // Logger.debug('Player "'+playerBody.playerId+'" hit change point.', changeData); playerBody.isChangingScene = true; // @NOTE: we do not need to change back the isChangingScene property back to false since in the new // scene a new body will be created with the value set to false by default. this.room.nextSceneInitialPosition(contactClient, changeData, playerBody).catch((error) => { Logger.error('There was an error while setting the next scene initial position.', error); }); this.room.events.emit('reldens.endPlayerHitChangePoint', { collisionsManager: this, playerSchema, playerBody, changePoint, changeData }); } objectHitObjectBegin(bodyA, bodyB) { // Logger.debug('Object hit object begin.', bodyA.bodyState?.key, bodyB.bodyState?.key); this.room.events.emit('reldens.startObjectHitObject', {bodyA, bodyB}); let aPriority = sc.hasOwn(bodyA, 'hitPriority'); let bPriority = sc.hasOwn(bodyB, 'hitPriority'); let onHitData = {bodyA: bodyA, bodyB: bodyB, room: this.room}; let priorityObject = (!aPriority && !bPriority) || (aPriority && (!bPriority || aPriority > bPriority)) ? bodyA : bodyB; if(priorityObject.roomObject && sc.isFunction(priorityObject.roomObject?.onHit)){ priorityObject.roomObject.onHit(onHitData); } if(bodyA.isBullet){ bodyA.roomObject.removeBullet(bodyA); } if(bodyB.isBullet){ bodyB.roomObject.removeBullet(bodyB); } this.findAlternativePath(bodyA); this.findAlternativePath(bodyB); this.room.events.emit('reldens.endObjectHitObject', {bodyA, bodyB, priorityObject}); } objectHitObjectEnd(bodyA, bodyB) { // Logger.debug('Object hit object end.', bodyA.bodyState?.key, bodyB.bodyState?.key); this.bodyFullStop(bodyA); this.bodyFullStop(bodyB); this.room.events.emit('reldens.objectHitObjectEnd', {bodyA, bodyB}); } objectHitWallBegin(objectBody, wall) { // Logger.debug('Object hit wall begin.', objectBody.bodyState?.key); let event = {objectBody, wall, continue: true}; this.room.events.emit('reldens.objectHitWallBegin', event); if(!event.continue){ return event; } if(objectBody.isBullet){ objectBody.roomObject.removeBullet(objectBody); } return event; } objectHitWallEnd(objectBody) { // Logger.debug('Object hit wall end.', objectBody.bodyState?.key); this.room.events.emit('reldens.startObjectHitWall', {objectBody}); // @NOTE: we can use wall.material to trigger an action over the player, like: // wall.material = lava > reduce player.hp in every step // if the player collides with something we need to restart the pathfinder if it was active: this.resetObjectAutoMove(objectBody); this.room.events.emit('reldens.endObjectHitWall', {objectBody}); } bodyFullStop(body) { if(!body){ return false; } let isBodyAMoving = body.autoMoving && 0 < body.autoMoving.length; if(!isBodyAMoving && !body.isBullet && body.isRoomObject && (body instanceof PhysicalBody)){ body.stopFull(true); } if(body.isBullet){ body.roomObject.removeBullet(body); } } findAlternativePath(body) { if(!body.autoMoving || 0 === body.autoMoving.length){ return false; } // Logger.debug('Find alternative path for body "'+body.bodyLogKey()+'".'); let currentPoint = body.autoMoving.shift(); let destPoint = body.autoMoving.pop(); body.autoMoving = body.getPathFinder().findPath(currentPoint, destPoint); } resetObjectAutoMove(body) { if(!(body instanceof PhysicalBody)){ return; } if(!body.world){ return; } let lastPoint = false; if(sc.isArray(body.autoMoving) && 0 < body.autoMoving.length){ lastPoint = body.autoMoving.pop(); } if(!lastPoint){ return; } body.world.applyGravity ? body.stopFull(true) : body.stopX(true); body.autoMovingResetRetries++; if(body.autoMovingResetMaxRetries === body.autoMovingResetRetries){ body.autoMovingResetRetries = 0; // Logger.debug('Reset object auto-move, returning to original point.'); return body.moveToOriginalPoint(); } /* Logger.debug( 'Body "'+body.bodyLogKey()+'" auto-move to points: '+lastPoint[0]+' / '+lastPoint[1]+'.' +' Retries: '+body.autoMovingResetRetries+' / '+body.autoMovingResetMaxRetries ); */ body.moveToPoint({column: lastPoint[0], row: lastPoint[1]}); } getWallBody(evt) { let {bodyA, bodyB} = evt; return bodyA && bodyA.isWall ? bodyA : (bodyB && bodyB.isWall ? bodyB : false); } getObjectBody(evt) { let {bodyA, bodyB} = evt; return bodyA && bodyA.isRoomObject ? bodyA : (bodyB && bodyB.isRoomObject ? bodyB : false); } getPlayerBody(evt) { let {bodyA, bodyB} = evt; return bodyA && bodyA.playerId ? bodyA : (bodyB && bodyB.playerId ? bodyB : false); } } module.exports.CollisionsManager = CollisionsManager;