js13kGames Tutorial – How to Make a Text Game with HTML5

Most games focus on impressing you with beautiful artwork and graphic assets. Computer and GPU performance is pushed further and further in order to render the most elaborated game graphics.

But what if you could make games that leave the rendering and graphic asset creation aspects to the world’s most power GPU: your imagination?

That’s when text games come into play. A text game uses the API of your brain to render it’s contents directly into your visual cortex, saving the developer lots of time, and giving the player the freedom to imagine (or “render”) the game content as they please, same as when reading a book.

This tutorial covers the creation of a text game and it’s inspired by and aimed to help those participating in the js13kGames competition, where the goal is to create a HTML5 game that’s 13kb or less. This event is organised by Andrzej Mazur and it runs every year.

The total size of this game is 7.7kb zipped, so we don’t even have to bother with minification.

The Game

The game we’ll create is inspired in the PC classic Oregon Trail, where the goal was to lead an expedition of settlers to the Far West in 19th century US.

This game will be created using only HTML, CSS and JavaScript. No external libraries will be used.

OregonTrailScreenshot

Whether you are familiar with this game or not, it doesn’t matter. The way our game works is as follows:

  • You command an expedition in which you have a number of people, food, money, oxen (to carry things) and firepower.
  • You have to reach 1000 km to your destination.
  • Food is consumed as you go.
  • You can only carry a certain amount of weight (depending on how many people and oxen you have).
  • If you run out of crew members or food, it’s game over.
  • As you travel, there will be random events and sometimes you have to make decisions.
  • If you find shops, you can buy things.
  • If you are under attack, you can choose to fight or flee.

This is what our final game will look like:

javascript text game js13kgames tutorial

Artwork Licenses

The caravan sprite was created by Andrettin, licenses CC-By-SA 3.0 and GPL 2.0. The rest is all public domain.

Tutorial Source Code

You can download the source code of this tutorial here(just double click on index.html to play, you don’t need a web server for this game).

BUILD GAMES

FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.

Basic Structure

We’ll start by putting together some of the files we’ll use.

For now we won’t be showing anything to the user. It will all be done via console. Afterwards we’ll take care of displaying everything on the screen. To begin, let’s create a minimal HTML file (we’ll add more stuff to it later). You can create a blank CSS file as well:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Learn Game Development at Zenva Academy | Zenva.com</title>
    <link rel="stylesheet" href="css/style.css"/>
  </head>
  <body>
    <script src="js/Caravan.js"></script>
    <script src="js/Game.js"></script>
  </body>
</html>

Caravan.js will contain our caravan object (OregonH.Caravan) with an init method.

The Caravan object will keep the caravan properties and will take care of things such as weight calculation, distance calculation and food consumption.

var OregonH = OregonH || {};

OregonH.Caravan = {};

OregonH.Caravan.init = function(stats){
  this.day = stats.day;
  this.distance = stats.distance;
  this.crew = stats.crew;
  this.food = stats.food;
  this.oxen = stats.oxen;
  this.money = stats.money;
  this.firepower = stats.firepower;
};

In regards to:

var OregonH = OregonH || {};

This is called a namespace. We are using the OregonH object in all different files so that we don’t pollute the global scope. If this object is not present, it is initialized by an empty object.

Game.js will contain some constants we’ll need in our game (you can tweak these to modify the game). They will be explained as they appear in the tutorial. The “Game” object OregonH.Game will take care of the main game aspects such as game starting, game pausing/resuming, and the simulated time step.

var OregonH = OregonH || {};

//constants
OregonH.WEIGHT_PER_OX = 20;
OregonH.WEIGHT_PER_PERSON = 2;
OregonH.FOOD_WEIGHT = 0.6;
OregonH.FIREPOWER_WEIGHT = 5;
OregonH.GAME_SPEED = 800;
OregonH.DAY_PER_STEP = 0.2;
OregonH.FOOD_PER_PERSON = 0.02;
OregonH.FULL_SPEED = 5;
OregonH.SLOW_SPEED = 3;
OregonH.FINAL_DISTANCE = 1000;
OregonH.EVENT_PROBABILITY = 0.15;
OregonH.ENEMY_FIREPOWER_AVG = 5;
OregonH.ENEMY_GOLD_AVG = 50;

