Zombie Apocalypse (part 1)

If you want to create a technically complex game - and any game involving combat will be complex - you should use the desktop version of the editor if you can. Sometimes, however, that is not an option. Here, then, is how to create the zombie apocalypse on the web version.

It does not have to be the zombie apocalypse, but the limitations of the web version lend themselves to a game with a limited variety of monsters. In a zombie apocalypse, you expect a lot of zombies and not much else.

I am assuming you at least know how to copy-and-paste code, and that you know what the game start script is. If you do not, I would respectfully suggest you start with something simpler, and come back to this when you have more experience.

Zombies

There will be a lot of zombies, all pretty much the same, and you really do not want to create each one individually. There may be more spawning all the time too, so we will have a function to create zombies as we need them. Call this SpawnZombie, no return type, with a single parameter, “room”. Paste in this code:

if (HasInt(game, "crittercount")) {
  game.crittercount = game.crittercount + 1
}
else {
  game.crittercount = 1
}
create ("critter" + game.crittercount)
obj = GetObject("critter" + game.crittercount)
obj.parent = room
obj.displayverbs = Split("Look at;Attack;Shoot", ";")
obj.dead = false
obj.changedhitpoints => {
  if (this.hitpoints < 1) {
    msg ("It is dead!")
    this.dead = true
  }
}
names = Split("decipit;decomposing;shambling;disgusting;filthy;falling-apart", ";")
obj.alias = StringListItem(names, game.crittercount % ListCount(names)) + " zombie"
obj.listalias = CapFirst(obj.alias)
obj.look = ProcessText("A " + obj.alias + ", {random:covered in maggots:missing an arm:one eye hanging out}.")
obj.hitpoints = 10
obj.damage = 3
obj.attack = 0
obj.defence = 0
obj.armour = 0

The code

So what does the code actually do? Let’s break it down.

if (HasInt(game, "crittercount")) {
  game.crittercount = game.crittercount + 1
}
else {
  game.crittercount = 1
}
create ("critter" + game.crittercount)
obj = GetObject("critter" + game.crittercount)

This is the creation process. This creates a new object, with the given name. That name has to be unique, so the first six lines track how many zombies have already been created, and if this is the first, we also need to add the count to the game object. So, first, does the “crittercount” attribute already exist? If so, increment it by one, otherwise create it and set it to 1. Now we can create the zombie with its unique name, critter1, critter2, etc. Then we can get that object and assign it to the local variable obj (I am keeping it general as you might not be doing zombies).

obj.parent = room
obj.displayverbs = Split("Look at;Attack", ";")
obj.dead = false

These lines are simple house-keeping. The first puts the zombie in the given room, the second sets up the verbs for it. The third tracks the state of the zombie.

obj.changedhitpoints => {
  if (this.hitpoints < 1) {
    msg ("It is dead!")
    this.dead = true
    this.listalias = this.listalias + " (dead)"
  }
}

This attribute, changedhitpoints, is a change script, it will fire when hitpoints changes. This will note when the zombie is dead.

names = Split("decipit;decomposing;shambling;disgusting;filthy;falling-apart", ";")
obj.alias = StringListItem(names, game.crittercount % ListCount(names)) + " zombie"
obj.listalias = CapFirst(obj.alias)
obj.look = ProcessText("A " + obj.alias + ", {random:covered in maggots:missing an arm:one eye hanging out}.")

We need to give the zombie an alias, otherwise the player will see it as critter1, and will have to type ATTACK CRITTER1. We also want zombies to have different names, so the player can tell them apart.

The first line here sets up the various names, and you should modify and expand this. Each descriptor must be separated by a semi-colon, and you want a few more than the largest number of zombies the player will see at one time.

The alias is generated by selecting one descriptor from the list, using modulo arithmetic. The list alias is how this appears in the game panes; it is the same but with a capital letter.

