Friday, October 14, 2011

roguelike tutorial 17: smarter monsters

Now that we've got all these nifty items and abilities, wouldn't it be cool if our cave monsters could use them too? Let's create a new creature capable of using weapons.

We can start with a GoblinAi that's almost exactly the same as the ZombieAi.

package rltut;

import java.util.List;

public class GoblinAi extends CreatureAi {
    private Creature player;

    public GoblinAi(Creature creature, Creature player) {
        super(creature);
        this.player = player;
    }

    public void onUpdate(){
        if (creature.canSee(player.x, player.y, player.z))
            hunt(player);
        else
            wander();
    }

    public void hunt(Creature target){
        List<Point> points = new Path(creature, target.x, target.y).points();
    
        int mx = points.get(0).x - creature.x;
        int my = points.get(0).y - creature.y;
    
        creature.moveBy(mx, my, 0);
    }
}


Now let's add newGoblin to the StuffFactory. Goblins start with random weapons and armor.
public Creature newGoblin(int depth, Creature player){
        Creature goblin = new Creature(world, 'g', AsciiPanel.brightGreen, "goblin", 66, 15, 5);
        goblin.equip(randomWeapon(depth));
        goblin.equip(randomArmor(depth));
        world.addAtEmptyLocation(goblin, depth);
        new GoblinAi(goblin, player);
        return goblin;
    }

Now update creature's equip method to make sure anything equipped is added to the inventory if it isn't already there.
public void equip(Item item){
        if (!inventory.contains(item)) {
            if (inventory.isFull()) {
                notify("Can't equip %s since you're holding too much stuff.", item.name());
                return;
            } else {
                world.remove(item);
                inventory.add(item);
            }
        }
    
        if (item.attackValue() == 0 && item.rangedAttackValue() == 0 && item.defenseValue() == 0)
            return;
    
        if (item.attackValue() + item.rangedAttackValue() >= item.defenseValue()){
            unequip(weapon);
            doAction("wield a " + item.name());
            weapon = item;
        } else {
            unequip(armor);
            doAction("put on a " + item.name());
            armor = item;
        }
    }

And add the missing method to the World class. This allows us to remove an object even if we don't know where it is.
public void remove(Item item) {
        for (int x = 0; x < width; x++){
            for (int y = 0; y < height; y++){
                for (int z = 0; z < depth; z++){
                if (items[x][y][z] == item) {
                    items[x][y][z] = null;
                    return;
                }
            }
        }
    }
}

We also need to make sure that when a creature dies it drops anything it was holding.
private void leaveCorpse(){
        Item corpse = new Item('%', color, name + " corpse");
        corpse.modifyFoodValue(maxHp);
        world.addAtEmptySpace(corpse, x, y, z);
        for (Item item : inventory.getItems()){
            if (item != null)
                drop(item);
            }
    }


After adding goblins to the addCreatures method of the PlayScreen you should be able to play and fight some rather tough goblins with armor and weapons of their own. Don't forget to tweak each creatures hp, attack, defense, the food values of corpses, and the xp gained or how much xp is needed for each level. You should also change the number of items and creatures per level. I prefer few items on the ground. That way the player almost has to confront goblins to get better loot.


So our goblins can use melee weapons and armor — that's nice — but they will run over and beat you with a bow instead of fire them. How about if they could throw things or fire from a distance? Time to work on the GoblinAi. My goblins will, in order of priority, try to: ranged attack, throw attack, melee attack, pickup stuff, and wander if they can't do anything else. Let's get to it.
public void onUpdate(){
        if (canRangedWeaponAttack(player))
            creature.rangedWeaponAttack(player);
        else if (canThrowAt(player))
            creature.throwItem(getWeaponToThrow(), player.x, player.y, player.z);
        else if (creature.canSee(player.x, player.y, player.z))
            hunt(player);
        else if (canPickup())
            creature.pickup();
        else
            wander();
    }
You should make your goblins do things in whatever order you want; maybe you want them to pick things up first so you could slow them down by putting some rocks or something between you.

The new helper methods are:
private boolean canRangedWeaponAttack(Creature other){
        return creature.weapon() != null
                && creature.weapon().rangedAttackValue() > 0
                && creature.canSee(other.x, other.y, other.z);
    }

    private boolean canThrowAt(Creature other) {
        return creature.canSee(other.x, other.y, other.z)
          && getWeaponToThrow() != null;
    }

    private Item getWeaponToThrow() {
        Item toThrow = null;
    
        for (Item item : creature.inventory().getItems()){
        if (item == null || creature.weapon() == item || creature.armor() == item)
            continue;
        
        if (toThrow == null || item.thrownAttackValue() > toThrow.attackValue())
            toThrow = item;
        }
    
        return toThrow;
    }

    private boolean canPickup() {
        return creature.item(creature.x, creature.y, creature.z) != null
          && !creature.inventory().isFull();
    }