OregonH.Game = {};

//initiate the game
OregonH.Game.init = function(){
  //setup caravan
  this.caravan = OregonH.Caravan;
  this.caravan.init({
    day: 0,
    distance: 0,
    crew: 30,
    food: 80,
    oxen: 2,
    money: 300,
    firepower: 2
  });
};

//init game
OregonH.Game.init();

When it comes to game parameters such as these, there is always the question of “how do you come up with them”. The way I usually do it is I try with any number for say “food weight”, then if the game doesn’t feel right I change it. I do that with all parameters until the game feels balanced.

You can type in the console of your browser OregonH.Game.caravan and it will give you access to the caravan object with the properties we game it.

The Caravan

Let’s implement the full caravan object:

  • Update walked distance
  • Calculate weight
  • Drop items if too much weight

Caravan.js:

var OregonH = OregonH || {};

OregonH.Caravan = {};

OregonH.Caravan.init = function(stats){
  this.day = stats.day;
  this.distance = stats.distance;
  this.crew = stats.crew;
  this.food = stats.food;
  this.oxen = stats.oxen;
  this.money = stats.money;
  this.firepower = stats.firepower;
};

//update weight and capacity
OregonH.Caravan.updateWeight = function(){
  var droppedFood = 0;
  var droppedGuns = 0;

  //how much can the caravan carry
  this.capacity = this.oxen * OregonH.WEIGHT_PER_OX + this.crew * OregonH.WEIGHT_PER_PERSON;

  //how much weight do we currently have
  this.weight = this.food * OregonH.FOOD_WEIGHT + this.firepower * OregonH.FIREPOWER_WEIGHT;

  //drop things behind if it's too much weight
  //assume guns get dropped before food
  while(this.firepower && this.capacity <= this.weight) {
    this.firepower--;
    this.weight -= OregonH.FIREPOWER_WEIGHT;
    droppedGuns++;
  }

  if(droppedGuns) {
    this.ui.notify('Left '+droppedGuns+' guns behind', 'negative');
  }

  while(this.food && this.capacity <= this.weight) {
    this.food--;
    this.weight -= OregonH.FOOD_WEIGHT;
    droppedFood++;
  }

  if(droppedFood) {
    this.ui.notify('Left '+droppedFood+' food provisions behind', 'negative');
  }
};

//update covered distance
OregonH.Caravan.updateDistance = function() {
  //the closer to capacity, the slower
  var diff = this.capacity - this.weight;
  var speed = OregonH.SLOW_SPEED + diff/this.capacity * OregonH.FULL_SPEED;
  this.distance += speed;
};

//food consumption
OregonH.Caravan.consumeFood = function() {
  this.food -= this.crew * OregonH.FOOD_PER_PERSON;

  if(this.food < 0) {
    this.food = 0;
  }
};

Regarding the distance update in updateDistance, the caravan goes faster or slower depending on the free capacity. If the caravan is running close to full capacity, it goes slower. This distance update method is meant to be called on each “game step”.

When it comes to weight calculation, both people and oxen can carry things (as defined by the corresponding constants).

If there is too much weight, guns are dropped first, if there is still overweight, then food is dropped.

What about this.ui.notify? We still need to create the UI object which will take care of showing things to the user. If you want to have a play in the console with what we have so far you’ll have to replace these by console.log() calls.

Basic Game Step

Now that we have the caravan model in place we can setup the basic game step and initialisation part, including winning the game or game over.

Let’s start by creating the UI object in UI.js. Add this file to index.html. Also add an Event.js file that we’ll use later:

<script src="js/Event.js"></script>
<script src="js/Caravan.js"></script>
<script src="js/UI.js"></script>
<script src="js/Game.js"></script>

Event.js skeleton. This will be used later to represent the logic behind random event generation:

var OregonH = OregonH || {};

OregonH.Event = {};

