diff --git a/index.js b/index.js index 2be7e8367a8ba94fea34955c4e5082f4810122b8..9768d87813bc3e67105e1d97768f468ab13ddcb7 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ const express = require('express'); +const fs = require('fs'); const session = require('express-session'); const bodyParser = require('body-parser'); const mongoose = require('mongoose'); @@ -6,7 +7,7 @@ const morgan = require('morgan'); const mongoDBStore = require('connect-mongodb-session')(session); // Config -const config = require('./config.json'); +const config = require('js-yaml').safeLoad(fs.readFileSync('./config.yml', 'utf8')); // Middlewares const userLoader = require('./middlewares/userLoader'); const loginChecker = require('./middlewares/loginChecker'); @@ -15,6 +16,7 @@ const render = require('./utils/render'); const { warn, error } = require('./utils/notifications'); // Routes const authRouter = require('./routes/auth'); +const charactersRouter = require('./routes/characters'); // Configuration const app = express(); @@ -36,14 +38,16 @@ app.use(session({ // Middlewares app.use(bodyParser.urlencoded({ extended: false })); +app.use('/media', express.static('media')); app.use(userLoader); app.use(loginChecker); // Routes -app.get('/', (req, res) => { +app.get('/', async (req, res) => { return render(req, res, 'home'); }); app.use('/auth', authRouter); +app.use('/characters', charactersRouter); mongoose.connect('mongodb://localhost/rolegame', err => { if (err) { diff --git a/media/script.js b/media/script.js new file mode 100644 index 0000000000000000000000000000000000000000..1ab47a6f37fdeba543778a8ee9466ab7478a0277 --- /dev/null +++ b/media/script.js @@ -0,0 +1,68 @@ +const changeAvatar = (avatar) => { + $('#avatar').attr('src', `/media/avatars/${avatar}`); +}; + +const attributes = { + 'strength': 10, + 'constitution': 10, + 'dexterity': 10, + 'intelligence': 10, + 'wisdom': 10, + 'charisma': 10 +}; + +const costTable = { + 7: -4, + 8: -2, + 9: -1, + 10: 0, + 11: 1, + 12: 2, + 13: 3, + 14: 5, + 15: 7, + 16: 10, + 17: 13, + 18: 17 +}; + +const getScore = () => Object.keys(attributes).reduce((total, el) => total + costTable[attributes[el]], 0); + +const reduceAttribute = (attribute) => { + attributes[attribute] --; + if (attributes[attribute]<7) { + attributes[attribute] ++; + alert("You can't have attribute under 7."); + } else { + document.getElementById(attribute).innerText = attributes[attribute]; + $(`#${attribute}-bar`).progress('decrement', 1); + document.getElementById('available-points').innerHTML = initAttributesPoints - getScore(); + } +}; + +const augmentAttribute = (attribute) => { + attributes[attribute] ++; + if (getScore()>initAttributesPoints) { + attributes[attribute] --; + alert("Not enough points."); + } else if (attributes[attribute]>18) { + alert("You can't have attribute over 18."); + attributes[attribute] --; + } else { + document.getElementById(attribute).innerText = attributes[attribute]; + $(`#${attribute}-bar`).progress('increment', 1); + document.getElementById('available-points').innerHTML = initAttributesPoints - getScore(); + } +}; + +const createCharacter = () => { + const form = document.getElementById('create-character'); + Object.keys(attributes).forEach(attribute => { + const input = document.createElement("input"); + input.setAttribute("name", attribute); + input.setAttribute("value", attributes[attribute]); + input.setAttribute("type", "hidden"); + form.appendChild(input); + }); + form.submit(); +}; diff --git a/models/character.js b/models/character.js index 95853d2b3d31cb6259b7101a57e0af77485c918a..942d5eea18b18e231936a2041f3f8e048511448c 100644 --- a/models/character.js +++ b/models/character.js @@ -1,8 +1,54 @@ const mongoose = require('mongoose'); +const random = require('random'); +const fs = require('fs'); +const { maxActionPoints } = require('js-yaml').safeLoad(fs.readFileSync('./config.yml', 'utf8')); const Character = new mongoose.Schema({ - name: String, - level: Number, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + name: { type: String, require: true }, + avatar: { type: String }, + birthDate: { type: Date, required: true, default: Date.now }, + actionPoints: { type: Number, default: maxActionPoints, min: 0, max: maxActionPoints }, + attributes: { + strength: { type: Number, default: 10 }, + constitution: { type: Number, default: 10 }, + dexterity: { type: Number, default: 10 }, + intelligence: { type: Number, default: 10 }, + wisdom: { type: Number, default: 10 }, + charisma: { type: Number, default: 10 }, + } }); +function randomAttribute() { + let dices = []; + let min = random.int(1,6); + for (let i = 0; i < 3; i++) { + let number = random.int(1,6); + if (number < min) { + dices.push(min); + min = number; + } else { + dices.push(number); + } + } + return dices.reduce((total, val) => total + val); +} + +// Character.statics.generateCharacter = async function(name, userId, avatar) { +// const character = await this.create({ +// name, +// user: userId, +// avatar, +// attributes: { +// strength: randomAttribute(), +// constitution: randomAttribute(), +// dexterity: randomAttribute(), +// intelligence: randomAttribute(), +// wisdom: randomAttribute(), +// charisma: randomAttribute(), +// } +// }); +// return character; +// } + module.exports = mongoose.model('Character', Character); \ No newline at end of file diff --git a/routes/characters.js b/routes/characters.js new file mode 100644 index 0000000000000000000000000000000000000000..2678e67d44c68c01fd450215fd74c9b2e9906de4 --- /dev/null +++ b/routes/characters.js @@ -0,0 +1,48 @@ +const express = require('express'); +const fs = require('fs'); +const { initAttributesPoints, costTable, attributes, maxActionPoints, maxCharactersNumber } = require('js-yaml').safeLoad(fs.readFileSync('./config.yml', 'utf8')); +const Character = require('../models/character'); +const render = require('../utils/render'); +const { warn, error } = require('../utils/notifications'); + +const router = express.Router(); + +const getScore = (data) => Object.keys(data).reduce((total, el) => total + costTable[data[el]], 0); + +router.get('/', async (req, res) => { + // TODO: Login required + const avatars = fs.readdirSync('media/avatars'); + Character.find({ user: req.session.user._id }, (err, characters) => { + return render(req, res, 'characters', { characters, avatars, initAttributesPoints, maxActionPoints }); + }); +}); + +// router.post('/generate', async (req, res) => { +// // TODO: Login required +// if (!req.body.name) { +// return error(req, res, 'Name required'); +// } +// const character = await Character.generateCharacter(req.body.name, req.session.user._id, req.body.avatar); +// return res.redirect('/characters'); +// }); + +router.post('/create', async (req, res) => { + const validatedAttributes = attributes.reduce((obj, name) => { + obj[name] = Number(req.body[name]); + return obj; + }, {}); + if (getScore(validatedAttributes) > initAttributesPoints || attributes.map(name => validatedAttributes[name]).some(value => value > 18 || value < 7)) { + return error(req, res, 'Bad form', "Character creation corrupted"); + } else if (maxCharactersNumber <= await Character.countDocuments({ user: req.session.user._id })) { + return error(req, res, 'Too many characters', "You are not allowed to create one more character."); + } + await Character.create({ + user: req.session.user._id, + name: req.body.name, + avatar: req.body.avatar, + attributes: validatedAttributes + }); + res.redirect('/characters'); +}); + +module.exports = router; \ No newline at end of file diff --git a/views/character_summary.pug b/views/character_summary.pug new file mode 100644 index 0000000000000000000000000000000000000000..ced5e41031650bf8d49403f01e394c6cbdf71e7d --- /dev/null +++ b/views/character_summary.pug @@ -0,0 +1,28 @@ +.ui.card.fluid + .content + img.right.floated.mini.image.ui(src="/media/avatars/"+character.avatar, alt="Avatar") + .header= character.name + .meta + .date Created on #{character.birthDate.toDateString()} + .ui.tiny.progress.yellow.progress.action-points(data-total=maxActionPoints, data-value=character.actionPoints) + .bar + .label #{character.actionPoints} action points + .content + .ui.label(class=character.attributes.strength<=8 ? "orange" : character.attributes.strength>=12 ? "green" : "" ) + | Strength + .detail= character.attributes.strength + .ui.label(class=character.attributes.constitution<=8 ? "orange" : character.attributes.constitution>=12 ? "green" : "" ) + | Constitution + .detail= character.attributes.constitution + .ui.label(class=character.attributes.dexterity<=8 ? "orange" : character.attributes.dexterity>=12 ? "green" : "" ) + | Dexterity + .detail= character.attributes.dexterity + .ui.label(class=character.attributes.intelligence<=8 ? "orange" : character.attributes.intelligence>=12 ? "green" : "" ) + | Intelligence + .detail= character.attributes.intelligence + .ui.label(class=character.attributes.wisdom<=8 ? "orange" : character.attributes.wisdom>=12 ? "green" : "" ) + | Wisdom + .detail= character.attributes.wisdom + .ui.label(class=character.attributes.charisma<=8 ? "orange" : character.attributes.charisma>=12 ? "green" : "" ) + | Charisma + .detail= character.attributes.charisma diff --git a/views/characters.pug b/views/characters.pug new file mode 100644 index 0000000000000000000000000000000000000000..9f44b9a428b362841c871a3491154c6afd5f4c4d --- /dev/null +++ b/views/characters.pug @@ -0,0 +1,56 @@ +extends base.pug + +block main + .ui.cards + each character in characters + include character_summary.pug + button.ui.basic.teal.labeled.icon.button(onClick="$('.ui.modal').modal('show');") + | Create character + i.add.icon + + .ui.modal + i.close.icon + .ui.header Create a new character + .ui.image.content + .ui.medium.image + img#avatar(src="/media/avatars/fazzlemare_by_charro_art_d8etbfa-pre.jpg", alt="Avatar") + .description + form.ui.form#create-character(name="create-character", action="/characters/create", method="post") + .ui.header Details + .ui.field + input#name(type="text", name="name", placeholder="name") + .ui.field + select#avatar-select.ui.selection.dropdown(name="avatar", onChange="changeAvatar($('select').dropdown('get value'))") + each avatar in avatars + option(value=avatar)= avatar + .ui.header Attributes:  + span#available-points= initAttributesPoints + |  points remaining + .ui.grid + each attribute in ['strength', 'constitution', 'dexterity', 'intelligence', 'wisdom', 'charisma'] + .eight.wide.column + .ui.segment + .ui.horizontal.statistic + .value(id=attribute) 10 + .label= attribute + button.ui.button.orange.icon.circular.left.floated(type="button", onClick=`reduceAttribute('${attribute}')`) + i.minus.icon + button.ui.button.teal.icon.circular.right.floated(type="button", onClick=`augmentAttribute('${attribute}')`) + i.add.icon + .ui.bottom.attached.progress(id=attribute+"-bar") + .bar + + + .actions + button.ui.basic.orange.button.labeled.icon(type="reset", onClick="$('.ui.modal').modal('hide');") + i.close.icon + | Cancel + button.ui.teal.labeled.icon.button(type="submit", onClick="createCharacter()") + i.check.icon + | Create + + + script $('.ui.dropdown').dropdown(); + script $('.ui.progress').progress({ total: 20, value: 10 }); + script let initAttributesPoints = #{initAttributesPoints}; + script(src="/media/script.js") \ No newline at end of file