Browse Source

Refactor to use ES6 syntax

pull/14/head
Armen138 9 months ago
parent
commit
2e8560a287
14 changed files with 2175 additions and 715 deletions
  1. +13
    -0
      .eslintrc.yml
  2. +18
    -16
      game/banner.js
  3. +86
    -79
      game/character.js
  4. +70
    -70
      game/errors.js
  5. +333
    -326
      game/game.js
  6. +30
    -27
      game/items.js
  7. +47
    -44
      game/menu.js
  8. +75
    -66
      game/monster.js
  9. +16
    -14
      game/monsters.js
  10. +23
    -21
      game/titles.js
  11. +49
    -45
      game/world.js
  12. +6
    -6
      index.js
  13. +1402
    -0
      package-lock.json
  14. +7
    -1
      package.json

+ 13
- 0
.eslintrc.yml View File

@@ -0,0 +1,13 @@
env:
browser: true
es6: true
node: true
extends:
- airbnb-base
globals:
Atomics: readonly
SharedArrayBuffer: readonly
parserOptions:
ecmaVersion: 2018
sourceType: module
rules: {}

+ 18
- 16
game/banner.js View File

@@ -1,8 +1,10 @@
const chalk = require('chalk');
const titles = require('./titles');
/* eslint-disable import/extensions */
/* eslint-disable no-restricted-syntax */
import chalk from 'chalk';
import titles from './titles.js';