UI.js for now:

var OregonH = OregonH || {};

OregonH.UI = {};

//show a notification in the message area
OregonH.UI.notify = function(message, type){
  console.log(message + ' - ' + type);
};


//refresh visual caravan stats
OregonH.UI.refreshStats = function() {
  console.log(this.caravan);
}

The “type” refers to whether it’s a positive, neutral or negative message (like you got killed for instance). When we add the UI this will be represented by a different color.

Game.js with the basic step and flow setup:

var OregonH = OregonH || {};

//constants
OregonH.WEIGHT_PER_OX = 20;
OregonH.WEIGHT_PER_PERSON = 2;
OregonH.FOOD_WEIGHT = 0.6;
OregonH.FIREPOWER_WEIGHT = 5;
OregonH.GAME_SPEED = 800;
OregonH.DAY_PER_STEP = 0.2;
OregonH.FOOD_PER_PERSON = 0.02;
OregonH.FULL_SPEED = 5;
OregonH.SLOW_SPEED = 3;
OregonH.FINAL_DISTANCE = 1000;
OregonH.EVENT_PROBABILITY = 0.15;
OregonH.ENEMY_FIREPOWER_AVG = 5;
OregonH.ENEMY_GOLD_AVG = 50;

OregonH.Game = {};

//initiate the game
OregonH.Game.init = function(){

  //reference ui
  this.ui = OregonH.UI;

  //reference event manager
  this.eventManager = OregonH.Event;

  //setup caravan
  this.caravan = OregonH.Caravan;
  this.caravan.init({
    day: 0,
    distance: 0,
    crew: 30,
    food: 80,
    oxen: 2,
    money: 300,
    firepower: 2
  });

  //pass references
  this.caravan.ui = this.ui;
  this.caravan.eventManager = this.eventManager;

  this.ui.game = this;
  this.ui.caravan = this.caravan;
  this.ui.eventManager = this.eventManager;

  this.eventManager.game = this;
  this.eventManager.caravan = this.caravan;
  this.eventManager.ui = this.ui;

  //begin adventure!
  this.startJourney();
};

//start the journey and time starts running
OregonH.Game.startJourney = function() {
  this.gameActive = true;
  this.previousTime = null;
  this.ui.notify('A great adventure begins', 'positive');

  this.step();
};

//game loop
OregonH.Game.step = function(timestamp) {

  //starting, setup the previous time for the first time
  if(!this.previousTime){
    this.previousTime = timestamp;
    this.updateGame();
  }

  //time difference
  var progress = timestamp - this.previousTime;

  //game update
  if(progress >= OregonH.GAME_SPEED) {
    this.previousTime = timestamp;
    this.updateGame();
  }
  
  //we use "bind" so that we can refer to the context "this" inside of the step method
  if(this.gameActive) window.requestAnimationFrame(this.step.bind(this));
};

//update game stats
OregonH.Game.updateGame = function() {
  //day update
  this.caravan.day += OregonH.DAY_PER_STEP;

  //food consumption
  this.caravan.consumeFood();
  
  //game over no food
  if(this.caravan.food === 0) {
    this.ui.notify('Your caravan starved to death', 'negative');
    this.gameActive = false;
    return;
  }

  //update weight
  this.caravan.updateWeight();

  //update progress
  this.caravan.updateDistance();

  //show stats
  this.ui.refreshStats();

  //check if everyone died
  if(this.caravan.crew <= 0) {
    this.caravan.crew = 0;
    this.ui.notify('Everyone died', 'negative');
    this.gameActive = false;
    return;
  }

  //check win game
  if(this.caravan.distance >= OregonH.FINAL_DISTANCE) {
    this.ui.notify('You have returned home!', 'positive');
    this.gameActive = false;
    return;
  }

  //random events logic will go here..
  
};

//pause the journey
OregonH.Game.pauseJourney = function() {
  this.gameActive = false;
};

//resume the journey
OregonH.Game.resumeJourney = function() {
  this.gameActive = true;
  this.step();
};


//init game
OregonH.Game.init();

