Tuesday, October 18, 2011

roguelike tutorial 18: potions and effects

Let's add some potions now. When the player, or smart monster, quaffs a potion it's effect will be applied to the creature. What can we say about effects? Most effects will only last for a certain duration. Some will apply every turn or every few turns (like poison or a slow heal). Others will have a change that lasts for the duration (like confuse or resist cold). This can be done with a start method that that applies the change when first quaffed, an end method that unapplies the change when the duration has run out, and an update method that is called every turn in between. I think this will cover the basics so let's get started on them. We'll create a base Effect class and subclass it with specific effects.

package rltut;

public class Effect {
        protected int duration;
        
        public boolean isDone() { return duration < 1; }
        
        public Effect(int duration){
                this.duration = duration;
        }
        
        public void update(Creature creature){
                duration--;
        }
        
        public void start(Creature creature){
                
        }
        
        public void end(Creature creature){
                
        }
}

And add a quaffEffect to Item class.

private Effect quaffEffect;
public Effect quaffEffect() { return quaffEffect; }
public void setQuaffEffect(Effect effect) { this.quaffEffect = effect; }

Now that we've got items that can have effects when quaffed it's time to add some potions to our StuffFactory. I'll start with three simple ones that show three ways of doing things.

Here's a simple one-time potion where the work happens in the start method:
public Item newPotionOfHealth(int depth){
    Item item = new Item('!', AsciiPanel.white, "health potion");
    item.setQuaffEffect(new Effect(1){
        public void start(Creature creature){
            if (creature.hp() == creature.maxHp())
                return;
                                
            creature.modifyHp(15);
            creature.doAction("look healthier");
        }
    });
                
    world.addAtEmptyLocation(item, depth);
    return item;
}

Here is a potion that affects the creature each turn.
public Item newPotionOfPoison(int depth){
    Item item = new Item('!', AsciiPanel.white, "poison potion");
    item.setQuaffEffect(new Effect(20){
        public void start(Creature creature){
            creature.doAction("look sick");
        }
                        
        public void update(Creature creature){
            super.update(creature);
            creature.modifyHp(-1);
        }
    });
                
    world.addAtEmptyLocation(item, depth);
    return item;
}

Here's one that will affect the creature at the start and restore it at the end.
public Item newPotionOfWarrior(int depth){
    Item item = new Item('!', AsciiPanel.white, "warrior's potion");
    item.setQuaffEffect(new Effect(20){
        public void start(Creature creature){
            creature.modifyAttackValue(5);
            creature.modifyDefenseValue(5);
            creature.doAction("look stronger");
        }
        public void end(Creature creature){
            creature.modifyAttackValue(-5);
            creature.modifyDefenseValue(-5);
            creature.doAction("look less strong");
        }
    });
                
    world.addAtEmptyLocation(item, depth);
    return item;
}

And add a randomizer to help us add the new potions.

public Item randomPotion(int depth){
                switch ((int)(Math.random() * 3)){
                case 0: return newPotionOfHealth(depth);
                case 1: return newPotionOfPoison(depth);
                default: return newPotionOfWarrior(depth);
                }
        }

Now let's go back to the creature class and make sure it's calling the right methods at the right times. It will need a list of effects that are currently applied to it.

private List<Effect> effects;
public ListList<Effect> effects(){ return effects; }

Don't forget to initialize it in the creature's constructor.

Add quaff method to Creature class. Since they're so similar, the quaff and eat methods can share some code.
public void quaff(Item item){
            doAction("quaff a " + item.name());
            consume(item);
        }

        public void eat(Item item){
            doAction("eat a " + item.name());
            consume(item);
        }
        
        private void consume(Item item){
            if (item.foodValue() < 0)
                notify("Gross!");
                
            addEffect(item.quaffEffect());
                
            modifyFood(item.foodValue());
            getRidOf(item);
        }
        
        private void addEffect(Effect effect){
            if (effect == null)
                return;
                
            effect.start(this);
            effects.add(effect);
        }

Create a new method to update the effects each turn and remove any that are done. Don't forget to call the end method before removing the effect.

private void updateEffects(){
            List<Effect> done = new ArrayList<Effect>();
                
            for (Effect effect : effects){
                effect.update(this);
                if (effect.isDone()) {
                    effect.end(this);
                    done.add(effect);
                }
            }
                
            effects.removeAll(done);
        }

Call this during the creature's update method. The player can't quaff anything until we create a QuaffScreen to go with our new behavior.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class QuaffScreen extends InventoryBasedScreen {

        public QuaffScreen(Creature player) {
                super(player);
        }

        protected String getVerb() {
                return "quaff";
        }

        protected boolean isAcceptable(Item item) {
                return item.quaffEffect() != null;
        }

        protected Screen use(Item item) {
                player.quaff(item);
                return null;
        }
}

I think I've said this before but that InventoryBasedScreen is really paying off. Now bind that to the 'q' key in the PlayScreen class and update the createItems method, also in the PlayScreen class, to add some potions. Try 3 or 4 per level to start with. Don't forget to update the HelpScreen too. Play around and change the durations or strengths or abundance of potions. Try adding some new ones that change the vision radius, heal over time, stop regeneration, consume extra food, or fill someone's stomach. I'm sure you can think of even more potions and effects. There you go; potions and effects that are simple and flexible. We're going to add a few more effects in the next tutorial when we add magic.



Wouldn't it be cool if throwing a potion at a creature caused the effect to apply to it? Easy-peasy.

private void throwAttack(Item item, Creature other) {
    commonAttack(other, attackValue / 2 + item.thrownAttackValue(), "throw a %s at the %s for %d damage", item.name(), other.name);
    other.addEffect(item.quaffEffect());
}

Then make sure the throw method removes the item if it has a quaffEffect and the target was a creature, otherwise it should add to the world like it already does. Now you can sit back and chuck poison bottles at goblins.

download the code

2 comments:

  1. We are of the firm view and the opinion regarding all those prospects and hopefully for he future these would govern better grounds to follow herewith. computer science research paper

    ReplyDelete
  2. Minor typo "public ListList effects(){ ..." should be "public List effects(){ ...". I really like the apperance of the anonymous subclass "technique" but it took me a while to parse it as I hadn't seen it yet in my squeaky new Java career.

    ReplyDelete