const banner = {
text: `@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@@ @@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@@@@ @@@@@@@
text: `@@@ @@@ @@@ @@@@@@@@ @@@ @@@ @@@@@@@ @@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@@@@ @@@@@@@
@@@@ @@@ @@@ @@@@@@@@@ @@@ @@@ @@@@@@@ @@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@
@@!@!@@@ @@! !@@ @@! @@@ @@! @@! @@! @@! @@@ @@! @@@ @@! @@@ @@! @@@
!@!!@!@! !@! !@! !@! @!@ !@! !@! !@! !@! @!@ !@! @!@ !@! @!@ !@! @!@
@@ -12,18 +14,18 @@ const banner = {
:!: !:! :!: :!: !:: :!: !:! :!: :!: :!: :!: !:! :!: !:! :!: !:! :!: !:!
:: :: :: ::: :::: :: ::: :: :: :: :::: :: ::: :: ::: ::::: :: :: :::
:: : : :: :: : : : : : : : :: :: : : : : : : : : : : : : `,
colored: function () {
let lines = banner.text.split("\n");
let aligned = [];
for(let line of lines) {
if(line.length < 106) {
line += " ".repeat(106 - line.length);
}
aligned.push(titles.header(line, banner.width));
}
return chalk.red(aligned.join('\n'));
},
width: process.stdout.columns //106
colored() {
const lines = banner.text.split('\n');
const aligned = [];
for (let line of lines) {
if (line.length < 106) {
line += ' '.repeat(106 - line.length);
}
aligned.push(titles.header(line, banner.width));
}
return chalk.red(aligned.join('\n'));
},
width: process.stdout.columns, // 106
};

module.exports = banner;
export default banner;

+ 86
- 79
game/character.js View File

@@ -1,89 +1,96 @@
const errors = require('./errors');
/* eslint-disable arrow-parens */
/* eslint-disable import/extensions */
/* eslint-disable no-restricted-syntax */
import errors from './errors.js';

class Character {
constructor() {
this.inventory = [];
this.inventorySlots = 2;
this.health = 10;
this.maxHealth = 10;
this.equipment = {
leftHand: null, // secondary weapon
rightHand: null, // weapon
finger: null, // rings
head: null, // head covering armor
torso: null, // shirts
legs: null, // pants
feet: null, // boots or shoes
neck: null, // amulets and charms
}
}
get equipped() {
return Object.values(this.equipment).filter(item => item !== null).map(item => item.name);
}
get armor() {
let armor = 0;
for(let equipSlot in this.equipment) {
if(this.equipment[equipSlot] && this.equipment[equipSlot].armor) {
armor += this.equipment[equipSlot].armor;
}
}
return armor;
constructor() {
this.inventory = [];
this.inventorySlots = 2;
this.health = 10;
this.maxHealth = 10;
this.equipment = {
leftHand: null, // secondary weapon
rightHand: null, // weapon
finger: null, // rings
head: null, // head covering armor
torso: null, // shirts
legs: null, // pants
feet: null, // boots or shoes
neck: null, // amulets and charms
};
}
get equipped() {
return Object.values(this.equipment).filter(item => item !== null).map(item => item.name);
}
get armor() {
let armor = 0;
for (const equipSlot in this.equipment) {
if (this.equipment[equipSlot] && this.equipment[equipSlot].armor) {
armor += this.equipment[equipSlot].armor;
}
}
get damage() {
let damage = 1; // bare knuckle punches, 1 damage
for(let equipSlot in this.equipment) {
if(this.equipment[equipSlot] && this.equipment[equipSlot].damage) {
damage += this.equipment[equipSlot].damage;
}
}
return damage;
}
take(world, item) {
let promise = new Promise((resolve, reject) => {
if(this.inventory.length < this.inventorySlots) {
let worldItem = world.take(item);
if(worldItem.error) {
reject(worldItem.error);
} else {
this.inventory.push(worldItem.item);
resolve(`You have added ${item} to your inventory.`);
}
} else {
reject(errors.inventoryfull());
}
});
return promise;
return armor;
}

get damage() {
let damage = 1; // bare knuckle punches, 1 damage
for (const equipSlot in this.equipment) {
if (this.equipment[equipSlot] && this.equipment[equipSlot].damage) {
damage += this.equipment[equipSlot].damage;
}
}
doDamage(roll) {
let damage = { damage: this.damage, message: 'You attack, and causes significant damage.' };
if(roll === 1) {
damage.damage = 0;
damage.message = 'You attack, but miss!';
}
if(roll === 20) {
damage.damage *= 2;
damage.message = 'You score a critical hit!';
return damage;
}

take(world, item) {
const promise = new Promise((resolve, reject) => {
if (this.inventory.length < this.inventorySlots) {
const worldItem = world.take(item);
if (worldItem.error) {
reject(worldItem.error);
} else {
this.inventory.push(worldItem.item);
resolve(`You have added ${item} to your inventory.`);
}
return damage;
} else {
reject(errors.inventoryfull());
}
});
return promise;
}

doDamage(roll) {
const damage = { damage: this.damage, message: 'You attack, and causes significant damage.' };
if (roll === 1) {
damage.damage = 0;
damage.message = 'You attack, but miss!';
}
die() {
return {
message: 'Ah, so this is it... this is where the adventure ends. We had a good run though, didn\'t we?',
status: "death"
}
if (roll === 20) {
damage.damage *= 2;
damage.message = 'You score a critical hit!';
}
defend(damage) {
let penalty = Math.max(damage.damage - this.armor, 0);
this.health -= penalty;
if(this.health <= 0) {
return this.die();
}
return { message: "'Tis but a scratch!" };
return damage;
}

defend(damage) {
const penalty = Math.max(damage.damage - this.armor, 0);
this.health -= penalty;
if (this.health <= 0) {
return {
message: 'Ah, so this is it... this is where the adventure ends. We had a good run though, didn\'t we?',
status: 'death',
};
}
attack() {
let roll = Math.random() * 20 | 0; // standard d20 roll
return this.doDamage(roll);
}
return { message: "'Tis but a scratch!" };
}

attack() {
const roll = Math.floor(Math.random() * 20); // standard d20 roll
return this.doDamage(roll);
}
}

module.exports = Character;
export default Character;

+ 70
- 70
game/errors.js View File

@@ -1,72 +1,72 @@
const errors = {
_not_found: [
"This item doesn't appear to be around here.",
"You look around for a bit, but can't find that anywhere.",
"You want WHAT?",
"Are you sure you saw that here?"
],
_not_in_inventory: [
"You don't have any of those",
"You're not carrying that.",
"You checked all your pockets, it's not there."
],
_inventory_full: [
"You don't have space to carry more items!",
"No pockets left to stuff that into.",
"No can do, buckaroo. No space!",
"You need more pockets!",
"Your inventory is full. Drop stuff to make space!"
],
_not_edible: [
"You can't eat that.",
"Pretty sure that won't taste good.",
"That doesn't look healthy.",
"Ew, that's nasty."
],
_static_item: [
"You can't take that.",
"You try to move it, but it won't budge.",
"Looks like it's stuck.",
"But... how??",
"That won't work.",
"Hmm.... looks heavy, better leave that where it is."
],
_cant_use: [
"You can't use that here.",
"There's nothing here to use that with",
"You can't do that.",
"No way, dude."
],
_slot_used: [
"You already have an item equipped for that slot",
"You can't equip more items of that type",
"That won't fit, try unequipping an item first."
],
random_error(list) {
let idx = Math.floor(Math.random() * list.length);
return list[idx];
},
notfound() {
return errors.random_error(errors._not_found);
},
inventoryfull() {
return errors.random_error(errors._inventory_full);
},
notedible() {
return errors.random_error(errors._not_edible);
},
notininventory() {
return errors.random_error(errors._not_in_inventory);
},
staticitem() {
return errors.random_error(errors._static_item);
},
cantuse() {
return errors.random_error(errors._cant_use);
},
slotused() {
return errors.random_error(errors._slot_used);
}
}
not_found: [
"This item doesn't appear to be around here.",
"You look around for a bit, but can't find that anywhere.",
'You want WHAT?',
'Are you sure you saw that here?',
],
not_in_inventory: [
"You don't have any of those",
"You're not carrying that.",
"You checked all your pockets, it's not there.",
],
inventory_full: [
"You don't have space to carry more items!",
'No pockets left to stuff that into.',
'No can do, buckaroo. No space!',
'You need more pockets!',
'Your inventory is full. Drop stuff to make space!',
],
not_edible: [
"You can't eat that.",
"Pretty sure that won't taste good.",
"That doesn't look healthy.",
"Ew, that's nasty.",
],
static_item: [
"You can't take that.",
"You try to move it, but it won't budge.",
"Looks like it's stuck.",
'But... how??',
"That won't work.",
'Hmm.... looks heavy, better leave that where it is.',
],
cant_use: [
"You can't use that here.",
"There's nothing here to use that with",
"You can't do that.",
'No way, dude.',
],
slot_used: [
'You already have an item equipped for that slot',
"You can't equip more items of that type",
"That won't fit, try unequipping an item first.",
],
random_error(list) {
const idx = Math.floor(Math.random() * list.length);
return list[idx];
},
notfound() {
return errors.random_error(errors.not_found);
},
inventoryfull() {
return errors.random_error(errors.inventory_full);
},
notedible() {
return errors.random_error(errors.not_edible);
},
notininventory() {
return errors.random_error(errors.not_in_inventory);
},
staticitem() {
return errors.random_error(errors.static_item);
},
cantuse() {
return errors.random_error(errors.cant_use);
},
slotused() {
return errors.random_error(errors.slot_used);
},
};

module.exports = errors;
export default errors;

+ 333
- 326
game/game.js View File

@@ -1,355 +1,362 @@
const Vorpal = require('vorpal');
const chalk = require('chalk');
const errors = require('./errors');
const yaml = require('js-yaml');
const fs = require('fs');
const World = require('./world');
const Character = require('./character');
const Monster = require('./monster');
const Items = require('./items');
/* eslint-disable no-mixed-operators */
/* eslint-disable arrow-parens */
/* eslint-disable no-restricted-syntax */
/* eslint-disable import/extensions */
import fs from 'fs';
import yaml from 'js-yaml';
import * as vorpal from 'vorpal';
import chalk from 'chalk';
import errors from './errors.js';
import World from './world.js';
import Character from './character.js';
// import Monster from './monster';
import Items from './items.js';

const worldConfig = yaml.safeLoad(fs.readFileSync(`data/world.yml`, 'utf8'));
const worldConfig = yaml.safeLoad(fs.readFileSync('data/world.yml', 'utf8'));

let world = new World(worldConfig);
let character = new Character();
let items = new Items();
const world = new World(worldConfig);
const character = new Character();
const items = new Items();


let healthScale = [
"red",
"orange",
"yellow",
"green"
const healthScale = [
'red',
'orange',
'yellow',
'green',
];

let Game = () => {
// game.log(vorpal);
const Vorpal = vorpal.default;
const Game = () => {
const game = Vorpal();
game.history('game-command-history');
const countdown = yaml.safeLoad(fs.readFileSync('data/countdown.yml', 'utf8'));

let game = Vorpal();
game.history("game-command-history");
let countdown = yaml.safeLoad(fs.readFileSync(`data/countdown.yml`, 'utf8'));

game.time = 0;
game.advance = function () {
game.time++;
if (world.location.monsters && world.location.monsters.length > 0) {
for (let monster of world.location.monsters) {
let damage = monster.attack();
console.log(damage.message);
let status = character.defend(damage);
console.log(status.message);
if (status.status === "death") {
game.over();
}
}
game.time = 0;
game.advance = () => {
game.time += 1;
if (world.location.monsters && world.location.monsters.length > 0) {
for (const monster of world.location.monsters) {
const damage = monster.attack();
game.log(damage.message);
const status = character.defend(damage);
game.log(status.message);
if (status.status === 'death') {
game.over();
}
if (countdown.length > 0) {
let message = countdown.shift();
console.log(chalk.grey(message));
} else {
game.over();
}
game.delimiter(`[${chalk.red("❤".repeat(character.health))}][${world.location.name}]$`)
}
}
game.over = function () {
console.log("Game over. You ded.");
if (countdown.length > 0) {
const message = countdown.shift();
game.log(chalk.grey(message));
} else {
game.over();
}
game.applyEffects = function (worldItem, itemName) {
if (worldItem.effect.connects) {
world.location.connects.push(worldItem.effect.connects);
}
if (worldItem.effect.removes) {
if (worldItem.effect.removes === itemName) {
// remove item from inventory
let removeItem = character.inventory.indexOf(worldItem.effect.removes);
if (removeItem !== -1) {
character.inventory.splice(removeItem, 1);
}
} else {
// remove item from world
let removeItem = world.location.items.indexOf(worldItem.effect.removes);
if (removeItem !== -1) {
world.location.items.splice(removeItem, 1);
}
}
}
if (worldItem.effect.adds) {
world.location.items.push(worldItem.effect.adds);
}
if (worldItem.effect.grow_inventory) {
character.inventorySlots += worldItem.effect.grow_inventory;
}
if (worldItem.effect.prints) {
game.log(worldItem.effect.prints);
}
game.delimiter(`[${chalk.red('❤'.repeat(character.health))}][${world.location.name}]$`);
};
game.over = () => {
game.log('Game over. You ded.');
};
game.applyEffects = (worldItem, itemName) => {
if (worldItem.effect.connects) {
world.location.connects.push(worldItem.effect.connects);
}
game.look = function () {
if (world.location.title) {
this.log(chalk.white.bold(world.location.title));
if (worldItem.effect.removes) {
if (worldItem.effect.removes === itemName) {
// remove item from inventory
const removeItem = character.inventory.indexOf(worldItem.effect.removes);
if (removeItem !== -1) {
character.inventory.splice(removeItem, 1);
}
this.log(world.location.description);
if (world.location.items && world.location.items.length > 0) {
let worldItems = world.location.items.map(itemName => items.render(itemName));
this.log(`Items here:\n${worldItems.join('\n')}`);
}
if (world.location.monsters && world.location.monsters.length > 0) {
let monsters = world.location.monsters; //.map(monsterConfig => new Monster(monsterConfig));
this.log(`Monsters here:\n${monsters.map(monster => monster.name).join('\n')}`);
}
if (world.location.connects) {
this.log(`This place connects to:\n${world.location.connects.join('\n')}`);
} else {
// remove item from world
const removeItem = world.location.items.indexOf(worldItem.effect.removes);
if (removeItem !== -1) {
world.location.items.splice(removeItem, 1);
}
}
}
if (worldItem.effect.adds) {
world.location.items.push(worldItem.effect.adds);
}
if (worldItem.effect.grow_inventory) {
character.inventorySlots += worldItem.effect.grow_inventory;
}
if (worldItem.effect.prints) {
game.log(worldItem.effect.prints);
}
game.command('menu', 'Return to main menu')
.action(function (args, callback) {
process.stdout.write("\u001B[2J\u001B[0;0f");
this.log(game.menu.intro);
game.hide();
game.menu.show();
callback();
});
};

game.command('look', 'Look around')
.action(function (args, callback) {
game.look.call(this);
callback();
});
game.look = () => {
if (world.location.title) {
game.log(chalk.white.bold(world.location.title));
}
game.log(world.location.description);
if (world.location.items && world.location.items.length > 0) {
const worldItems = world.location.items.map(itemName => items.render(itemName));
game.log(`Items here:\n${worldItems.join('\n')}`);
}
if (world.location.monsters && world.location.monsters.length > 0) {
const { monsters } = world.location;
game.log(`Monsters here:\n${monsters.map(monster => monster.name).join('\n')}`);
}
if (world.location.connects) {
game.log(`This place connects to:\n${world.location.connects.join('\n')}`);
}
};

game.command('inventory', 'See what you have stored in your inventory')
.action(function (args, callback) {
let usable = function (itemName, itemRender) {
let prefix = "";
for (let worldItem of world.location.items) {
let item = items.get(worldItem);
if (item["reacts with"] && item["reacts with"] === itemName) {
prefix = chalk.magenta("*");
}
}
return prefix + itemRender;
}
this.log(`${character.inventorySlots} slots, ${character.inventorySlots - character.inventory.length} free.`)
if (character.inventory.length > 0) {
this.log(character.inventory.map(item => usable(item, items.render(item))).join('\n'));
}
callback();
});
game.command('menu', 'Return to main menu')
.action((args, callback) => {
process.stdout.write('\u001B[2J\u001B[0;0f');
game.log(game.menu.intro);
game.hide();
game.menu.show();
callback();
});

game.command('go <place...>', 'Go to connecting location')
.autocomplete(() => world.location.connects)
.action(function (args, callback) {
let place = args.place.join(" ");
world.go(place).then(data => {
process.stdout.write("\u001B[2J\u001B[0;0f");
// this.delimiter(`[${chalk.yellow(data.name)}]$`);
game.delimiter(`[${chalk.red("❤︎".repeat(character.health))}][${world.location.name}]$`)
game.look.call(this);
// not sure if moving locations should advance the game,
// since the location itself has a lot of text attached already.
// game.advance();
callback();
});
});
game.command('look', 'Look around')
.action(function look(args, callback) {
game.look.call(this);
callback();
});

game.command('take <item...>', 'Take an item')
.autocomplete(() => world.location.items)
.action(function (args, callback) {
let item = args.item.join(" ");
character.take(world, item).then(message => {
this.log(message);
game.advance();
callback();
}).catch(error => {
this.log(chalk.red(error));
callback();
});
});
game.command('inventory', 'See what you have stored in your inventory')
.action((args, callback) => {
function usable(itemName, itemRender) {
let prefix = '';
for (const worldItem of world.location.items) {
const item = items.get(worldItem);
if (item['reacts with'] && item['reacts with'] === itemName) {
prefix = chalk.magenta('*');
}
}
return prefix + itemRender;
}
game.log(`${character.inventorySlots} slots, ${character.inventorySlots - character.inventory.length} free.`);
if (character.inventory.length > 0) {
game.log(character.inventory.map(item => usable(item, items.render(item))).join('\n'));
}
callback();
});

game.command('drop <item...>', 'Drop an item')
.autocomplete(() => character.inventory)
.action(function (args, callback) {
let itemName = args.item.join(" ");
let inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
this.log(chalk.red(errors.notininventory()));
} else {
world.location.items.push(itemName);
character.inventory.splice(inventorySlot, 1);
this.log(`You drop the ${itemName}.`);
game.advance();
}
callback();
});
game.command('go <place...>', 'Go to connecting location')
.autocomplete(() => world.location.connects)
.action(function go(args, callback) {
const place = args.place.join(' ');
world.go(place).then(() => {
process.stdout.write('\u001B[2J\u001B[0;0f');
// this.delimiter(`[${chalk.yellow(data.name)}]$`);
game.delimiter(`[${chalk.red('❤︎'.repeat(character.health))}][${world.location.name}]$`);
game.look.call(this);
// not sure if moving locations should advance the game,
// since the location itself has a lot of text attached already.
// game.advance();
callback();
});
});

game.command('unequip <item...>', 'Equip an item from your inventory')
.autocomplete(() => character.equipped)
.action(function (args, callback) {
let used = false;
let itemName = args.item.join(" ");
for(let slot in character.equipment) {
if(character.equipment[slot] && character.equipment[slot].name == itemName) {
character.equipment[slot] = null;
if(character.inventory.length < character.inventorySlots) {
character.inventory.push(itemName);
this.log(`You've unequipped ${itemName}.`);
} else {
world.location.items.push(itemName);
this.log(`You've unequipped ${itemName}, and dropped it on the floor in front of you.`);
}
used = true;
break;
}
}
if (!used) {
this.log(chalk.red(errors.cantuse()));
} else {
game.advance();
}
callback();
});
game.command('take <item...>', 'Take an item')
.autocomplete(() => world.location.items)
.action((args, callback) => {
const item = args.item.join(' ');
character.take(world, item).then(message => {
game.log(message);
game.advance();
callback();
}).catch(error => {
game.log(chalk.red(error));
callback();
});
});