The look attribute is what the player will see if she does LOOK AT ZOMBIE. We want some variety there, so not all zombies are the same. By using ProcessText, it will not change each time the player looks at a specific zombie. The more options the better; you should get creative here, so there is plenty of difference in your zombie hoard.

obj.hitpoints = 10
obj.damage = 3
obj.attack = 0
obj.defence = 0
obj.armour = 0

These set up the combat stats for the zombie.

Putting zombies in the game

To add zombies to a room, go to the Scripts tab of the room, and look for the “Before entering the room for the first time” section. Add these two lines (for two zombies).

SpawnZombie(this)
SpawnZombie(this)

When you go in game, two zombies should be there. Try looking at them - there is not much else they can do yet.

You can add any number of zombies to any number of rooms in this way. You can also have new zombies spawn when something happens, such as the player entering a new region or setting off a car alarm.

Not only zombies

You can do the same sort of thing for any monsters in your game, just create a function for each, named appropriately, and change the word “zombie” in the code. If you want ghouls, you have a function, SpawnGhoul, use the same code as before, editing the last nine lines as required, for instance:

names = Split("foul;disgusting;horrid", ";")
obj.alias = StringListItem(names, game.crittercount % ListCount(names)) + " ghoul"
obj.listalias = CapFirst(obj.alias)
obj.look = ProcessText("A " + obj.alias + ", {random:with long talons:its pale eyes glaring at you}.")
obj.hitpoints = 20
obj.damage = 1d8
obj.attack = 1
obj.defence = 0
obj.armour = 20

The player

The player needs the same attributes for combat as the zombies. If you were using the desktop version, you would set attributes on the Attributes tab of the player object, but on the web version we do not have that. Prior to Quest 5.7, you would have to do this in the start script of the game object, which is fine at first, but as your game gets more complex, the script will get huge, and increasely difficult to maintain. So instead will will set up attributes for an object in its initialisation script.

On the player object’s Features tab, tick “Run an initialisation script for this object”. Then go to the Initialisation script tab, and put in this code, which will set its attributes.

player.alias = "you"
player.hitpoints = 25
player.damage = 3
player.attack = 0
player.defence = 0
player.armour = 0
player.changedhitpoints => {
  if (player.hitpoints > 0) {
    msg ("Hits points now " + player.hitpoints)
  }
  else {
    msg ("You are dead!")
    finish
  }
}

Attacking

We are getting close to being able to kill those zombies (do you kill zombies, if they are already dead?). Create a command, and put in this pattern:

attack #object#

Paste in this code:

if (not HasBoolean(object, "dead")) {
  msg ("That's not something you can attack.")
}
else if (object.dead) {
  msg ("That one is already dead.")
}
else {
  if (player.equipped = null) {
    DoAttack (player, player, object)
  }
  else {
    DoAttack (player, player.equipped, object)
  }
}

If the object the player wants to attack has no dead attribute, it is not something that can be attacked. If the dead attribute is true, it is already dead. Otherwise, we do the attack, with the DoAttack function, either with the current weapon, which we will store in the equipped attribute, or bare handed.

This is a standard format for commands; a series of if/else tests to catch situations where the command cannot be done (with an appropriate message), and a final section where the action is performed, once we know it can be.

So now we need a DoAttack function. The zombies will be using this later, by the way. It has no return type, but three parameters; “attacker”, “weapon” and “target” (in that order!). Here is the code:

roll = GetRandomInt(1, 20) + weapon.attack - target.defence
damage = DiceRoll(weapon.damage) * (100 - target.armour) / 100
if (damage < 1) {
  damage = 1
}
if (roll > 15) {
  damage = damage * 3
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and gets a critical (" + damage + " hits)!")
  target.hitpoints = target.hitpoints - damage
}
else if (roll > 10) {
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and hit (" + damage + " hits).")
  target.hitpoints = target.hitpoints - damage
}
else {
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and misses...")
}

The code

So what does the code actually do? Let’s break it down again.

