public void attack(Creature other){There's many many different ways to handle how much damage is done but I'll stick with something simple: the damage amount is a random number from 1 to the attackers attack value minus the defenders defense value. It's easy to code, easy to understand, and using only two variables worked fine for Catlevania: Symphony Of The Night. The IDE tells us what else we need to add to our creature class to support this.
int amount = Math.max(0, attackValue() - other.defenseValue());
amount = (int)(Math.random() * amount) + 1;
other.modifyHp(-amount);
}
public void modifyHp(int amount) {
hp += amount;
if (hp < 1)
world.remove(this);
}
private int maxHp;
public int maxHp() { return maxHp; }
private int hp;
public int hp() { return hp; }
private int attackValue;
public int attackValue() { return attackValue; }
private int defenseValue;
public int defenseValue() { return defenseValue; }
We can rely on constructor injection to set the values.
public Creature(World world, char glyph, Color color, int maxHp, int attack, int defense){
this.world = world;
this.glyph = glyph;
this.color = color;
this.maxHp = maxHp;
this.hp = maxHp;
this.attackValue = attack;
this.defenseValue = defense;
}
Then update our CreatureFactory.
public Creature newPlayer(){
Creature player = new Creature(world, '@', AsciiPanel.brightWhite, 100, 20, 5);
world.addAtEmptyLocation(player);
new PlayerAi(player);
return player;
}
public Creature newFungus(){
Creature fungus = new Creature(world, 'f', AsciiPanel.green, 10, 0, 0);
world.addAtEmptyLocation(fungus);
new FungusAi(fungus, this);
return fungus;
}
As always, play around with the numbers and find something you like.
Now that we have some stats, let's display them on the PlayScreen. Here's what I added to the end of displayOutput:
String stats = String.format(" %3d/%3d hp", player.hp(), player.maxHp());
terminal.write(stats, 1, 23);
That went well. Adding new functionality was kept to a few small and isolated changes. The earlier decision to move all the creature creation to a factory also reduced the number of places we had to update. Now go fight some fungi!
Since this post is so short let's add another feature: messages. There are so many ways to do this but we're going to think of a way that avoids globals. A global message queue seems like the easiest way to do messaging - and it probably is - but sometimes it's a good idea to stick to a guideline like "don't use global variables" to see if it works in all scenarios and find where the guideline works against you and should be abandoned. I've found that small roguelikes are a perfect place for this kind of experimentation.
Since we're not using a global message queue to hold messages, where should we put the messages? Messages are just extra text for the GUI so maybe they should be part of the PlayScreen? But they're created by things in the world and everything has access to the world so maybe there? That doesn't seem right though. Messages are meant for the player so maybe the PlayerAi should be the receiver of the messages? That kind of make sense because it already gets called by the creature class and creatures are probably going to be the source of most messages. We can pass messages to the ai and any non-player ai can just ignore the messages.
Add a notify method to the creature class. To make it easier for the callers to build messages you can take the string and any parameters the caller passes and format the string for them. A nice convenience.
public void notify(String message, Object ... params){
ai.onNotify(String.format(message, params));
}
Which means you need the corresponding empty method for the CreatureAi class. We'll make the PlayerAi class add the messages to a list. Other CreatureAi's will just ignore it.
package rltut;
import java.util.List;
public class PlayerAi extends CreatureAi {
private List<String> messages;
public PlayerAi(Creature creature, List<String> messages) {
super(creature);
this.messages = messages;
}
public void onEnter(int x, int y, Tile tile){
if (tile.isGround()){
creature.x = x;
creature.y = y;
} else if (tile.isDiggable()) {
creature.dig(x, y);
}
}
public void onNotify(String message){
messages.add(message);
}
}
Instead of creating a getter for the message list we rely on constructor injection. That means the list comes from somewhere else that may already have a reference to it. We can create the list in the PlayScreen and pass it to the creature factory which passes it to the new PlayerAi. Since the PlayScreen already has the list, it can easily display any messages that show up and clear the list afterwards.
Here's the update to the CreatureFactory:
public Creature newPlayer(List<String> messages){
Creature player = new Creature(world, '@', AsciiPanel.brightWhite, 100, 20, 5);
world.addAtEmptyLocation(player);
new PlayerAi(player, messages);
return player;
}
Here's the update to the PlayScreen:
private List<String> messages;
public PlayScreen(){
screenWidth = 80;
screenHeight = 21;
messages = new ArrayList<String>();
createWorld();
CreatureFactory creatureFactory = new CreatureFactory(world);
createCreatures(creatureFactory);
}
private void createCreatures(CreatureFactory creatureFactory){Displaying messages can also be done many different ways. If you haven't guessed by now I like to start simple. The simplest way I can think of is to just list them all on the screen at once.
player = creatureFactory.newPlayer(messages);
for (int i = 0; i < 8; i++){
creatureFactory.newFungus();
}
}
private void displayMessages(AsciiPanel terminal, List<String> messages) {Just call that from the displayOutput method. Before clearing the message list, the messages could be copied into a separate list (or list of lists) so the history is preserved.
int top = screenHeight - messages.size();
for (int i = 0; i < messages.size(); i++){
terminal.writeCenter(messages.get(i), top + i);
}
messages.clear();
}
All we need now is some actual messages. Go ahead and notify the creature wherever it does something interesting or has something happen to it. Here's a sample of what I added to the attack method:
notify("You attack the '%s' for %d damage.", other.glyph, amount);
other.notify("The '%s' attacks you for %d damage.", glyph, amount);
And now the player can receive notices of what's going on.
What about notifying nearby creatures when something happens? Here's one way to do that:
public void doAction(String message, Object ... params){The method makeSecondPerson does a small bit of string manipulation to make it grammatically correct. It assumes the first word is the verb, but that's easy enough to do as long as you don't plan on supporting other languages. It's best to avoid implicit rules like this since the only way to know about it is to already know it or watch it fail when you don't follow the implicit rule. It feels dirty to have gramer rules in with the Creature code so remember to move it somewhere better.
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);
if (other == null)
continue;
if (other == this)
other.notify("You " + message + ".", params);
else
other.notify(String.format("The '%s' %s.", glyph, makeSecondPerson(message)), params);
}
}
}
private String makeSecondPerson(String text){
String[] words = text.split(" ");
words[0] = words[0] + "s";
StringBuilder builder = new StringBuilder();
for (String word : words){
builder.append(" ");
builder.append(word);
}
return builder.toString().trim();
}
Then you can call doAction in your creature code and anyone nearby will be notified. Here's some examples:
when the FungusAi spawns a child:
creature.doAction("spawn a child");while attacking:
doAction("attack the '%s' for %d damage", other.glyph, amount);or when dying:
public void modifyHp(int amount) {
hp += amount;
if (hp < 1) {
doAction("die");
world.remove(this);
}
}
Now when playing you can get messages about all these details going on in the world. The more you add the more detailed and deep the world will be.
download the code
No comments:
Post a Comment