Friday, October 7, 2011

roguelike tutorial 15: help, examine, and look screens

We've got the basics of a decent game so far. Let's take some time out to add some more screens.

We'll start with the easiest, a HelpScreen.

package rltut.screens;

import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;

public class HelpScreen implements Screen {

    public void displayOutput(AsciiPanel terminal) {
        terminal.clear();
        terminal.writeCenter("roguelike help", 1);
        terminal.write("Descend the Caves Of Slight Danger, find the lost Teddy Bear, and return to", 1, 3);
        terminal.write("the surface to win. Use what you find to avoid dying.", 1, 4);
    
        int y = 6;
        terminal.write("[g] or [,] to pick up", 2, y++);
        terminal.write("[d] to drop", 2, y++);
        terminal.write("[e] to eat", 2, y++);
        terminal.write("[w] to wear or wield", 2, y++);
        terminal.write("[?] for help", 2, y++);
        terminal.write("[x] to examine your items", 2, y++);
        terminal.write("[;] to look around", 2, y++);
    
        terminal.writeCenter("-- press any key to continue --", 22);
    }

    public Screen respondToUserInput(KeyEvent key) {
        return null;
    }
}

That's sufficient but kind of lame. I'm sure you could come up with a better story and maybe say something about what all the symbols are. Don't forget to add this new screen to the respondToUserInput method in the PlayScreen class.


Now lets make a screen that tells us details about what's in our inventory. I'll call it the ExamineScreen and map it to the x key in the PlayScreen.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class ExamineScreen extends InventoryBasedScreen {

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

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

    protected boolean isAcceptable(Item item) {
        return true;
    }

    protected Screen use(Item item) {
        String article = "aeiou".contains(item.name().subSequence(0, 1)) ? "an " : "a ";
        player.notify("It's " + article + item.name() + "." + item.details());
        return null;
    }
}

And add a details method to the Item class. You can do whatever you like but here's what I came up with:
public String details() {
    String details = "";

    if (attackValue != 0)
        details += "     attack:" + attackValue;

    if (defenseValue != 0)
        details += "     defense:" + defenseValue;

    if (foodValue != 0)
        details += "     food:" + foodValue;
    
    return details;
}

You could also display some extra description that gets passed into the item constructor.


Let's start our screen to let us look around. We'll let the user pick a tile and then tell them what it is. If you think about it, this isn't the only time the user will pick a tile through. Throwing, firing bows, and aiming spells all involve picking a tile. Since the InventoryBasedScreen has payed off so well, I think we should create a TargetBasedScreen. Let's get to it.

package rltut.screens;

import java.awt.event.KeyEvent;
import rltut.Creature;
import rltut.Line;
import rltut.Point;
import asciiPanel.AsciiPanel;

public abstract class TargetBasedScreen implements Screen {

    protected Creature player;
    protected String caption;
    private int sx;
    private int sy;
    private int x;
    private int y;

    public TargetBasedScreen(Creature player, String caption, int sx, int sy){
        this.player = player;
        this.caption = caption;
        this.sx = sx;
        this.sy = sy;
    }
}

We'll keep track of the player, a caption representing what we're targeting, the screen coordinates where the player is looking from, and the s and y offset of where we're targeting. The player and caption are protected so our subclasses can use them. Don't worry, it will make sense.

When it's time to display the output, we need to draw a line from the player to the target. I chose a line of magenta *s, but that's up to you. We also need to display the caption to the user.
public void displayOutput(AsciiPanel terminal) {
    for (Point p : new Line(sx, sy, sx + x, sy + y)){
        if (p.x < 0 || p.x >= 80 || p.y < 0 || p.y >= 24)
            continue;
        
        terminal.write('*', p.x, p.y, AsciiPanel.brightMagenta);
    }
    
    terminal.clear(' ', 0, 23, 80, 1);
    terminal.write(caption, 0, 23);
}

The user can change what's being targeted with the movement keys, select a target with Enter, or cancel with Escape. If the user tries to target something it can't, like firing out of range, then we go back to where we were targeting before.

public Screen respondToUserInput(KeyEvent key) {
        int px = x;
        int py = y;

        switch (key.getKeyCode()){
        case KeyEvent.VK_LEFT:
        case KeyEvent.VK_H: x--; break;
        case KeyEvent.VK_RIGHT:
        case KeyEvent.VK_L: x++; break;
        case KeyEvent.VK_UP:
        case KeyEvent.VK_J: y--; break;
        case KeyEvent.VK_DOWN:
        case KeyEvent.VK_K: y++; break;
        case KeyEvent.VK_Y: x--; y--; break;
        case KeyEvent.VK_U: x++; y--; break;
        case KeyEvent.VK_B: x--; y++; break;
        case KeyEvent.VK_N: x++; y++; break;
        case KeyEvent.VK_ENTER: selectWorldCoordinate(player.x + x, player.y + y, sx + x, sy + y); return null;
        case KeyEvent.VK_ESCAPE: return null;
        }
    
        if (!isAcceptable(player.x + x, player.y + y)){
            x = px;
            y = py;
        }
    
        enterWorldCoordinate(player.x + x, player.y + y, sx + x, sy + y);
    
        return this;
    }

