fr.ritaly.dungeonmaster.map.Element.java Source code

Java tutorial

Introduction

Here is the source code for fr.ritaly.dungeonmaster.map.Element.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.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&lt;Sector, Creature&gt. 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&lt;Creature&gt. 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&lt;Sector&gt;. Ne retourne jamais null.
     */
    public EnumSet<Sector> getOccupiedSectors() {
        return creatureManager.getOccupiedSectors();
    }

    /**
     * Retourne les {@link Sector}s libres de cet {@link Element}.
     *
     * @return un EnumSet&lt;Sector&gt;. 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();
    }
}