fr.ritaly.dungeonmaster.item.ItemDef.java Source code

Java tutorial

Introduction

Here is the source code for fr.ritaly.dungeonmaster.item.ItemDef.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.item;

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.HashSet;
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.Skill;
import fr.ritaly.dungeonmaster.champion.Champion;
import fr.ritaly.dungeonmaster.champion.Champion.Level;
import fr.ritaly.dungeonmaster.champion.body.BodyPart;
import fr.ritaly.dungeonmaster.item.Item.AffectedStatistic;
import fr.ritaly.dungeonmaster.item.Item.Type;
import fr.ritaly.dungeonmaster.stat.Stats;

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

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

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

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

        private ItemDef definition;

        private ItemDefParser() {
        }

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

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

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

            if ("items".equals(elementName) || "locations".equals(elementName) || "actions".equals(elementName)
                    || "effects".equals(elementName)) {
                // Do nothing
            } else if ("item".equals(elementName)) {
                this.definition = new ItemDef();
                this.definition.id = attributes.getValue("id");
                this.definition.weight = Float.parseFloat(attributes.getValue("weight"));

                if (attributes.getValue("damage") != null) {
                    this.definition.damage = Integer.parseInt(attributes.getValue("damage"));
                }
                if (attributes.getValue("activation") != null) {
                    this.definition.activationBodyPart = BodyPart.Type.valueOf(attributes.getValue("activation"));
                }
                if (attributes.getValue("shield") != null) {
                    this.definition.shield = Integer.parseInt(attributes.getValue("shield"));
                }
                if (attributes.getValue("anti-magic") != null) {
                    this.definition.antiMagic = Integer.parseInt(attributes.getValue("anti-magic"));
                }
                if (attributes.getValue("decay-rate") != null) {
                    this.definition.decayRate = Integer.parseInt(attributes.getValue("decay-rate"));
                }
                if (attributes.getValue("distance") != null) {
                    this.definition.distance = Integer.parseInt(attributes.getValue("distance"));
                }
                if (attributes.getValue("shoot-damage") != null) {
                    this.definition.shootDamage = Integer.parseInt(attributes.getValue("shoot-damage"));
                }
            } else if ("location".equals(elementName)) {
                this.definition.carryLocations.add(CarryLocation.valueOf(attributes.getValue("id")));
            } else if ("action".equals(elementName)) {
                final ActionDef actionDef = new ActionDef();
                actionDef.action = Action.valueOf(attributes.getValue("id"));

                if (attributes.getValue("min-level") != null) {
                    actionDef.minLevel = Champion.Level.valueOf(attributes.getValue("min-level"));
                }
                if (attributes.getValue("use-charges") != null) {
                    actionDef.useCharges = Boolean.valueOf(attributes.getValue("use-charges"));
                }

                this.definition.actions.add(actionDef);
            } else if ("effect".equals(elementName)) {
                final Effect effect = new Effect();
                effect.statistic = AffectedStatistic.valueOf(attributes.getValue("stat"));
                effect.strength = Integer.valueOf(attributes.getValue("strength"));

                this.definition.effects.add(effect);
            } 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 ("items".equals(elementName) || "locations".equals(elementName) || "actions".equals(elementName)
                    || "effects".equals(elementName)) {
                // Do nothing
            } else if ("item".equals(elementName)) {
                this.definitions.add(definition);

                this.definition = null;
            } else if ("location".equals(elementName)) {
                // Do nothing
            } else if ("action".equals(elementName)) {
                // Do nothing
            } else if ("effect".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 item definitions", definitions.size()));
            }
        }
    }

    /**
     * An item effect can provide a bonus or a malus to a champion's stat.
     *
     * @author <a href="mailto:francois.ritaly@gmail.com">Francois RITALY</a>
     */
    public static final class Effect {

        /**
         * The statistic affected by this effect.
         */
        private AffectedStatistic statistic;

        /**
         * The strength of this effect.
         */
        private int strength;

        private Effect() {
        }

        /**
         * Returns the statistic affected by this effect.
         *
         * @return the affected statistic. Never returns null.
         */
        public AffectedStatistic getStatistic() {
            return statistic;
        }

        /**
         * Returns the strength of this effect as an integer.
         *
         * @return an integer value representing the effect's strength. Can be
         *         positive or negative.
         */
        public int getStrength() {
            return strength;
        }

        /**
         * Applies the effect to the given champion
         *
         * @param champion
         *            the champion to who apply the effect. Can't be null.
         */
        public void affect(Champion champion) {
            Validate.notNull(champion, "The given champion is null");

            final Stats stats = champion.getStats();

            switch (statistic) {
            case DEXTERITY:
                stats.getDexterity().incMax(strength);
                stats.getDexterity().inc(strength);
                break;
            case MANA:
                stats.getMana().incMax(strength);
                stats.getMana().inc(strength);
                break;
            case STRENGTH:
                stats.getStrength().incMax(strength);
                stats.getStrength().inc(strength);
                break;
            case MAX_LOAD:
                // MaxLoad is a special stat for which the boost is handled
                // separately from the base value (this value is computed
                // dynamically)
                stats.getMaxLoadBoost().inc(strength);
                break;
            case WISDOM:
                stats.getWisdom().incMax(strength);
                stats.getWisdom().inc(strength);
                break;
            case ANTI_FIRE:
                stats.getAntiFire().incMax(strength);
                stats.getAntiFire().inc(strength);
                break;
            case ANTI_MAGIC:
                stats.getAntiMagic().incMax(strength);
                stats.getAntiMagic().inc(strength);
                break;
            case LUCK:
                stats.getLuck().incMax(strength);
                stats.getLuck().inc(strength);
                break;
            case WIZARD_LEVEL:
                champion.getExperience(Skill.WIZARD).incBoost(strength);
                break;
            case HEAL_SKILL:
                champion.getExperience(Skill.HEAL).incBoost(strength);
                break;
            case DEFEND_SKILL:
                champion.getExperience(Skill.DEFEND).incBoost(strength);
                break;
            case INFLUENCE_SKILL:
                champion.getExperience(Skill.INFLUENCE).incBoost(strength);
                break;
            case ALL_SKILLS:
                for (Skill skill : Skill.values()) {
                    champion.getExperience(skill).incBoost(strength);
                }
                break;
            default:
                throw new UnsupportedOperationException("Unsupported statistic " + statistic);
            }
        }

        /**
         * Neutralizes the effect for the given champion.
         *
         * @param champion
         *            the champion to who neutralize the effect. Can't be null.
         */
        public void unaffect(Champion champion) {
            Validate.notNull(champion, "The given champion is null");

            final Stats stats = champion.getStats();

            switch (statistic) {
            case DEXTERITY:
                stats.getDexterity().dec(strength);
                stats.getDexterity().decMax(strength);
                break;
            case MANA:
                stats.getMana().dec(strength);
                stats.getMana().decMax(strength);
                break;
            case STRENGTH:
                stats.getStrength().dec(strength);
                stats.getStrength().decMax(strength);
                break;
            case MAX_LOAD:
                // MaxLoad is a special stat for which the boost is handled
                // separately from the base value (this value is computed
                // dynamically)
                stats.getMaxLoadBoost().dec(strength);
                break;
            case WISDOM:
                stats.getWisdom().dec(strength);
                stats.getWisdom().decMax(strength);
                break;
            case ANTI_FIRE:
                stats.getAntiFire().dec(strength);
                stats.getAntiFire().decMax(strength);
                break;
            case ANTI_MAGIC:
                stats.getAntiMagic().dec(strength);
                stats.getAntiMagic().decMax(strength);
                break;
            case LUCK:
                stats.getLuck().dec(strength);
                stats.getLuck().decMax(strength);
                break;
            case WIZARD_LEVEL:
                champion.getExperience(Skill.WIZARD).decBoost(strength);
                break;
            case HEAL_SKILL:
                champion.getExperience(Skill.HEAL).decBoost(strength);
                break;
            case DEFEND_SKILL:
                champion.getExperience(Skill.DEFEND).decBoost(strength);
                break;
            case INFLUENCE_SKILL:
                champion.getExperience(Skill.INFLUENCE).decBoost(strength);
                break;
            case ALL_SKILLS:
                for (Skill skill : Skill.values()) {
                    champion.getExperience(skill).decBoost(strength);
                }
                break;
            default:
                throw new UnsupportedOperationException("Unsupported statistic " + statistic);
            }
        }
    }

    public static final class ActionDef {

        private Action action;

        /**
         * The minimum skill level necessary for using this action. The default
         * value is hard-coded here
         */
        private Champion.Level minLevel = Champion.Level.NONE;

        /**
         * Whether the use of this action is limited by a number of available
         * charges. The default value is hard-coded here
         */
        private boolean useCharges = false;

        // FIXME Add a damage property ?
        private ActionDef() {
        }

        public Action getAction() {
            return action;
        }

        public Champion.Level getMinLevel() {
            return minLevel;
        }

        public boolean isUseCharges() {
            return useCharges;
        }

        /**
         * Tells whether the given champion can use this action.
         *
         * @param champion
         *            the champion to test. Can't be null.
         * @return whether the given champion can use this action.
         */
        public boolean isUsable(Champion champion) {
            Validate.notNull(champion, "The given champion is null");

            // What's the minimal level required to use this skill ?
            final Level level = getMinLevel();

            if (Level.NONE.equals(level)) {
                // In most cases, there's no min skill level required
                return true;
            }

            // What's the skill involved ?
            final Skill skill = action.getSkill();

            // Is the champion skilled enough to use this action ?
            return (champion.getLevel(skill).compareTo(level) >= 0);
        }
    }

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

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

            final ItemDefParser parser = new ItemDefParser();

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

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

    private String id;

    private int shield, antiMagic;

    /**
     * The amount of energy lost by the projectile every time it moves.
     */
    private int decayRate = -1;

    /**
     * The amount of damage associated with fired projectiles. Value within
     * [0,255].
     */
    private int shootDamage = -1;

    /**
     * This value determines how far the item will go when thrown. If a weapon
     * is used to "Shoot", this will be a part of how far the item being shot
     * will travel. The farther the projectile goes, the more damage it does
     * (Damage is decreased as it flies). Valeur dans l'intervalle [0-255].
     */
    private int distance = -1;

    /**
     * The base value used for computing the damages caused by an attack with
     * this item. Relevant only for a weapon item for which the value is
     * positive. For other items, the value is set to -1.
     */
    private int damage = -1;

    /**
     * TODO Ensure the weight is >= 0.
      * The item's weight (in Kg) as a float.
     */
    private float weight;

    /**
     * The carry locations where this item can be stored.
     */
    private final Set<CarryLocation> carryLocations = new HashSet<CarryLocation>();

    private BodyPart.Type activationBodyPart;

    private List<ItemDef.ActionDef> actions = new ArrayList<ItemDef.ActionDef>();

    private List<ItemDef.Effect> effects = new ArrayList<ItemDef.Effect>();

    ItemDef() {
    }

    public int getDamage() {
        return damage;
    }

    /**
     * Returns the effects bestowed by this item to a champion when activated.
     *
     * @return a list of effects. Never returns null.
     */
    public List<ItemDef.Effect> getEffects() {
        return Collections.unmodifiableList(effects);
    }

    /**
     * Returns the actions associated to this item.
     *
     * @return a list of actions. Never returns null.
     */
    public List<ItemDef.ActionDef> getActions() {
        return Collections.unmodifiableList(actions);
    }

    public int getDistance() {
        return distance;
    }

    public int getShootDamage() {
        return shootDamage;
    }

    public int getDecayRate() {
        return decayRate;
    }

    /**
     * Returns the fire resistance bonus bestowed by this item when activated.
     *
     * @return an integer representing a fire resistance bonus. Returns zero if
     *         the item doesn't bestow any bonus.
     */
    public int getAntiMagic() {
        return antiMagic;
    }

    /**
     * Returns the shield bonus bestowed by this item when activated.
     *
     * @return an integer representing a shield bonus. Returns zero if the item
     *         doesn't bestow any bonus.
     */
    public int getShield() {
        return shield;
    }

    /**
     * Returns the body part that activates this item (if any). Returns null if
     * the item can't be activated. When activated an item provides (in general)
     * a bonus to the champion (be it a defense bonus, a stat bonus, etc).
     * Examples: This method returns {@link BodyPart.Type#NECK} for an amulet,
     * {@link BodyPart.Type#WEAPON_HAND} for a weapon item.
     *
     * @return the body part which activates this item (if any) or null.
     */
    public BodyPart.Type getActivationBodyPart() {
        return activationBodyPart;
    }

    /**
     * Returns the item's weight as a float.
     *
     * @return a float representing a weight in Kg.
     */
    public float getWeight() {
        return weight;
    }

    public String getId() {
        return id;
    }

    /**
     * Returns the carry locations where this item can be stored.
     *
     * @return a set of carry locations. Never returns null.
     */
    public Set<CarryLocation> getCarryLocations() {
        return Collections.unmodifiableSet(carryLocations);
    }

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

    public static ItemDef getDefinition(Item.Type type) {
        Validate.notNull(type, "The given item type is null");

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

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

        Collections.sort(types, new Comparator<Item.Type>() {
            @Override
            public int compare(Type o1, 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("items");
        writer.writeDefaultNamespace("yadmjc:items:1.0");

        for (Item.Type type : types) {
            writer.writeStartElement("item");
            writer.writeAttribute("id", type.name());
            writer.writeAttribute("weight", Float.toString(type.getWeight()));

            if (type.getDamage() != -1) {
                writer.writeAttribute("damage", Integer.toString(type.getDamage()));
            }

            if (type.getActivationBodyPart() != null) {
                writer.writeAttribute("activation", type.getActivationBodyPart().name());
            }

            if (type.getShield() != 0) {
                writer.writeAttribute("shield", Integer.toString(type.getShield()));
            }
            if (type.getAntiMagic() != 0) {
                writer.writeAttribute("anti-magic", Integer.toString(type.getAntiMagic()));
            }

            if (type.getDecayRate() != -1) {
                writer.writeAttribute("decay-rate", Integer.toString(type.getDecayRate()));
            }

            if (type.getDistance() != -1) {
                writer.writeAttribute("distance", Integer.toString(type.getDistance()));
            }

            if (type.getShootDamage() != -1) {
                writer.writeAttribute("shoot-damage", Integer.toString(type.getShootDamage()));
            }

            if (!type.getCarryLocations().isEmpty()) {
                writer.writeStartElement("locations");

                // Sort the locations to ensure they're always serialized in a consistent way
                for (CarryLocation location : new TreeSet<CarryLocation>(type.getCarryLocations())) {
                    writer.writeEmptyElement("location");
                    writer.writeAttribute("id", location.name());
                }
                writer.writeEndElement();
            }

            if (!type.getActions().isEmpty()) {
                writer.writeStartElement("actions");
                for (ActionDef actionDef : type.getActions()) {
                    writer.writeEmptyElement("action");
                    writer.writeAttribute("id", actionDef.getAction().name());

                    if (actionDef.getMinLevel() != Champion.Level.NONE) {
                        writer.writeAttribute("min-level", actionDef.getMinLevel().name());
                    }

                    if (actionDef.isUseCharges()) {
                        writer.writeAttribute("use-charges", Boolean.toString(actionDef.isUseCharges()));
                    }
                }
                writer.writeEndElement(); // </actions>
            }

            if (!type.getEffects().isEmpty()) {
                writer.writeStartElement("effects");
                for (Effect effect : type.getEffects()) {
                    writer.writeEmptyElement("effect");
                    writer.writeAttribute("stat", effect.getStatistic().name());
                    writer.writeAttribute("strength", String.format("%+d", effect.getStrength()));
                }
                writer.writeEndElement(); // </effects>
            }

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

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

        System.out.println(stringWriter);
    }
}