roll = GetRandomInt(1, 20) + weapon.attack - target.defence
damage = DiceRoll(weapon.damage) * (100 - target.armour) / 100
if (damage < 1) {
  damage = 1
}

The first line makes the attack roll, which we will use to see if it hit or not. Then we work out the damage (it is easier to do it now, ready for later, even though it may not have hit). Note that you can set the damage for a weapon or zombie to be a number, which is used “as is”, or a string, in the form “2d4”, which will be determined randomly, as DiceRoll will handle it either way. The damage is modified for armour, which is assumed to be a percentage, so will range from 0, unprotected, to 100, full protected. However, it will be a minimum of 1.

if (roll > 15) {
  damage = damage * 3
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and gets a critical (" + damage + " hits)!")
  target.hitpoints = target.hitpoints - damage
}
else if (roll > 10) {
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and hit (" + damage + " hits).")
  target.hitpoints = target.hitpoints - damage
}
else {
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and misses...")
}

Now we determine the result. If the roll was over 15, it was a critical, and does three times damage, over 10 it was a hit.

You may choose to adjust these numbers, or to have a totally different way to resolve combat; hopefully this gives an idea as to how to do that. We will modify this function later, and you might want to finish this tutorial before making any significant changes.

A big advantage of having all attacks use this one function is that changes need only be done once. If you decide criticals will only do double damage, one change in this function will affect everything in your game.

Weapons

We need a weapon to attack the zombies. We will just do one for now, but you may want several in your game. If you were using the desktop version, you would set attributes on the Attributes tab, and use verbs to interact, set up on a weapon type. On the web version, we need to set attributes in an initialisation script for the item as we did with the player object, and are better off using commands, as you can have a single command to handle all the weapons.

We will add a spade; give it a description, such as “Good for whacking zombies”, and tick that it can be picked up on the Inventory tab. On the Object tab, give it an “Alias to display…” - “Spade” (this is so we can flag it as equipped later; if you are not using the game panes, this is not required). At the bottom, in the inventory verbs, remove “Use” and add “Equip”.

We also need to give it some stats. On the spade’s Features tab, tick “Run an initialisation script for this object”. Then go to the Initialisation script tab, and put in this code:

this.damage = "1d6"
this.attack = 3

By the way, this refers to the object the script belongs to. I am using it here because later on you might want to create more weapons, and the easiest way is to copy the spade object (use the copy button at the top of the screen). If we used the item name in the script, we would have to change that in each copy.

The player will equip and unequip weapons to use them, and the best way to do that is with commands, as we can set two commands to handle all weapons. Create a new command, with the pattern:

equip #object#

Paste in this code:

if (HasBoolean(object, "dead")) {
  msg ("That's not something you can wield.")
}
else if (not HasAttribute(object, "damage")) {
  msg ("That's not something you can wield.")
}
else if (not object.parent = player) {
  msg ("You are not carrying it.")
}
else if (object = player.equipped) {
  msg ("You already have.")
}
else {
  if (player.equipped = null) {
    msg ("You equip your " + GetDisplayAlias(object) + ".")
  }
  else {
    msg ("You put away your " + GetDisplayAlias(player.equipped) + " and equip your " + GetDisplayAlias(object) + ".")
    list add (player.equipped.inventoryverbs, "Equip")
    list remove (player.equipped.inventoryverbs, "Unequip")
    player.equipped.listalias = Replace(player.equipped.listalias, " (equipped)", "")
  }
  player.equipped = object
  list add (object.inventoryverbs, "Unequip")
  list remove (object.inventoryverbs, "Equip")
  object.listalias = object.listalias + " (equipped)"
}

Again, this follows the standard format for commands; a series of tests to catch when it cannot be done, and a final section where the action is performed.

How do we know whether the object is a weapon? We could have a flag that we set on every weapon, but there is no need; if it has a damage attribute, it is either a weapon or zombie, and if it has no dead attribute, it must be a weapon.