game.command('drop <item...>', 'Drop an item')
.autocomplete(() => character.inventory)
.action((args, callback) => {
const itemName = args.item.join(' ');
const inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
game.log(chalk.red(errors.notininventory()));
} else {
world.location.items.push(itemName);
character.inventory.splice(inventorySlot, 1);
game.log(`You drop the ${itemName}.`);
game.advance();
}
callback();
});

game.command('equip <item...>', 'Equip an item from your inventory')
.autocomplete(() => character.inventory)
.action(function (args, callback) {
let used = false;
let itemName = args.item.join(" ");
let inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
this.log(chalk.red(errors.notininventory()));
used = true;
} else {
let inventoryItem = items.get(itemName);
let equipmentSlot = inventoryItem.equip;
if (character.equipment[equipmentSlot] === null) {
character.equipment[equipmentSlot] = inventoryItem;
character.inventory.splice(inventorySlot, 1);
this.log(`You've equipped ${itemName} in the ${equipmentSlot} slot`);
used = true;
} else {
this.log(chalk.red(errors.slotused()));
used = true;
}
}
if (!used) {
this.log(chalk.red(errors.cantuse()));
} else {
game.advance();
}
callback();
});
game.command('unequip <item...>', 'Equip an item from your inventory')
.autocomplete(() => character.equipped)
.action((args, callback) => {
let used = false;
const itemName = args.item.join(' ');
for (const slot in character.equipment) {
if (character.equipment[slot] && character.equipment[slot].name === itemName) {
character.equipment[slot] = null;
if (character.inventory.length < character.inventorySlots) {
character.inventory.push(itemName);
game.log(`You've unequipped ${itemName}.`);
} else {
world.location.items.push(itemName);
game.log(`You've unequipped ${itemName}, and dropped it on the floor in front of you.`);
}
used = true;
break;
}
}
if (!used) {
game.log(chalk.red(errors.cantuse()));
} else {
game.advance();
}
callback();
});