It's a good idea to take these helper methods, and the hunt method, and move them up to the CreatureAi class. You can use the Pull Up refactoring in Eclipse. That way they can be used by any future creatures we add.

After adding intelligent goblins things should be much more difficult. Play test many times and tweak all the values and behavior you can.


Goblins should also see if they are holding better weapon or armor and switch to it if they are.

Here's the helper methods I came up with:
protected boolean canUseBetterEquipment() {
        int currentWeaponRating = creature.weapon() == null ? 0 : creature.weapon().attackValue() + creature.weapon().rangedAttackValue();
        int currentArmorRating = creature.armor() == null ? 0 : creature.armor().defenseValue();
    
        for (Item item : creature.inventory().getItems()){
            if (item == null)
                continue;
        
            boolean isArmor = item.attackValue() + item.rangedAttackValue() < item.defenseValue();
        
            if (item.attackValue() + item.rangedAttackValue() > currentWeaponRating
                || isArmor && item.defenseValue() > currentArmorRating)
                return true;
        }
    
        return false;
    }

protected void useBetterEquipment() {
        int currentWeaponRating = creature.weapon() == null ? 0 : creature.weapon().attackValue() + creature.weapon().rangedAttackValue();
        int currentArmorRating = creature.armor() == null ? 0 : creature.armor().defenseValue();
    
        for (Item item : creature.inventory().getItems()){
            if (item == null)
                continue;
        
            boolean isArmor = item.attackValue() + item.rangedAttackValue() < item.defenseValue();
        
            if (item.attackValue() + item.rangedAttackValue() > currentWeaponRating
                || isArmor && item.defenseValue() > currentArmorRating) {
                creature.equip(item);
            }
        }
    }

And now you have goblins that will chase you when they can, attack from afar when they can, and switch to better equipment they find. That's a monster that's more intelligent than many rogulike denizens. You can make them even more intellegent by having them remember where the player is. If they can't see the player anymore then they go to that location. This way they won't lose interest in you just because you step out of view for one turn.


These goblins are tough — sometimes too tough. It would be easier if we could regenerate some health. Add this to the creature class:
private int regenHpCooldown;
    private int regenHpPer1000;
    public void modifyRegenHpPer1000(int amount) { regenHpPer1000 += amount; }

Set the regenHpPer1000 to something reasonable in the constructor. I use 10. You could also use constructor injection or call modifyRegenHpPer1000 in our factory to make some monsters regenerate very quickly.

Then create a new method to regenerateHealth and call it as part of the update method.

private void regenerateHealth(){
        regenHpCooldown -= regenHpPer1000;
        if (regenHpCooldown < 0){
            modifyHp(1);
            modifyFood(-1);
            regenHpCooldown += 1000;
        }
    }

Since we have a new stat, we can add to our level up options too.

download the code

5 comments:

  1. Great chapter again man!
    Really dig your coding and the way to learn us about the code. Thanks!

    ReplyDelete
  2. In equip(Item):
    unequip(armor);
    doAction("put on a " + item.name());
    weapon = item; <--- Shouldn't it be 'armor'?

    ReplyDelete
  3. if (item.attackValue() + item.rangedAttackValue() >= item.defenseValue())

    I think a class hierarchy for Items would be good,
    e.g. a Sword is a MeleeWeapon is a Weapon is an Item. This would simplify such 'if' conditions.

    Hopefully you understand this as constructive critisism and not as rant.
    I would like to create my own Roguelike and a find your blog very inspiring. :-)

    ReplyDelete
  4. @ubersam, I fixed the weapon/armor bug.

    A class hierarchy for Items is a common approach. One downside is that it becomes hard to make items that belong to multiple roles, like a baguette that is both food and a weapon or a "shield with writing" that is both a shield and a scroll. These are contrived examples, but I always think it's fun to see things like that. Just having on e Item class can get a little ugly but its usually easy to make rules that say you can eat anything with a non-zero foodValue, shoot anything with a non-zero rangedAttackValue, etc. I'm not sure if there is a clear "right" way to do it but each approach seems to make some things nicer and others messier. Thanks for bringing it up!

    ReplyDelete
  5. The various objects have been well placed out above and hopefully for the future would even proved to be much better for the individuals. check it

    ReplyDelete