Tuesday, October 25, 2011

roguelike tutorial 20: item appearance and identification

One of the things I enjoy the most about roguelikes is identifying items. Usually not by scroll of identify or funky unicorn tricks, but by boldly quaffing and reading — yeah, I tend to die a lot. Identification is something I haven't seen in other games and something that I think any good roguelike needs. The basic idea is that if we've identified an item, or given it a name of our own, then we see the name, otherwise we see it's appearance.

So let's start by adding an appearance to the Item class.

private String appearance;
public String appearance() {
if (appearance == null)
return name;

return appearance;
}

Then have it passed in to the constructor.

Since the appearance has to be passed in, our code is broken in all the places that create items - this is a good thing because now we can see what areas we need to change. If appearance is null then the name is used since most things look like what they are; e.g. a rock looks like a rock and a sword looks like a sword. Because of this we can pass a null appearance to all items except the potions. Before we get to the potions we need to set up some colors and text to use in our factory.

private Map<String, Color> potionColors;
private List<String> potionAppearances;

public StuffFactory(World world){
this.world = world;

setUpPotionAppearances();
}

private void setUpPotionAppearances(){
potionColors = new HashMap<String, Color>();
potionColors.put("red potion", AsciiPanel.brightRed);
potionColors.put("yellow potion", AsciiPanel.brightYellow);
potionColors.put("green potion", AsciiPanel.brightGreen);
potionColors.put("cyan potion", AsciiPanel.brightCyan);
potionColors.put("blue potion", AsciiPanel.brightBlue);
potionColors.put("magenta potion", AsciiPanel.brightMagenta);
potionColors.put("dark potion", AsciiPanel.brightBlack);
potionColors.put("grey potion", AsciiPanel.white);
potionColors.put("light potion", AsciiPanel.brightWhite);

potionAppearances = new ArrayList<String>(potionColors.keySet());
Collections.shuffle(potionAppearances);
}

This creates some text ("red potion", "green potion") and some corresponding colors (red, green), and shuffles the text so each time you create a new factory they will be in a different order. Since we create a new factory once per game, each game will have different text and colors.

Now to set the potion color, just use the new data when creating potions.
String appearance = potionAppearances.get(0);
Item item = new Item('!', potionColors.get(appearance), "health potion", appearance);
...and...
String appearance = potionAppearances.get(1);
Item item = new Item('!', potionColors.get(appearance), "mana potion", appearance);
...etc.

Not the best way but it works well enough and is easy to show.

If you play the game the potions should be random colors now.



Our CreatureAi class needs to record what items have been identified and what the names of identified or renamed items are. We could have this tracked by the items themselves or some global variables, but this way creatures can identify and converse about item appearances too. By modeling it as close to reality as possible, i.e. where creature's have their own mind and names for items, interesting possibilities and emergent behavior are more likely.

private Map<String, String> itemNames;

public String getName(Item item){
String name = itemNames.get(item.name());
return name == null ? item.appearance() : name;
}

public void setName(Item item, String name){
itemNames.put(item.name(), name);
}

And we need this to be available to the creatures.

public String nameOf(Item item){
return ai.getName(item);
}

public void learnName(Item item){
notify("The " + item.appearance() + " is a " + item.name() + "!");
ai.setName(item, item.name());
}

Wherever we use an item's name we need to use nameOf instead. The easiest way I can think of doing this is to make the name method private and see where the code breaks. Those places need to use the creature's nameOf method instead. The only places we need the real name is in the PlayScreen where we check to see if the player has the Teddy Bear, or whatever the victory object is, and in the CreatureAi getName and setName methods. Once we make all the other changes we can make the name method public and everything should compile again.

Run it and you should see that potions are now listed by color instead of the real name. I got an error when starting but that was fixed by setting up the GoblinAi before the Goblin is given a weapon and armor.


Now we need to let the player identify things when used. In the StuffFactory, for each quaffEffect make the creature learn the name of the potion if any effect happens. Here's an example:

