Java tutorial
/** * 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.map; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.Validate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import fr.ritaly.dungeonmaster.Clock; import fr.ritaly.dungeonmaster.Direction; import fr.ritaly.dungeonmaster.HasPosition; import fr.ritaly.dungeonmaster.Place; import fr.ritaly.dungeonmaster.Position; import fr.ritaly.dungeonmaster.Sector; import fr.ritaly.dungeonmaster.Teleport; import fr.ritaly.dungeonmaster.ai.Creature; import fr.ritaly.dungeonmaster.ai.CreatureManager; import fr.ritaly.dungeonmaster.champion.HasParty; import fr.ritaly.dungeonmaster.champion.Party; import fr.ritaly.dungeonmaster.event.ChangeEvent; import fr.ritaly.dungeonmaster.event.ChangeEventSource; import fr.ritaly.dungeonmaster.event.ChangeEventSupport; import fr.ritaly.dungeonmaster.event.ChangeListener; import fr.ritaly.dungeonmaster.item.HasItems; import fr.ritaly.dungeonmaster.item.Item; import fr.ritaly.dungeonmaster.item.ItemManager; import fr.ritaly.dungeonmaster.projectile.Projectile; /** * Abstract class used for defining elements, that is, the building blocks for * creating levels. * * @author <a href="mailto:francois.ritaly@gmail.com">Francois RITALY</a> */ public abstract class Element implements ChangeEventSource, HasPosition, HasParty, HasItems<Sector> { protected final Log log = LogFactory.getLog(this.getClass()); /** * Enumerates the possible element types. * * @author <a href="mailto:francois.ritaly@gmail.com">Francois RITALY</a> */ public static enum Type { /** * A regular floor tile. */ FLOOR, /** * A floor tile triggering a switch. */ FLOOR_SWITCH, /** * A wall switch. */ WALL_SWITCH, /** * A wall with a lock. */ WALL_LOCK, /** * A wall with a slot (for coins). */ WALL_SLOT, /** * A regular wall. */ WALL, /** * A wall that can be retracted. */ RETRACTABLE_WALL, /** * An invisible wall. */ INVISIBLE_WALL, /** * A fake wall that can be traversed. */ FAKE_WALL, /** * A pillar. */ PILLAR, /** * A door. */ DOOR, /** * A pit. */ PIT, /** * A teleporter. */ TELEPORTER, /** * Some stairs. */ STAIRS, /** * A wall whose 4 sides feature an alcove. */ FOUR_SIDE_ALCOVE, /** * A wall whose one side features an alcove. */ ALCOVE, /** * A fontain. */ FOUNTAIN, /** * A lever on a wall. */ LEVER, /** * A wall with a torch holder. */ TORCH_WALL, /** * An altar to resurrect champions. */ ALTAR, /** * A wall with a writing. */ TEXT_WALL, /** * A wall with a champion portrait (ro reincarnate champions). */ PORTRAIT, /** * A decorated wall. */ DECORATED_WALL, /** * A decorated floor tile. */ DECORATED_FLOOR, /** * A generator of creatures. */ GENERATOR, /** * A wall generating (item or spell) projectiles. */ PROJECTILE_LAUNCHER; /** * Tells whether this type of element can store items. Examples: a * regular wall can't store items but an altar (or an alcove) can. A * regular floor tile can store items too. * * @return whether this type of element can store items. */ public boolean isItemStorage() { switch (this) { case ALCOVE: case FOUR_SIDE_ALCOVE: case ALTAR: case FAKE_WALL: case DOOR: case FLOOR: case FLOOR_SWITCH: case RETRACTABLE_WALL: case PIT: case STAIRS: case TELEPORTER: case DECORATED_FLOOR: case GENERATOR: return true; case PILLAR: case INVISIBLE_WALL: case DECORATED_WALL: case TEXT_WALL: case PORTRAIT: case PROJECTILE_LAUNCHER: case TORCH_WALL: case WALL: case WALL_LOCK: case WALL_SLOT: case WALL_SWITCH: case LEVER: case FOUNTAIN: return false; default: throw new UnsupportedOperationException("Method unsupported for type " + this); } } /** * Tells whether this type of element is "concrete". Concrete elements * can be used as external walls to delimit a level. A regular wall is * concrete but a fake or invisible wall isn't concrete. * * @return whether this type of element is "concrete". */ public boolean isConcrete() { /* * According to Attack::IsCellFluxcage(..), the stairs can't have a * flux cage * TODO JUNIT: Unit test this behavior */ switch (this) { case ALCOVE: case FOUNTAIN: case FOUR_SIDE_ALCOVE: case LEVER: case TORCH_WALL: case WALL: case WALL_LOCK: case WALL_SLOT: case WALL_SWITCH: case ALTAR: case DECORATED_WALL: case TEXT_WALL: case PORTRAIT: case PROJECTILE_LAUNCHER: case STAIRS: return true; case FAKE_WALL: case DOOR: case FLOOR: case FLOOR_SWITCH: case INVISIBLE_WALL: case PILLAR: case RETRACTABLE_WALL: case PIT: case TELEPORTER: case DECORATED_FLOOR: case GENERATOR: return false; default: throw new UnsupportedOperationException("Method unsupported for type " + this); } } } /** * The level this element belong to. */ private Level level; /** * The element's position inside the level. */ private Position position; /** * The element type. */ private final Type type; /** * The party occupying this element (if any). Populated when the party steps * into the element and reset when the party steps off. */ private Party party; // FIXME Only instantiate a creature manager if the element can be occupied by creatures /** * The object responsible for managing the presence of creatures on this element. */ private final CreatureManager creatureManager = new CreatureManager(this); /** * The possible projectiles currently on this element. Can be null. */ private Map<Sector, Projectile> projectiles; /** * The possible poison clouds currently on this element. Can be null. */ private List<PoisonCloud> poisonClouds; /** * The possible flux cage on this element. Can be null. There can be only * one flux cage per element. */ private FluxCage fluxCage; /** * Stores the items for this element. */ private final ItemManager itemManager = new ItemManager(); /** * Support class used for firing change events. */ private final ChangeEventSupport eventSupport = new ChangeEventSupport(); protected Element(Type type) { Validate.notNull(type, "The given type is null"); this.type = type; } @Override public void addItem(Item item, Sector sector) { itemManager.addItem(item, sector); if (log.isDebugEnabled()) { log.debug(String.format("%s dropped on %s at %s", item, getId(), sector)); } afterItemAdded(item, sector); fireChangeEvent(); } @Override public boolean removeItem(Item item) { final Sector sector = getPlace(item); if (sector != null) { return itemManager.removeItem(item); } return false; } public Item removeItem(Sector sector) { final Item item = itemManager.removeItem(sector); if (log.isDebugEnabled()) { log.debug(String.format("%s picked from %s at %s", item, getId(), sector)); } afterItemRemoved(item, sector); fireChangeEvent(); return item; } public final Type getType() { return type; } public Level getLevel() { return level; } // FIXME Protect the call of this method with an aspect // This method should only be called from the Level class. However we can't // declare it package protected because we need to call it from the A* // algorithm when searching paths... public void setLevel(Level level) { // The level argument can be null (when the element is detached from its // parent level) this.level = level; } @Override public Position getPosition() { return position; } // FIXME Protect the call of this method with an aspect // This method should only be called from the Level class. However we can't // declare it package protected because we need to call it from the A* // algorithm when searching paths... public void setPosition(Position position) { // The position argument can be null (when the element is detached from // its parent level) this.position = position; } /** * Tells whether this element can be traversed by the given party. * * @param party * the party of champions. Can't be null. * @return whether this element can be traversed by the given party. */ public abstract boolean isTraversable(Party party); /** * Tells whether this element can be traversed by the given creature. The * returned value depends on: * <ul> * <li>the type of the element: a fake wall can always be traversed, a * regular wall can't.</li> * <li>the materiality of the creature: a ghost can traverse a regular wall, * a mummy can't.</li> * <li>the state of this element: a door can be traversed depending on the * creature's height and its aperture (open, 3/4 open, 1/2 open, 1/4 open, * closed).</li> * </ul> * * @param creature * the creature to test. Can't be null. * @return whether this element can be traversed by the given creature. */ public abstract boolean isTraversable(Creature creature); /** * Tells whether this element can be traversed projectiles. * * @return whether this element can be traversed projectiles. */ public abstract boolean isTraversableByProjectile(); /** * Callback method invoked after an item has been dropped onto this element. * * @param item * the dropped item. Can't be null. * @param sector * the sector where the item has been dropped. Can't be null. */ protected void afterItemAdded(Item item, Sector sector) { } /** * Callback method invoked after an item has been picked from this element. * * @param item * the picked item. Can't be null. * @param sector * the sector where the item has been picked. Can't be null. */ protected void afterItemRemoved(Item item, Sector sector) { } public final void addProjectile(Projectile projectile, Sector sector) { Validate.notNull(projectile, "The given projectile is null"); Validate.notNull(sector, "The given sector is null"); if (!isTraversableByProjectile() && !Type.DOOR.equals(getType())) { // Une porte peut accueillir un projectile mme si celle-ci est // ferme afin qu'il puisse exploser throw new UnsupportedOperationException("The projectile can't arrive on " + getId()); } if (log.isDebugEnabled()) { log.debug(projectile.getId() + " arrived on " + getId() + " (sector: " + sector + ")"); } if (projectiles == null) { // Crer la Map la vole projectiles = new EnumMap<Sector, Projectile>(Sector.class); } // L'emplacement doit initialement tre vide if (projectiles.get(sector) != null) { throw new IllegalArgumentException("The cell " + sector + " of element " + getId() + " is already occupied by a projectile (" + projectiles.get(sector) + ")"); } // Mmoriser le projectile projectiles.put(sector, projectile); afterProjectileArrived(projectile); } public final void removeProjectile(Projectile projectile, Sector sector) { if (projectile == null) { throw new IllegalArgumentException("The given projectile is null"); } if (sector == null) { throw new IllegalArgumentException("The given sector is null"); } if (!isTraversableByProjectile() && !Type.DOOR.equals(getType())) { // Une porte peut accueillir un projectile mme si celle-ci est // ferme afin qu'il puisse exploser throw new UnsupportedOperationException( "The projectile " + projectile.getId() + " can't leave " + getId()); } if (log.isDebugEnabled()) { log.debug(projectile.getId() + " left " + getId() + " (sector: " + sector + ")"); } final Projectile removed = projectiles.remove(sector); if (removed != projectile) { throw new IllegalArgumentException( "Removed: " + removed + " / Projectile: " + projectile + " / Sector: " + sector); } if (projectiles.isEmpty()) { // Purger la Map la vole projectiles = null; } afterProjectileLeft(projectile); } protected void afterProjectileLeft(Projectile projectile) { } protected void afterProjectileArrived(Projectile projectile) { } protected void afterCreatureSteppedOn(Creature creature) { } protected void afterCreatureSteppedOff(Creature creature) { } public final Sector getSector(Creature creature) { return creatureManager.getSector(creature); } @Override public final Sector getPlace(Item item) { return itemManager.getPlace(item); } @Override public final void setParty(Party party) { if (party == null) { throw new IllegalArgumentException("The given party is null"); } if (!isTraversable(party)) { throw new UnsupportedOperationException("The party can't step on element " + getId()); } if (log.isDebugEnabled()) { log.debug("Party stepped on " + getId()); } // Mmoriser la rfrence this.party = party; afterPartySteppedOn(); } protected void afterPartySteppedOn() { } protected void afterPartySteppedOff(Party party) { } /** * Notifie l'lment que le groupe de champions vient de tourner sur place. * Note: Cette mthode permet un lement de type STAIRS de dplacer le * groupe de champions quand celui-ci tourne sur lui-mme. */ public void partyTurned() { if (party == null) { throw new IllegalStateException("The party isn't on " + getId()); } if (log.isDebugEnabled()) { log.debug("Party turned on " + getId()); } } /** * Notifie l'lment que le groupe de champions vient de quitter sa * position. */ public final void removeParty() { if (this.party == null) { throw new IllegalStateException("The party isn't located on this " + getId()); } if (!isTraversable(party)) { throw new UnsupportedOperationException("The party can't step off element " + type); } // Rinitialiser la rfrence final Party backup = this.party; this.party = null; if (log.isDebugEnabled()) { log.debug("Party stepped off " + getId()); } afterPartySteppedOff(backup); } @Override public boolean hasParty() { return (party != null); } /** * Indique si l'lment est occup par au moins une crature. * * @return si l'lment est occup par au moins une crature. */ public boolean hasCreatures() { return creatureManager.hasCreatures(); } /** * Indique si l'lment est occup par au moins un projectile. * * @return si l'lment est occup par au moins un projectile. */ public boolean hasProjectiles() { return (projectiles != null) && !projectiles.isEmpty(); } /** * Indique si l'lment est vide, c'est--dire non occup par des cratures, * par le groupe de champions ou tout autre chose qui empcherait de s'y * placer. * * @return si l'lment est vide. */ public boolean isEmpty() { return !hasParty() && !hasCreatures(); } @Override public final String toString() { if (position != null) { return this.type.name() + position; } else { return this.type.name() + "[?:?,?]"; } } @Override public final Party getParty() { return party; } /** * Retourne les cratures occupant cet lment sous forme de Map. * * @return une Map<Sector, Creature>. Cette mthode ne retourne jamais * null. */ public final Map<Sector, Creature> getCreatureMap() { // Ne pas utiliser en dehors des tests unitaires (accs trop bas niveau) // Utiliser getCreatures() la place return creatureManager.getCreatureMap(); } /** * Retourne les cratures occupant cet lment sous forme de {@link List}. * * @return une Set<Creature>. Cette mthode ne retourne jamais null. */ public final Set<Creature> getCreatures() { return creatureManager.getCreatures(); } public final Map<Sector, Projectile> getProjectiles() { if (projectiles == null) { return Collections.emptyMap(); } // Recopie dfensive return Collections.unmodifiableMap(projectiles); } /** * Retourne la crature occupant l'emplacement donn s'il y a lieu. * * @param sector * l'emplacement sur lequel rechercher la crature. * @return une instance de {@link Creature} ou null s'il n'y en a aucune * cet emplacement. */ public final Creature getCreature(Sector sector) { return creatureManager.getCreature(sector); } @Override public final void addChangeListener(ChangeListener listener) { eventSupport.addChangeListener(listener); } @Override public final void removeChangeListener(ChangeListener listener) { eventSupport.addChangeListener(listener); } protected final void fireChangeEvent() { eventSupport.fireChangeEvent(new ChangeEvent(this)); } /** * Indique si l'lment est "en dur". C'est le cas d'un mur au send large * (mur simple, mur dcor) mais pas d'un mur invisible ou d'un faux mur. * Permet de dterminer si un lment peut tre utilis en bordure de * niveau. * * @return si l'lment est "en dur". */ public final boolean isConcrete() { return type.isConcrete(); } /** * Retourne l'identifiant de cet lment sous forme de {@link String}. * * @return un {@link String} identifiant cet lment. */ public abstract String getSymbol(); public final String getId() { if (position != null) { return this.type.name() + position; } else { return this.type.name() + "[?:?,?]"; } } @Override public final List<Item> getItems() { return itemManager.getItems(); } @Override public final int getItemCount() { return itemManager.getItemCount(); } public final int getCreatureCount() { return creatureManager.getCreatureCount(); } @Override public final int getItemCount(Sector sector) { return itemManager.getItemCount(sector); } @Override public List<Item> getItems(Sector sector) { return itemManager.getItems(sector); } @Override public boolean hasItems() { return itemManager.hasItems(); } /** * Calcule et retourne une instance de {@link Teleport} indiquant comment * dplacer un groupe de champions se dplaant dans la direction donne. * Dans la majorit des cas, le groupe se retrouvera sur l'lment situ * dans la direction donne mais pour certains lments (tlporteurs, * escaliers) le dplacement du groupe n'est pas aussi simple. * * @param direction * la {@link Direction} dans laquelle se dplace le groupe de * champions. * @return une instance de {@link Teleport} indiquant comment dplacer le * groupe de champions. */ public Teleport getTeleport(Direction direction) { Validate.notNull(direction, "The given direction is null"); if (!hasParty()) { throw new IllegalStateException("The party isn't on this element"); } // Dans le cas gnral, l'lment ne modifie pas la position finale return new Teleport(getPosition().towards(direction), getParty().getLookDirection()); } // /** // * Notifie l'lment que le groupe de champions qui l'occupe est sur le // * point de bouger dans la direction donne et retourne la position finale // * du groupe. Cette mthode est spcialement conue pour la classe // * {@link Stairs} qui a un mode de fonctionnement un peu particulier. // * // * @param direction // * la {@link Direction} de dplacement du groupe. // * @return la {@link Position} finale aprs dplacement du groupe. // */ // public Position computeTargetPosition(Direction direction) { // Validate.notNull(direction, "The given direction is null"); // if (!hasParty()) { // throw new IllegalStateException("The party isn't on this element"); // } // // // Dans le cas gnral, l'lment ne modifie pas la position finale // return getPosition().towards(direction); // } // // public Direction computeTargetDirection(Direction direction) { // Validate.notNull(direction, "The given direction is null"); // if (!hasParty()) { // throw new IllegalStateException("The party isn't on this element"); // } // // // Dans le cas gnral, l'lment ne modifie pas la direction du groupe // return getParty().getLookDirection(); // } /** * Calcule et retourne la place libre restante pour accueillir de nouvelles * {@link Creature}s sous forme d'un entier (reprsentant un nombre de * {@link Sector}s). * * @return un entier dans l'intervalle [0-4] reprsentant le nombre de * {@link Sector}s libres. */ public int getFreeRoom() { return creatureManager.getFreeSectors().size(); } /** * Retourne les {@link Sector}s occupes par les {@link Creature}s * prsentes sur cet {@link Element}. * * @return un EnumSet<Sector>. Ne retourne jamais null. */ public EnumSet<Sector> getOccupiedSectors() { return creatureManager.getOccupiedSectors(); } /** * Retourne les {@link Sector}s libres de cet {@link Element}. * * @return un EnumSet<Sector>. Ne retourne jamais null. */ public Set<Sector> getFreeSectors() { return creatureManager.getFreeSectors(); } /** * Indique si cet {@link Element} peut accueillir la {@link Creature} donne * compte tenu de sa taille et de la place restante. * * @param creature * une {@link Creature}. * @return si cet {@link Element} peut accueillir la {@link Creature} donne * compte tenu de sa taille et de la place restante. */ public boolean canHost(Creature creature) { return creatureManager.canHost(creature); } public abstract void validate() throws ValidationException; // FIXME Crer mthode Element.setVisited(boolean) pour magic footprints protected final Position getPartyPosition() { final Level level = getLevel(); if (level != null) { final Dungeon dungeon = level.getDungeon(); if (dungeon != null) { final Party party = dungeon.getParty(); if (party != null) { return party.getPosition(); } } } return null; } public boolean hasCreature(Creature creature) { return creatureManager.hasCreature(creature); } protected final CreatureManager getCreatureManager() { return creatureManager; } public Place removeCreature(Creature creature) { final Place place = creatureManager.removeCreature(creature); creature.setElement(null); afterCreatureSteppedOff(creature); return place; } public void removeCreature(Creature creature, Place place) { creatureManager.removeCreature(creature, place); creature.setElement(null); afterCreatureSteppedOff(creature); } public void addCreature(Creature creature, Place place) { creatureManager.addCreature(creature, place); creature.setElement(this); afterCreatureSteppedOn(creature); } public void addCreature(Creature creature) { creatureManager.addCreature(creature); creature.setElement(this); afterCreatureSteppedOn(creature); } public boolean hasPoisonClouds() { return (poisonClouds != null) && !poisonClouds.isEmpty(); } public int getPoisonCloudCount() { if (poisonClouds != null) { return poisonClouds.size(); } return 0; } // TODO Prendre en compte la force du nuage de poison en paramtre public void createPoisonCloud() { if (this.poisonClouds == null) { this.poisonClouds = new ArrayList<PoisonCloud>(); } // if (log.isDebugEnabled()) { // log.debug("Creating new poison cloud on " + this + " ..."); // } final PoisonCloud poisonCloud = new PoisonCloud(this); // S'enregistrer pour savoir quand le nuage disparat poisonCloud.addChangeListener(new ChangeListener() { @Override public void onChangeEvent(ChangeEvent event) { if (log.isDebugEnabled()) { log.debug(event.getSource() + " vanished into thin air"); } poisonClouds.remove(event.getSource()); if (poisonClouds.isEmpty()) { poisonClouds = null; } } }); // Mmoriser le nuage this.poisonClouds.add(poisonCloud); if (log.isDebugEnabled()) { log.debug("Created a new poison cloud on " + this); } // Enregistrer ce nuage Clock.getInstance().register(poisonCloud); } public boolean hasFluxCage() { return (fluxCage != null); } public void createFluxCage() { // On ne peut crer une cage s'il y en a dj une en place if (hasFluxCage()) { // TODO Grer le cas d'une seconde cage qui renforce la premire ? throw new IllegalStateException("There is already a flux cage on " + this); } // if (log.isDebugEnabled()) { // log.debug("Creating new flux cage on " + this + " ..."); // } final FluxCage fluxCage = new FluxCage(this); // S'enregistrer pour savoir quand la cage disparat fluxCage.addChangeListener(new ChangeListener() { @Override public void onChangeEvent(ChangeEvent event) { if (log.isDebugEnabled()) { log.debug(event.getSource() + " vanished into thin air"); } Element.this.fluxCage = null; } }); // Mmoriser la cage this.fluxCage = fluxCage; if (log.isDebugEnabled()) { log.debug("Created a flux cage on " + this); } // Enregistrer la cage Clock.getInstance().register(fluxCage); } public List<Element> getSurroundingElements() { final List<Element> elements = new ArrayList<Element>(); for (Position position : getPosition().getSurroundingPositions()) { if (!getLevel().contains(position)) { // Position situe en dehors des limites du niveau continue; } elements.add(getLevel().getElement(position.x, position.y)); } return elements; } public List<Element> getAdjacentElements() { return getAdjacentElements(true); } public List<Element> getAdjacentElements(boolean material) { // At best 4 positions are adjacent (north, sourth, east & west) final List<Element> result = new ArrayList<Element>(4); for (Position position : getPosition().getAttackablePositions()) { if (!level.contains(position)) { // The position doesn't exist for this level continue; } result.add(level.getElement(position.x, position.y)); } return result; } @Override public Sector addItem(Item item) { final Sector sector = itemManager.addItem(item); if (log.isDebugEnabled()) { log.debug(String.format("%s dropped on %s at %s", item, getId(), sector)); } afterItemAdded(item, sector); fireChangeEvent(); return sector; } @Override public Item removeItem() { final Sector sector = getRandomPlace(); if (sector != null) { return null; } // This will fire an event return removeItem(sector); } @Override public Sector getRandomPlace() { return itemManager.getRandomPlace(); } /** * Tells whether a flux cage can be created on this element. * * @return whether a flux cage can be created on this element. */ public abstract boolean isFluxCageAllowed(); /** * Tells whether this element is occupied by at least a creature or a champion. * * @return whether this element is occupied by at least a creature or a champion. */ public final boolean isOccupied() { return hasParty() || hasCreatures(); } }