Browse Source

Now with webness

pull/14/head
Armen138 9 months ago
parent
commit
9a59e8a97b
15 changed files with 248 additions and 45 deletions
  1. +2
    -2
      game/character.js
  2. +20
    -15
      game/game.js
  3. +10
    -5
      game/items.js
  4. +3
    -3
      game/menu.js
  5. +5
    -4
      game/monster.js
  6. +10
    -6
      game/monsters.js
  7. +3
    -2
      game/terminalrenderer.js
  8. +29
    -0
      game/webloader.js
  9. +101
    -0
      game/webrenderer.js
  10. +5
    -7
      game/world.js
  11. +25
    -0
      index.html
  12. +2
    -1
      package.json
  13. +8
    -0
      readme.md
  14. +0
    -0
      terminal.js
  15. +25
    -0
      web.js

+ 2
- 2
game/character.js View File

@@ -53,8 +53,8 @@ class Character {
if (worldItem.error) {
reject(worldItem.error);
} else {
this.inventory.push(worldItem.item);
resolve(`You have added ${item} to your inventory.`);
this.inventory.push(item.name);
resolve(`You have added ${item.name} to your inventory.`);
}
} else {
reject(messages.inventory_full);


+ 20
- 15
game/game.js View File

@@ -6,6 +6,7 @@ import World from './world.js';
import Character from './character.js';
import Items from './items.js';
import Events from './events.js';
// import Monsters from './monsters.js';

const healthScale = [
'red',
@@ -19,6 +20,8 @@ class Game extends Events {
super();
this.time = 0;
this.renderer = renderer;
// this.monsters = new Monsters(loader);
this.items = new Items(renderer, loader);
loader.get('data/world.yml').then(worldConfig => {
loader.get(`data/locations/${worldConfig.spawn.replace(/ /g, '_')}.yml`).then(location => {
this.world = new World(location, loader);
@@ -32,7 +35,6 @@ class Game extends Events {
});
});
});
this.items = new Items(renderer);

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

@@ -68,7 +70,7 @@ class Game extends Events {
case 'examine':
return () => this.character.inventory.concat(this.world.location.items);
case 'attack':
return () => this.world.location.monsters.map(monster => monster.name);
return () => this.world.location.spawned.map(monster => monster.name);
case 'go':
return () => this.world.location.connects;
default:
@@ -107,18 +109,18 @@ class Game extends Events {
}

attack(monster, callback) {
const monsterIdx = this.world.location.monsters.map(creature => creature.name).indexOf(monster);
const monsterIdx = this.world.location.spawned.map(item => item.name).indexOf(monster);
if (monsterIdx !== -1) {
const damage = this.character.attack();
this.renderer.text(damage.message);
const status = this.world.location.monsters[monsterIdx].defend(damage);
this.renderer.text(status.message);
const status = this.world.location.spawned[monsterIdx].defend(damage);
this.renderer.text(status.message, null, monster);
if (status.status === 'death') {
this.world.location.monsters.splice(monsterIdx, 1);
this.world.location.spawned.splice(monsterIdx, 1);
if (status.drops && status.drops.length > 0) {
this.world.location.items = this.world.location.items.concat(status.drops);
for (const drop of status.drops) {
this.renderer.text(`It dropped ${this.items.render(drop)}`);
this.renderer.text(`It dropped ${this.items.render(drop)}`, null, monster);
}
}
}
@@ -242,7 +244,8 @@ class Game extends Events {
callback();
}

take(item, callback) {
take(itemName, callback) {
const item = this.items.get(itemName);
this.character.take(this.world, item).then(message => {
this.renderer.text(message);
this.advance();
@@ -296,7 +299,7 @@ class Game extends Events {
examine(item, callback) {
const allItems = this.character.inventory.concat(this.world.location.items);
if (allItems.indexOf(item) !== -1) {
this.renderer.text(this.items.get(item).description);
this.renderer.text(this.items.get(item).description, null, item);
this.advance();
} else {
this.renderer.text(this.messages.not_found, { color: 'red' });
@@ -313,9 +316,9 @@ class Game extends Events {
const worldItems = this.world.location.items.map(itemName => this.items.render(itemName));
this.renderer.text(`Items here:\n${worldItems.join('\n')}`);
}
if (this.world.location.monsters && this.world.location.monsters.length > 0) {
const { monsters } = this.world.location;
this.renderer.text(`Monsters here:\n${monsters.map(monster => monster.name).join('\n')}`);
if (this.world.location.spawned && this.world.location.spawned.length > 0) {
const { spawned } = this.world.location;
this.renderer.text(`Monsters here:\n${spawned.map(item => item.name).join('\n')}`);
}
if (this.world.location.connects) {
this.renderer.text(`This place connects to:\n${this.world.location.connects.join('\n')}`);
@@ -328,11 +331,13 @@ class Game extends Events {
advance() {
this.time += 1;
if (this.world.location.monsters && this.world.location.monsters.length > 0) {
for (const monster of this.world.location.monsters) {
const monstersHere = this.world.location.spawned;

for (const monster of monstersHere) {
const damage = monster.attack();
this.renderer.text(damage.message);
this.renderer.text(damage.message, null, monster.name);
const status = this.character.defend(damage);
this.renderer.text(status.message);
this.renderer.text(status.message, null, 'you');
if (status.status === 'death') {
this.emit('death');
}


+ 10
- 5
game/items.js View File

@@ -1,6 +1,7 @@
/* eslint-disable import/extensions */
/* eslint-disable arrow-parens */
/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import yaml from 'js-yaml';
import Events from './events.js';

const itemColors = {
common: 'green',
@@ -11,10 +12,14 @@ const itemColors = {
static: 'grey',
};

class Items {
constructor(renderer) {
class Items extends Events {
constructor(renderer, loader) {
super();
this.renderer = renderer;
this.data = yaml.safeLoad(fs.readFileSync('data/items.yml', 'utf8'));
loader.get('data/items.yml').then(data => {
this.data = data;
this.emit('ready');
});
}

get(itemName) {


+ 3
- 3
game/menu.js View File

@@ -33,9 +33,9 @@ class Menu extends Events {

render() {
this.renderer.clear();
this.renderer.text(this.data.header.text, this.data.header.style, true);
this.renderer.text(this.data.title.text, this.data.title.style, true);
this.renderer.text(this.subtitle, this.data.subtitle.style, true);
this.renderer.text(this.data.header.text, this.data.header.style);
this.renderer.text(this.data.title.text, this.data.title.style);
this.renderer.text(this.subtitle, this.data.subtitle.style);
this.renderer.text(this.help());
this.renderer.prompt(this.prompt);
this.renderer.show();


+ 5
- 4
game/monster.js View File

@@ -1,8 +1,8 @@
/* eslint-disable import/extensions */
/* eslint-disable no-restricted-syntax */
import Items from './items.js';
// import Items from './items.js';

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

const dropChances = {
@@ -21,7 +21,7 @@ const healthStatus = [
];

class Monster {
constructor(config) {
constructor(config, items) {
// - name: Ancient Zombie
// description: More bones than flesh, this zombie had obviously spent some time under ground
// before coming back to ruin your day.
@@ -30,6 +30,7 @@ class Monster {
// drops:
// - thigh bone
// - leather boots
this.items = items;
this.name = config.name || 'Monster';
this.health = config.health || 1;
this.damage = config.damage || 1;
@@ -40,7 +41,7 @@ class Monster {
drop() {
const drops = [];
for (const itemName of this.drops) {
const item = items.get(itemName);
const item = this.items.get(itemName);
const chance = dropChances[item.prevalence];
const roll = Math.random();
if (roll <= chance) {


+ 10
- 6
game/monsters.js View File

@@ -1,11 +1,15 @@
/* eslint-disable arrow-parens */
/* eslint-disable import/extensions */
/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import yaml from 'js-yaml';
// import chalk from 'chalk';
import Events from './events.js';

class Monsters {
constructor() {
this.data = yaml.safeLoad(fs.readFileSync('data/monsters.yml', 'utf8'));
class Monsters extends Events {
constructor(loader) {
super();
loader.get('data/monsters.yml').then(data => {
this.data = data;
this.emit('ready');
});
}

get(name) {


+ 3
- 2
game/terminalrenderer.js View File

@@ -83,8 +83,9 @@ class TerminalRenderer {
return value;
}

text(text, style) {
this.vorpal.log(this.style(text, style));
text(text, style, source) {
const prefix = source ? this.style(`${source} `, { color: 'magenta' }) : '';
this.vorpal.log(prefix + this.style(text, style));
}
}



+ 29
- 0
game/webloader.js View File

@@ -0,0 +1,29 @@
/* eslint-disable import/extensions */
/* eslint-disable arrow-parens */
// import yaml from 'js-yaml';
// import * as yaml from '../node_modules/js-yaml/dist/js-yaml.js';

class WebLoader {
constructor() {
this.files = {};
// console.log(yaml);
}

get(filename) {
const promise = new Promise((resolve, reject) => {
if (this.files[filename]) {
resolve(this.files[filename]);
} else {
fetch(filename).then(data => data.text()).then(data => {
// eslint-disable-next-line no-undef
const parsed = jsyaml.safeLoad(data);
this.files[filename] = parsed;
resolve(parsed);
}).catch(reject);
}
});
return promise;
}
}

export default WebLoader;

+ 101
- 0
game/webrenderer.js View File

@@ -0,0 +1,101 @@
/* eslint-disable arrow-parens */
/* eslint-disable no-control-regex */
/* eslint-disable no-restricted-syntax */
/* eslint-disable class-methods-use-this */
// import Vorpal from 'vorpal';
// import chalk from 'chalk';

class WebRenderer {
constructor() {
// this.vorpal = Vorpal();
this.container = document.querySelector('#game');
this.messages = [];
this.title = '';
this.commands = {};
}

show() {
const noop = () => {
this.input.focus();
window.scrollTo(0, document.body.scrollHeight);
};
this.input = document.querySelector('#prompt');
const cleanInput = this.input.cloneNode(true);
this.input.parentNode.replaceChild(cleanInput, this.input);
this.input = cleanInput;
this.input.addEventListener('keydown', (e) => {
if (e.keyCode === 9) { // tab
e.preventDefault();
const input = this.input.value.split(' ');
const command = input.shift();
const args = input.join(' ');
if (input.length === 0) {
const search = Object.keys(this.commands).filter(item => item.startsWith(command));
this.input.value = search.length > 0 ? search[0] : this.input.value;
} else {
const haystack = this.autocomplete(command)();
if (haystack) {
const search = haystack.filter(item => item.startsWith(args));
this.input.value = search.length > 0 ? `${command} ${search[0]}` : this.input.value;
}
}
}
if (e.keyCode === 13) {
const input = this.input.value.split(' ');
const command = input.shift();
const args = input.join(' ');
this.text(`>> ${this.input.value}`);
this.input.value = '';
if (!this.commands[command]) {
if (this.commands.help) {
this.text(this.commands.help());
}
} else {
this.commands[command](args, noop);
}
}
});

const prompt = document.createElement('div');
prompt.innerHTML = this.title;
this.container.appendChild(prompt);
noop();
}

prompt(promptString) {
this.title = promptString;
}

clear() {
this.messages = [];
}

register(commands, autocomplete) {
this.autocomplete = autocomplete;
this.commands = commands;
}

style(text, style) {
if (!text) return '';
const span = document.createElement('span');
let styleAttribute = '';
for (const item in style) {
if (Object.prototype.hasOwnProperty.call(style, item)) {
styleAttribute += `${item}:${style[item]};`;
}
}
span.setAttribute('style', styleAttribute);
span.innerHTML = text.replace(/\n/g, '<br>');
return span.outerHTML;
}

text(text, style, source) {
const prefix = source ? this.style(`${source} `, { color: 'magenta' }) : '';
this.messages.push(prefix + this.style(text, style));
const message = document.createElement('div');
message.innerHTML = prefix + this.style(text, style);
this.container.appendChild(message);
}
}

export default WebRenderer;

+ 5
- 7
game/world.js View File

@@ -4,15 +4,13 @@
import Messages from './messages.js';
import Monsters from './monsters.js';
import Monster from './monster.js';
import Items from './items.js';

const items = new Items();
const monsters = new Monsters();
const messages = new Messages('data/messages.yml');

class World {
constructor(location, loader) {
this.loader = loader;
this.monsters = new Monsters(loader);
this.location = location;
}

@@ -28,12 +26,12 @@ class World {
}

take(item) {
const idx = this.location.items.indexOf(item);
const idx = this.location.items.indexOf(item.name);
if (idx === -1) {
return { error: messages.not_found };
}
const worldItem = items.get(this.location.items[idx]);
if (worldItem.static) {
// const worldItem = items.get(this.location.items[idx]);
if (item.static) {
return { error: messages.static_item };
}
return { item: this.location.items.splice(idx, 1)[0] };
@@ -44,7 +42,7 @@ class World {
try {
this.loader.get(`data/locations/${location.replace(/ /g, '_')}.yml`).then(data => {
if (data.monsters) {
data.monsters = data.monsters.map((monster) => new Monster(monsters.get(monster)));
data.spawned = data.monsters.map((monster) => new Monster(this.monsters.get(monster)));
}
this.location = data;
resolve(data);


+ 25
- 0
index.html View File

@@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<script src='node_modules/js-yaml/dist/js-yaml.min.js'></script>
<script src='web.js' type='module'></script>
<style>
html {
color: grey;
background-color: black;
font-family: "Lucida Console", Monaco, monospace
}
input {
width: 100%;
border: none;
color: gray;
background: black;
font-size: 20px;
}
</style>
</head>
<body>
<div id='game'></div>
<input type='text' id='prompt'>
</body>
</html>

+ 2
- 1
package.json View File

@@ -6,7 +6,8 @@
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node --experimental-modules index.js"
"start": "node --experimental-modules terminal.js",
"web": "http-server ."
},
"author": "",
"license": "ISC",


+ 8
- 0
readme.md View File

@@ -19,3 +19,11 @@ Run with terminal front end:
npm install
npm start
```

Run with web-terminal front end:

```
npm install http-server -g
npm install
npm run web
```

index.js → terminal.js View File


+ 25
- 0
web.js View File

@@ -0,0 +1,25 @@
/* eslint-disable arrow-parens */
/* eslint-disable import/extensions */
import Game from './game/game.js';
import Menu from './game/menu.js';
import WebRenderer from './game/webrenderer.js';
import WebLoader from './game/webloader.js';

const loader = new WebLoader();
const game = new Game(new WebRenderer(), loader);

function main(menu, death) {
menu.on('play', game.play.bind(game));
death.on('play', game.play.bind(game));
game.on('menu', menu.render.bind(menu));
game.on('death', death.render.bind(death));
menu.render();
}

loader.get('data/world.yml').then(data => {
const menu = new Menu(data, new WebRenderer());
loader.get('data/death.yml').then(deathMenu => {
const death = new Menu(deathMenu, new WebRenderer());
main(menu, death);
});
});

Loading…
Cancel
Save