If you load this up with the console open, you’ll be able to see how over time distance is covered and food is consumed. If food runs out it’s game over, if you have zero people it’s game over, if you reach the distance defined in the constant OregonH.FINAL_DISTANCE you win the game.

The step method gets called the first time from within startJourney. That first time we capture the current timestamp and save it as a “previous time”. At the end of step, we call requestAnimationFrame with the same step method.

When step is called again, we will still need to access the Game object with “this”, so we use bind for that. To learn more about bind go to this Mozilla Foundation tutorial.

The game is only running if this.gameActive is true, so we can use that to stop the game both for pausing (and switch back for resuming) and for game over / winning.

Visual Stats and Progress Tracking

Let’s add some HTML and CSS so that we can see what’s going on and get a visual progress tracker. We’ll also use this occasion to add containers and style for the shop and battle panels.

Important: Since I’m trying to stick to the 13kb limit, I’ll be doing all DOM accessing and manipulation with raw JavaScript. If you are not under this limit I’d strongly recommend using a framework like Angular (that’s what I would use) instead. There are also microframeworks that can be very lightweight if you don’t mind the learning curve.

Index.html will look like so:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Learn Game Development at Zenva Academy | Zenva.com</title>
    <link rel="stylesheet" href="css/style.css"/>
  </head>
  <body>
    <div id="home"></div>
    <div id="journey">
      <div id="stats-area">
        <div class="stat">Day <span id="stat-day" class="stat-value"></span></div>
        <div class="stat">Distance <span id="stat-distance" class="stat-value"></span></div>
        <div class="stat">Crew <span id="stat-crew" class="stat-value"></span></div>
        <div class="stat">Oxen <span id="stat-oxen" class="stat-value"></span></div>
        <div class="stat">Food <span id="stat-food" class="stat-value"></span></div>
        <div class="stat">Money <span id="stat-money" class="stat-value"></span></div>
        <div class="stat">Firepower <span id="stat-firepower" class="stat-value"></span></div>
        <div class="stat">Weight <span id="stat-weight" class="stat-value"></span><span id="stat-capacity" class="stat-value"></span></div>
      </div>
      <div class="updates-area" id="updates-area"></div>
      <div id="shop" class="hidden">
        <div id="prods">
        </div>
        <button>Leave shop</button>
      </div>
      <div id="attack" class="hidden">
        <div id="attack-description"></div>
        <button id="fight">Fight</button>
        <button id="runaway">Run away</button>
      </div>
      <div id="progress-area">
        <img id="caravan" src="images/caravan.png" />
        <img id="trees" src="images/trees.png" />
      </div>
    </div>

    <div id="event"></div>
    <script src="js/Event.js"></script>
    <script src="js/Caravan.js"></script>
    <script src="js/UI.js"></script>
    <script src="js/Game.js"></script>
  </body>
</html>

And style.css:

html {
  height: 100%;
}

body {
  padding: 0;
  margin: 0;
  border: 0;
  height: 100%;
  overflow: hidden;
  font-family: Courier;
}

.hidden {
  display: none;
}

#home {
  background-color: red;
}

#journey {
  height: 100%;
  background-color: #5F9FFF;
}

#event {
  background-color: magenta;
}

#progress-area {
  float: left;
  position: relative;
  width: 400px;
  margin: 10px;
}

#trees {
  width: 100%;
  image-rendering: pixelated;
}

#caravan {
  position: absolute;
  bottom: 30px;
  left: 0;
}

#stats-area {
  background-color: white;
  float: left;
  height: 150px;
  width: 156px;
  margin: 10px;
  padding: 10px;
  font-size: 14px;
}



.stat-value {
  float: right;
}

#updates-area {
  background-color: white;
  float: left;
  width: 350px;
  margin: 10px;
  height: 150px;
  padding: 10px;
  font-size: 12px;
  overflow-y: scroll;
}

.update-positive {
  color: #669966;
}

.update-negative {
  color: red;
}

.update-neutral {
  color: #90908F;
}

#shop {
  float: left;
  background-color: white;
  margin: 10px;
  padding: 10px;
  height: 90px;
  width: 350px;
}

