fr.ritaly.dungeonmaster.ai.CreatureDef.java Source code

Java tutorial

Introduction

Here is the source code for fr.ritaly.dungeonmaster.ai.CreatureDef.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package fr.ritaly.dungeonmaster.ai;

import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javanet.staxutils.IndentingXMLStreamWriter;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamWriter;

import org.apache.commons.lang.Validate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import fr.ritaly.dungeonmaster.Utils;
import fr.ritaly.dungeonmaster.ai.Creature.Height;
import fr.ritaly.dungeonmaster.ai.Creature.Size;
import fr.ritaly.dungeonmaster.champion.Champion;
import fr.ritaly.dungeonmaster.item.Item;
import fr.ritaly.dungeonmaster.item.ItemFactory;
import fr.ritaly.dungeonmaster.magic.PowerRune;
import fr.ritaly.dungeonmaster.magic.Spell;

/**
 * A definition of creature.
 *
 * @author <a href="mailto:francois.ritaly@gmail.com">Francois RITALY</a>
 */
final class CreatureDef {

    /**
     * SAX handler to parse the resource file 'creatures.xml' defining items.
     *
     * @author <a href="mailto:francois.ritaly@gmail.com">Francois RITALY</a>
     */
    private static final class CreatureDefParser extends DefaultHandler {

        private final Log log = LogFactory.getLog(this.getClass());

        private final List<CreatureDef> definitions = new ArrayList<CreatureDef>();

        private CreatureDef definition;

        private CreatureDefParser() {
        }