game.command('use <item...>', 'Use an item from your inventory')
.autocomplete(() => character.inventory)
.action(function (args, callback) {
let used = false;
let itemName = args.item.join(" ");
let inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
this.log(chalk.red(errors.notininventory()));
used = true;
} else {
let inventoryItem = items.get(itemName);
if (inventoryItem.consumable) {
game.applyEffects(inventoryItem, itemName);
used = true;
} else {
for (let worldItemName of world.location.items) {
let worldItem = items.get(worldItemName);
if (worldItem["reacts with"] && worldItem["reacts with"].indexOf(itemName) !== -1) {
game.applyEffects(worldItem, itemName);
used = true;
break;
}
}
}
}
if (!used) {
this.log(chalk.red(errors.cantuse()));
} else {
game.advance();
}
callback();
});

game.command('equip <item...>', 'Equip an item from your inventory')
.autocomplete(() => character.inventory)
.action((args, callback) => {
let used = false;
const itemName = args.item.join(' ');
const inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
game.log(chalk.red(errors.notininventory()));
used = true;
} else {
const inventoryItem = items.get(itemName);
const equipmentSlot = inventoryItem.equip;
if (character.equipment[equipmentSlot] === null) {
character.equipment[equipmentSlot] = inventoryItem;
character.inventory.splice(inventorySlot, 1);
game.log(`You've equipped ${itemName} in the ${equipmentSlot} slot`);
used = true;
} else {
game.log(chalk.red(errors.slotused()));
used = true;
}
}
if (!used) {
game.log(chalk.red(errors.cantuse()));
} else {
game.advance();
}
callback();
});