.product {
  float: left;
  margin: 5px;
  padding: 2px;
  border: 1px solid black;
  font-size: 12px;
}

#shop button {
  clear: both;
  float: left;
  margin: 5px;
}

#attack {
  float: left;
  background-color: white;
  margin: 10px;
  padding: 10px;
  height: 60px;
  width: 150px;
}

We can now add proper DOM manipulation in UI.js and keep track of the progress of the caravan, so that when it indicates how close you are to the goal:

var OregonH = OregonH || {};

OregonH.UI = {};

//show a notification in the message area
OregonH.UI.notify = function(message, type){
  document.getElementById('updates-area').innerHTML = '<div class="update-' + type + '">Day '+ Math.ceil(this.caravan.day) + ': ' + message+'</div>' + document.getElementById('updates-area').innerHTML;
};

//refresh visual caravan stats
OregonH.UI.refreshStats = function() {
  //modify the dom
  document.getElementById('stat-day').innerHTML = Math.ceil(this.caravan.day);
  document.getElementById('stat-distance').innerHTML = Math.floor(this.caravan.distance);
  document.getElementById('stat-crew').innerHTML = this.caravan.crew;
  document.getElementById('stat-oxen').innerHTML = this.caravan.oxen;
  document.getElementById('stat-food').innerHTML = Math.ceil(this.caravan.food);
  document.getElementById('stat-money').innerHTML = this.caravan.money;
  document.getElementById('stat-firepower').innerHTML = this.caravan.firepower;
  document.getElementById('stat-weight').innerHTML = Math.ceil(this.caravan.weight) + '/' + this.caravan.capacity;

  //update caravan position
  document.getElementById('caravan').style.left = (380 * this.caravan.distance/OregonH.FINAL_DISTANCE) + 'px';
};

This is what the UI should look like:

basic ui text game

Random Events

How often a random event will occur is defined by the constant OregonH.EVENT_PROBABILITY. I’ve set a value of 0.15 which means on each step there is a 15% chance that there will be a random event.

We’ll have 3 types of random events, but it should be pretty straight forward to add more types, and more variations of these 3 types:

  • Caravan stat change: any stat of the caravan can increase or decrease in value. For example you can find wild berries and increase your food, or there can be a flu event and some of your crew can perish.
  • Shop: a place where you can buy things.
  • Attacks: you are under attack, you can either fight or flee.

We’ll trigger random events in the updateGame of our Game object. Add the following by the end of that method (after we check for winning the game):

//random events
  if(Math.random() <= OregonH.EVENT_PROBABILITY) {
    this.eventManager.generateEvent();
  }

Let’s begin implementing “Caravan stat change” events first. The generateEvent mentioned there will randomly pick and event from a list and will “execute it”.

Event.js will have the following code:

var OregonH = OregonH || {};

OregonH.Event = {};

OregonH.Event.eventTypes = [
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'crew',
    value: -3,
    text: 'Food intoxication. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'crew',
    value: -4,
    text: 'Flu outbreak. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'food',
    value: -10,
    text: 'Worm infestation. Food lost: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'money',
    value: -50,
    text: 'Pick pockets steal $'
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'oxen',
    value: -1,
    text: 'Ox flu outbreak. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'food',
    value: 20,
    text: 'Found wild berries. Food added: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'food',
    value: 20,
    text: 'Found wild berries. Food added: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'oxen',
    value: 1,
    text: 'Found wild oxen. New oxen: '
  }

];

OregonH.Event.generateEvent = function(){
  //pick random one
  var eventIndex = Math.floor(Math.random() * this.eventTypes.length);
  var eventData = this.eventTypes[eventIndex];

  //events that consist in updating a stat
  if(eventData.type == 'STAT-CHANGE') {
    this.stateChangeEvent(eventData);
  }

  
};

OregonH.Event.stateChangeEvent = function(eventData) {
  //can't have negative quantities
  if(eventData.value + this.caravan[eventData.stat] >= 0) {
    this.caravan[eventData.stat] += eventData.value;
    this.ui.notify(eventData.text + Math.abs(eventData.value), eventData.notification);
  }
};