We'll provide a simple method to determine if a tile is an acceptable target. Subclasses can override this if they want something more specific.

public boolean isAcceptable(int x, int y) {
        return true;
    }

After each time the target moves, we let subclasses do whatever they want, usually this will be to update the caption or do nothing.
public void enterWorldCoordinate(int x, int y, int screenX, int screenY) {
    }

And we do the same once the user has selected a specific location.

public void selectWorldCoordinate(int x, int y, int screenX, int screenY){
    }

This should provide a good base for any kind of targeting action.


The simplest targeting action is looking at surroundings; a LookScreen.

package rltut.screens;

import rltut.Creature;
import rltut.Item;
import rltut.Tile;

public class LookScreen extends TargetBasedScreen {

    public LookScreen(Creature player, String caption, int sx, int sy) {
        super(player, caption, sx, sy);
    }

    public void enterWorldCoordinate(int x, int y, int screenX, int screenY) {
        Creature creature = player.creature(x, y, player.z);
        if (creature != null){
            caption = creature.glyph() + " "     + creature.name() + creature.details();
            return;
        }
    
        Item item = player.item(x, y, player.z);
        if (item != null){
            caption = item.glyph() + " "     + item.name() + item.details();
            return;
        }
    
        Tile tile = player.tile(x, y, player.z);
        caption = tile.glyph() + " " + tile.details();
    }
}

This will display details to the user about whatever they are targeting. If you use this code then you'll need to create a few methods to get details about creatures and tiles. Don't forget to map it to the ';' key, or whatever key you want, in the PlayScreen.


Add a details method to the Creature class.
public String details() {
        return String.format("     level:%d     attack:%d     defense:%d     hp:%d", level, attackValue(), defenseValue(), hp);
    }

And add details to the Tile class.

FLOOR((char)250, AsciiPanel.yellow, "A dirt and rock cave floor."),
    WALL((char)177, AsciiPanel.yellow, "A dirt and rock cave wall."),
    BOUNDS('x', AsciiPanel.brightBlack, "Beyond the edge of the world."),
    STAIRS_DOWN('>', AsciiPanel.white, "A stone staircase that goes down."),
    STAIRS_UP('<', AsciiPanel.white, "A stone staircase that goes up."),
    UNKNOWN(' ', AsciiPanel.white, "(unknown)");

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

    Tile(char glyph, Color color, String details){
        this.glyph = glyph;
        this.color = color;
        this. details = details;
    }


You may have noticed a problem with the LookScreen in that you can get details about things the player can't see. Let's fix it by making some small changes to the Creature class. Basically, only return what the creature can see or remember.

public Tile realTile(int wx, int wy, int wz) {
        return world.tile(wx, wy, wz);
    }

public Tile tile(int wx, int wy, int wz) {
        if (canSee(wx, wy, wz))
            return world.tile(wx, wy, wz);
        else
            return ai.rememberedTile(wx, wy, wz);
    }

public Creature creature(int wx, int wy, int wz) {
        if (canSee(wx, wy, wz))
            return world.creature(wx, wy, wz);
        else
            return null;
    }

public Item item(int wx, int wy, int wz) {
        if (canSee(wx, wy, wz))
            return world.item(wx, wy, wz);
        else
            return null;
    }

The CreatureAi canSee method needs to use the new realTile method to avoid getting caught in an infinite recursion loop.

Here's a good-enough-for-now implementation of the CreatureAi rememberedTile method since they don't actually have a memory:
public Tile rememberedTile(int wx, int wy, int wz) {
        return Tile.UNKNOWN;
    }

And the PlayerAi override:

public Tile rememberedTile(int wx, int wy, int wz) {
        return fov.tile(wx, wy, wz);
    }

Now we've got a help screen, a way to see details about what's in our inventory, and our surroundings. Nothing very glamorous or game changing this time but it's all very helpful for the user. The TargetBasedScreen should also make future screens easier.

download the code

4 comments:

  1. thanks for doing this!
    Great reading so far!

    Ps. Still get flickering in the screen while playing :(

    ReplyDelete
  2. The asciipanel.jar file in the zip file in my blog is not the most current one. Can you try replacing that with the most recent one?

    https://github.com/downloads/trystan/AsciiPanel/asciiPanel.jar

    That should help. If not, let me know.

    ReplyDelete
  3. Won't it throw a out of bounds exception when we try to view "outside"(like x being -1) the world?

    ReplyDelete
  4. The tutorial has been mentioned with better techniques and programs and these would further help them to proceed further with all those instances for the better cause and success.

    ReplyDelete