public Item newPotionOfHealth(int depth){
String appearance = potionAppearances.get(0);
final Item item = new Item('!', potionColors.get(appearance), "health potion", appearance);
item.setQuaffEffect(new Effect(1){
public void start(Creature creature){
if (creature.hp() == creature.maxHp())
return;

creature.modifyHp(15);
creature.doAction("look healthier");
creature.learnName(item);
}
});

world.addAtEmptyLocation(item, depth);
return item;
}

The Creature's throwAttack should also let the thrower learn the name of anything that has a quaffEffect. Now when you play you can learn the identity of potions by quaffing or throwing them.



It's still possible to learn the name of an Effect that you shouldn't learn. If you kill a bat with a yellow potion you shouldn't be able to see the effect it had or identify what the potion is since the bat died from being hit by a bottle and there wasn't anything left for the effect to apply to. Let's create a version of the doAction method that handles this logic. This version of doAction will take a message and an item. Anyone who can see the creature should see the message and learn the identity of the item, unless the creature is dead, then nothing happens. We can reuse some of the code in the current doAction.

public void doAction(String message, Object ... params){
for (Creature other : getCreaturesWhoSeeMe()){
if (other == this){
other.notify("You " + message + ".", params);
} else {
other.notify(String.format("The %s %s.", name, makeSecondPerson(message)), params);
}
}
}

public void doAction(Item item, String message, Object ... params){
if (hp < 1)
return;

for (Creature other : getCreaturesWhoSeeMe()){
if (other == this){
other.notify("You " + message + ".", params);
} else {
other.notify(String.format("The %s %s.", name, makeSecondPerson(message)), params);
}
other.learnName(item);
}
}

private List<Creature> getCreaturesWhoSeeMe(){
List<Creature> others = new ArrayList<Creature>();
int r = 9;
for (int ox = -r; ox < r+1; ox++){
for (int oy = -r; oy < r+1; oy++){
if (ox*ox + oy*oy > r*r)
continue;

Creature other = world.creature(x+ox, y+oy, z);

if (other == null)
continue;

others.add(other);
}
}
return others;
}

A possibility:
The goblin quaffs a cyan potion.
The goblin looks stronger.
The cyan potion is a strength potion!

If the potions' quaffEffect use this new doAction then things will work a little better and we can remove the learnName from throwAttack since everyone who see's the creature will identify the potion based on the effect. This also means that if your creatures quaff potions everyone who watches can figure out what it is.


And that's one way of doing item identification. We had to make a lot of little changes all over the place, like using the creature's nameOf instead of the item's name method, but it wasn't difficult or error-prone. Each creature has it's own idea of what things are named so you could even let intelligent creatures tell each other the names. Maybe each goblin should quaff one unidentified potion during it's life and then discuss with others.

public void discussItemName(Item item, Creature other){
creature.doAction(item, "say \"%ss are %ss\"", item.appearance(), item.name());
other.learnName(item);
}

Or all goblins could share the same itemNames map — although that's kind of cheating.

You could also let the player give names to things. A RenameItemScreen that let's you rename a specific item or all items with a specific appearance. That way if the player deduces that red potions are healing potions then they can rename all red potions to healing potions or if they like their individual sword they can rename it Excalibur.


One last addition: add a "cause of death" string to the creature class and pass one in to modifyHp.

private String causeOfDeath;
public String causeOfDeath() { return causeOfDeath; }

public void modifyHp(int amount, String causeOfDeath) {
hp += amount;
this.causeOfDeath = causeOfDeath;

if (hp > maxHp) {
hp = maxHp;
} else if (hp < 1) {
doAction("die");
leaveCorpse();
world.remove(this);
}
}
Then update the places that modify hp. Here's an example:
public void modifyFood(int amount) { 
food += amount;

if (food > maxFood) {
maxFood = (maxFood + food) / 2;
food = maxFood;
notify("You can't belive your stomach can hold that much!");
modifyHp(-1, "Killed by overeating.");
} else if (food < 1 && isPlayer()) {
modifyHp(-1000, "Starved to death.");
}
}
Then pass the player to the WinScreen and LoseScreen and you can tell the user details about how they died, what they were carrying, conduct, etc. download the code

No comments:

Post a Comment

Popular Posts