Event.eventTypes will have all possible events. Properties:

  • type: refers to what type of event this is. See how they are all of type “STAT-CHANGE” for now.
  • notification: positive, neutral or negative
  • stat: which property of the caravan we are changing
  • value: how much are we changing this property by (it can be positive or negative)
  • text: what we show to the user in the message log

For stat change events we have the stateChangeEvent method which carries out the increment (checking of course that we don’t have negative properties).

random event

Battles

Roads can be pretty dangerous and in this game you will be attacked often. When you are attacked you can either flight or flee. It all depends on your firepower, crew members and risk profile. If you do engage in battle and win you’ll get money from the loot.

Event.js with battles:

var OregonH = OregonH || {};

OregonH.Event = {};

OregonH.Event.eventTypes = [
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'crew',
    value: -3,
    text: 'Food intoxication. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'crew',
    value: -4,
    text: 'Flu outbreak. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'food',
    value: -10,
    text: 'Worm infestation. Food lost: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'money',
    value: -50,
    text: 'Pick pockets steal $'
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'oxen',
    value: -1,
    text: 'Ox flu outbreak. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'food',
    value: 20,
    text: 'Found wild berries. Food added: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'food',
    value: 20,
    text: 'Found wild berries. Food added: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'oxen',
    value: 1,
    text: 'Found wild oxen. New oxen: '
  },
  {
    type: 'ATTACK',
    notification: 'negative',
    text: 'Bandits are attacking you'
  },
  {
    type: 'ATTACK',
    notification: 'negative',
    text: 'Bandits are attacking you'
  },
  {
    type: 'ATTACK',
    notification: 'negative',
    text: 'Bandits are attacking you'
  }
];

OregonH.Event.generateEvent = function(){
  //pick random one
  var eventIndex = Math.floor(Math.random() * this.eventTypes.length);
  var eventData = this.eventTypes[eventIndex];

  //events that consist in updating a stat
  if(eventData.type == 'STAT-CHANGE') {
    this.stateChangeEvent(eventData);
  }

  //attacks
  else if(eventData.type == 'ATTACK') {
    //pause game
    this.game.pauseJourney();

    //notify user
    this.ui.notify(eventData.text, eventData.notification);

    //prepare event
    this.attackEvent(eventData);
  }
};

OregonH.Event.stateChangeEvent = function(eventData) {
  //can't have negative quantities
  if(eventData.value + this.caravan[eventData.stat] >= 0) {
    this.caravan[eventData.stat] += eventData.value;
    this.ui.notify(eventData.text + Math.abs(eventData.value), eventData.notification);
  }
};


//prepare an attack event
OregonH.Event.attackEvent = function(eventData){
  var firepower = Math.round((0.7 + 0.6 * Math.random()) * OregonH.ENEMY_FIREPOWER_AVG);
  var gold = Math.round((0.7 + 0.6 * Math.random()) * OregonH.ENEMY_GOLD_AVG);

  this.ui.showAttack(firepower, gold);
};

The firepower and money of the enemies gets randomly modified. showAttack goes in our UI.js file and it shows an attack screen where you can make your choice.

As I mentioned before, if it wasn’t for the 13kb limit I would prefer to use Angular. Feel free to explore microframeworks for some lightweight DOM manipulation libraries.

//show attack
OregonH.UI.showAttack = function(firepower, gold) {
  var attackDiv = document.getElementById('attack');
  attackDiv.classList.remove('hidden');

  //keep properties
  this.firepower = firepower;
  this.gold = gold;

  //show firepower
  document.getElementById('attack-description').innerHTML = 'Firepower: ' + firepower;

  //init once
  if(!this.attackInitiated) {

    //fight
    document.getElementById('fight').addEventListener('click', this.fight.bind(this));

    //run away
    document.getElementById('runaway').addEventListener('click', this.runaway.bind(this));

    this.attackInitiated = true;
  }
};

