Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Object-Oriented design - Spells

Tags:

java

oop

I work on my first Java project, which is a basic roleplaying game. Now I work on spells, and I need some OOD guidance.

I have Character, which is an abstract class. Character has some subclasses (like mage, fighter, rogue, cleric).

Mage and cleric(as for now, cleric doesn't have mana, but it might change) are both spell-casters.

I also have a Spell class, with some info (like spell name, mana cost etc). MageSpellsList and ClericSpellsList are another classes and both have lists of class Spell. and I also have Effects class(casting a spell should use it).

What would be a good object oriented design for dealing with spells (the solution shouldn't include Effects class, I can deal with that later) ?

Maybe using a "SpellCaster" interface with some methods like castSpell and showSpellbook, so Mage and Cleric will implement the interface? .

Maybe MageSpellsList and ClericSpellsList should be a subclass of Spell ? My goal is to use castSpell("spell name here") and let castSpell do the job, by using a good OOD, rather than writing a specific method for each spell (and without duplicate code between mage and Cleric)

Mage.java:

public class Mage extends Character {

    private List<Spell> spellBook;
    private int mana;
    private int CurrentMana;

    public Mage(String name) {

        super(name);

        setName(name);
        setCharacterClass("Mage");
        setLevel(1);
        setHitDice(4);

        setStrength(10);
        setConstitution(10);
        setDexterity(14);
        setIntelligence(16);
        setWisdom(14);
        setCharisma(10);

        setHp((int) (4 + getModifier(getConstitution())));
        setCurrentHp(getHp());
        setArmorClass(10 + getModifier(getDexterity()));
        setBaseAttackBonus(0);

        setMana(20 + 2 * getModifier(getIntelligence()));
        setCurrentMana(getMana());
        spellBook = new ArrayList<Spell>();

    }

    public int getMana() {
        return mana;
    }

    public int getCurrentMana() {
        return CurrentMana;
    }

    protected void setMana(int mna) {
        mana = mna;
    }

    protected void setCurrentMana(int CurrMana) {
        CurrentMana = CurrMana;
    }

    public void showSpellBook() {

        for (Iterator<Spell> iter = spellBook.iterator(); iter.hasNext(); ) {
            Spell spell = iter.next();
            System.out.println("Spell name: " + spell.getSpellName());
            System.out.println("Spell effect: " + spell.getEffect());
        }
    }

    public void addToSpellBook(String spellName) {

        Spell newSpell;
        newSpell = MageSpellsList.getSpell(spellName);
        spellBook.add(newSpell);
        System.out.println(newSpell.getSpellName() + " has been added to the spellbook");

    }


    public void chooseSpells() {
        System.out.println();
    }

    void castSpell(String spellName, Character hero, Character target) {
        try {
            Spell spell = MageSpellsList.getSpell(spellName);
            System.out.println("You casted: " + spellName);
            System.out.println("Spell effect: " + spell.getEffect());
        } catch (Exception e) {
            System.out.println("No such spell");
        }
    }
}

Spell.java:

public class Spell {
    private String name;
    private int spellLevel;
    private String effect;
    private int manaCost;
    private int duration;

    Spell(String name, int spellLevel, String effect, int manaCost, int duration) {
        this.name = name;
        this.spellLevel = spellLevel;
        this.effect = effect;
        this.manaCost = manaCost;
        this.duration= duration;
    }

    String getSpellName() { return name; }

    int getSpellLevel() { return spellLevel; }

    String getEffect() { return effect; }

    int getManaCost() {
        return manaCost;
    }

    int getDuration() { return  duration; }
}

MageSpellsList.java:

public class MageSpellsList {
    static List<Spell> MageSpellsList = new ArrayList<Spell>();

    static {
        MageSpellsList.add(new Spell("Magic Missiles", 1, "damage", 2, 0));
        MageSpellsList.add(new Spell("Magic Armor", 1, "changeStat", 2, 0));
        MageSpellsList.add(new Spell("Scorching Ray ", 2, "damage", 4, 0));
        MageSpellsList.add(new Spell("Fireball", 3, "damage", 5,0 ));
        MageSpellsList.add(new Spell("Ice Storm", 4, "damage", 8, 0));
    }

    static void  showSpellsOfLevel(int spellLevel) {
        try {
            for (Iterator<Spell> iter = MageSpellsList.iterator(); iter.hasNext(); ) {
                Spell spell = iter.next();
                if (spellLevel == spell.getSpellLevel()) {
                    System.out.println("Spell name: " + spell.getSpellName());
                    System.out.println("Spell effect: " + spell.getEffect());
                }
            }
        } catch (Exception e){
            System.out.println("Epells of level " + spellLevel + " haven't been found in spells-list");
        }
    }

    static Spell getSpell(String spellName) {
        try {
            for (Iterator<Spell> iter = MageSpellsList.iterator(); iter.hasNext(); ) {
                Spell spell = iter.next();
                if (spellName.equals(spell.getSpellName())) {
                    return spell;
                }
            }
        } catch (Exception e){
            System.out.println(spellName + " haven't been found in spells-list");
            return null;
        }
        return null;
    }
}

Effects.java:

public class Effects {

    public  void  damage(int dice, Character attacker, Character target){

        int damage = DiceRoller.roll(dice);
        System.out.println(attacker.getName() + " dealt " + damage + " damage to " + target.getName());
        target.setCurrentHp(target.getCurrentHp() - damage);
    }

    public static void damage(int n, int dice, int bonus, Character target) {

        int damage = DiceRoller.roll(n,dice,bonus);
        System.out.println("You dealt" + damage + "damage to " + target.getName());
        target.setCurrentHp(target.getCurrentHp() - damage);
    }

    public static void heal(int n, int dice, int bonus, Character target) {

        int heal = DiceRoller.roll(n,dice,bonus);
        if (heal + target.getCurrentHp() >= target.getHp()) {
            target.setCurrentHp(target.getHp());
        } else {
            target.setCurrentHp(target.getCurrentHp() + heal);
        }

        System.out.println("You healed" + heal + " hit points!");
    }

    public static void changeStat(String stat, int mod, Character target){

        System.out.println(stat + " + " + mod);

        switch (stat) {
            case "strength":
                target.setStrength(target.getStrength() + mod);
                break;
            case "constitution":
                target.setConstitution(target.getConstitution() + mod);
                break;
            case "dexterity":
                target.setDexterity(target.getDexterity() + mod);
                break;
            case "intelligence":
                target.setIntelligence(target.getIntelligence() + mod);
                break;
            case "wisdom":
                target.setWisdom(target.getWisdom() + mod);
                break;
            case "charisma":
                target.setCharisma(target.getCharisma() + mod);
                break;
            case "armorClass":
                target.setArmorClass(target.getArmorClass() + mod);
                break;
        }
    }
}
like image 804
Niminim Avatar asked Oct 31 '15 10:10

Niminim


1 Answers

Preamble

I try to generalise the classes as much as possible, so I do not end up with lots of specific classes that just represent different data, instead of a different structure. Also, I try to separate data structures from game mechanics. In particular, I try to keep the combat mechanics all in one place, instead of splitting them across different classes, and I try not to hard-code any data. In this answer, we will cover the characters, their abilities/spells, the effects of the abilities, and the combat mechanics.

Characters

Consider, for instance, a PlayableCharacter, that represents your characters. This is a standard data class. It provides methods for increasing or decreasing health and mana, and a collection of available abilities.

class PlayableCharacter {
    private final int maxHealth;
    private int health;
    private final int maxResource;    // mana, energy and so on
    private int resource;
    private final Collection<Ability> abilities;

    // getters and setters
}

Abilities

Abilities are equally data classes. They represent mana costs, triggered effects, and so on. I often represent this as a normal class, and then read the individual abilities from external data files. Here we can skip that and declare them with enumerations.

enum Ability {
    FIREBALL("Fireball", 3, 5, new Effect[] {
        new Effect(Mechanic.DAMAGE, 10, 0),
        new Effect(Mechanic.BURN, 2, 3)
    });

    private final String name;
    private final int level;
    private final int cost;
    private final List<Effect> effects;
}

Effects

Finally the effects tell what an ability does. How much damage, how long it lasts, how it affects a character. Again, this is all data, no game logic.

class Effect {
    private final Mechanic effect;
    private final int value;
    private final int duration;
}

The mechanics are just an enumeration.

enum Mechanic {
    DAMAGE, BURN;
}

Mechanics

Now it is time to make things work properly. This is the class that your game loop will be interacting with, and you must feed it the game state (which characters are battling, for instance).

class BattleEngine {
    void useAbility(PlayableCharacter source, PlayableCharacter target, Ability ability) {
        // ...
    }
}

How you implement each mechanic is up to you. It can range from an infernal switch or if/else for each Mechanic, or you can move the code to the Mechanic enum, or to private nested classes and use an EnumMap to retrieve each handler.

Example Mechanic

interface MechanicHandler {
    void apply(PlayableCharacter source, PlayableCharacter target, Effect effect);
}
class BattleEngine {
    private final Map<Mechanic, MechanicHandler> mechanics;

    void useAbility(PlayableCharacter source, PlayableCharacter target, Ability ability) {
        source.decreaseResource(ability.getCost());
        for (Effect effect: ability.getEffects()) {
            MechanicHandler mh = mechanics.get(e.getMechanic());
            mh.apply(source, target, effect);
        }
    }

    private static final class DicePerLevel implements MechanicHandler {
        @Override
        public void apply(PlayableCharacter source, PlayableCharacter target, Effect effect) {
            int levels = Math.min(effect.getValue(), source.getLevel());
            int damage = 0;
            for (int i = 0; i < levels; ++i) {
                int roll; // roll a d6 die
                damage += roll;
            }
            target.decreaseHealth(damage);
        }
    }
}
like image 62
afsantos Avatar answered Oct 05 '22 15:10

afsantos