So the player can only equip weapons, and we check that that is the case, and then check the player is carrying the object, and it is not already equipped. If all is well, the object is equipped, which may involve unequipping something else.

Create a second command, with the pattern:

unequip #object#

Paste in this code:

if (not object = player.equipped) {
  msg ("You are not wielding it.")
}
else {
  msg ("You put away your spade.")
  player.equipped = null
  list add (object.inventoryverbs, "Equip")
  list remove (object.inventoryverbs, "Unequip")
  object.listalias = Replace(object.listalias, " (equipped)", "")
}

Now go into the game, grab that spade and smack some zombies with it!

Zombie attack!

So far our game is pretty easy - the zombies just stand there and let you hit them. We need them to attack back. We need a turn script to do that, so go to the Scripts tab of the game object, and in the “turn scripts” section, click “Add”. Give it some name, perhaps “attackturnscript”, and tick it to be enabled at the start. Paste in this code:

foreach (obj, GetDirectChildren(player.parent)) {
  if (HasBoolean(obj, "dead")) {
    if (not obj.dead) {
      DoAttack(obj, obj, player)
    }
  }
}

This is pretty simple. GetDirectChildren(player.parent) gets all the things in the current room, and we go through them, one-by-one. First it checks the object is a zombie (or anything; if you later add other monsters, as long as you set dead to false, this will work), then check it is still alive. If it is, call the DoAttack function we created earlier.

Now go in game and those zombies will fight back!

…But not for a typo

When the zombies fought back, you might have found that they do so even if you mistyped something. That seems to give the zombies an unfair advantage, so let’s change it so they only attack if Quest has understood the command (even if Quest then says no).

Go to the game object, and on the Features tab, tick “Show advanced scripts…”. Then, on the Advanced scripts tab, for the “Unresolved command script”, add this code:

msg("Er, what..?")
game.notarealturn = true

Then for the turn script, change it to this:

if (not GetBoolean(game, "notarealturn")) {
  player.attackers = NewObjectList()
  foreach (obj, GetDirectChildren(player.parent)) {
    if (HasBoolean(obj, "dead")) {
      if (not obj.dead) {
        DoAttack (obj, obj, player)
      }
    }
  }
}
game.notarealturn = false

Zombies that follow

You will also have noticed that the zombies stay in one place. Let’s get them moving.

What we will do is save the list of zombies that are attacking the player, and then next turn, any zombies on that list but not attacking (and not dead), will be moved to the current room.

First, then, in the game start script (Scripts tab of the game object), add this line:

// List of attackers
player.attackers = NewObjectList()

Now back to the turn script, and change the code to this:

if (not GetBoolean(game, "notarealturn")) {
  list = NewObjectList()
  foreach (obj, GetDirectChildren(player.parent)) {
    if (HasBoolean(obj, "dead")) {
      if (not obj.dead) {
        DoAttack (obj, obj, player)
        list add (list, obj)
      }
    }
  }
  foreach (obj, ListExclude(player.attackers, list)) {
    if (not obj.dead and RandomChance(80)) {
      obj.parent = player.parent
      msg (CapFirst(obj.alias) + " shambles into the area.")
      list add (list, obj)
    }
  }
  player.attackers = list
}
game.notarealturn = false

This will have a zombie follow 80% of the time, you can adjust as seems fit.

Safe room

If you want to stop the zombies following the player through a certain exit, go to the exit, and tick the “Run a script” box. Paste in this code (editing the text as required):

player.attackers = NewObjectList()
msg ("You slam the door, safe at last!")
player.parent = this.to

Firearms

Now we will add a second weapon, a pistol. The pistol will have ammo, and will need to be reloaded. The first thing to do is to create the pistol, and the easiest way is to copy the spade. Click the “Copy” button, select a destination, and click “Paste” (if you are using the desktop version, find these on the Edit menu). On the Setup tab, give it the name “pistol”, and this as the description, so it will display the ammo:

Good for shooting zombies. Ammo: {pistol.ammo}/{pistol.ammomax}

Once you have done that, go to the Initialisation script tab. You should already have this:

this.damage = "1d6"
this.attack = 3

We will give firearms two sets of stats, so the player can try to smash a zombie with a weapon when the ammo runs out. As a melee weapon, a pistol sucks compared to our spade, so we will set it up like this:

this.damage = "2"
this.attack = 0
this.firearmdamage = "2d8"
this.firearmattack = 3
this.ammo = 3
this.ammomax = 6

Now we need a shoot command. This will be similar to the attack command, with this pattern:

shoot #object#

And this code:

if (not HasBoolean(object, "dead")) {
  msg ("That's not something you need to shoot.")
}
else if (object.dead) {
  msg ("That one is already dead.")
}
else if (player.equipped = null) {
  msg ("You don't have a firearm equipped.")
}
else if (not HasInt(player.equipped, "ammo")) {
  msg ("You can't shoot a " + GetDisplayAlias(player.equipped) + ".")
}
else if (not player.equipped.ammo > 0) {
  msg ("You aim your " + GetDisplayAlias(player.equipped) + " and pull the trigger. Click. No ammo loaded.")
}
else {
  DoAttack (player, player.equipped, object, true)
}

As you can see, we have a few more things to check; is it a firearm (i.e., does it have an “ammo” attribute), are there any bullets in it? If all is well, we call the same DoAttack, but with a new parameter that flags this as a firearm attack. This means we have to update DoAttack, giving it a new parameter, “firearm”, and this new code (the first 9 lines are new, the next six edited, the rest are the same):

if (firearm) {
  damageatt = "firearmdamage"
  attackatt = "firearmattack"
  weapon.ammo = weapon.ammo - 1
}
else {
  damageatt = "damage"
  attackatt = "attack"
}
roll = GetRandomInt(1, 20) + weapon.attack - target.defence
damage = DiceRoll(weapon.damage) * (100 - target.armour) / 100
if (damage < 1) {
  damage = 1
}
if (roll > 15) {
  damage = damage * 3
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and gets a critical (" + damage + " hits)!")
  target.hitpoints = target.hitpoints - damage
}
else if (roll > 10) {
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and hit (" + damage + " hits).")
  target.hitpoints = target.hitpoints - damage
}
else {
  msg (CapFirst(attacker.alias) + " attacks " + target.alias + " and misses...")
}

Finally, we need to change other places where we used DoAttack so they have the new parameter (which should be false). In the attack command, the last but one line now needs to be:

DoAttack (player, player.equipped, object, false)

In the turn script, the sixth line needs to be this:

DoAttack (obj, obj, player, false)

Finally, the tenth line of SpawnZombie (and any other spawn functions) should be changed to:

obj.displayverbs = Split("Look at;Attack;Shoot", ";")

If you go in game, you should be able to shoot the zombies, and when the pistol has no more ammo, whack them. However, we want to reload the gun. In the game start script, add this line:

player.ammo = 35

This will give the player 35 spare bullets at the start. If she buys more, you need to add the extra to that amount. Go to the Inventory tab of the pistol, and add “Reload” as an inventory verb.

Now we need a reload command. This is the pattern:

reload #object#

Here is the code:

if (not HasInt(object, "ammo")) {
  msg ("You can't reload a " + GetDisplayAlias(player.equipped) + ".")
}
else if (player.ammo < 1) {
  msg ("You have no ammo.")
}
else {
  bullets = object.ammomax - object.ammo
  if (bullets > player.ammo) {
    bullets = player.ammo
  }
  player.ammo = player.ammo - bullets
  object.ammo = object.ammo + bullets
  msg ("You put " + bullets + " bullets in it.")
}

A Working Game!

So now we have a working game, with two weapons and lots of zombies. That may be all you need, but if you want to implement some more advanced features, such as searching corpses and varying zombie attacks, you might like to go on to part two.