//fight
OregonH.UI.fight = function(){

  var firepower = this.firepower;
  var gold = this.gold;

  var damage = Math.ceil(Math.max(0, firepower * 2 * Math.random() - this.caravan.firepower));

  //check there are survivors
  if(damage < this.caravan.crew) {
    this.caravan.crew -= damage;
    this.caravan.money += gold;
    this.notify(damage + ' people were killed fighting', 'negative');
    this.notify('Found $' + gold, 'gold');
  }
  else {
    this.caravan.crew = 0;
    this.notify('Everybody died in the fight', 'negative');
  }

  //resume journey
  document.getElementById('attack').classList.add('hidden');
  this.game.resumeJourney();
};

//runing away from enemy
OregonH.UI.runaway = function(){

  var firepower = this.firepower;

  var damage = Math.ceil(Math.max(0, firepower * Math.random()/2));

  //check there are survivors
  if(damage < this.caravan.crew) {
    this.caravan.crew -= damage;
    this.notify(damage + ' people were killed running', 'negative');
  }
  else {
    this.caravan.crew = 0;
    this.notify('Everybody died running away', 'negative');
  }

  //remove event listener
  document.getElementById('runaway').removeEventListener('click');

  //resume journey
  document.getElementById('attack').classList.add('hidden');
  this.game.resumeJourney();

};

Basically if you choose to run away, it won’t matter how much firepower you have, but if you fight, you can have a chance at winning the battle and getting some extra cash.

Shops

Shops will appear on your journey in the same way as the other random events. The contents of a shop will be random, based in what the initial shop items:

var OregonH = OregonH || {};

OregonH.Event = {};

OregonH.Event.eventTypes = [
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'crew',
    value: -3,
    text: 'Food intoxication. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'crew',
    value: -4,
    text: 'Flu outbreak. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'food',
    value: -10,
    text: 'Worm infestation. Food lost: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'money',
    value: -50,
    text: 'Pick pockets steal $'
  },
  {
    type: 'STAT-CHANGE',
    notification: 'negative',
    stat: 'oxen',
    value: -1,
    text: 'Ox flu outbreak. Casualties: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'food',
    value: 20,
    text: 'Found wild berries. Food added: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'food',
    value: 20,
    text: 'Found wild berries. Food added: '
  },
  {
    type: 'STAT-CHANGE',
    notification: 'positive',
    stat: 'oxen',
    value: 1,
    text: 'Found wild oxen. New oxen: '
  },
  {
    type: 'SHOP',
    notification: 'neutral',
    text: 'You have found a shop',
    products: [
      {item: 'food', qty: 20, price: 50},
      {item: 'oxen', qty: 1, price: 200},
      {item: 'firepower', qty: 2, price: 50},
      {item: 'crew', qty: 5, price: 80}
    ]
  },
  {
    type: 'SHOP',
    notification: 'neutral',
    text: 'You have found a shop',
    products: [
      {item: 'food', qty: 30, price: 50},
      {item: 'oxen', qty: 1, price: 200},
      {item: 'firepower', qty: 2, price: 20},
      {item: 'crew', qty: 10, price: 80}
    ]
  },
  {
    type: 'SHOP',
    notification: 'neutral',
    text: 'Smugglers sell various goods',
    products: [
      {item: 'food', qty: 20, price: 60},
      {item: 'oxen', qty: 1, price: 300},
      {item: 'firepower', qty: 2, price: 80},
      {item: 'crew', qty: 5, price: 60}
    ]
  },
  {
    type: 'ATTACK',
    notification: 'negative',
    text: 'Bandits are attacking you'
  },
  {
    type: 'ATTACK',
    notification: 'negative',
    text: 'Bandits are attacking you'
  },
  {
    type: 'ATTACK',
    notification: 'negative',
    text: 'Bandits are attacking you'
  }
];