game.command('eat <item...>', 'Eat something from your inventory')
.autocomplete(() => character.inventory)
.action(function (args, callback) {
let itemName = args.item.join(" ");
let inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
this.log(chalk.red(errors.notininventory()));
} else {
let item = items.get(itemName);
if (!item.health) {
this.log(chalk.red(errors.notedible()));
} else {
character.health += item.health;
if (character.health > character.maxHealth) {
character.health = character.maxHealth;
}
character.inventory.splice(inventorySlot, 1);
let color = (healthScale.length / character.maxHealth * character.health | 0) - 1;
let health = chalk[healthScale[color]](`${character.health}/${character.maxHealth}`);
this.log(`You ate the ${items.render(itemName)}. Your health is now ${health}`);
game.advance();
}
}
callback();
});
game.command('info', 'Get character information')
.autocomplete(() => character.inventory.concat(world.location.items))
.action(function (args, callback) {
this.log(`Health: ${character.health}`);
this.log(`Armor: ${character.armor}`);
this.log(`Damage: ${character.damage}`);
this.log(`Inventory: ${character.inventory.join(',')}`);
this.log(`Equipment: ${character.equipped}`);
callback();
});
game.command('examine <item...>', 'Examine an item')
.autocomplete(() => character.inventory.concat(world.location.items))
.action(function (args, callback) {
let item = args.item.join(" ");
let allItems = character.inventory.concat(world.location.items);
if (allItems.indexOf(item) !== -1) {
this.log(items.get(item).description);
game.advance();
} else {
this.log(chalk.red(errors.notfound()));
game.command('use <item...>', 'Use an item from your inventory')
.autocomplete(() => character.inventory)
.action((args, callback) => {
let used = false;
const itemName = args.item.join(' ');
const inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
game.log(chalk.red(errors.notininventory()));
used = true;
} else {
const inventoryItem = items.get(itemName);
if (inventoryItem.consumable) {
game.applyEffects(inventoryItem, itemName);
used = true;
} else {
for (const worldItemName of world.location.items) {
const worldItem = items.get(worldItemName);
if (worldItem['reacts with'] && worldItem['reacts with'].indexOf(itemName) !== -1) {
game.applyEffects(worldItem, itemName);
used = true;
break;
}
callback();
});
game.command('attack <monster...>', 'Attack a nearby monster')
.autocomplete(() => world.location.monsters.map(monster => monster.name))
.action(function (args, callback) {
let monster = args.monster.join(" ");
let monsterIdx = world.location.monsters.map(monster => monster.name).indexOf(monster);
if (monsterIdx !== -1) {
let damage = character.attack();
this.log(damage.message);
let status = world.location.monsters[monsterIdx].defend(damage);
this.log(status.message);
if (status.status === "death") {
world.location.monsters.splice(monsterIdx, 1);
if (status.drops && status.drops.length > 0) {
world.location.items = world.location.items.concat(status.drops);
for (let drop of status.drops) {
this.log(`It dropped ${items.render(drop)}`);
}
}
}
game.advance();
} else {
this.log(chalk.red(errors.notfound()));
}
}
}
if (!used) {
game.log(chalk.red(errors.cantuse()));
} else {
game.advance();
}
callback();
});


game.command('eat <item...>', 'Eat something from your inventory')
.autocomplete(() => character.inventory)
.action((args, callback) => {
const itemName = args.item.join(' ');
const inventorySlot = character.inventory.indexOf(itemName);
if (inventorySlot === -1) {
game.log(chalk.red(errors.notininventory()));
} else {
const item = items.get(itemName);
if (!item.health) {
game.log(chalk.red(errors.notedible()));
} else {
character.health += item.health;
if (character.health > character.maxHealth) {
character.health = character.maxHealth;
}
character.inventory.splice(inventorySlot, 1);
const color = Math.floor(healthScale.length / character.maxHealth * character.health) - 1;
const health = chalk[healthScale[color]](`${character.health}/${character.maxHealth}`);
game.log(`You ate the ${items.render(itemName)}. Your health is now ${health}`);
game.advance();
}
}
callback();
});
game.command('info', 'Get character information')
.autocomplete(() => character.inventory.concat(world.location.items))
.action((args, callback) => {
game.log(`Health: ${character.health}`);
game.log(`Armor: ${character.armor}`);
game.log(`Damage: ${character.damage}`);
game.log(`Inventory: ${character.inventory.join(',')}`);
game.log(`Equipment: ${character.equipped}`);
callback();
});
game.command('examine <item...>', 'Examine an item')
.autocomplete(() => character.inventory.concat(world.location.items))
.action((args, callback) => {
const item = args.item.join(' ');
const allItems = character.inventory.concat(world.location.items);
if (allItems.indexOf(item) !== -1) {
game.log(items.get(item).description);
game.advance();
} else {
game.log(chalk.red(errors.notfound()));
}
callback();
});
game.command('attack <monster...>', 'Attack a nearby monster')
.autocomplete(() => world.location.monsters.map(monster => monster.name))
.action((args, callback) => {
const monster = args.monster.join(' ');
const monsterIdx = world.location.monsters.map(creature => creature.name).indexOf(monster);
if (monsterIdx !== -1) {
const damage = character.attack();
game.log(damage.message);
const status = world.location.monsters[monsterIdx].defend(damage);
game.log(status.message);
if (status.status === 'death') {
world.location.monsters.splice(monsterIdx, 1);
if (status.drops && status.drops.length > 0) {
world.location.items = world.location.items.concat(status.drops);
for (const drop of status.drops) {
game.log(`It dropped ${items.render(drop)}`);
}
callback();
});
// game.help(cmd => {
// return "HALP";
// });
game.delimiter(`[${chalk.red("❤".repeat(character.health))}][${world.location.name}]$`)
}
}
game.advance();
} else {
game.log(chalk.red(errors.notfound()));
}
callback();
});
// game.help(cmd => {
// return "HALP";
// });
game.delimiter(`[${chalk.red('❤'.repeat(character.health))}][${world.location.name}]$`);

return game;
}
return game;
};

