diff --git a/controllers/actions-controller.js b/controllers/actions-controller.js index 88b8ee3..22ca2cd 100644 --- a/controllers/actions-controller.js +++ b/controllers/actions-controller.js @@ -4,50 +4,160 @@ const Mob = require('../models/mob'); const mobsController = require('./mobs-controller'); ////And the log model const Log = require('../models/log'); +////And the battle model +const Battle = require('../models/battle'); + ////Create the actions controller object const actionsController = {}; ////Give it logic +/* XXX Trying this shit again.*/ //attacking +//actionsController.attack = function(req, res){ +// //First make sure that both parties are in the same battle... +// Battle.confirmCompatibleCombatants(req.params.attacker, req.params.defender) +// .then(response => { +// if(response.length === 0){ +// let error = { +// message: "Attack rejected: You and your target are not in a battle or in different battles." +// } +// throw error; +// } +// //...Then we grab the attacker(?) and request that the defender takes damage according to specified calculations... +// Mob.findById(req.params.attacker) +// .then(attacker => { +// return Mob.takeDamage(req.params.defender, attacker.attribute.strength) +// .then(defender => { +// let content = ""; +// if (defender.attribute.living){ +// content = `${attacker.name} attacks ${defender.name} for ${attacker.attribute.strength} damage! // ${attacker._id} =${attacker.attribute.strength}=> ${defender._id} //`; +// } +// //...But if the defender is already dead then don't do anything and throw and error. +// else if (defender.attackFailed){ +// console.log('ERROR, CATCH!!!'); +// let error = {}; +// error.message = "Attack rejected: target is already downed and can't recieve anymore damage." +// throw error; +// } +// else { +// content = `${attacker.name} attacks ${defender.name} for ${attacker.attribute.strength} damage, knocking them to the ground! // ${attacker._id} =x!${attacker.attribute.strength}!x=> ${defender._id} //`; +// } +// //Log the action for future reference... +// let newLog = { +// content, +// timestamp: Date.now(), +// type: "action", +// room: null +// } +// return Log.create(newLog) +// .then(logResponse => { +// res.status(200) +// .json({ +// message: "Attack completed successfully!", +// log: logResponse, +// attacker, +// defender, +// }) +// }) +// }) +// }) +// }) +// //General catchall +// .catch(err => { +// console.log(err) +// res.status(500).json({error: err}) +// }) +//} + actionsController.attack = function(req, res){ - Mob.findById(req.params.attacker) - .then(attacker => { - return Mob.takeDamage(req.params.defender, attacker.attribute.strength) - .then(defender => { - let content = ""; - if (defender.attribute.living){ - content = `${attacker.name} attacks ${defender.name} for ${attacker.attribute.strength} damage! // ${attacker._id} =${attacker.attribute.strength}=> ${defender._id} //`; - } - else if (defender.attackFailed){ - console.log('ERROR, CATCH!!!'); - let error = {}; - error.message = "Attack rejected: target is already downed and can't recieve anymore damage." - throw error; + //See if the combatants are in the same battle. + Battle.confirmCompatibleCombatants(req.params.attacker, req.params.defender) + .then(response => { + if (response.length === 0){ + let error = { + message: "Attack rejected: characters are not in the same battle" } - else { - content = `${attacker.name} attacks ${defender.name} for ${attacker.attribute.strength} damage, knocking them to the ground! // ${attacker._id} =x!${attacker.attribute.strength}!x=> ${defender._id} //`; - } - let newLog = { - content, - timestamp: Date.now(), - type: "action", - room: null - } - return Log.create(newLog) - .then(logResponse => { - res.status(200) - .json({ - message: "Attack completed successfully!", - log: logResponse, - attacker, - defender, + throw error; + } + else { + //Grab both the attacker and the defender + return Promise.all([Mob.findById(req.params.attacker), Mob.findById(req.params.defender)]) + .then(promiseResponse => { + //Find the attacker in the battle... + for (let combatant in response[0].combatants){ + if (response[0].combatants[combatant].mobId === req.params.attacker){ + //If it's not their turn, stop the attack + if (response[0].combatants[combatant].turnCount !== 0){ + let error = { + message: "Attack rejected: not the character's turn" + } + throw error; + } + //If it is, then we update the battle with an increased turn count for the attacker + response[0].combatants[combatant].turnCount += 50 - (promiseResponse[0].attribute.agility * 2) + //Battle.update(response[0]) + break; + } + } + //Then we find who is next in line for attacking + let lowest = response[0].combatants[0].turnCount; + console.log('finding next in line...'); + for (let combatant in response[0].combatants){ + if (response[0].combatants[combatant].turnCount < lowest){ + lowest = response[0].combatants[combatant].turnCount; + console.log('...new lowest found...'); + } + } + //...And finally progress everyone's turn timer + for (let combatant in response[0].combatants){ + response[0].combatants[combatant].turnCount -= lowest; + console.log('buzz!'); + } + //Apply the update + Battle.update(response[0]) + + //Deal damage based on the attacker's strength + return Mob.takeDamage(req.params.defender, promiseResponse[0].attribute.strength) + .then(postDamageResponse => { + //Check if the attack failed + if (postDamageResponse.attackFailed){ + console.log('ERROR, CATCH!!!'); + let error = {}; + error.message = "Attack rejected: target is already downed and can't recieve anymore damage."; + throw error; + } + + //Build the log message + let content = ""; + if (promiseResponse[1].attribute.living){ + content = `${promiseResponse[0].name} attacks ${promiseResponse[1].name} for ${promiseResponse[0].attribute.strength} damage! // ${promiseResponse[0]._id} =${promiseResponse[0].attribute.strength}=> ${promiseResponse[1]._id} //`; + } + else { + content = `${promiseResponse[0].name} attacks ${promiseResponse[1].name} for ${promiseResponse[0].attribute.strength} damage, knocking them to the ground!! // ${promiseResponse[0]._id} =x${promiseResponse[0].attribute.strength}x=> ${promiseResponse[1]._id} //`; + } + //build the log + let newLog = { + content, + timestamp: Date.now(), + type: "action", + room: null + } + return Log.create(newLog) + .then(logResponse => { + res.status(200) + .json({ + message: "Attack completed successfully!", + log: logResponse, + attacker: promiseResponse[0], + defender: promiseResponse[1] + }) + }) }) }) - }) - }) - .catch(err => { + } + }).catch(err => { console.log(err) - res.status(500).json({error: err}) + res.status(500).json(err) }) } diff --git a/controllers/battle-controller.js b/controllers/battle-controller.js new file mode 100644 index 0000000..4fd7557 --- /dev/null +++ b/controllers/battle-controller.js @@ -0,0 +1,73 @@ +//Import the battle model +const Battle = require('../models/battle'); +//And the Mob model too. +const Mob = require('../models/mob'); +//Might need the log model too +const Log = require('../models/log'); + +//Create the battlesController object +const battlesController = {}; + +//TODO: Maybe we can have a controller helper in the same way we have a model helper to avoid CRUD function regurgitation?? +//Short information about all battles +battlesController.index = function(req, res){ + Battle.findAll() + .then(battles => { + res.status(200) + .json({ + message: "Battle index retrieved successfully!", + battles + }) + }).catch(err => { + console.log(err); + res.status(500).json({error: err}); + }) +} + +//in depth information about a single battle +battlesController.show = function(req, res){ + Battle.findById(req.params.id) + .then(battle => { + res.status(200) + .json({ + message: "Battle retrieved successfully!", + battle + }) + }).catch(err => { + console.log(err); + res.status(500).json({error: err}); + }) +} + +//Creating a new battle +battlesController.create = function(req, res){ + Battle.create(req.body) + .then(battle => { + res.status(201) + .json({ + message: "Battle created successfully!", + battle + }) + }).catch(err => { + console.log(err); + res.status(500).json({error: err}); + }) +} + +//Adding combatants to an existing battle +battlesController.addToBattle = function(req, res){ + Battle.insertIntoCombatants(req.params.id, req.body) + .then(battle => { + res.status(200) + .json({ + message: "Combatant added to battle successfully!", + battle + }) + }).catch(err => { + console.log(err); + res.status(500).json({error: err}); + }) +} + +//Export the file +module.exports = battlesController diff --git a/db/migrations/migration-00003.js b/db/migrations/migration-00003.js new file mode 100644 index 0000000..cce7e80 --- /dev/null +++ b/db/migrations/migration-00003.js @@ -0,0 +1,45 @@ +db = new Mongo().getDB('psg'); +db.createCollection('battles', { + validationLevel: "strict", + validator: { + $jsonSchema: { + bsonType: "object", + required: ["active","currentTurn","turnTimer","combatants"], + additionalProperties: false, + properties: { + _id: { + bsonType: "objectId" + }, + //Going to skimp out on the descriptions for now, they have no purpose. + active: { + bsonType: "bool" + }, + currentTurn: { + bsonType: "string" + }, + turnTimer: { + bsonType: "int" + }, + combatants: { + bsonType: "array", + items: { + bsonType: "object", + required: ["mobId", "turnCount", "team"], + additionalProperties: false, + properties: { + mobId: { + bsonType: "string" + }, + turnCount: { + bsonType: "int" + }, + team: { + bsonType: "int" + } + } + } + } + } + } + } +}); diff --git a/models/battle.js b/models/battle.js new file mode 100644 index 0000000..7dc4b3a --- /dev/null +++ b/models/battle.js @@ -0,0 +1,212 @@ +//Import the database +const db = require('../db/config'); + +//Import modelHelper object +const modelHelper = require('./_model-helper'); + +//Import the mob model +const Mob = require('./mob'); + +//Create the battle object +const Battle = {}; + +//XXX Do we need this?? +//finding all battles +Battle.findAll = function(){ + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + .find({}) + .toArray() + .then(response => modelHelper.serverLog('Battle.findAll', response)) + .then(response => { + connection.close(); + return response; + }) + }) +} + +//finding a single battle +Battle.findById = function(id){ + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + .findOne({"_id": db.objectId.createFromHexString(id)}) + .then(response => modelHelper.serverLog('Battle.findById', response)) + .then(response => { + connection.close(); + return response; + }) + }) +} + +//Finding a single battle by combatant +Battle.findByCombatant = function(combatantId){ + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + .find({"combatants.mobId": combatantId}) + .toArray() + .then(response => modelHelper.serverLog('Battle.findByCombatant', response)) + .then(response => { + connection.close(); + return response; + }) + }) +} + +//creating a single battle +Battle.create = function(battle){ + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + .insertOne(battle) + .then(response => modelHelper.serverLog('Battle.create', response.ops[0])) + .then(response => { + connection.close(); + return response; + }) + }) +} + +//Confirming the validity of a potential combatant +Battle.confirmEligibleCombatant = function(combatantId){ + return Mob.findById(combatantId) + .then(response => { + if (response){ + return response.mob; + } + else { + return null; + } + }) + .then(response => { + console.log(response); + return response; + }) +} + +//Adding combatants +Battle.insertIntoCombatants = function(id, combatant){ + return Battle.confirmEligibleCombatant(combatant.mobId) + .then(confirmationResponse => { + if (confirmationResponse === null){ + let error = { + message: "Mob not found, cancelling operation." + } + throw error; + } + return Battle.findByCombatant(combatant.mobId) + .then(combatantConfirmationResponse => { + if (combatantConfirmationResponse.length){ + let error = { + message: "Mob already in a battle, cancelling operation." + } + throw error; + } + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + .findOneAndUpdate( + {"_id": db.objectId.createFromHexString(id)}, + {$push: {combatants: combatant}}, + {returnOriginal: false} + ).then(response => modelHelper.serverLog('Battle.insertIntoCombatants', response.value)) + .then(response => { + connection.close(); + return response; + }) + }) + }) + }) +} + +//Confirming the presence of two combatants in the same battle +Battle.confirmCompatibleCombatants = function(combatantA, combatantB){ + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + //.find({ + // $and: [{ + // "combatants.mobId": combatantA + // }, + // { + // "combatants.mobId": combatantB + // } + // ] + //}) + //.toArray() + .aggregate([ + //TODO: On confirmation, return involved mob's data to be modified. + //XXX: Due to poor code practices we have no reliable way to grab data from another collection + // by the object id. To solve this, I'd need to change how the object ids are stored in the + // database itself which would require a lot of refactoring. I'll do that later. + { $match: { $and: [ + { "combatants.mobId": combatantA }, + { "combatants.mobId": combatantB } + ]}}//, + // { $unwind: "$combatants" }, + // { $lookup: { + // from: "mobs", + // localField: "combatants.mobId", + // foreignField: "_id", + // as: "fetchedMob" + // }} + ]) + .toArray() + .then(response => modelHelper.serverLog('Battle.confirmCompatibleCombatants', response)) + .then(response => { + connection.close(); + return response; + }) + }) +} + +//updating a battle +Battle.update = function(updatedBattle){ + return db.client.connect(db.url) + .then(connection => { + let selectedDb = connection.db(db.name); + return selectedDb.collection('battles') + .findOneAndUpdate( + { '_id': updatedBattle._id }, + { $set: updatedBattle }, + { returnOriginal: false } + ).then(response => modelHelper.serverLog('Battle.update', response)) + .then(response => { + connection.close(); + return response; + }) + }) +} + +//Progressing the turn timers to allow the next person to go +Battle.kickStart = function(id){ + return Battle.findById(id) + .then( battle => { + let lowest = battle.combatants[0].turnCount; + console.log('finding next in line...'); + for (let combatant in battle.combatants){ + if (battle.combatants[combatant].turnCount < lowest){ + lowest = battle.combatants[combatant].turnCount; + console.log('...new lowest found...'); + } + } + //...And finally progress everyone's turn timer + for (let combatant in battle.combatants){ + battle.combatants[combatant].turnCount -= lowest; + console.log('buzz!'); + } + //Apply the update + Battle.update(battle) + }) +} + +//Export it +module.exports = Battle; diff --git a/models/mob.js b/models/mob.js index 289aaca..f0d37d8 100644 --- a/models/mob.js +++ b/models/mob.js @@ -102,17 +102,23 @@ Mob.takeDamage = function(id, damage){ .then(target => { //Forgive me for the following 14 lines... //TODO: Refactor this shit + //Create an empty object that will tell the database how to update involved parties in the future... let updateInstructions = {}; + //...If the target has less health than damage about to be inflicted... if (target.attribute.health < damage){ + //...And the target is already dead, then do nothing and tell the controller the attack failed. if (!target.attribute.living){ updateInstructions.invalidate = true; damage = 0; } + //Otherwise set the target to 'dead'... updateInstructions['attribute.living'] = false; } + //If the target is alive and will survive the coming attack (or was dead and is being brought back to life, set the target to 'alive'... else { updateInstructions['attribute.living'] = true; } + //apply the damage... updateInstructions['attribute.health'] = target.attribute.health - damage; return selectedDb.collection('mobs') .findOneAndUpdate( @@ -121,6 +127,7 @@ Mob.takeDamage = function(id, damage){ { returnOriginal: false } ).then(response => modelHelper.serverLog('Mob.takeDamage', response.value)) .then(response => { + //...If the target didn't take damage then the attack failed if (target.attribute.health === response.attribute.health){ response.attackFailed = true; } diff --git a/routes/battle-routes.js b/routes/battle-routes.js new file mode 100644 index 0000000..5a9320d --- /dev/null +++ b/routes/battle-routes.js @@ -0,0 +1,20 @@ +//Import the controller +const battlesController = require('../controllers/battle-controller'); + +//Create the router instance +const battleRoutes = require('express').Router(); + +//Index +battleRoutes.get('/', battlesController.index); + +//Create single +battleRoutes.post('/', battlesController.create); + +//Get single +battleRoutes.get('/:id', battlesController.show); + +//Add combatant +battleRoutes.post('/:id', battlesController.addToBattle); + +//Export +module.exports = battleRoutes diff --git a/server.js b/server.js index ea2d326..7dce207 100644 --- a/server.js +++ b/server.js @@ -33,6 +33,9 @@ server.use('/action', require('./routes/action-routes')); //"/log" server.use('/log', require('./routes/log-routes')); +//"/battle" +server.use('/battle', require('./routes/battle-routes')); + //Root server.use('/', (req, res) => { res.status(200).json({