        @Override
        public void startDocument() throws SAXException {
            this.definitions.clear();

            if (log.isDebugEnabled()) {
                log.debug("Parsing creature definitions ...");
            }
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                throws SAXException {
            final String elementName = qName;

            if ("creatures".equals(elementName) || "weaknesses".equals(elementName) || "spells".equals(elementName)
                    || "items".equals(elementName)) {
                // Do nothing
            } else if ("creature".equals(elementName)) {
                this.definition = new CreatureDef();
                this.definition.id = attributes.getValue("id");
                this.definition.baseHealth = Integer.valueOf(attributes.getValue("base-health"));
                this.definition.height = Height.valueOf(attributes.getValue("height"));
                this.definition.size = Size.valueOf(attributes.getValue("size"));
                this.definition.awareness = Integer.valueOf(attributes.getValue("awareness"));
                this.definition.bravery = Integer.valueOf(attributes.getValue("bravery"));
                this.definition.experienceMultiplier = Integer
                        .valueOf(attributes.getValue("experience-multiplier"));
                this.definition.moveDuration = Integer.valueOf(attributes.getValue("move-duration"));
                this.definition.sightRange = Integer.valueOf(attributes.getValue("sight-range"));
                this.definition.absorbsItems = Boolean.valueOf(attributes.getValue("absorbs-items"));
                this.definition.levitates = Boolean.valueOf(attributes.getValue("levitates"));
                this.definition.archenemy = Boolean.valueOf(attributes.getValue("archenemy"));
                this.definition.nightVision = Boolean.valueOf(attributes.getValue("night-vision"));
                this.definition.seesInvisible = Boolean.valueOf(attributes.getValue("sees-invisible"));
            } else if ("defense".equals(elementName)) {
                this.definition.antiMagic = Integer.valueOf(attributes.getValue("anti-magic"));
                this.definition.armor = Integer.valueOf(attributes.getValue("armor"));
                this.definition.shield = Integer.valueOf(attributes.getValue("shield"));
                this.definition.poisonResistance = Integer.parseInt(attributes.getValue("poison"));
            } else if ("attack".equals(elementName)) {
                this.definition.attackSkill = Champion.Level.valueOf(attributes.getValue("skill"));
                this.definition.attackAnimationDuration = Integer
                        .parseInt(attributes.getValue("animation-duration"));
                this.definition.attackDuration = Integer.parseInt(attributes.getValue("duration"));
                this.definition.attackPower = Integer.parseInt(attributes.getValue("power"));
                this.definition.attackType = AttackType.valueOf(attributes.getValue("type"));
                this.definition.attackRange = Integer.parseInt(attributes.getValue("range"));
                this.definition.attackProbability = Integer.parseInt(attributes.getValue("probability"));
                this.definition.sideAttack = Boolean.valueOf(attributes.getValue("side-attack"));
            } else if ("poison".equals(elementName)) {
                if (attributes.getValue("strength") != null) {
                    this.definition.poison = Integer.parseInt(attributes.getValue("strength"));
                }
            } else if ("spell".equals(elementName)) {
                this.definition.spells.add(Spell.Type.valueOf(attributes.getValue("id")));
            } else if ("weakness".equals(elementName)) {
                this.definition.weaknesses.add(Weakness.valueOf(attributes.getValue("id")));
            } else if ("item".equals(elementName)) {
                final ItemDef def = new ItemDef();
                def.type = Item.Type.valueOf(attributes.getValue("type"));
                def.min = Integer.parseInt(attributes.getValue("min"));
                def.max = Integer.parseInt(attributes.getValue("max"));

                if (attributes.getValue("curse") != null) {
                    def.curse = PowerRune.valueOf(attributes.getValue("curse"));
                }

                this.definition.itemDefs.add(def);
            } else {
                throw new SAXException(String.format("Unexpected element name '%s'", elementName));
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            final String elementName = qName;

            if ("creatures".equals(elementName) || "weaknesses".equals(elementName) || "spells".equals(elementName)
                    || "items".equals(elementName)) {
                // Do nothing
            } else if ("creature".equals(elementName)) {
                this.definitions.add(definition);

                this.definition = null;
            } else if ("defense".equals(elementName)) {
                // Do nothing
            } else if ("attack".equals(elementName)) {
                // Do nothing
            } else if ("poison".equals(elementName)) {
                // Do nothing
            } else if ("weakness".equals(elementName)) {
                // Do nothing
            } else if ("spell".equals(elementName)) {
                // Do nothing
            } else if ("item".equals(elementName)) {
                // Do nothing
            } else {
                throw new SAXException(String.format("Unexpected element name '%s'", elementName));
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
        }

        @Override
        public void endDocument() throws SAXException {
            if (log.isInfoEnabled()) {
                log.info(String.format("Parsed %d creature definitions", definitions.size()));
            }
        }
    }

    public static final class ItemDef {

        private Item.Type type;

        private int min;

        private int max;

        private PowerRune curse;

        private ItemDef() {
        }

        public List<Item> getItems() {
            final List<Item> list = new ArrayList<Item>();

            for (int i = 0; i < Utils.random(min, max); i++) {
                final Item item = ItemFactory.getFactory().newItem(type);

                if (curse != null) {
                    item.curse(curse);
                }

                list.add(item);
            }

            return list;
        }
    }

    private final static Map<String, CreatureDef> DEFINITIONS = new LinkedHashMap<String, CreatureDef>();

    static {
        try {
            // Parse the definitions of creatures from resource file "creatures.xml"
            final InputStream stream = CreatureDef.class.getResourceAsStream("creatures.xml");

            final CreatureDefParser parser = new CreatureDefParser();

            SAXParserFactory.newInstance().newSAXParser().parse(stream, parser);

            for (CreatureDef definition : parser.definitions) {
                DEFINITIONS.put(definition.id, definition);
            }
        } catch (Exception e) {
            throw new RuntimeException("Error when parsing creature definitions", e);
        }
    }

    private String id;

    /**
     * These two bits define the height of the creature. It is used to check if
     * missiles can fly over the creatures (for example Fireballs can fly over
     * small creatures). This value is also used to define how to animate a door
     * that is closed upon the creature:
     * <ul>
     * <li>'0': the door is not animated and stays fully open. The creature
     * still takes damage.</li>
     * <li>'1': the door is animated from the top to 1/4th of its size. This
     * applies to tall creatures like Mummies.</li>
     * <li>'2': the door is animated between 1/4th of its size to half of its
     * size. This applies to medium sized creatures like Screamers.</li>
     * <li>'3': the door is animated from half of its size to 3/4th of its size.
     * This applies to small creatures like the Worm.</li>
     * Note: This value is ignored for non material creatures and the door
     * always closes normally without causing any damage to such creatures.
     */
    private Height height;

    private Creature.Size size;

    /**
     * The base health is used to calculate the health of creatures
     * generated during the game.
     */
    private int baseHealth;

    /**
     * Maximum number of tiles between a creature and party needed to detect and
     * "turn" towards the party, perhaps to shoot a projectile. This applies
     * even if the creature is not facing the party. Value within [0,15] (aka
     * "Detection range").
     */
    private int awareness;

    /**
     * Resistance to War Cry, Calm, Brandish and Blow Horn (maybe also
     * Confuse). Value within [0,15]. The special value 15 means the
     * creature can't be frightened.
     */
    private int bravery;

    /**
     * This value is used as a multiplier to compute the experience earned
     * by a champion killing this creature
     */
    private int experienceMultiplier;

    /**
     * Defines how long it takes for the creature to move from one position
     * to another (in number of clock ticks). Value within [0,255]. The
     * special value 255 means that the creature can't move.
     */
    private int moveDuration;

    /**
     * Maximum number of tiles between creature and party needed to see the
     * party. This applies only if the creature is facing the party. This value
     * is affected by the current light level in the dungeon (the value is
     * halved for each level of darkness). Value within [0,15].
     */
    private int sightRange;

    /**
     * When this bit is set to '1', the creature can absorb some items when they
     * are thrown at the creature (like the Mummy). The list of items that can
     * be absorbed is hard coded in the program (Arrow, Slayer, Poison Dart,
     * Throwing Star and Dagger). If a thrown item is not absorbed by the
     * creature, it falls on the floor (it is never destroyed). This is not
     * linked to the ability of the Giggler to steal items in champion hands
     * which is hard coded. See
     * http://www.gamefaqs.com/snes/588299-dungeon-master/faqs/33244
     */
    private boolean absorbsItems;

    /**
     * cf http://www.gamefaqs.com/snes/588299-dungeon-master/faqs/33244. If this
     * bit is set to '1', the creature can pass over pits without falling
     */
    private boolean levitates;

    /**
     * When this bit is set, the creature never takes any damage (health is not
     * decreased), it can teleport up to two tiles away and it cannot move to a
     * tile containing a flux cage.
     */
    private boolean archenemy;

    /**
     * When this bit is set to '1', the creature can see the party in darkness
     * because it ignores the sight range reduction caused by low light levels
     * in the dungeon.
     */
    private boolean nightVision;

    /**
     * When this bit is set to '1', the creature can see the party even if it is
     * under the effect of the 'Invisibility' spell.
     */
    private boolean seesInvisible;

    /**
     * Resistance to magical spells like Fireball. Value within [0,15]. The
     * special value 15 means the creature is immune to magic attacks. aka
     * "Fire resistance".
     */
    private int antiMagic;

    /**
     * This is the resistance to damage including Dispell on non material
     * creatures. The armor value is always within range [0,255]. The special
     * value 255 means that the creature is invincible.
     */
    private int armor;

    /**
     * Value within [0,255]. The special value 255 means the creature is
     * untouchable. This value represents the difficulty for champions to
     * hit the creature
     */
    private int shield;

    private Champion.Level attackSkill;

    /**
     * The number of clock ticks during which the animation of an attacking
     * creature should be displayed. Value within [0,255].
     */
    private int attackAnimationDuration;

    /**
     * This is the number of clock ticks per attack, defining the attack
     * speed of the creature. This is the minimum amount of time required
     * between two attacks. Value within [0,255].
     */
    private int attackDuration;

    /**
     * The base value for computing how much damage a creature's attack will
     * inflict.
     */
    private int attackPower;

    /**
     * This "number" is used to determine what kind of attack the creature
     * executes. Changing this value will result in a different "protection"
     * to be used when calculating the damage:
     *
     * 1: Use Anti-Fire to determine damage 2: Half the hero's armor and do
     * physical damage 3: Unknown 4: Deal physical piercing damage 5: Use
     * Anti-Magic to determine damage 6: Use Wisdom to determine damage
     */
    private AttackType attackType;

    /**
     * Maximum number of tiles between creature and party needed to perform a
     * distance attack (cast a spell). Valeur dans l'intervalle [0-15]. aka
     * "Spell casting range".
     */
    private int attackRange;

    /**
     * The odds of hitting a {@link Champion}. Value within [0,255].
     */
    private int attackProbability;

    /**
     * The amount of poison inflicted when the creature successfully hits a
     * character.Value within [0,255].
     */
    private int poison;

    /**
     * Resistance to magical spells involving poison. Value within [0,15].
     * The special value 15 means the creature is immune to poison attacks.
     */
    private int poisonResistance;

    /**
     * The creature does not need to face the party to attack. This flag is set
     * only for creatures that have the same image for all sides. It affects
     * their attack frequency because they don't need to turn to face the party
     * before attacking.
     */
    private boolean sideAttack;

    private final Set<Weakness> weaknesses = new TreeSet<Weakness>();

    private final Set<Spell.Type> spells = new TreeSet<Spell.Type>();

    private final List<ItemDef> itemDefs = new ArrayList<CreatureDef.ItemDef>();

    CreatureDef() {
    }

    public boolean isSeesInvisible() {
        return seesInvisible;
    }

    public boolean isNightVision() {
        return nightVision;
    }

    public boolean isArchenemy() {
        return archenemy;
    }

    public boolean isLevitates() {
        return levitates;
    }

    public boolean isAbsorbsItems() {
        return absorbsItems;
    }

    public boolean isSideAttack() {
        return sideAttack;
    }

    public int getAttackProbability() {
        return attackProbability;
    }

    public int getBaseHealth() {
        return baseHealth;
    }

    public int getAntiMagic() {
        return antiMagic;
    }

    public int getArmor() {
        return armor;
    }

    public int getAttackAnimationDuration() {
        return attackAnimationDuration;
    }

    public int getAttackDuration() {
        return attackDuration;
    }

    public int getAttackPower() {
        return attackPower;
    }

    public int getAttackRange() {
        return attackRange;
    }

    public AttackType getAttackType() {
        return attackType;
    }

    public int getAwareness() {
        return awareness;
    }

    public int getBravery() {
        return bravery;
    }

    public int getExperienceMultiplier() {
        return experienceMultiplier;
    }

    public Height getHeight() {
        return height;
    }

    public String getId() {
        return id;
    }

    public int getMoveDuration() {
        return moveDuration;
    }

    public int getPoison() {
        return poison;
    }

    public int getPoisonResistance() {
        return poisonResistance;
    }

    public int getShield() {
        return shield;
    }

    public int getSightRange() {
        return sightRange;
    }

    public Creature.Size getSize() {
        return size;
    }

    public Champion.Level getAttackSkill() {
        return attackSkill;
    }

    public Set<Spell.Type> getSpells() {
        return Collections.unmodifiableSet(spells);
    }

    public Set<Weakness> getWeaknesses() {
        return Collections.unmodifiableSet(weaknesses);
    }

    public List<ItemDef> getItemDefs() {
        return Collections.unmodifiableList(itemDefs);
    }

    public static Map<String, CreatureDef> getDefinitions() {
        return DEFINITIONS;
    }

    public static List<CreatureDef> getAllDefinitions() {
        return new ArrayList<CreatureDef>(DEFINITIONS.values());
    }

    public static CreatureDef getDefinition(Creature.Type type) {
        Validate.notNull(type, "The given creature type is null");

        return DEFINITIONS.get(type.name());
    }

    /**
     * Generates and returns a list of items corresponding to the items
     * dropped by the creature when killed.
     *
     * @return a list of items. Never returns null.
     */
    public List<Item> getItems() {
        if (itemDefs.isEmpty()) {
            return Collections.emptyList();
        }

        final List<Item> list = new ArrayList<Item>();

        for (ItemDef def : itemDefs) {
            list.addAll(def.getItems());
        }

        return list;
    }

    public static void main(String[] args) throws Exception {
        final List<Creature.Type> types = Arrays.asList(Creature.Type.values());

        Collections.sort(types, new Comparator<Creature.Type>() {
            @Override
            public int compare(Creature.Type o1, Creature.Type o2) {
                return o1.name().compareTo(o2.name());
            }
        });

        final StringWriter stringWriter = new StringWriter(32000);

        final XMLStreamWriter writer = new IndentingXMLStreamWriter(
                XMLOutputFactory.newFactory().createXMLStreamWriter(stringWriter));

        writer.writeStartDocument();
        writer.writeStartElement("creatures");
        writer.writeDefaultNamespace("yadmjc:creatures:1.0");

        for (Creature.Type type : types) {
            writer.writeStartElement("creature");
            writer.writeAttribute("id", type.name());
            writer.writeAttribute("base-health", Integer.toString(type.getBaseHealth()));
            writer.writeAttribute("height", type.getHeight().name());
            writer.writeAttribute("size", type.getSize().name());
            writer.writeAttribute("awareness", Integer.toString(type.getAwareness()));
            writer.writeAttribute("bravery", Integer.toString(type.getBravery()));
            writer.writeAttribute("experience-multiplier", Integer.toString(type.getExperienceMultiplier()));
            writer.writeAttribute("move-duration", Integer.toString(type.getMoveDuration()));
            writer.writeAttribute("sight-range", Integer.toString(type.getSightRange()));
            writer.writeAttribute("absorbs-items", Boolean.toString(type.isAbsorbsItems()));
            writer.writeAttribute("levitates", Boolean.toString(type.levitates()));
            writer.writeAttribute("archenemy", Boolean.toString(type.isArchenemy()));
            writer.writeAttribute("night-vision", Boolean.toString(type.isNightVision()));
            writer.writeAttribute("sees-invisible", Boolean.toString(type.isSeesInvisible()));

            writer.writeEmptyElement("defense");
            writer.writeAttribute("anti-magic", Integer.toString(type.getAntiMagic()));
            writer.writeAttribute("armor", Integer.toString(type.getArmor()));
            writer.writeAttribute("shield", Integer.toString(type.getShield()));
            writer.writeAttribute("poison", Integer.toString(type.getPoisonResistance()));

            writer.writeEmptyElement("attack");
            writer.writeAttribute("skill", type.getAttackSkill().name());
            writer.writeAttribute("animation-duration", Integer.toString(type.getAttackAnimationDuration()));
            writer.writeAttribute("duration", Integer.toString(type.getAttackDuration()));
            writer.writeAttribute("power", Integer.toString(type.getAttackPower()));
            writer.writeAttribute("type", type.getAttackType().name());
            writer.writeAttribute("range", Integer.toString(type.getAttackRange()));
            writer.writeAttribute("probability", Integer.toString(type.getAttackProbability()));
            writer.writeAttribute("side-attack", Boolean.toString(type.isSideAttackAllowed()));

            writer.writeEmptyElement("poison");
            if (type.getPoison() != 0) {
                writer.writeAttribute("strength", Integer.toString(type.getPoison()));
            }

            if (!type.getSpells().isEmpty()) {
                writer.writeStartElement("spells");

                // Sort the spells to ensure they're always serialized in a
                // consistent way
                for (Spell.Type spell : new TreeSet<Spell.Type>(type.getSpells())) {
                    writer.writeEmptyElement("spell");
                    writer.writeAttribute("id", spell.name());
                }

                writer.writeEndElement(); // </spells>
            }

            if (!type.getWeaknesses().isEmpty()) {
                writer.writeStartElement("weaknesses");

                // Sort the weaknesses to ensure they're always serialized in a
                // consistent way
                for (Weakness weakness : new TreeSet<Weakness>(type.getWeaknesses())) {
                    writer.writeEmptyElement("weakness");
                    writer.writeAttribute("id", weakness.name());
                }

                writer.writeEndElement(); // </weaknesses>
            }

            if (!type.getDefinition().getItemDefs().isEmpty()) {
                writer.writeStartElement("items");

                for (ItemDef itemDef : type.getDefinition().getItemDefs()) {
                    writer.writeEmptyElement("item");
                    writer.writeAttribute("type", itemDef.type.name());
                    writer.writeAttribute("min", Integer.toString(itemDef.min));
                    writer.writeAttribute("max", Integer.toString(itemDef.max));

                    if (itemDef.curse != null) {
                        writer.writeAttribute("curse", itemDef.curse.name());
                    }
                }

                writer.writeEndElement(); // </items>
            }

            writer.writeEndElement(); // </creature>
        }

        writer.writeEndElement(); // </creatures>
        writer.writeEndDocument();

        System.out.println(stringWriter);
    }
}