module.exports = Game;
export default Game;

+ 30
- 27
game/items.js View File

@@ -1,35 +1,38 @@
const fs = require('fs');
const yaml = require('js-yaml');
const chalk = require('chalk');
/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import yaml from 'js-yaml';
import chalk from 'chalk';

const itemColors = {
"common": "green",
"uncommon": "yellow",
"rare": "cyan",
"very rare": "magenta",
"unique": "white",
"static": "grey"
common: 'green',
uncommon: 'yellow',
rare: 'cyan',
'very rare': 'magenta',
unique: 'white',
static: 'grey',
};

class Items {
constructor() {
this.data = yaml.safeLoad(fs.readFileSync(`data/items.yml`, 'utf8'));
}
get(itemName) {
for(let item of this.data) {
if(item.name == itemName) {
return item;
}
}
return null;
}
render(itemName) {
let item = this.get(itemName);
let damage = item.damage ? "🗡️".repeat(item.damage) : "";
let health = item.health ? chalk.red("❤︎".repeat(item.health)) : "";
let armor = item.armor ? chalk.yellow("🛡️".repeat(item.armor)) : "";
return item ? chalk[itemColors[item.prevalence]](item.name) + " " + damage + health + armor: "unknown item";
constructor() {
this.data = yaml.safeLoad(fs.readFileSync('data/items.yml', 'utf8'));
}

get(itemName) {
for (const item of this.data) {
if (item.name === itemName) {
return item;
}
}
return null;
}

render(itemName) {
const item = this.get(itemName);
const damage = item.damage ? '🗡️'.repeat(item.damage) : '';
const health = item.health ? chalk.red('❤︎'.repeat(item.health)) : '';
const armor = item.armor ? chalk.yellow('🛡️'.repeat(item.armor)) : '';
return item ? `${chalk[itemColors[item.prevalence]](item.name)} ${damage}${health}${armor}` : 'unknown item';
}
}

module.exports = Items;
export default Items;

+ 47
- 44
game/menu.js View File

@@ -1,47 +1,50 @@
const Vorpal = require('vorpal');
const chalk = require('chalk');
const banner = require('./banner');
const titles = require('./titles');
/* eslint-disable import/extensions */
import * as vorpal from 'vorpal';
import chalk from 'chalk';
import banner from './banner.js';
import titles from './titles.js';

let Menu = (game) => {
let menu = Vorpal();
menu.history("menu-command-history");
const Vorpal = vorpal.default;

menu.delimiter("[menu]$");
menu.command('play', 'Play ' + chalk.red('Night Terror'))
.action(function (args, callback) {
this.log("Starting game. Beware the dark.")
setTimeout(function() {
process.stdout.write ("\u001B[2J\u001B[0;0f");
game.look();
game.show();
callback();
}, 2000);
});
menu.command('save', 'Save game')
.action(function (args, callback) {
game.show();
callback();
});
menu.command('load', 'Load game')
.action(function (args, callback) {
menu.hide();
game.show();
callback();
});
let header = chalk.white.bgHex("##540a00");
menu.intro = [
header.bold(titles.header("A text horror adventure by Armen138", banner.width)),
banner.colored(),
header(titles.random(banner.width))
].join("\n");
menu.intro += "\n";
let help = [
"play", "save", "load", "exit"
];
menu.intro += titles.header(help.map(item => chalk.black.bgYellow(` ${item} `)).join(" "), banner.width);
return menu;
}
const Menu = (game) => {
const menu = Vorpal();
menu.history('menu-command-history');

module.exports = Menu;
menu.delimiter('[menu]$');
menu.command('play', `Play ${chalk.red('Night Terror')}`)
.action(function play(args, callback) {
this.log('Starting game. Beware the dark.');
setTimeout(() => {
process.stdout.write('\u001B[2J\u001B[0;0f');
game.look();
game.show();
callback();
}, 2000);
});
menu.command('save', 'Save game')
.action((args, callback) => {
game.show();
callback();
});
menu.command('load', 'Load game')
.action((args, callback) => {
menu.hide();
game.show();
callback();
});

const header = chalk.white.bgHex('##540a00');
menu.intro = [
header.bold(titles.header('A text horror adventure by Armen138', banner.width)),
banner.colored(),
header(titles.random(banner.width)),
].join('\n');
menu.intro += '\n';
const help = [
'play', 'save', 'load', 'exit',
];
menu.intro += titles.header(help.map((item) => chalk.black.bgYellow(` ${item} `)).join(' '), banner.width);
return menu;
};

export default Menu;

+ 75
- 66
game/monster.js View File

@@ -1,81 +1,90 @@
const errors = require('./errors');
const Items = require('./items');
/* eslint-disable import/extensions */
/* eslint-disable no-restricted-syntax */
import Items from './items.js';

let items = new Items();
const items = new Items();
// # Drop Chances: 60%, 40%, 20%, 10%, 1%

const dropChances = {
"common": 0.6,
"uncommon": 0.4,
"rare": 0.2,
"very rare": 0.1,
"unique": 0.01
common: 0.6,
uncommon: 0.4,
rare: 0.2,
'very rare': 0.1,
unique: 0.01,
};

const healthStatus = [
"strong",
"weakened",
"weak",
"barely alive"
'strong',
'weakened',
'weak',
'barely alive',
];

class Monster {
constructor(config) {
// - name: Ancient Zombie
// description: More bones than flesh, this zombie had obviously spent some time under ground before coming back to ruin your day.
// health: 5
// damage: 4
// drops:
// - thigh bone
// - leather boots
this.name = config.name || "Monster";
this.health = config.health || 1;
this.damage = config.damage || 1;
this.drops = config.drops || [];
this.maxHealth = this.health;
}
drop() {
let drops = [];
for(let itemName of this.drops) {
let item = items.get(itemName);
let chance = dropChances[item.prevalence];
let roll = Math.random();
if(roll <= chance) {
drops.push(itemName);
}
}
return drops;
}
doDamage(roll) {
let damage = { damage: this.damage, message: `The ${this.name} attacks, and causes significant damage.` };
if(roll === 1) {
damage.damage = 0;
damage.message = `The ${this.name} attacks, but misses!`;
}
if(roll === 20) {
damage.damage *= 2;
damage.message = `The ${this.name} scores a critical hit! Oh, the humanity!`;
}
return damage;
constructor(config) {
// - name: Ancient Zombie
// description: More bones than flesh, this zombie had obviously spent some time under ground
// before coming back to ruin your day.
// health: 5
// damage: 4
// drops:
// - thigh bone
// - leather boots
this.name = config.name || 'Monster';
this.health = config.health || 1;
this.damage = config.damage || 1;
this.drops = config.drops || [];
this.maxHealth = this.health;
}

drop() {
const drops = [];
for (const itemName of this.drops) {
const item = items.get(itemName);
const chance = dropChances[item.prevalence];
const roll = Math.random();
if (roll <= chance) {
drops.push(itemName);
}
}
die() {
return {
message: `The ${this.name} dies with a rattle, staring you right in the eye as it expires`,
drops: this.drop(),
status: "death"
}
return drops;
}

doDamage(roll) {
const damage = { damage: this.damage, message: `The ${this.name} attacks, and causes significant damage.` };
if (roll === 1) {
damage.damage = 0;
damage.message = `The ${this.name} attacks, but misses!`;
}
defend(damage) {
this.health -= damage.damage;
if(this.health <= 0) {
return this.die();
}
let statusIndex = (healthStatus.length / this.maxHealth * this.health | 0) - 1;
return { message: "It just seems to make it angier!", status: healthStatus[statusIndex] };
if (roll === 20) {
damage.damage *= 2;
damage.message = `The ${this.name} scores a critical hit! Oh, the humanity!`;
}
attack() {
let roll = Math.random() * 20 | 0; // standard d20 roll
return this.doDamage(roll);
return damage;
}

die() {
return {
message: `The ${this.name} dies with a rattle, staring you right in the eye as it expires`,
drops: this.drop(),
status: 'death',
};
}

defend(damage) {
this.health -= damage.damage;
if (this.health <= 0) {
return this.die();
}
// eslint-disable-next-line no-mixed-operators
const statusIndex = Math.floor(healthStatus.length / this.maxHealth * this.health) - 1;
return { message: 'It just seems to make it angier!', status: healthStatus[statusIndex] };
}

attack() {
const roll = Math.floor(Math.random() * 20); // standard d20 roll
return this.doDamage(roll);
}
}

module.exports = Monster;
export default Monster;

+ 16
- 14
game/monsters.js View File

@@ -1,19 +1,21 @@
const fs = require('fs');
const yaml = require('js-yaml');
const chalk = require('chalk');
/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import yaml from 'js-yaml';
// import chalk from 'chalk';

class Monsters {
constructor() {
this.data = yaml.safeLoad(fs.readFileSync(`data/monsters.yml`, 'utf8'));
}
get(name) {
for(let monster of this.data) {
if(monster.name == name) {
return monster;
}
}
return null;
constructor() {
this.data = yaml.safeLoad(fs.readFileSync('data/monsters.yml', 'utf8'));
}

get(name) {
for (const monster of this.data) {
if (monster.name === name) {
return monster;
}
}
return null;
}
}

module.exports = Monsters;
export default Monsters;

+ 23
- 21
game/titles.js View File

@@ -1,23 +1,25 @@
let titles = {
_titles: [
"I think I heard something. Did you hear something?",
"Welcome, unfortunate soul. Make yourself comfortable.",
"There are things that go GHAWRAWARAAAAGGHHH in the night."
],
header: (title, container_size) => {
// strip ansi escape codes (colors) that artificially inflate the text length
let clean_title = title.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
if(clean_title.length < container_size) {
let spacer = " ".repeat((container_size - clean_title.length) / 2);
title = spacer + title + spacer;
}
return title;
},
random: (container_size) => {
let idx = Math.random() * titles._titles.length | 0;
let title = titles._titles[idx];
return titles.header(title, container_size);
/* eslint-disable no-control-regex */
const titles = {
titles: [
'I think I heard something. Did you hear something?',
'Welcome, unfortunate soul. Make yourself comfortable.',
'There are things that go GHAWRAWARAAAAGGHHH in the night.',
],
header: (title, containerSize) => {
// strip ansi escape codes (colors) that artificially inflate the text length
const cleanTitle = title.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
if (cleanTitle.length < containerSize) {
const spacer = ' '.repeat((containerSize - cleanTitle.length) / 2);
const spacedTitle = spacer + title + spacer;
return spacedTitle;
}
}
return title;
},
random: (containerSize) => {
const idx = Math.floor(Math.random() * titles.titles.length);
const title = titles.titles[idx];
return titles.header(title, containerSize);
},
};

module.exports = titles;
export default titles;

+ 49
- 45
game/world.js View File

@@ -1,55 +1,59 @@
const yaml = require('js-yaml');
const fs = require('fs');
const banner = require('./banner');
const errors = require('./errors');
const Items = require('./items');
const Monsters = require('./monsters');
const Monster = require('./monster');
/* eslint-disable import/extensions */
import fs from 'fs';
import yaml from 'js-yaml';
import errors from './errors.js';
import Monsters from './monsters.js';
import Monster from './monster.js';
import Items from './items.js';

const items = new Items();
const monsters = new Monsters();

class World {
constructor(config) {
this.config = config;
this.location = yaml.safeLoad(fs.readFileSync(`data/locations/${config.spawn.replace(/ /g, '_')}.yml`, 'utf8'));
constructor(config) {
this.config = config;
this.location = yaml.safeLoad(fs.readFileSync(`data/locations/${config.spawn.replace(/ /g, '_')}.yml`, 'utf8'));
}

go(location) {
const promise = new Promise((resolve, reject) => {
this.loadLocation(location).then((data) => {
// this.location = data;
resolve(data);
}).catch((e) => {
reject(e);
});
});
return promise;
}

take(item) {
const idx = this.location.items.indexOf(item);
if (idx === -1) {
return { error: errors.notfound() };
}
go(location) {
let promise = new Promise((resolve, reject) => {
this.loadLocation(location).then(data => {
this.location = data;
resolve(data);
}).catch(e => {
reject(e);
});
});
return promise;
const worldItem = items.get(this.location.items[idx]);
if (worldItem.static) {
return { error: errors.staticitem() };
}
take(item) {
let idx = this.location.items.indexOf(item);
if(idx == -1) {
return { error: errors.notfound() };
}
let worldItem = items.get(this.location.items[idx]);
if(worldItem.static) {
return { error: errors.staticitem() };
return { item: this.location.items.splice(idx, 1)[0] };
}

loadLocation(location) {
const promise = new Promise((resolve, reject) => {
try {
const data = yaml.safeLoad(fs.readFileSync(`data/locations/${location.replace(/ /g, '_')}.yml`, 'utf8'));
if (data.monsters) {
data.monsters = data.monsters.map((monster) => new Monster(monsters.get(monster)));
}
return { item: this.location.items.splice(idx, 1)[0] };
}
loadLocation(location) {
let promise = new Promise((resolve, reject) => {
try {
let data = yaml.safeLoad(fs.readFileSync(`data/locations/${location.replace(/ /g, '_')}.yml`, 'utf8'));
if(data.monsters) {
data.monsters = data.monsters.map(monster => new Monster(monsters.get(monster)))
}
resolve(data);
} catch (e) {
reject(e);
}
});
return promise;
}
this.location = data;
resolve(data);
} catch (e) {
reject(e);
}
});
return promise;
}
}

module.exports = World;
export default World;

+ 6
- 6
index.js View File

@@ -1,9 +1,9 @@
const Menu = require('./game/menu');
const Game = require('./game/game');

let game = Game();
let menu = Menu(game);
/* eslint-disable import/extensions */
import Game from './game/game.js';
import Menu from './game/menu.js';

const game = Game();
const menu = Menu(game);
game.menu = menu;
console.log(menu.intro);
menu.log(menu.intro);
menu.show();

+ 1402
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 7
- 1
package.json View File

@@ -3,9 +3,10 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
"start": "node --experimental-modules index.js"
},
"author": "",
"license": "ISC",
@@ -13,5 +14,10 @@
"chalk": "^2.4.2",
"js-yaml": "^3.13.1",
"vorpal": "^1.12.0"
},
"devDependencies": {
"eslint": "^6.1.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.18.2"
}
}

Loading…
Cancel
Save