OregonH.Event.generateEvent = function(){
  //pick random one
  var eventIndex = Math.floor(Math.random() * this.eventTypes.length);
  var eventData = this.eventTypes[eventIndex];

  //events that consist in updating a stat
  if(eventData.type == 'STAT-CHANGE') {
    this.stateChangeEvent(eventData);
  }

  //shops
  else if(eventData.type == 'SHOP') {
    //pause game
    this.game.pauseJourney();

    //notify user
    this.ui.notify(eventData.text, eventData.notification);

    //prepare event
    this.shopEvent(eventData);
  }

  //attacks
  else if(eventData.type == 'ATTACK') {
    //pause game
    this.game.pauseJourney();

    //notify user
    this.ui.notify(eventData.text, eventData.notification);

    //prepare event
    this.attackEvent(eventData);
  }
};

OregonH.Event.stateChangeEvent = function(eventData) {
  //can't have negative quantities
  if(eventData.value + this.caravan[eventData.stat] >= 0) {
    this.caravan[eventData.stat] += eventData.value;
    this.ui.notify(eventData.text + Math.abs(eventData.value), eventData.notification);
  }
};

OregonH.Event.shopEvent = function(eventData) {
  //number of products for sale
  var numProds = Math.ceil(Math.random() * 4);

  //product list
  var products = [];
  var j, priceFactor;

  for(var i = 0; i < numProds; i++) {
    //random product
    j = Math.floor(Math.random() * eventData.products.length);

    //multiply price by random factor +-30%
    priceFactor = 0.7 + 0.6 * Math.random();

    products.push({
      item: eventData.products[j].item,
      qty: eventData.products[j].qty,
      price: Math.round(eventData.products[j].price * priceFactor)
    });
  }

  this.ui.showShop(products);
};

//prepare an attack event
OregonH.Event.attackEvent = function(eventData){
  var firepower = Math.round((0.7 + 0.6 * Math.random()) * OregonH.ENEMY_FIREPOWER_AVG);
  var gold = Math.round((0.7 + 0.6 * Math.random()) * OregonH.ENEMY_GOLD_AVG);

  this.ui.showAttack(firepower, gold);
};

When it comes to show displaying, Angular would have made this very clean but constraints are contraints!

In UI.js

//show shop
OregonH.UI.showShop = function(products){

  //get shop area
  var shopDiv = document.getElementById('shop');
  shopDiv.classList.remove('hidden');

  //init the shop just once
  if(!this.shopInitiated) {

    //event delegation
    shopDiv.addEventListener('click', function(e){
      //what was clicked
      var target = e.target || e.src;

      //exit button
      if(target.tagName == 'BUTTON') {
        //resume journey
        shopDiv.classList.add('hidden');
        OregonH.UI.game.resumeJourney();
      }
      else if(target.tagName == 'DIV' && target.className.match(/product/)) {

        OregonH.UI.buyProduct({
          item: target.getAttribute('data-item'),
          qty: target.getAttribute('data-qty'),
          price: target.getAttribute('data-price')
        });

      }
    });

    this.shopInitiated = true;
  }

  //clear existing content
  var prodsDiv = document.getElementById('prods');
  prodsDiv.innerHTML = '';

  //show products
  var product;
  for(var i=0; i < products.length; i++) {
    product = products[i];
    prodsDiv.innerHTML += '<div class="product" data-qty="' + product.qty + '" data-item="' + product.item + '" data-price="' + product.price + '">' + product.qty + ' ' + product.item + ' - $' + product.price + '</div>';
  }
};

//buy product
OregonH.UI.buyProduct = function(product) {
  //check we can afford it
  if(product.price > OregonH.UI.caravan.money) {
    OregonH.UI.notify('Not enough money', 'negative');
    return false;
  }

  OregonH.UI.caravan.money -= product.price;

  OregonH.UI.caravan[product.item] += +product.qty;

  OregonH.UI.notify('Bought ' + product.qty + ' x ' + product.item, 'positive');

  //update weight
  OregonH.UI.caravan.updateWeight();

  //update visuals
  OregonH.UI.refreshStats();
};

html5 game shop

Game Finished!

You’ve completed a basic text game with HTML5, all of it in under 13kb!

battles text game html5

Leave a Comment!

Got some questions? feedback? what other topics would you like me to write about?