We're going to have spells and each will have it's own effect. But when we add that effect to a creature, we want each creature to get it's own effect, otherwise weird things will happen because the spell will have an effect being applied to many creatures and the shared state (like duration) will be wonky. Instead, a spell will have an effect and when applying it to something we can create a copy and apply the copy. That way each time you cast a spell the effect will have it's own state. Add a copy constructor to the Effect class like this:
public Effect(Effect other){
this.duration = other.duration;
}
The first new class we'll need is a Spell class to tie together a spell name, cost, and effect.
package rltut;See how we return a copy of the effect instead of the original?
public class Spell {
private String name;
public String name() { return name; }
private int manaCost;
public int manaCost() { return manaCost; }
private Effect effect;
public Effect effect() { return new Effect(effect); }
public Spell(String name, int manaCost, Effect effect){
this.name = name;
this.manaCost = manaCost;
this.effect = effect;
}
}
Let's add the mana related stuff to the Creature class.
private int maxMana;
public int maxMana() { return maxMana; }
private int mana;
public int mana() { return mana; }
public void modifyMana(int amount) { mana = Math.max(0, Math.min(mana+amount, maxMana));
private int regenManaCooldown;
private int regenManaPer1000;
public void modifyRegenManaPer1000(int amount) { regenManaPer1000 += amount; }
private void regenerateMana(){
regenManaCooldown -= regenManaPer1000;
if (regenManaCooldown < 0){
if (mana < maxMana) {
modifyMana(1);
modifyFood(-1);
}
regenManaCooldown += 1000;
}
}
Don't forget to initialize regenManaPer1000 in the constructor and to call regenerateMana during the update method. We also have something else we can gain during a level-up.
public void gainMaxMana() {
maxMana += 5;
mana += 5;
doAction("look more magical");
}
public void gainRegenMana(){
regenManaPer1000 += 5;
doAction("look a little less tired");
}
And add the new options to the LevelUpController.
new LevelUpOption("Increased mana"){
public void invoke(Creature creature) { creature.gainMaxMana(); }
},new LevelUpOption("Increased mana regeneration"){
public void invoke(Creature creature) { creature.gainRegenMana(); }
}
It would also be nice to add our new stats to the displayOutput method of the PlayScreen class.
String stats = String.format(" %3d/%3d hp %d/%d mana %8s",
player.hp(), player.maxHp(), player.mana(), player.maxMana(), hunger());
Adding spell books could be done many different ways. I think we should stick with the Item class and extend it with what we need.
private List<Spell> writtenSpells;
public List<Spell> writtenSpells() { return writtenSpells; }
public void addWrittenSpell(String name, int manaCost, Effect effect){
writtenSpells.add(new Spell(name, manaCost, effect));
}
Don't forget to initialize writtenSpells in the Item constructor. This should allow us to create scrolls, spell books, notes, or even engraved items. Very simple and flexible. If we create an effect that just displays a note to the user then we could even let the player add his own non-magical engravings to items.
Let's add some spell books. I'm going to create two simple ones but you should add more spells and more books. These are just examples of what can be done. The first is a healer type book:
public Item newWhiteMagesSpellbook(int depth) {
Item item = new Item('+', AsciiPanel.brightWhite, "white mage's spellbook");
item.addWrittenSpell("minor heal", 4, new Effect(1){
public void start(Creature creature){
if (creature.hp() == creature.maxHp())
return;
creature.modifyHp(20);
creature.doAction("look healthier");
}
});
item.addWrittenSpell("major heal", 8, new Effect(1){
public void start(Creature creature){
if (creature.hp() == creature.maxHp())
return;
creature.modifyHp(50);
creature.doAction("look healthier");
}
});
item.addWrittenSpell("slow heal", 12, new Effect(50){
public void update(Creature creature){
super.update(creature);
creature.modifyHp(2);
}
});
item.addWrittenSpell("inner strength", 16, new Effect(50){
public void start(Creature creature){
creature.modifyAttackValue(2);
creature.modifyDefenseValue(2);
creature.modifyVisionRadius(1);
creature.modifyRegenHpPer1000(10);
creature.modifyRegenManaPer1000(-10);
creature.doAction("seem to glow with inner strength");
}
public void update(Creature creature){
super.update(creature);
if (Math.random() < 0.25)
creature.modifyHp(1);
}
public void end(Creature creature){
creature.modifyAttackValue(-2);
creature.modifyDefenseValue(-2);
creature.modifyVisionRadius(-1);
creature.modifyRegenHpPer1000(-10);
creature.modifyRegenManaPer1000(10);
}
});
world.addAtEmptyLocation(item, depth);
return item;
}
And the second has a hodgepodge of spells that do weird things, mostly to show what can be done within the effects:
public Item newBlueMagesSpellbook(int depth) {
Item item = new Item('+', AsciiPanel.brightBlue, "blue mage's spellbook");
item.addWrittenSpell("blood to mana", 1, new Effect(1){
public void start(Creature creature){
int amount = Math.min(creature.hp() - 1, creature.maxMana() - creature.mana());
creature.modifyHp(-amount);
creature.modifyMana(amount);
}
});
item.addWrittenSpell("blink", 6, new Effect(1){
public void start(Creature creature){
creature.doAction("fade out");
int mx = 0;
int my = 0;
do
{
mx = (int)(Math.random() * 11) - 5;
my = (int)(Math.random() * 11) - 5;
}
while (!creature.canEnter(creature.x+mx, creature.y+my, creature.z)
&& creature.canSee(creature.x+mx, creature.y+my, creature.z));
creature.moveBy(mx, my, 0);
creature.doAction("fade in");
}
});
item.addWrittenSpell("summon bats", 11, new Effect(1){
public void start(Creature creature){
for (int ox = -1; ox < 2; ox++){
for (int oy = -1; oy < 2; oy++){
int nx = creature.x + ox;
int ny = creature.y + oy;
if (ox == 0 && oy == 0
|| creature.creature(nx, ny, creature.z) != null)
continue;
Creature bat = newBat(0);
if (!bat.canEnter(nx, ny, creature.z)){
world.remove(bat);
continue;
}
bat.x = nx;
bat.y = ny;
bat.z = creature.z;
creature.summon(bat);
}
}
}
});
item.addWrittenSpell("detect creatures", 16, new Effect(75){
public void start(Creature creature){
creature.doAction("look far off into the distance");
creature.modifyDetectCreatures(1);
}
public void end(Creature creature){
creature.modifyDetectCreatures(-1);
}
});
world.addAtEmptyLocation(item, depth);
return item;
}
I had to add a couple methods to the creature class to support these.
public void summon(Creature other) {
world.add(other);
}
private int detectCreatures;
public void modifyDetectCreatures(int amount) { detectCreatures += amount; }
public void castSpell(Spell spell, int x2, int y2) {
Creature other = creature(x2, y2, z);
if (spell.manaCost() > mana){
doAction("point and mumble but nothing happens");
return;
} else if (other == null) {
doAction("point and mumble at nothing");
return;
}
other.addEffect(spell.effect());
modifyMana(-spell.manaCost());
}
And update one method:
public boolean canSee(int wx, int wy, int wz){
return (detectCreatures > 0 && world.creature(wx, wy, wz) != null
|| ai.canSee(wx, wy, wz));
}
You should also add a potion to restore mana.
Now we just need a ReadScreen to select an item to read, a ReadSpell screen to select a spell from the item, and a CastSpell screen to select a target for the spell. You should be able to figure those out but let's do them in the opposite order:
package rltut.screens;
import rltut.Creature;
import rltut.Spell;
public class CastSpellScreen extends TargetBasedScreen {
private Spell spell;
public CastSpellScreen(Creature player, String caption, int sx, int sy, Spell spell) {
super(player, caption, sx, sy);
this.spell = spell;
}
public void selectWorldCoordinate(int x, int y, int screenX, int screenY){
player.castSpell(spell, x, y);
}
}
The ReadSpellScreen is very similar to the InventoryBasedScreen.
package rltut.screens;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import rltut.Creature;
import rltut.Item;
import rltut.Spell;
import asciiPanel.AsciiPanel;
public class ReadSpellScreen implements Screen {
protected Creature player;
private String letters;
private Item item;
private int sx;
private int sy;
public ReadSpellScreen(Creature player, int sx, int sy, Item item){
this.player = player;
this.letters = "abcdefghijklmnopqrstuvwxyz";
this.item = item;
this.sx = sx;
this.sy = sy;
}
public void displayOutput(AsciiPanel terminal) {
ArrayList<String> lines = getList();
int y = 23 - lines.size();
int x = 4;
if (lines.size() > 0)
terminal.clear(' ', x, y, 20, lines.size());
for (String line : lines){
terminal.write(line, x, y++);
}
terminal.clear(' ', 0, 23, 80, 1);
terminal.write("What would you like to read?", 2, 23);
terminal.repaint();
}
private ArrayList<String> getList() {
ArrayList<String> lines = new ArrayList<String>();
for (int i = 0; i < item.writtenSpells().size(); i++){
Spell spell = item.writtenSpells().get(i);
String line = letters.charAt(i) + " - " + spell.name() + " (" + spell.manaCost() + " mana)";
lines.add(line);
}
return lines;
}
public Screen respondToUserInput(KeyEvent key) {
char c = key.getKeyChar();
Item[] items = player.inventory().getItems();
if (letters.indexOf(c) > -1
&& items.length > letters.indexOf(c)
&& items[letters.indexOf(c)] != null) {
return use(item.writtenSpells().get(letters.indexOf(c)));
} else if (key.getKeyCode() == KeyEvent.VK_ESCAPE) {
return null;
} else {
return this;
}
}
protected Screen use(Spell spell){
return new CastSpellScreen(player, "", sx, sy, spell);
}
}
And the ReadScreen is simple enough.
package rltut.screens;
import rltut.Creature;
import rltut.Item;
public class ReadScreen extends InventoryBasedScreen {
private int sx;
private int sy;
public ReadScreen(Creature player, int sx, int sy) {
super(player);
this.sx = sx;
this.sy = sy;
}
protected String getVerb() {
return "read";
}
protected boolean isAcceptable(Item item) {
return !item.writtenSpells().isEmpty();
}
protected Screen use(Item item) {
return new ReadSpellScreen(player, sx, sy, item);
}
}
Update the PlayScreen to add spell books, map the r key to the new ReadScreen, and update the HelpScreen.
I know this is already a very long tutorial, but there's one little thing that bugs me. The creature class has some special methods that are only called when gaining a level, the gain* methods. They're only called from one place and they're small enough that we can just inline them. Eclipse, and most IDEs, can do this for you, just select the method name, right click, Refactor, Inline.
(days later....) I just realized a different, and almost certainly better, way of handling effects for each spell. Instead of each spell having a reference to an Effect and using the Effect's copy constructor, the spell class should act as an Effect factory and have an abstract newEffect method. Each individual spell would subclass Spell and implement newEffect. I'll have to do that with my next roguelike.
download the code
No comments:
Post a Comment