public final class Assets implements Disposable {

    public static final String FONTS_ROMANICA_TTF = "fonts/Romanica.ttf";
    public static final String UISKIN_JSON = "uiskin/uiskin.json";
    private final Skin skin;

    public enum Icon {
        DISCOVERY("discovery-bulb"),
        DIPLOMACY("diplomacy-handshake"),
        BUILDINGS("buildings-hammer"),
        REVOLT("revolt-clenched-fist"),
        FLAG("finish-flag"),
        MILITARY("military-swords"),
        DISEASE("disease-skull"),
        END_TURN("empty-hourglass");

        private final String name;

        Icon(String name) {
            this.name = name;
        }
    }

    private final AssetManager manager;

    private final EnumMap<Icon, Drawable> drawables = new EnumMap<>(Icon.class);

    private final EnumMap<Icon, TextureRegion> regions = new EnumMap<>(Icon.class);

    private final Map<String, BitmapFont> fonts = new HashMap<>();

    public Assets() {
        manager = loadAssets();

        ObjectMap<String, Object> fonts = new ObjectMap<>();
        fonts.put("default-font", getFont(16));
        fonts.put("colored-font", getFont(16, true));
        manager.load(UISKIN_JSON, Skin.class, new SkinLoader.SkinParameter(fonts));
        manager.finishLoading();
        skin = manager.get(UISKIN_JSON);

        for (Icon icon : Icon.values())
            addIcon(icon, icon.name);
    }

    private static AssetManager loadAssets() {
        AssetManager manager = new AssetManager();
        manager.finishLoading();
        return manager;
    }


    private void addIcon(Icon type, String name) {
        drawables.put(type, skin.getDrawable(name));
        regions.put(type, skin.getRegion(name));
    }

    public BitmapFont getFont(int size) {
        return getFont(size, false);
    }

    public BitmapFont getFont(int size, boolean markupEnabled) {
        String key = "romanica-" + size + "-" + markupEnabled;
        synchronized (fonts) {
            BitmapFont font = fonts.get(key);
            if (font == null) {
                fonts.put(key, font = createFont(size, markupEnabled));
            }
            return font;
        }
    }

    private static BitmapFont createFont(int size, boolean markupEnabled) {
        FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("fonts/romanica.ttf"));
        FreeTypeFontParameter parameter = new FreeTypeFontParameter();
        try {
            parameter.size = size;
            BitmapFont font = generator.generateFont(parameter);
            font.getData().markupEnabled = markupEnabled;
            return font;
        } finally {
            generator.dispose(); // don't forget to dispose to avoid memory leaks!
        }
    }

    public Drawable getDrawable(Icon type) {
        return drawables.get(type);
    }

    public TextureRegion getTexture(Icon type) {
        return regions.get(type);
    }

    public Skin getSkin() {
        return skin;
    }


    @Override
    public void dispose() {
        manager.dispose();
    }
} public class EntityFactory {

  public static Entity createClick(World world, int x, int y, float startScale, float speed) {
    Entity e = world.createEntity();
    EntityEdit edit = e.edit();

    MutableMapPosition pos = edit.create(MutableMapPosition.class);
    pos.x = x;
    pos.y = y;

    Sprite sprite = new Sprite();
    sprite.name = "click";
    sprite.color = new Color(1f, 1f, 1f, 0.5f);
    sprite.rotation = 0f;
    sprite.scaleX = startScale;
    sprite.scaleY = startScale;
    edit.add(sprite);

    Expires expires = edit.create(Expires.class);
    expires.delay = 1f;

    ScaleAnimation scaleAnimation = edit.create(ScaleAnimation.class);
    scaleAnimation.speed = speed;

    ColorAnimation colorAnimation = edit.create(ColorAnimation.class);
    colorAnimation.alphaAnimate = true;
    colorAnimation.alphaSpeed = -1f;

    return e;
  }

  public static Entity createEmpire(World world, int x, int y, String name, Empire empire) {
    Entity e = world.createEntity();

    EntityEdit edit = e.edit();

    ArmyCommand command = new ArmyCommand();
    edit.add(empire).add(new EventsCount()).add(new Discoveries()).add(new Policies()).add(new Diplomacy())
        .add(command).add(new Score());

    Sprite sprite = new Sprite();
    sprite.name = "camp";
    sprite.rotation = 0f;
    sprite.scaleX = 1f;
    sprite.scaleY = 1f;
    sprite.color = empire.color;
    edit.add(sprite);

    InfluenceSource source = new InfluenceSource();
    edit.add(new MapPosition(x, y)).add(source).add(new Destination(command.forbiddenTiles, 2));

    edit.add(new Name(name)).add(new Description("Tribe of " + name))
        .add(new TextBox(() -> name + ": " + source.power()));

    if (empire.isComputerControlled())
      edit.add(new AIControlled());

    return e;
  }

  public static Entity createArmy(World world, MapPosition pos, String name, Empire empire, Entity source,
      int militaryPower) {
    Entity e = world.createEntity();
    EntityEdit edit = e.edit();

    edit.add(new Counter(Colors.contrast(empire.color), empire.color, militaryPower));

    edit.add(pos).add(new Name(name)).add(new Description(name))
        .add(new Destination(source.getComponent(ArmyCommand.class).forbiddenTiles, 1))
        .add(new Army(source, militaryPower)).add(empire);

    if (empire.isComputerControlled())
      edit.add(new AIControlled());

    return e;
  }

  public static Entity createFadingTileLabel(World world, String label, Color color, float x, float y, float duration) {
    Entity e = world.createEntity();
    EntityEdit edit = e.edit();

    MutableMapPosition position = edit.create(MutableMapPosition.class);
    position.x = x;
    position.y = y;

    FadingMessage fading = edit.create(FadingMessage.class);
    fading.label = label;
    fading.color = color;
    fading.duration = duration;
    fading.vx = 0f;
    fading.vy = 1.3f;

    return e;
  }

  public static Entity createFadingTileIcon(World world, TextureRegion icon, Color color, float x, float y,
      float duration) {
    Entity e = world.createEntity();
    EntityEdit edit = e.edit();

    MutableMapPosition position = edit.create(MutableMapPosition.class);
    position.x = x;
    position.y = y;

    FadingMessage fading = edit.create(FadingMessage.class);
    fading.icon = icon;
    fading.color = color;
    fading.duration = duration;
    fading.vx = 0f;
    fading.vy = 1.3f;

    return e;
  }

} public class ThingsThatWereGame extends Game {

  public int windowWidth;

  public int windowHeight;

  public Assets assets;

  private World world;

  private SpriteBatch batch;

  private CustomizeGameMenuScreen customScreen;

  private MainMenuScreen menuScreen;

  private SessionSettings settings;

  public ThingsThatWereGame(int width, int height) {
    /*
     * FIXME If height is greater that real screen height then the menu is
     * incorrectly set until the screen is resized. It appears easily on AC
     * laptop as default resolution is smaller. What happens is probably that
     * the OverworldScreen.stage is resized automatically but the resize()
     * method is not called.
     */
    windowWidth = width;
    windowHeight = height;
  }

  @Override
  public void create() {
    assets = new Assets();
    settings = new SessionSettings();

    startGame();
  }

  private void startGame() {
    world = new World();
    batch = new SpriteBatch();

    world.setSystem(new SpriteAnimationSystem());
    world.setSystem(new ScaleAnimationSystem());
    world.setSystem(new ExpiringSystem());
    world.setSystem(new ColorAnimationSystem());
    world.initialize();

    // this screen can modify the settings instance
    customScreen = new CustomizeGameMenuScreen(this, world, batch, settings);
    menuScreen = new MainMenuScreen(this, world, batch, customScreen);
    setScreen(menuScreen);
  }

  /** Quit current game and goes back to main menu. */
  public void returnToMainMenu() {
    if (getScreen() instanceof OverworldScreen)
      getScreen().dispose();

    // simply dispose everything and create anew!
    world.dispose();
    batch.dispose();
    customScreen.dispose();
    menuScreen.dispose();
    startGame();
  }

  /** Start a game. */
  public void startGame(boolean resetSettings) {
    OverworldScreen worldScreen = new OverworldScreen(this, batch, world,
            // copy settings because we want to keep for next game only the ones that
            // were customized in dedicated screen
            resetSettings ? new SessionSettings() : new SessionSettings(settings));
    setScreen(worldScreen);

    // show tutorial when starting a new game
    worldScreen.tutorialMenu();
  }

  @Override
  public void dispose() {
    super.dispose();
    world.dispose();
  }

  public void exit() {
    Gdx.app.exit();
  }

} @Wire
public final class AIArmyCommandSystem extends EntityProcessingSystem {

  private ComponentMapper<ArmyCommand> commands;

  private ComponentMapper<InfluenceSource> sources;

  private ComponentMapper<AIControlled> intelligences;

  private ArmiesSystem armiesSystem;

  private final GameMap map;

  private final OverworldScreen screen;

  @SuppressWarnings("unchecked")
  public AIArmyCommandSystem(GameMap map, OverworldScreen screen) {
    super(Aspect.getAspectForAll(AIControlled.class, ArmyCommand.class, InfluenceSource.class));
    this.map = map;
    this.screen = screen;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void process(Entity e) {
    ArmyCommand command = commands.get(e);
    int available = command.militaryPower - command.usedPower;
    if (available > 0 && available > command.usedPower / 2) {
      armiesSystem.createNewArmy(e, available);
    }

    // do not compute destinations at every turn
    AIControlled ai = intelligences.get(e);
    InfluenceSource source = sources.get(e);
    if (ai.armiesTargets.size() < source.secondarySources.size()
        || ai.armiesTargetsComputationTurn + 10 < screen.getTurnNumber()) {
      ai.armiesTargets.clear();

      ai.armiesTargets.addAll(source.influencedTiles);
      // send the armies to the influenced tiles with the smallest difference
      // between ours and second influence
      ai.armiesTargets.sort(Comparator.comparingInt(p -> map.getInfluenceAt(p).getSecondInfluenceDiff()));
    }

  }

} @Wire
public final class AIArmyMovementSystem extends EntityProcessingSystem {

  private static final Logger log = LoggerFactory.getLogger(AIArmyMovementSystem.class);

  private ComponentMapper<Destination> destinations;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<Army> armies;

  private ComponentMapper<AIControlled> intelligences;

  private DestinationSystem destinationSystem;

  private final OverworldScreen screen;

  private final GameMap map;

  @SuppressWarnings("unchecked")
  public AIArmyMovementSystem(GameMap map, OverworldScreen screen) {
    super(Aspect.getAspectForAll(AIControlled.class, Army.class));
    this.map = map;
    this.screen = screen;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void process(Entity e) {
    Destination dest = destinations.getSafe(e);
    if (dest == null)
      return;

    AIControlled ai = intelligences.get(e);
    // no need to also check if destination is still influenced by army
    // controller: worst case is it is recreated at capital
    if (dest.target == null) {
      setNewTarget(e, ai);
    } else {
      // check we are not stuck
      MapPosition current = positions.get(e);
      if (current.equals(ai.lastPosition)) {
        // are we stuck for 3 turns?
        if (screen.getTurnNumber() - ai.lastMove > 3)
          setNewTarget(e, ai);
      } else {
        ai.lastMove = screen.getTurnNumber();
        ai.lastPosition = current;
      }
    }
  }

  private void setNewTarget(Entity army, AIControlled ai) {
    AIControlled empire = intelligences.get(armies.get(army).source);

    for (int i = 0; i < empire.armiesTargets.size(); i++) {
      MapPosition pos = empire.armiesTargets.get(i);
      if (!pos.equals(positions.get(army)) && !map.hasEntity(pos)) {
        List<MapPosition> path = destinationSystem.computePath(army, pos);
        if (path != null) {
          ai.lastMove = screen.getTurnNumber();
          ai.lastPosition = positions.get(army);
          empire.armiesTargets.remove(i);
          return;
        }
      }
    }

    log.warn("Cannot find a destination for {}", army.getComponent(Description.class));
  }

} @Wire
public final class AIDiplomaticSystem extends EntityProcessingSystem {

  private static final Logger log = LoggerFactory.getLogger(AIDiplomaticSystem.class);

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<InfluenceSource> influences;

  private DiplomaticSystem diplomaticSystem;

  private final GameMap map;

  @SuppressWarnings("unchecked")
  public AIDiplomaticSystem(GameMap map) {
    super(Aspect.getAspectForAll(AIControlled.class, Diplomacy.class));
    this.map = map;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void process(Entity entity) {
    Diplomacy diplo = relations.get(entity);

    // first check if we could revolt from overlord
    List<Entity> lords = diplo.getEmpires(State.TRIBUTE);
    if (lords.size() > 0)
      for (Entity lord : lords)
        if (isWeaker(lord, entity))
          if (makeProposal(entity, diplo, lord, Action.DECLARE_WAR))
            return;

    // TODO have a real AI algorithm for diplomacy
    // neighbors from the nicest to the baddest
    List<Entity> neighbors = getNeighboringSources(entity);

    // sign treaty with half the neighbors, not the last one (WAR for him!)
    if (diplo.knownStates.contains(State.TREATY) && neighbors.size() > 2) {
      for (int i = 0; i < neighbors.size() / 2 && i < neighbors.size() - 1; i++) {
        Entity target = neighbors.get(i);
        makeProposal(entity, diplo, target, Action.SIGN_TREATY);
      }
    }

    // try to be at war with somebody, and only that somebody
    if (diplo.knownStates.contains(State.WAR)) {
      List<Entity> atWarWith = diplo.getEmpires(State.WAR);
      Entity target = neighbors.isEmpty() ? null : neighbors.get(neighbors.size() - 1);
      // to change our war target, first make peace with preceding one
      for (Entity war : atWarWith) {
        if (target != war)
          makeProposal(entity, diplo, war, Action.MAKE_PEACE);
      }
      if (target != null && !atWarWith.contains(target))
        makeProposal(entity, diplo, target, Action.DECLARE_WAR);
    }
  }

  private boolean isWeaker(Entity lord, Entity entity) {
    return influences.get(entity).power() >= influences.get(lord).power();
  }

  private boolean makeProposal(Entity entity, Diplomacy diplo, Entity target, Action action) {
    if (diplomaticSystem.getPossibleActions(diplo, target).contains(action)
    // do not change status if same proposal is already on the table
        && diplo.proposals.get(target) != action
        // do no make proposal to ourself
        && entity != target) {
      log.debug("{} wants to {} with {}", entity.getComponent(Name.class), action.str, target.getComponent(Name.class));
      diplo.proposals.put(target, action);
      return true;
    }
    return false;
  }

  /** Neighbors from the nicest to the worst. */
  private List<Entity> getNeighboringSources(Entity entity) {
    IntIntMap neighbors = new IntIntMap(16);
    MapPosition pos = positions.get(entity);
    for (int i = 1; i < 10; i++) {
      // TODO really search for all tiles
      addInfluencer(neighbors, entity, pos.x + i, pos.y + i);
      addInfluencer(neighbors, entity, pos.x - i, pos.y + i);
      addInfluencer(neighbors, entity, pos.x + i, pos.y - i);
      addInfluencer(neighbors, entity, pos.x - i, pos.y - i);
    }
    List<Entry> entries = new ArrayList<>();
    neighbors.entries().forEach(e -> entries.add(e));
    entries.sort((e1, e2) -> Integer.compare(e1.value, e2.value));
    List<Entity> res = new ArrayList<>(entries.size());
    entries.forEach(e -> res.add(world.getEntity(e.key)));
    return res;
  }

  private void addInfluencer(IntIntMap neighbors, Entity capital, int x, int y) {
    if (map.isOnMap(x, y)) {
      Influence inf = map.getInfluenceAt(x, y);
      if (inf != null && !inf.isMainInfluencer(capital) && inf.hasMainInfluence())
        neighbors.getAndIncrement(inf.getMainInfluenceSource(), 0, 1);
    }
  }
} @Wire
public final class AIDiscoverySystem extends EntityProcessingSystem {

  private static final Logger log = LoggerFactory.getLogger(AIDiscoverySystem.class);

  private ComponentMapper<Policies> policies;

  private ComponentMapper<Empire> empires;

  private DiscoverySystem system;

  @SuppressWarnings("unchecked")
  public AIDiscoverySystem() {
    super(Aspect.getAspectForAll(AIControlled.class, Discoveries.class, Empire.class));
  }

  @Override
  protected boolean checkProcessing() {
    return false;
  }

  @Override
  protected void process(Entity e) {
    // should not be called, selectNewDiscovery will be called when an event
    // triggers a discovery
  }

  public void selectNewDiscovery(Entity empire, Discoveries lab) {
    Culture culture = empires.get(empire).culture;
    Entry<Faction, Research> prefered = null;
    float max = Float.NEGATIVE_INFINITY;
    for (Entry<Faction, Research> possible : lab.nextPossible.entrySet()) {
      Faction faction = possible.getKey();
      float score = culture.ai.get(faction) * possible.getValue().target.factions.get(faction, 1);
      if (score > max) {
        max = score;
        prefered = possible;
      }
    }

    if (prefered != null) {
      log.debug("{} follows {} advice to investigate {} from {}", empire.getComponent(Name.class), prefered.getKey(),
          prefered.getValue().target, prefered.getValue().previous);
      system.discoverNew(empire, lab, prefered.getValue());
    }

    Set<Policy> newPolicies;
    if (lab.last != null && (newPolicies = PoliciesSystem.getPolicies(lab.last.target)) != null) {
      Policies empirePolicies = policies.get(empire);
      for (Policy p : newPolicies) {
        Discovery newD = lab.last.target;
        Discovery oldD = empirePolicies.policies.get(p);
        float oldScore = policyScore(oldD, culture);
        float newScore = policyScore(newD, culture);
        if (newScore > oldScore) {
          log.info("{} replaced policy {}={}(score={}) by {}(score={})", empire.getComponent(Name.class), p, oldD,
              oldScore, newD, newScore);
          empirePolicies.policies.put(p, newD);
        } else
          log.debug("{} did not replace policy {}={}(score={}) by {}(score={})", empire.getComponent(Name.class), p,
              oldD, oldScore, newD, newScore);
      }
    }
  }

  private static float policyScore(Discovery d, Culture culture) {
    if (d == null)
      return 0f;
    float score = 0f;
    for (Faction f : Faction.values())
      score += d.factions.get(f, 0f) * culture.ai.get(f).floatValue();
    return score;
  }

} @Wire
public final class AISourceDestinationSystem extends EntityProcessingSystem {

  private static final Logger log = LoggerFactory.getLogger(AISourceDestinationSystem.class);

  private final GameMap map;

  private ComponentMapper<Destination> destinations;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<AIControlled> intelligences;

  private DestinationSystem destinationSystem;

  private final OverworldScreen screen;

  @SuppressWarnings("unchecked")
  public AISourceDestinationSystem(GameMap gameMap, OverworldScreen screen) {
    super(Aspect.getAspectForAll(AIControlled.class, Destination.class, InfluenceSource.class));
    this.map = gameMap;
    this.screen = screen;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void process(Entity e) {
    Destination dest = destinations.getSafe(e);
    if (dest == null)
      return;

    AIControlled ai = intelligences.get(e);
    if (dest.target == null) {
      setNewTarget(e, dest, ai);
    } else {
      // check we are not stuck
      MapPosition current = positions.get(e);
      if (current.equals(ai.lastPosition)) {
        // are we stuck for 3 turns?
        if (screen.getTurnNumber() - ai.lastMove > 3)
          setNewTarget(e, dest, ai);
      } else {
        ai.lastMove = screen.getTurnNumber();
        ai.lastPosition = current;
      }
    }
  }

  private void setNewTarget(Entity e, Destination dest, AIControlled ai) {
    // ...must find another tile to influence
    int bestScore = 0;
    MapPosition best = null;
    for (MapPosition p : destinationSystem.getTargetTiles(e)) {
      if (p.equals(dest.target))
        // if it fails, do not select it again
        continue;
      int score = estimate(e, p);
      if (score > bestScore) {
        bestScore = score;
        best = p;
      }
    }

    if (best != null && !best.equals(positions.get(e))) {
      destinationSystem.computePath(e, best);
      ai.lastMove = screen.getTurnNumber();
      ai.lastPosition = positions.get(e);
    } else
      log.warn("Cannot find a destination for {}", e.getComponent(Description.class));
  }

  private int estimate(Entity e, MapPosition p) {
    int score = 0;
    for (MapPosition n : map.getNeighbors(p.x, p.y, 2)) {
      // worst case when near other city
      if (map.getEntityAt(n) != null)
        score -= 10;
      // some terrains are best
      Terrain terrain = map.getTerrainAt(p);
      if (terrain == Terrain.GRASSLAND || terrain == Terrain.PLAIN)
        score += 1;
      // go on the offensive!
      Influence inf = map.getInfluenceAt(p);
      if (!inf.isMainInfluencer(e))
        score += 1;
      // favor empty tiles
      if (!inf.hasMainInfluence())
        score *= 2;
    }
    return score;
  }
} @Wire
public final class ArmiesSystem extends EntitySystem {

  private static final Logger log = LoggerFactory.getLogger(ArmiesSystem.class);

  private ComponentMapper<Counter> counters;

  private ComponentMapper<InfluenceSource> sources;

  private ComponentMapper<ArmyCommand> commands;

  private ComponentMapper<Army> armies;

  private ComponentMapper<Description> descriptions;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<Name> names;

  private ComponentMapper<Empire> empires;

  private ComponentMapper<TextBox> boxes;

  private ComponentMapper<AIControlled> ai;

  private DestinationSystem destinationSystem;

  private NotificationsSystem notifications;

  private final GameMap map;

  private final OverworldScreen screen;

  @SuppressWarnings("unchecked")
  public ArmiesSystem(GameMap map, OverworldScreen screen) {
    super(Aspect.getAspectForAll(InfluenceSource.class));
    this.map = map;
    this.screen = screen;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity city : entities) {
      InfluenceSource source = sources.get(city);
      for (Iterator<Entity> it = source.secondarySources.iterator(); it.hasNext();) {
        Entity s = it.next();
        Army army = armies.get(s);
        if (onOtherInfluence(city, s)) {
          army.currentPower--;
          if (!ai.has(city))
            notifications.addNotification(() -> screen.select(s, true), null, Icon.MILITARY,
                "%s is depleting in other empire!", names.get(s));
        } else {
          if (army.currentPower < army.maxPower)
            army.currentPower += getArmyPowerIncrease(city, 1);
          else if (army.currentPower > army.maxPower)
            army.currentPower--;
        }

        if (army.currentPower <= 0) {
          // army is defeated, move it to capital
          army.currentPower = 0;
          MapPosition newPos = emptyPositionNextTo(city);
          if (newPos != null) {
            destinationSystem.moveTo(s, newPos);
            if (!ai.has(city))
              notifications.addNotification(() -> screen.select(s, true), null, Icon.MILITARY,
                  "Depleted %s moved to capital.", names.get(s));
          } else {
            s.deleteFromWorld();
            it.remove();
            if (!ai.has(city))
              notifications.addNotification(() -> screen.select(s, true), null, Icon.MILITARY,
                  "Depleted %s destroyed.", names.get(s));
          }
        }
        counters.get(s).value = army.currentPower;
      }
    }
  }

  private boolean onOtherInfluence(Entity city, Entity army) {
    Influence inf = map.getInfluenceAt(positions.get(army));
    return inf.hasMainInfluence() && !inf.isMainInfluencer(city);
  }

  public List<Entity> getArmies(Entity empire) {
    InfluenceSource source = sources.get(empire);
    return source.secondarySources;
  }

  public Entity createNewArmy(Entity empire, int power) {
    MapPosition pos = emptyPositionNextTo(empire);
    if (pos != null) {
      power = getArmyPowerIncrease(empire, power);
      ArmyCommand command = commands.get(empire);
      Entity army = EntityFactory.createArmy(world, pos, armyName(command, empire), empires.get(empire), empire, power);
      sources.get(empire).secondarySources.add(army);
      command.usedPower += power;

      map.setEntity(army, pos);
      return army;
    } else {
      log.warn("Cannot create an army near {}, no empty tile.", descriptions.get(empire));
      return null;
    }
  }

  private int getArmyPowerIncrease(Entity empire, int increase) {
    InfluenceSource source = sources.get(empire);
    increase = min(increase, source.power());
    source.addToPower(-increase);

    // update power displayed on screen
    TextBox box = boxes.get(empire);
    box.text = box.generator.get();

    return increase;
  }

  private String armyName(ArmyCommand command, Entity empire) {
    int nb = ++command.counter;
    return "Army of " + names.get(empire).name + " " + RomanNumbers.toRoman(nb);
  }

  private MapPosition emptyPositionNextTo(Entity empire) {
    MapPosition pos = positions.get(empire);
    for (int dist = 1; dist < 4; dist++) {
      for (MapPosition neighbor : map.getNeighbors(pos.x, pos.y, dist))
        if (!map.hasEntity(neighbor) && map.getInfluenceAt(neighbor).isMainInfluencer(empire))
          return neighbor;
    }
    // should rarely happen
    return null;
  }
} @Wire
public final class DestinationSystem extends EntitySystem {

    private static final Logger log = LoggerFactory.getLogger(DestinationSystem.class);

    private ComponentMapper<Destination> destinations;

    private ComponentMapper<MapPosition> positions;

    private ComponentMapper<Diplomacy> relations;

    private ComponentMapper<InfluenceSource> sources;

    private ComponentMapper<Army> armies;

    private ComponentMapper<Name> names;

    private ComponentMapper<AIControlled> ai;

    private NotificationsSystem notifications;

    private final GameMap map;

    private final PathFinding astar;

    private final OverworldScreen screen;

    @SuppressWarnings("unchecked")
    public DestinationSystem(GameMap map, OverworldScreen screen) {
        super(Aspect.getAspectForAll(Destination.class, MapPosition.class));
        this.map = map;
        this.screen = screen;
        this.astar = new PathFinding(map);
    }

    @Override
    protected void inserted(Entity e) {
        super.inserted(e);
        if (!ai.has(e))
            // make sure player moves the entity at least one time
            notifications.addNotification(() -> screen.select(e, true), () -> !needDestination(e),
                    Icon.FLAG, "Select destination for %s...", names.get(e));
    }

    private boolean needDestination(Entity e) {
        Destination d = destinations.getSafe(e);
        if (d == null)
            return false;
        else
            return d.target == null || d.path == null || d.path.isEmpty();
    }

    @Override
    protected void processEntities(ImmutableBag<Entity> entities) {
        for (Entity e : entities) {
            Destination destination = destinations.get(e);
            // Destination component might have been removed during this turn
            if (destination != null && destination.path != null && !destination.path.isEmpty())
                moveToNext(e, destination);
            // else no current destination
        }
    }

    private void moveToNext(Entity e, Destination dest) {
        MapPosition current = positions.get(e);
        MapPosition next = dest.path.get(0);
        if (canMoveTo(e, dest, next)) {
            if (++dest.progress >= dest.turnsToMove) {
                dest.progress = 0;
                dest.path.remove(0);
                e.edit().remove(current).add(next);
                map.moveEntity(e, current, next);
                if (sources.has(e)) {
                    Influence inf = map.getInfluenceAt(next);
                    // just make sure to have enough influence
                    inf.setInfluence(e, inf.getMaxInfluence());
                    if (inf.hasMainInfluence() && !inf.isMainInfluencer(e))
                        inf.setInfluence(e, inf.getMaxInfluence() + 1);
                }
                if (dest.path.isEmpty()) {
                    dest.target = null;
                    dest.path = null;
                    if (!ai.has(e)) {
                        // do not force player to move its units if he does not want to
                        notifications.addNotification(() -> screen.select(e, true), null,
                                Icon.FLAG, "Finished moving %s!", names.get(e));
                    }
                    log.debug("Moved {} to {}", names.get(e), next);
                }
            }
        }
    }

    public void moveTo(Entity e, MapPosition target) {
        MapPosition current = positions.get(e);
        e.edit().remove(current).add(target);
        map.moveEntity(e, current, target);
        if (destinations.has(e)) {
            Destination dest = destinations.get(e);
            dest.path = null;
            dest.target = null;
            dest.progress = 0;
        }
    }

    private boolean canMoveTo(Entity e, Destination dest, MapPosition next) {
        Terrain terrain = map.getTerrainAt(next);
        if (terrain.moveBlock() || dest.forbiddenTiles.contains(terrain) || map.hasEntity(next))
            return false;

        if (armies.has(e))
            return map.getInfluenceAt(next).isMainInfluencer(armies.get(e).source);
        else
            return map.getInfluenceAt(next).isMainInfluencer(e);
    }

    /**
     * Return the possible move target tiles for the source.
     */
    public Collection<MapPosition> getTargetTiles(Entity e) {
        Destination dest = destinations.get(e);
        Entity empire = armies.has(e) ? armies.get(e).source : e;
        Set<MapPosition> set = new HashSet<>();
        InfluenceSource source = sources.get(empire);
        Diplomacy treaties = relations.get(empire);
        for (MapPosition pos : source.influencedTiles) {
            for (MapPosition neighbor : map.getNeighbors(pos)) {
                if (!dest.forbiddenTiles.contains(map.getTerrainAt(neighbor))) {
                    Influence inf = map.getInfluenceAt(neighbor);
                    if (!inf.hasMainInfluence())
                        set.add(neighbor);
                    else if (treaties.getRelationWith(inf.getMainInfluenceSource(world)) != State.TREATY)
                        set.add(neighbor);
                }
            }
        }
        return set;
    }

    /**
     * Compute the path for the entity to the target. Returns <code>null</code> if
     * no path can be found.
     */
    public List<MapPosition> computePath(Entity e, MapPosition target) {
        Destination dest = destinations.get(e);
        MapPosition start = positions.get(e);
        List<MapPosition> path = astar.aStarSearch(start, target,//
                p -> (!dest.forbiddenTiles.contains(map.getTerrainAt(p)) || map.getTerrainAt(start) == map.getTerrainAt(p))
                        && map.getEntityAt(p) == null);

        if (path == null)
            return null;

        dest.target = target;
        dest.path = path;
        dest.progress = 0;
        return path;
    }

} @Wire
public final class DiplomaticSystem extends EntitySystem {

  private static final Logger log = LoggerFactory.getLogger(DiplomaticSystem.class);

  public enum State {
    NONE, WAR, TREATY, PROTECTORATE, TRIBUTE;
  }

  public enum Action {

    NO_CHANGE("no change", null, null, allOf(State.class), a -> false),

    DECLARE_WAR("declare war", State.WAR, State.WAR, complementOf(of(State.WAR)), a -> true),

    MAKE_PEACE("make peace", State.NONE, State.NONE, of(State.WAR), a -> false),

    SIGN_TREATY("sign a treaty", State.TREATY, State.TREATY, of(State.WAR, State.NONE, State.TRIBUTE,
        State.PROTECTORATE), a -> a == MAKE_PEACE),

    SURRENDER("surrender", State.TRIBUTE, State.PROTECTORATE, of(State.WAR), a -> a == MAKE_PEACE || a == SIGN_TREATY);

    public final String str;

    public final Set<State> before;

    private final State afterMe;

    private final State afterYou;

    private final Predicate<Action> compatibleWith;

    private Action(String str, State afterMe, State afterYou, Set<State> before, Predicate<Action> compatibleWith) {
      this.str = str;
      this.afterMe = afterMe;
      this.afterYou = afterYou;
      this.before = before;
      this.compatibleWith = compatibleWith;
    }

    public boolean compatibleWith(Action targetProposal) {
      return targetProposal == this || compatibleWith.test(targetProposal);
    }

  }

  private NotificationsSystem notifications;

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<AIControlled> ai;

  private final OverworldScreen screen;

  private final boolean startWithDiplomacy;

  @SuppressWarnings("unchecked")
  public DiplomaticSystem(OverworldScreen screen, boolean startWithDiplomacy) {
    super(Aspect.getAspectForAll(Diplomacy.class));
    this.screen = screen;
    this.startWithDiplomacy = startWithDiplomacy;
  }

  @Override
  protected void inserted(Entity e) {
    super.inserted(e);
    if (startWithDiplomacy)
      relations.get(e).knownStates.addAll(Arrays.asList(State.values()));
  }

  @Override
  protected void removed(Entity removed) {
    super.removed(removed);
    log.debug("Remove {} from all empires relations.", removed.getComponent(Description.class));
    for (Entity e : getActives()) {
      Diplomacy diplo = relations.get(e);
      diplo.lastChange.remove(removed, 0);
      diplo.proposals.remove(removed);
      diplo.relations.remove(removed);
    }
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity entity : entities) {
      Diplomacy diplo = relations.get(entity);
      for (Iterator<Entry<Entity, Action>> it = diplo.proposals.entrySet().iterator(); it.hasNext();) {
        Entry<Entity, Action> proposal = it.next();
        Entity target = proposal.getKey();
        Action action = proposal.getValue();
        Diplomacy targetDiplo = relations.get(target);
        if (action.compatibleWith(targetDiplo.proposals.get(entity))) {
          // remove the accepted proposal
          it.remove();
          changeRelation(entity, diplo, target, targetDiplo, action);
        }
      }
    }
  }

  void changeRelation(Entity entity, Diplomacy diplo, Entity target, Diplomacy targetDiplo, Action action) {
    diplo.relations.put(target, action.afterMe);
    targetDiplo.relations.put(entity, action.afterYou);
    targetDiplo.proposals.remove(entity);
    // prevent changing state for a few turns
    diplo.lastChange.put(target, screen.getTurnNumber());
    targetDiplo.lastChange.put(entity, screen.getTurnNumber());
    // notify the player
    log.info("Relation between {} and {} is now {}/{}", entity.getComponent(Name.class),
        target.getComponent(Name.class), diplo.getRelationWith(target), targetDiplo.getRelationWith(entity));
    if (!ai.has(entity))
      notifications.addNotification(screen::diplomacyMenu, null, Icon.DIPLOMACY,
              "Your relation with %s is now %s!", target.getComponent(Name.class), action.afterMe);
    else if (!ai.has(target))
      notifications.addNotification(screen::diplomacyMenu, null, Icon.DIPLOMACY,
              "Your relation with %s is now %s!", entity.getComponent(Name.class), action.afterYou);
  }

  public void clearRelations(Entity empire, Diplomacy diplo) {
    diplo.proposals.clear();
    for (Entity e : diplo.relations.keySet()) {
      relations.get(e).relations.remove(empire);
    }
    diplo.relations.clear();
  }

  public Collection<Action> getPossibleActions(Diplomacy diplo, Entity target) {
    Set<Action> actions = EnumSet.of(Action.NO_CHANGE);
    for (Action action : Action.values()) {
      if (diplo.knownStates.contains(action.afterMe) && action.before.contains(diplo.getRelationWith(target)))
        actions.add(action);
    }
    return actions;
  }
} @Wire
public final class DisasterSystem extends EntitySystem implements EventHandler {

    private ComponentMapper<InfluenceSource> sources;

    private ComponentMapper<Empire> empires;

    private ComponentMapper<MapPosition> positions;

    private NotificationsSystem notifications;

    private FadingMessageRenderSystem fadingSystem;

    private final OverworldScreen screen;

    @SuppressWarnings("unchecked")
    public DisasterSystem(OverworldScreen screen) {
        super(Aspect.getAspectForAll(InfluenceSource.class));
        this.screen = screen;
    }

    @Override
    public String getType() {
        return "Disaster";
    }

    @Override
    protected void initialize() {
        super.initialize();

        world.getSystem(EventsSystem.class).addEventType(this);
    }

    @Override
    protected boolean checkProcessing() {
        return false;
    }

    @Override
    protected void processEntities(ImmutableBag<Entity> entities) {
        // done from events
    }

    @Override
    public int getProgress(Entity e) {
        InfluenceSource source = sources.get(e);
        int unhealth = source.power() - source.health;
        return max(0, unhealth / 2);
    }

    @Override
    public String getReason(Entity e) {
        InfluenceSource source = sources.get(e);
        int power = source.power();
        int health = source.health;
        if (health >= power)
            return "Health (" + health + ") >= Power (" + power + ")";
        else
            return "Health (" + health + ") < Power (" + power + ")";
    }

    @Override
    public boolean execute(Entity e) {
        InfluenceSource source = sources.get(e);
        int old = source.power();
        source.setPower(source.health / 2);
        int delta = old - source.power();

        fadingSystem.createFadingIcon(Icon.DISEASE, empires.get(e).color, positions.get(e), 3f);
        notifications.addNotification(() -> screen.select(e, false), null, Icon.DISEASE,
                "Mortal disease strikes %s!\nLost %d power...", e.getComponent(Description.class), delta);

        return true;
    }

} @Wire
public final class DiscoverySystem extends EntitySystem implements EventHandler {

  private static final Logger log = LoggerFactory.getLogger(DiscoverySystem.class);

  /** Increase to speed progress up. */
  private static final int PROGRESS_PER_TILE = 1;

  private final OverworldScreen screen;

  private final Map<String, Discovery> discoveries;

  /** Discoveries free to be discovered by any empire. */
  private final Map<String, Discovery> toDiscover;

  private final GameMap map;

  private final Random rand = new Random();

  private NotificationsSystem notifications;

  private EffectsSystem effects;

  private SpecialDiscoveriesSystem special;

  private AIDiscoverySystem aiSystem;

  private ComponentMapper<Discoveries> empires;

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<InfluenceSource> influences;

  private ComponentMapper<AIControlled> ai;

  @SuppressWarnings("unchecked")
  public DiscoverySystem(SessionSettings s, GameMap map, OverworldScreen screen) {
    super(Aspect.getAspectForAll(Discoveries.class));
    this.discoveries = s.getDiscoveries();
    this.toDiscover = new HashMap<>(discoveries);
    this.map = map;
    this.screen = screen;
  }

  @Override
  public String getType() {
    return "Discovery";
  }

  @Override
  protected void initialize() {
    super.initialize();

    special.checkDiscoveries(discoveries);

    // check previous exist
    // set factions scores
    Set<String> groups = new HashSet<>();
    for (Discovery d : discoveries.values())
      groups.addAll(d.groups);
    for (Discovery d : discoveries.values()) {
      d.factions = getFactionsScores(d);

      for (Set<String> list : d.previous)
        for (String previous : list)
          if (!discoveries.containsKey(previous) && !groups.contains(previous))
            log.warn("Cannot find previous {} for discovery {}", previous, d);
    }

    world.getSystem(EventsSystem.class).addEventType(this);
  }

  private ObjectFloatMap<Faction> getFactionsScores(Discovery discovery) {
    ObjectFloatMap<Faction> scores = effects.getFactionsScores(discovery.effects);
    for (String group : discovery.groups)
      if (Policy.get(group) != null) {
        for (Faction f : Faction.values())
          scores.put(f, scores.get(f, 0f) * 1.5f);
        scores.getAndIncrement(Faction.CULTURAL, 0, 0.5f);
      }
    if (scores.size == 0)
      scores.getAndIncrement(Faction.CULTURAL, 0, 0.5f);
    return scores;
  }

  @Override
  protected boolean checkProcessing() {
    return false;
  }

  @Override
  protected void inserted(Entity e) {
    super.inserted(e);
    Discoveries d = empires.get(e);
    d.nextPossible = possibleDiscoveries(e, d);
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    // progress toward next discovery is triggered by EventsSystem
  }

  /**
   * Compute the possible discoveries for the empire, associated to the faction
   * that recommends them.
   */
  private Map<Faction, Research> possibleDiscoveries(Entity empire, Discoveries labs) {
    // collect current state
    Set<String> done = new HashSet<>();
    Map<String, List<String>> groups = new HashMap<>();
    for (Discovery d : labs.done) {
      done.add(d.name);
      done.addAll(d.groups);
      for (String g : d.groups) {
        List<String> list = groups.get(g);
        if (list == null)
          groups.put(g, list = new ArrayList<>());
        list.add(d.name);
      }
    }

    Set<Discovery> toDiscover = new HashSet<>(this.toDiscover.values());

    // can only discover discoveries from already known groups if discovered by
    // neighbors, avoid having the same empire discovering all cereals
    toDiscover.removeIf(d -> !d.groups.isEmpty() && groups.keySet().containsAll(d.groups));

    // add discoveries already made by neighboring empires
    for (Entity neighbor : getNeighbors(empire))
      for (Discovery d : empires.get(neighbor).done)
        if (!done.contains(d.name))
          toDiscover.add(d);

    // select new possible discoveries
    List<Research> possible = toDiscover.stream().filter(d -> !done.contains(d.name) && hasTerrain(empire, d.terrains))
        .map(d -> toResearch(d, done, groups)).filter(Objects::nonNull).collect(toList());

    Map<Faction, Research> res = new EnumMap<>(Faction.class);
    for (Faction f : Faction.values()) {
      if (possible.isEmpty()) {
        log.debug("Found no {} research among {} for {}", f, possible, empire.getComponent(Description.class));
        continue;
      }

      possible.sort((Research d1, Research d2) -> -Float.compare(
              d1.factions.get(f, 0f),
              d2.factions.get(f, 0f)
      ));

      Research selected = null;
      for (int i = rand.nextInt(min(2, possible.size())); i >= 0 && selected == null; i--)
        if (possible.get(i).factions.get(f, 0f) >= 0f)
          selected = possible.remove(i);

      if (selected != null)
        res.put(f, selected);
      else
        log.info("Found no {} research among {} for {}", f, possible, empire.getComponent(Description.class));
    }
    return res;
  }

  private Research toResearch(Discovery d, Set<String> done, Map<String, List<String>> groups) {
    if (d.previous.isEmpty())
      return new Research(d, Collections.emptyList());

    List<Discovery> previous = new ArrayList<>(d.previous.size());
    for (Collection<String> l : d.previous)
      if (done.containsAll(l)) {
        for (String p : l) {
          List<String> group = groups.get(p);
          previous.add(discoveries.get(group == null ? p : group.get(rand.nextInt(group.size()))));
        }
        return new Research(d, previous);
      }
    return null;
  }

  private boolean hasTerrain(Entity entity, Set<Terrain> terrains) {
    if (terrains == null || terrains.isEmpty())
      return true;

    InfluenceSource influence = influences.get(entity);
    for (MapPosition pos : influence.influencedTiles)
      if (terrains.contains(map.getTerrainAt(pos)))
        return true;

    return false;
  }

  private Iterable<Entity> getNeighbors(Entity empire) {
    IntMap<Entity> neighbors = new IntMap<>();

    // collect entities we have relations with
    for (Entity e : relations.get(empire).relations.keySet())
      neighbors.put(e.getId(), e);

    // collect entities we share influence with
    for (MapPosition p : influences.get(empire).influencedTiles)
      for (Entry inf : map.getInfluenceAt(p))
        if (inf.key != empire.getId() && !neighbors.containsKey(inf.key)
        // may be pending insertion into world if just revolted
            && world.getEntityManager().isActive(inf.key))
          neighbors.put(inf.key, world.getEntity(inf.key));

    return neighbors.values();
  }

  public List<String> effectsStrings(Discovery d) {
    return effects.toString(d.effects);
  }

  public void copyDiscoveries(Entity from, Entity to) {
    Discoveries fromD = empires.get(from);
    Discoveries toD = empires.get(to);
    for (Discovery d : fromD.done)
      if (toD.done.add(d)) {
        effects.apply(d.effects, to, false);
        special.apply(to, d, toD);
      }
  }

  @Override
  public int getProgress(Entity e) {
    Discoveries discovery = empires.get(e);
    InfluenceSource influence = influences.get(e);

    int progress = discovery.progressPerTurn;
    Set<Terrain> terrains = discovery.last != null ? discovery.last.target.terrains : null;
    if (terrains != null && !terrains.isEmpty()) {
      for (MapPosition pos : influence.influencedTiles) {
        if (terrains.contains(map.getTerrainAt(pos)))
          progress += PROGRESS_PER_TILE;
      }
    }
    return progress;
  }

  @Override
  public String getReason(Entity e) {
    InfluenceSource influence = influences.get(e);
    return influence.influencedTiles.size() + " tiles";
  }

  @Override
  public boolean execute(Entity e) {
    return discoverNext(e, empires.get(e));
  }

  private boolean discoverNext(Entity empire, Discoveries discovery) {
    discovery.nextPossible = possibleDiscoveries(empire, discovery);

    if (discovery.nextPossible.isEmpty())
      return false;

    if (!ai.has(empire)) {
      Research last = discovery.last;
      notifications.addNotification(screen::askDiscovery, () -> discovery.last != last, Icon.DISCOVERY,
          "We can make a new discovery!");
    } else
      aiSystem.selectNewDiscovery(empire, discovery);

    return true;
  }

  /** Called when the next discovery is chosen. */
  public void discoverNew(Entity empire, Discoveries discovery, Research research) {
    Discovery target = research.target;
    log.info("{} discovered {} from {}.", empire.getComponent(Name.class), target, research.previous);
    discovery.done.add(target);
    discovery.nextPossible.clear();

    if (!ai.has(empire)) {
      Set<Policy> policies = PoliciesSystem.getPolicies(target);
      if (policies != null) {
        for (Policy policy : policies)
          notifications.addNotification(screen::policiesMenu, null, Icon.DISCOVERY,
                  "New %s policy: %s", policy, target);
      }
    }

    // Once an empire made a discovery, none other can research it. Those
    // already researching it can continue. It makes sure starting discoveries
    // are not found only by a single empire.
    if (!target.previous.isEmpty())
      toDiscover.remove(target.name);

    // remove last discovery 2x bonus (to keep only the basic effect)
    if (discovery.last != null)
      effects.apply(discovery.last.target.effects, empire, true);
    discovery.last = research;

    // apply new discovery
    effects.apply(target.effects, empire, false);
    // ... and bonus time until next discovery!
    effects.apply(target.effects, empire, false);

    // may be some 'special discovery'
    special.apply(empire, target, discovery);

    MapPosition pos = empire.getComponent(MapPosition.class);
    EntityFactory.createFadingTileLabel(world, target.name, empire.getComponent(Empire.class).color, pos.x, pos.y, 3f);
  }

} @Wire
public final class EffectsSystem extends VoidEntitySystem {

  private ComponentMapper<Policies> policies;

  private ComponentMapper<ArmyCommand> armies;

  private ComponentMapper<Discoveries> discoveries;

  private ComponentMapper<Diplomacy> diplomacies;

  private ComponentMapper<InfluenceSource> influences;

  private final class TerrainEffect implements Effect<Number> {
    private final Terrain terrain;

    public TerrainEffect(Terrain t) {
      this.terrain = t;
    }

    @Override
    public void apply(Number value, Entity empire, boolean revert) {
      InfluenceSource source = influences.get(empire);
      int delta = value.intValue();
      Integer current = source.modifiers.terrainBonus.get(terrain);
      if (revert)
        source.modifiers.terrainBonus.put(terrain, current == null ? 0 : current - delta);
      else
        source.modifiers.terrainBonus.put(terrain, current == null ? delta : current + delta);
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, Number value) {
      float delta = value.intValue();
      scores.getAndIncrement(Faction.MILITARY, 0, delta / 2f);
      scores.getAndIncrement(Faction.ECONOMIC, 0, delta);
    }

    @Override
    public String toString(Number value) {
      int i = value.intValue();
      return terrain.getDesc() + (i > 0 ? ": +" : " ") + i;
    }
  }

  private final class StabilityEffect implements Effect<Number> {
    @Override
    public void apply(Number value, Entity empire, boolean revert) {
      Policies p = policies.get(empire);
      if (revert) {
        p.stabilityGrowth -= value.intValue();
        p.stabilityMax -= value.intValue();
      } else {
        p.stabilityGrowth += value.intValue();
        p.stabilityMax += value.intValue();
      }
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, Number value) {
      float delta = value.intValue();
      scores.getAndIncrement(Faction.ECONOMIC, 0, delta / 2f);
      scores.getAndIncrement(Faction.CULTURAL, 0, delta);
    }

    @Override
    public String toString(Number value) {
      int i = value.intValue();
      return (i > 0 ? "stability: +" : "stability: ") + i;
    }
  }

  private final class MilitaryEffect implements Effect<Number> {
    @Override
    public void apply(Number value, Entity empire, boolean revert) {
      ArmyCommand army = armies.get(empire);
      int delta = 2 * value.intValue();
      if (revert)
        army.militaryPower -= delta;
      else
        army.militaryPower += delta;
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, Number value) {
      float delta = value.intValue();
      scores.getAndIncrement(Faction.MILITARY, 0, delta * 3f);
      scores.getAndIncrement(Faction.CULTURAL, 0, -delta * 3f);
    }

    @Override
    public String toString(Number value) {
      int delta = 2 * value.intValue();
      return (delta > 0 ? "military power: +" : "military power: ") + delta;
    }
  }

  private final class DiplomacyEffect implements Effect<String> {
    @Override
    public void apply(String value, Entity empire, boolean revert) {
      // cannot revert that one
      Diplomacy diplomacy = diplomacies.get(empire);
      diplomacy.knownStates.add(State.valueOf(value));
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, String value) {
      scores.getAndIncrement(Faction.MILITARY, 0, 3f);
      scores.getAndIncrement(Faction.ECONOMIC, 0, 1f);
      scores.getAndIncrement(Faction.CULTURAL, 0, 0.5f);
    }

    @Override
    public String toString(String value) {
      return "diplomacy: " + value;
    }
  }

  private final class DiscoveryEffect implements Effect<Number> {
    @Override
    public void apply(Number value, Entity empire, boolean revert) {
      Discoveries d = discoveries.get(empire);
      if (revert)
        d.progressPerTurn -= value.intValue();
      else
        d.progressPerTurn += value.intValue();
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, Number value) {
      float delta = value.intValue();
      scores.getAndIncrement(Faction.CULTURAL, 0, delta * 2);
    }

    @Override
    public String toString(Number value) {
      int i = value.intValue();
      return (i > 0 ? "discovery: +" : "discovery: ") + i;
    }
  }

  private final class GrowthEffect implements Effect<Number> {
    @Override
    public void apply(Number value, Entity empire, boolean revert) {
      InfluenceSource source = influences.get(empire);
      if (revert)
        source.growth -= value.intValue();
      else
        source.growth += value.intValue();
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, Number value) {
      float delta = value.intValue();
      scores.getAndIncrement(Faction.ECONOMIC, 0, delta * 2);
      scores.getAndIncrement(Faction.CULTURAL, 0, delta / 2);
    }

    @Override
    public String toString(Number value) {
      int i = value.intValue();
      return (i > 0 ? "growth: +" : "growth: ") + (i / 10f) + "%/turn";
    }
  }

  private final class HealthEffect implements Effect<Number> {
    @Override
    public void apply(Number value, Entity empire, boolean revert) {
      InfluenceSource source = influences.get(empire);
      if (revert)
        source.health -= value.intValue();
      else
        source.health += value.intValue();
    }

    @Override
    public void addFactionsScores(ObjectFloatMap<Faction> scores, Number value) {
      float delta = value.intValue();
      scores.getAndIncrement(Faction.ECONOMIC, 0, delta * 2);
      scores.getAndIncrement(Faction.CULTURAL, 0, delta / 2);
    }

    @Override
    public String toString(Number value) {
      int i = value.intValue();
      return (i > 0 ? "health: +" : "health: ") + i;
    }
  }

  private interface Effect<V> {

    void apply(V value, Entity empire, boolean revert);

    void addFactionsScores(ObjectFloatMap<Faction> scores, V value);

    String toString(V value);

  }

  private final Map<String, Effect<?>> effects = new HashMap<>();
  {
    effects.put("stability", new StabilityEffect());
    effects.put("discovery", new DiscoveryEffect());
    effects.put("growth", new GrowthEffect());
    effects.put("health", new HealthEffect());
    effects.put("diplomacy", new DiplomacyEffect());
    effects.put("militaryPower", new MilitaryEffect());

    for (Terrain t : Terrain.values())
      effects.put(t.name(), new TerrainEffect(t));
  }

  public EffectsSystem() {
  }

  @Override
  protected void processSystem() {
    // nothing to do, called by other systems to add and remove effects
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  public void apply(Map<String, Object> effectsMap, Entity empire, boolean revert) {
    for (Entry<String, Object> e : effectsMap.entrySet()) {
      Effect effect = effects.get(e.getKey());
      effect.apply(e.getValue(), empire, revert);
    }
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  public List<String> toString(Map<String, Object> effectsMap) {
    List<String> list = new ArrayList<>();
    for (Entry<String, Object> e : effectsMap.entrySet()) {
      Effect effect = effects.get(e.getKey());
      list.add(effect.toString(e.getValue()));
    }
    return list;
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  public ObjectFloatMap<Faction> getFactionsScores(Map<String, Object> effectsMap) {
    ObjectFloatMap<Faction> scores = new ObjectFloatMap<>(Faction.values().length);
    for (Entry<String, Object> effect : effectsMap.entrySet()) {
      Effect e = effects.get(effect.getKey());
      e.addFactionsScores(scores, effect.getValue());
    }
    return scores;
  }
} @Wire
public final class EventsSystem extends EntitySystem {

  public interface EventHandler {

    /** Get the score progress toward next event of this type. */
    int getProgress(Entity e);

    /** Get the reason for this event. */
    String getReason(Entity e);

    /**
     * Execute the event on the entity.
     * 
     * @return true if it could execute the event (false if some system
     *         condition prevented it)
     */
    boolean execute(Entity e);

    /** A pretty printing name for the event. */
    String getType();

  }

  private ComponentMapper<EventsCount> counts;

  private final List<EventHandler> types = new ArrayList<>();

  public EventsSystem() {
    super(Aspect.getAspectForAll(EventsCount.class));
  }

  public void addEventType(EventHandler type) {
    types.add(type);
  }

  @Override
  protected void inserted(Entity e) {
    super.inserted(e);
    EventsCount count = counts.get(e);
    for (EventHandler type : types) {
      count.scores.put(type, 0);
      count.reasons.put(type, type.getReason(e));
    }
    count.increment.putAll(count.scores);
    count.display.putAll(count.scores);
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity e : entities)
      checkEvents(e);
  }

  private void checkEvents(Entity e) {
    int maxScore = Integer.MIN_VALUE;
    EventHandler selected = null;
    EventsCount count = counts.get(e);
    for (EventHandler type : count.scores.keys()) {
      int progress = type.getProgress(e);
      int newScore = progress + count.scores.get(type, 0);
      count.increment.put(type, progress);
      count.display.put(type, newScore);
      count.scores.put(type, newScore);
      count.reasons.put(type, type.getReason(e));
      if (newScore > maxScore) {
        selected = type;
        maxScore = newScore;
      }
    }
    if (selected != null) {
      if (selected.execute(e))
        // also reset score
        count.scores.put(selected, 0);
    }
  }
} @Wire
public final class InfluenceSystem extends EntitySystem {

  private static final Logger log = LoggerFactory.getLogger(InfluenceSystem.class);

  public static final int INITIAL_POWER = 100;

  private ComponentMapper<Empire> data;

  private ComponentMapper<InfluenceSource> sources;

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<ArmyCommand> commands;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<Army> armies;

  private DiplomaticSystem diplomaticSystem;

  private final GameMap map;

  @SuppressWarnings("unchecked")
  public InfluenceSystem(GameMap gameMap) {
    super(Aspect.getAspectForAll(InfluenceSource.class));
    this.map = gameMap;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void inserted(Entity e) {
    super.inserted(e);
    InfluenceSource source = sources.get(e);
    if (source.power() > 0) {
      MapPosition pos = positions.get(e);

      // first influence own tile
      Influence tile = map.getInfluenceAt(pos);
      if (tile.hasMainInfluence()) {
        Entity main = tile.getMainInfluenceSource(world);
        tile.setInfluence(e, tile.getMaxInfluence() + tile.getDelta(main) + 1);
      } else {
        tile.setInfluence(e, tile.getMaxInfluence());
      }
      source.influencedTiles.add(pos);
    }
  }

  @Override
  protected void removed(Entity e) {
    super.removed(e);
    for (int x = 0; x < map.width; x++)
      for (int y = 0; y < map.height; y++)
        map.getInfluenceAt(x, y).removeInfluence(e);
    map.setEntity(null, positions.get(e));

    InfluenceSource source = sources.get(e);
    source.influencedTiles.clear();
    for (Entity army : source.secondarySources)
      map.setEntity(null, positions.get(army));
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> empires) {
    // must apply each step to all sources to have a consistent behavior

    // prepare new influence target that will be computed
    for (int x = 0; x < map.width; x++)
      for (int y = 0; y < map.height; y++)
        map.getInfluenceAt(x, y).clearInfluenceTarget();

    // and update source power
    for (Entity empire : empires) {
      updateInfluencedTiles(empire);
      accumulatePower(empire);
      checkInfluencedByOther(sources.get(empire), empire);
    }

    // then compute the new delta for every entity and tile
    for (Entity empire : empires) {
      IntIntMap armyInfluenceOn = new IntIntMap();
      Diplomacy diplo = relations.get(empire);
      int armyPower = commands.get(empire).militaryPower;
      for (Entity enemy : diplo.getEmpires(State.WAR))
        armyInfluenceOn.put(enemy.getId(), armyPower - commands.get(enemy).militaryPower);

      updateInfluenceTarget(sources.get(empire), empire, armyInfluenceOn);
    }

    // finally compute new influence at start of turn
    for (int x = 0; x < map.width; x++)
      for (int y = 0; y < map.height; y++)
        updateTileInfluence(x, y);
  }

  /**
   * When a city tile main influence belongs to an other empire, we add a
   * tribute diplomatic relation.
   */
  private void checkInfluencedByOther(InfluenceSource source, Entity empire) {
    Influence tile = map.getInfluenceAt(positions.get(empire));
    if (!tile.isMainInfluencer(empire) && tile.hasMainInfluence()) {
      Diplomacy loser = relations.get(empire);
      Entity influencer = tile.getMainInfluenceSource(world);
      if (empire != influencer && loser.getRelationWith(influencer) != State.TRIBUTE) {
        log.info("{} conquered by {}, will now be tributary to its conqueror.", empire.getComponent(Description.class),
            influencer.getComponent(Description.class));
        source.addToPower(-1);
        sources.get(influencer).addToPower(1);
        if (source.power() <= 0) {
          log.info("{} conquered by {}, was destroyed.", empire.getComponent(Description.class),
              influencer.getComponent(Description.class));
          delete(empire);
        } else {
          diplomaticSystem.clearRelations(empire, loser);
          diplomaticSystem.changeRelation(empire, loser, influencer, relations.get(influencer), Action.SURRENDER);
          loser.proposals.remove(influencer);

          // keep influence on own tile...
          tile.moveInfluence(influencer, empire);
          source.influencedTiles.add(tile.position);
          // ...and neighbors
          for (Border b : Border.values()) {
            Influence t = map.getInfluenceAt(b.getNeighbor(tile.position));
            // do not modify influence if there is an army or city on the tile
            if (t != null && !map.hasEntity(t.position)) {
              t.moveInfluence(influencer, empire);
              if (t.isMainInfluencer(empire))
                source.influencedTiles.add(t.position);
            }
          }
        }
      }
    }
  }

  private void delete(Entity entity) {
    log.info("{} ({}) is deleted", entity.getComponent(Description.class), entity);
    map.setEntity(null, positions.get(entity));
    if (sources.has(entity))
      for (Entity s : sources.get(entity).secondarySources)
        delete(s);
    entity.edit().deleteEntity();
  }

  private static final String[] DELTA_STR = { "----", "---", "--", "-", "", "+", "++", "+++", "++++" };

  private void updateTileInfluence(int x, int y) {
    Influence tile = map.getInfluenceAt(x, y);
    float shift = 0f;
    for (IntIntMap.Entry e : tile.getDelta()) {
      if (e.value != 0) {
        Entity empire = world.getEntity(e.key);
        String text = abs(e.value) > 4 ? (e.value > 0 ? "+" + e.value : Integer.toString(e.value))
            : DELTA_STR[e.value + 4];
        EntityFactory.createFadingTileLabel(world, text, data.get(empire).color, x, y + shift, 1f);
        shift += 0.2;
      }
    }
    tile.computeNewInfluence();
    // do not forget to update the main source
    Entity main = tile.getMainInfluenceSource(world);
    if (main != null)
      sources.get(main).influencedTiles.add(map.getPositionAt(x, y));
  }

  /**
   * Spread source influence around the source. For each tile already influenced
   * or next to one we compute the minimal distance to the source. Then from the
   * distance we compute the target influence level. Then apply the delta.
   */
  private void updateInfluenceTarget(InfluenceSource source, Entity e, IntIntMap armyInfluenceOn) {
    // first compute the target influence from the city itself
    for (ObjectIntMap.Entry<Influence> entry : getTargetInfluence(e, positions.get(e), source.power())) {
      Influence tile = entry.key;
      int target = entry.value;

      // start losing influence when no neighboring tile
      if (canInfluence(e, tile))
        tile.increaseTarget(e, target);
      else {
        // pass influence to protectorates
        for (Entry inf : tile) {
          Entity influencer = world.getEntity(inf.key);
          if (relations.get(influencer).getRelationWith(e) == State.PROTECTORATE)
            tile.increaseTarget(influencer, target);
        }
      }

      // do not forget the military bonus from war
      if (tile.hasMainInfluence())
        tile.increaseTarget(e, armyInfluenceOn.get(tile.getMainInfluenceSource(), 0));
    }

    // then add influence from its armies
    for (Entity s : source.secondarySources) {
      for (ObjectIntMap.Entry<Influence> entry : getTargetInfluence(e, positions.get(s), armies.get(s).currentPower)) {
        // armies only add to influence, they do not reduce it
        Influence inf = entry.key;
        int target = entry.value;

        if (target > 0 && canInfluence(e, inf))
          inf.increaseTarget(e, target);
      }
    }
  }

  /**
   * Compute the target influence on all tiles around the starting position.
   * <p>
   * Note: resulting target can be negative. It stops on tiles where there is no
   * influence from source AND target is not positive.
   */
  private ObjectIntMap<Influence> getTargetInfluence(Entity source, MapPosition startPos, int startingPower) {
    Map<Terrain, Integer> costs = terrainCosts(source);
    Queue<Pos> frontier = new PriorityQueue<>();
    frontier.add(new Pos(startPos, startingPower));
    ObjectIntMap<Influence> targets = new ObjectIntMap<>();
    targets.put(map.getInfluenceAt(startPos), startingPower);

    while (!frontier.isEmpty()) {
      Pos current = frontier.poll();

      for (MapPosition next : map.getNeighbors(current.pos)) {
        Influence inf = map.getInfluenceAt(next);
        if (!inf.terrain.moveBlock()) {
          int newTarget = current.target - costs.get(inf.terrain);
          int oldTarget = targets.get(inf, Integer.MIN_VALUE);
          if (newTarget > oldTarget) {
            targets.put(inf, newTarget);
            /*
             * Increase only one tile from already influenced tiles. Decreases
             * wherever we have some influence (makes no sense to decrease
             * elsewhere.
             */
            if (inf.hasInfluence(source))
              frontier.offer(new Pos(next, newTarget));
          }
        }
      }
    }
    return targets;
  }

  private static final class Pos implements Comparable<Pos> {
    private final MapPosition pos;

    private final int target;

    private Pos(MapPosition pos, int target) {
      this.pos = pos;
      this.target = target;
    }

    @Override
    public int compareTo(Pos o) {
      return -Integer.compare(target, o.target);
    }
  }

  private Map<Terrain, Integer> terrainCosts(Entity source) {
    Modifiers modifiers = sources.get(source).modifiers;
    Map<Terrain, Integer> res = new EnumMap<>(Terrain.class);
    for (Terrain t : Terrain.values())
      res.put(t, max(1, t.moveCost() - modifiers.terrainBonus.get(t)));
    return res;
  }

  /**
   * Can only influence a tile if it belongs to the source or one of its
   * neighbor does. Cannot influence if we have a treaty.
   */
  private boolean canInfluence(Entity source, Influence inf) {
    if (inf.isMainInfluencer(source))
      return true;

    // cannot influence on tiles from empires we have a treaty with
    if (inf.hasMainInfluence()) {
      Diplomacy treaties = relations.get(source);
      State relation = treaties.getRelationWith(inf.getMainInfluenceSource(world));
      if (relation == State.TREATY || relation == State.PROTECTORATE)
        return false;
    }

    // need a neighbor we already have influence on
    for (Border b : Border.values()) {
      Influence tile = map.getInfluenceAt(b.getNeighbor(inf.position));
      if (tile != null && tile.isMainInfluencer(source))
        return true;
    }
    return false;
  }

  /**
   * Remove from {@link InfluenceSource#influencedTiles} all the tiles the
   * entity is no longer the main influencer on.
   */
  private void updateInfluencedTiles(Entity e) {
    InfluenceSource source = sources.get(e);

    source.influencedTiles.removeIf(p -> !map.getInfluenceAt(p).isMainInfluencer(e));
  }

  /**
   * Update the source power. Each turn we add the number of influenced tiles to
   * the advancement. When it reaches the threshold then power is increased by 1
   * and advancement is reset.
   * <p>
   * Power is lost when cities revolt.
   */
  private void accumulatePower(Entity empire) {
    InfluenceSource source = sources.get(empire);

    int increase = source.growth * source.power();
    if (increase > 0) {
      Diplomacy diplomacy = relations.get(empire);
      List<Entity> tributes = diplomacy.getEmpires(State.TRIBUTE);
      int remains = increase;
      for (Entity other : tributes) {
        int tribute = min(remains, increase / tributes.size());
        sources.get(other).addToPower(tribute / 1000f);
        remains -= tribute;
      }
      increase = remains;
    }

    source.addToPower(increase / 1000f);
  }

} @Wire
public final class PoliciesSystem extends EntitySystem {

  private static final int STABILITY_LOSS_WHEN_SWITCHING = 20;

  @SuppressWarnings("unused")
  private static final Logger log = LoggerFactory.getLogger(PoliciesSystem.class);

  private ComponentMapper<Policies> policies;

  private ComponentMapper<Discoveries> discoveries;

  private EffectsSystem effects;

  @SuppressWarnings("unchecked")
  public PoliciesSystem() {
    super(Aspect.getAspectForAll(Policies.class));
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity entity : entities) {
      Policies empire = policies.get(entity);
      if (empire.stability < empire.stabilityMax)
        empire.stability = min(empire.stabilityMax, empire.stability + empire.stabilityGrowth);
      else if (empire.stability > empire.stabilityMax)
        // decrease only one at a time
        empire.stability--;
    }
  }

  public void applyPolicy(Entity entity, Policy policy, Discovery selected) {
    Policies empire = policies.get(entity);
    Discovery old = empire.policies.put(policy, selected);
    if (old != null) {
      effects.apply(old.effects, entity, true);
      empire.stability -= STABILITY_LOSS_WHEN_SWITCHING;
    }
    if (selected != null) {
      effects.apply(selected.effects, entity, false);
    }
  }

  public List<Discovery> getAvailablePolicies(Entity empire, Policy choice) {
    return discoveries.get(empire).done.stream().filter(d -> d.groups.contains(choice.name())).collect(toList());
  }

  public static Set<Policy> getPolicies(Discovery d) {
    Set<Policy> set = null;
    for (String group : d.groups) {
      Policy p = Policy.get(group);
      if (p != null) {
        if (set == null)
          set = EnumSet.of(p);
        else
          set.add(p);
      }
    }
    return set;
  }
} @Wire
public final class RevoltSystem extends EntitySystem implements EventHandler {

  private static final Logger log = LoggerFactory.getLogger(RevoltSystem.class);

  private ComponentMapper<Empire> empires;

  private ComponentMapper<ArmyCommand> army;

  private ComponentMapper<Policies> policies;

  private ComponentMapper<InfluenceSource> sources;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<AIControlled> ai;

  private DiscoverySystem discoveries;

  private NotificationsSystem notifications;

  private FadingMessageRenderSystem fadingSystem;

  private final GameMap map;

  private final SessionSettings settings;

  private final OverworldScreen screen;

  @SuppressWarnings("unchecked")
  public RevoltSystem(GameMap gameMap, SessionSettings settings, OverworldScreen screen) {
    super(Aspect.getAspectForAll(InfluenceSource.class));
    this.map = gameMap;
    this.settings = settings;
    this.screen = screen;
  }

  @Override
  public String getType() {
    return "Revolt";
  }

  @Override
  protected void initialize() {
    super.initialize();

    world.getSystem(EventsSystem.class).addEventType(this);
  }

  @Override
  protected boolean checkProcessing() {
    return false;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> empires) {
    // progress toward next revolt is triggered by EventsSystem
  }

  @Override
  public int getProgress(Entity empire) {
    return max(0, getInstability(empire) / 2);
  }

  @Override
  public String getReason(Entity e) {
    int instability = getInstability(e);
    if (instability <= 0)
      return "Instability (" + instability + ") <= 0";
    else
      return "Instability (" + instability + ") > 0";
  }

  public int getInstability(Entity empire) {
    InfluenceSource source = sources.get(empire);
    Policies empirePolicies = policies.get(empire);
    int totalStability = empirePolicies.stability + max(0, army.get(empire).militaryPower);
    return source.influencedTiles.size() - totalStability;
  }

  /**
   * Cities revolt when the source power is above its stability. The higher it
   * is the higher chance it will revolt.
   */
  @Override
  public boolean execute(Entity empire) {
    int instability = getInstability(empire);
    if (instability <= 0)
      return false;

    InfluenceSource source = sources.get(empire);
    // revolt happens, select the tile!
    Optional<Influence> tile = source.influencedTiles.stream() //
        .filter(p -> isValidRevoltTile(empire, p, instability)) //
        .map(p -> map.getInfluenceAt(p)) //
        .min(comparingInt(Influence::getSecondInfluenceDiff));

    if (tile.isPresent()) {
      if (ai.has(empire)) {
        revolt(empire, instability, source, tile);
        return true;
      } else {
        Map<String, Runnable> choices = new HashMap<>();
        choices.put("Let them revolt!",
                () -> revolt(empire, instability, source, tile));
        choices.put("Try to ease the tension, losing up to " + instability + " power...",
                () -> reduceInstability(empire, instability));
        notifications.addNotification(
                () -> screen.askEvent("Our people are revolting, what should we do?", choices),
                () -> getInstability(empire) <= 0, Icon.REVOLT, "Revolt is sprawling!");
        // TODO display choice screen asking what to do
        // - lose power but keep tiles
        // - create revoltee
        // - change policies?
        return true;
      }
    } else if (instability > 50) {
      updateEmpireAfterRevolt(empire, instability, source);

      log.warn("Found no tile to revolt for {} with instability at {}%, decrease power to {}",
          empire.getComponent(Name.class), instability, source.power());
      if (!ai.has(empire))
        notifications.addNotification(() -> screen.select(empire, false), null, Icon.REVOLT,
            "Instability decrease %s power to %s!", empire.getComponent(Description.class), source.power());
      return true;
    }
    return false;
  }

  private void revolt(Entity empire, int instability, InfluenceSource source, Optional<Influence> tile) {
    updateEmpireAfterRevolt(empire, instability, source);
    createRevoltingEmpire(empire, instability, tile.get());
  }

  private void reduceInstability(Entity empire, int instability) {
    // stop the revolt
    policies.get(empire).stability += instability;
    // decrease power by instability to avoid popping revolts in loop
    InfluenceSource source = sources.get(empire);
    source.setPower(max(max(1, source.power() / 2), source.power() - instability));
  }

  private void updateEmpireAfterRevolt(Entity empire, int instability, InfluenceSource source) {
    // decrease power by instability to avoid popping revolts in loop
    policies.get(empire).stability += instability;
    source.setPower(max(max(1, source.power() / 2), source.power() - instability / 5));
  }

  private boolean isValidRevoltTile(Entity empire, MapPosition pos, int instability) {
    if (!map.getTerrainAt(pos).canStart() || map.hasEntity(pos))
      return false;

    if (instability < map.getInfluenceAt(pos).getMaxInfluence())
      return false;

    boolean atInfluenceBorder = false;
    for (Border b : Border.values()) {
      MapPosition neighbor = b.getNeighbor(pos);
      // don't accept tiles on map border
      if (!map.isOnMap(neighbor))
        return false;

      // don't accept near other entities
      if (map.hasEntity(neighbor))
        return false;

      // accept if different overlord
      atInfluenceBorder |= !map.getInfluenceAt(neighbor).isMainInfluencer(empire);
    }
    return atInfluenceBorder;
  }

  private void createRevoltingEmpire(Entity empire, int instability, Influence inf) {
    Culture culture = empires.get(empire).culture;
    Empire data = new Empire(settings.guessColor(), culture, true);
    settings.empires.add(data);
    Entity revoltee = EntityFactory.createEmpire(world, inf.position.x, inf.position.y, culture.newCityName(), data);
    map.setEntity(revoltee, inf.position);

    // get starting power from generating instability
    // ensure it has enough power to control its own tile and resist a bit
    int sourcePower = sources.get(empire).power();
    sources.get(revoltee).setPower(max(min(sourcePower, instability + sourcePower / 2), inf.getMaxInfluence() + 1));
    inf.setInfluence(revoltee, sources.get(revoltee).power());

    // do not forget neighboring tiles
    for (MapPosition n : map.getNeighbors(inf.position))
      map.getInfluenceAt(n).setInfluence(revoltee, min(sourcePower, instability + sourcePower / 2));

    // add all discoveries from original empire
    discoveries.copyDiscoveries(empire, revoltee);

    // notify player
    log.info("{} revolted from {} (instability={})", revoltee.getComponent(Description.class),
        empire.getComponent(Description.class), instability);
    fadingSystem.createFadingIcon(Icon.REVOLT, empires.get(revoltee).color, positions.get(revoltee), 3f);
    if (!ai.has(empire))
      notifications.addNotification(() -> screen.select(revoltee, false), null, Icon.REVOLT, "%s revolted from %s!",
          revoltee.getComponent(Description.class), empire.getComponent(Description.class));
  }

} @Wire
public final class ScoreSystem extends EntitySystem {

  @SuppressWarnings("unused")
  private static final Logger log = LoggerFactory.getLogger(ScoreSystem.class);

  private ComponentMapper<Score> scores;

  private ComponentMapper<InfluenceSource> sources;

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<Discoveries> discoveries;

  private Item winner;

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

  private final int nbDiscoveries;

  private final OverworldScreen screen;

  @SuppressWarnings("unchecked")
  public ScoreSystem(SessionSettings s, OverworldScreen screen) {
    super(Aspect.getAspectForAll(Score.class, InfluenceSource.class));
    this.screen = screen;

    nbDiscoveries = s.getDiscoveries().size();
  }

  @Override
  protected void inserted(Entity e) {
    super.inserted(e);
    Score score = scores.get(e);
    score.nbDiscoveriesMax = nbDiscoveries;
    list.add(new Item(e, score));
  }

  @Override
  protected void removed(Entity e) {
    super.removed(e);

    for (Iterator<Item> it = list.iterator(); it.hasNext();)
      if (it.next().empire.equals(e)) {
        it.remove();
        return;
      }
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity empire : entities) {
      Score score = scores.get(empire);
      score.lastTurnPoints = 0;
      // every empire controls itself
      score.controlledEmpires.clear();
      score.controlledEmpires.add(empire);
    }
    int nbControlledMax = entities.size();

    // compute current score (may modify other entities' scores)
    for (Entity empire : entities) {
      InfluenceSource source = sources.get(empire);
      add(empire, source.influencedTiles.size(), Collections.emptySet());

      Score score = scores.get(empire);
      score.nbDiscoveries = discoveries.get(empire).done.size();
      score.nbControlledMax = nbControlledMax;
    }

    // now score is stable, sort by rank and search for winners
    Collections.sort(list, Comparator.comparingInt((Item i) -> i.score.totalScore).reversed());
    for (int r = 0; r < list.size(); r++) {
      Item i = list.get(r);
      i.score.rank = r + 1;
      if (winner == null && hasWon(i.score))
        winner = i;
    }

    if (winner != null)
      screen.scoresMenu();
  }

  private static boolean hasWon(Score score) {
    // also test "greater" in case other empires were destroyed
    // it should not really happen but it does not cost anything
    return score.controlledEmpires.size() >= score.nbControlledMax || score.nbDiscoveries >= score.nbDiscoveriesMax;
  }

  /** Recursive method to add score to all overlords and partners. */
  private void add(Entity empire, int delta, Set<Entity> controlledEmpires) {
    if (delta > 0
    // do not forget an empire can be deleted
        && scores.has(empire)) {
      Diplomacy diplomacy = relations.get(empire);

      Score score = scores.get(empire);
      score.lastTurnPoints += delta;
      score.totalScore += delta;
      score.controlledEmpires.addAll(controlledEmpires);

      // add to overlords and allies
      for (Entry<Entity, State> e : diplomacy.relations.entrySet()) {
        if (e.getValue() == State.TREATY)
          add(e.getKey(), delta / 4, Collections.emptySet());
        else if (e.getValue() == State.TRIBUTE)
          // overlord controls me and my vassals
          add(e.getKey(), delta / 2, score.controlledEmpires);
      }
    }
  }

  public final class Item {

    public final Entity empire;

    public final Score score;

    Item(Entity empire, Score score) {
      this.empire = empire;
      this.score = score;
    }

  }

  /** Get the winner for current game. <code>null</code> if nobody has won yet. */
  public Item getWinner() {
    return winner;
  }

  public List<Item> getScores() {
    return list;
  }

} @Wire
public final class SpecialDiscoveriesSystem extends VoidEntitySystem {

  private static final Logger log = LoggerFactory.getLogger(SpecialDiscoveriesSystem.class);

  private static final String VILLAGE = "Village";

  private static final String CITY = "City";

  private static final String RAFT = "Raft";

  private static final String BOAT = "Boat";

  private ComponentMapper<Name> names;

  private ComponentMapper<Description> descriptions;

  private ComponentMapper<InfluenceSource> sources;

  private ComponentMapper<Destination> destinations;

  private ComponentMapper<ArmyCommand> commanders;

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<AIControlled> ai;

  private NotificationsSystem notifications;

  private SpriteRenderSystem sprites;

  private interface Special {
    void apply(Entity empire, Discovery d, Discoveries discoveries);
  }

  private final Map<String, Special> effects = new HashMap<>();

  private final OverworldScreen screen;

  public SpecialDiscoveriesSystem(OverworldScreen screen) {
    this.screen = screen;
    effects.put(VILLAGE, new VillageDiscovery());
    effects.put(CITY, new CityDiscovery());
    effects.put(RAFT, new RaftDiscovery());
    effects.put(BOAT, new BoatDiscovery());
  }

  private class VillageDiscovery implements Special {
    @Override
    public void apply(Entity empire, Discovery d, Discoveries discoveries) {
      if (!discoveries.done.contains(CITY)) {
        String name = names.get(empire).name;
        String desc = "Village of " + name;
        setDescription(empire, name, desc);
        sprites.setSprite(empire, "village");
        cannotMove(empire);
      }
    }
  }

  private class CityDiscovery implements Special {
    @Override
    public void apply(Entity empire, Discovery d, Discoveries discoveries) {
      String name = names.get(empire).name;
      String desc = "City of " + name;
      setDescription(empire, name, desc);
      sprites.setSprite(empire, "city");
      cannotMove(empire);
    }
  }

  private class RaftDiscovery implements Special {
    @Override
    public void apply(Entity empire, Discovery d, Discoveries discoveries) {
      for (Entity army : sources.get(empire).secondarySources)
        destinations.get(army).forbiddenTiles.add(Terrain.SHALLOW_WATER);
      commanders.get(empire).forbiddenTiles.add(Terrain.SHALLOW_WATER);
    }
  }

  private class BoatDiscovery implements Special {
    @Override
    public void apply(Entity empire, Discovery d, Discoveries discoveries) {
      List<Terrain> list = asList(Terrain.DEEP_WATER, Terrain.SHALLOW_WATER);
      for (Entity army : sources.get(empire).secondarySources)
        destinations.get(army).forbiddenTiles.addAll(list);
      commanders.get(empire).forbiddenTiles.addAll(list);
    }
  }

  @Override
  protected void processSystem() {
    // nothing to do, called by other systems to activate special effects
  }

  private void setDescription(Entity empire, String name, String desc) {
    descriptions.get(empire).desc = desc;
    log.info("{} is now known as '{}'", name, desc);
    // notify only player empire, avoid spamming
    if (!ai.has(empire) || relations.get(empire).hasRelationWith(screen.player))
      notifications.addNotification(() -> screen.select(empire, false), null, Icon.BUILDINGS,
          "%s is now known as '%s'", name, desc);
  }

  private void cannotMove(Entity capital) {
    capital.edit().remove(Destination.class);
  }

  public void checkDiscoveries(Map<String, Discovery> discoveries) {
    for (String d : effects.keySet())
      if (!discoveries.containsKey(d))
        log.warn("Cannot find discovery '{}' that has a special effect.", d);
  }

  public void apply(Entity empire, Discovery d, Discoveries discoveries) {
    Special effect = effects.get(d.name);
    if (effect != null)
      effect.apply(empire, d, discoveries);
  }
} public final class InfluenceSource extends Component {

  private float power = InfluenceSystem.INITIAL_POWER;

  /** In per mille. */
  public int growth;

  /** Usually compared to power. */
  public int health = InfluenceSystem.INITIAL_POWER;

  public final Set<MapPosition> influencedTiles = new HashSet<>();

  public final List<Entity> secondarySources = new ArrayList<>();

  public final Modifiers modifiers = new Modifiers();

  public static final class Modifiers {

    public final Map<Terrain, Integer> terrainBonus = new EnumMap<Terrain, Integer>(Terrain.class);

    public Modifiers() {
      for (Terrain t : Terrain.values())
        terrainBonus.put(t, Integer.valueOf(0));
    }

  }

  public int power() {
    return (int) power;
  }

  public void addToPower(float f) {
    power += f;
  }

  public void setPower(int power) {
    this.power = power;
  }

} public final class Empire extends Component {

  private static final AtomicInteger COUNTER = new AtomicInteger(0);

  public final int id;

  public final Color color;

  public final Color backColor;

  public final Culture culture;

  private final boolean computer;

  public Empire(Color color, Culture culture, boolean computer) {
    this.id = COUNTER.getAndIncrement();
    this.color = color;
    this.backColor = Colors.contrast(color);
    this.culture = culture;
    this.computer = computer;
  }

  public boolean isComputerControlled() {
    return computer;
  }

  public Color getColor() {
    return color;
  }

  public Culture getCulture() {
    return culture;
  }

  public String newCityName() {
    return culture.newCityName();
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this)
      return true;
    else if (obj instanceof Empire)
      return id == ((Empire) obj).id;
    else
      return false;
  }

  @Override
  public int hashCode() {
    return id;
  }

  @Override
  public String toString() {
    return culture.name + "(ia=" + computer + ")";
  }

} public final class SessionSettings implements Cloneable {

  /** List different colors that are easy to recognize on-screen. */
  public static final List<Color> COLORS = Arrays.asList(Color.RED, Color.GREEN, Color.BLUE, Color.PURPLE,
      Color.YELLOW, Color.TEAL, Color.WHITE, Color.BLACK, Color.GRAY, Color.PINK, Color.LIGHT_GRAY, Color.ORANGE,
      Color.MAGENTA, Color.DARK_GRAY, Color.CYAN, Color.OLIVE, Color.MAROON, Color.NAVY);

  private final Map<String, Culture> cultures;

  private final Map<String, Discovery> discoveries;

  public final List<Empire> empires = new ArrayList<>();

  public final Properties map;

  public final EnumValue<MapGenerator.Generator> mapType;

  public final BooleanValue startWithDiplomacy = new BooleanValue(false);

  public SessionSettings() {
    // init map parameters
    mapType = new EnumValue<>(MapGenerator.Generator.ISLAND);
    map = mapType.get().algo.getDefaultValues();

    // load cultures from data file
    Json json = new Json();
    this.cultures = cultures(json).stream().collect(toMap(Culture::getName, c -> c));
    this.discoveries = discoveries(json).stream().collect(toMap(Discovery::getName, c -> c));

    // generate some default empires
    boolean ai = false;
    for (Culture culture : cultures.values()) {
      empires.add(new Empire(guessColor(), culture, ai));
      ai = true;
    }
  }

  public SessionSettings(SessionSettings settings) {
    // load cultures and discoveries from data file
    Json json = new Json();
    this.cultures = cultures(json).stream().collect(toMap(Culture::getName, c -> c));
    this.discoveries = discoveries(json).stream().collect(toMap(Discovery::getName, c -> c));

    // copy map creation options
    this.map = new Properties();
    this.map.putAll(settings.map);
    this.mapType = new EnumValue<>(MapGenerator.Generator.ISLAND);
    this.mapType.set(settings.mapType.get());
    this.startWithDiplomacy.set(settings.startWithDiplomacy.get());

    // copy empires using new cultures (as they have a city iterator)
    for (Empire empire : settings.empires)
      this.empires.add(new Empire(empire.color, cultures.get(empire.culture.name), empire.isComputerControlled()));
  }

  private static List<Culture> cultures(Json json) {
    json.setSerializer(Culture.class, Culture.SER);
    List<Culture> cultures = new ArrayList<>();
    for (FileHandle f : files("data/cultures/", "antiquity.json"))
      cultures.addAll(Arrays.asList(json.fromJson(Culture[].class, f)));
    return cultures;
  }

  private static List<Discovery> discoveries(Json json) {
    // TODO add ideas from http://doodlegod.wikia.com/wiki/Doodle_Devil
    // TODO add ideas from http://doodlegod.wikia.com/wiki/Doodle_God_2
    // TODO The mythic discoveries could be used as 'totem' animals
    json.setSerializer(Discovery.class, Discovery.SER);
    List<Discovery> discoveries = new ArrayList<>();
    for (FileHandle f : files("data/discoveries/", "nature.json", "prehistory.json", "antiquity.json", "classic.json"))
      discoveries.addAll(Arrays.asList(json.fromJson(Discovery[].class, f)));
    return discoveries;
  }

  private static Iterable<FileHandle> files(String dir, String ... files) {
    return Arrays.stream(files).map(f -> Gdx.files.internal(dir + f)).collect(toList());
  }

  public Map<String, Discovery> getDiscoveries() {
    return discoveries;
  }

  public Collection<Culture> getCultures() {
    return Collections.unmodifiableCollection(cultures.values());
  }

  public Culture getCulture(String name) {
    return cultures.get(name);
  }

  public Color guessColor() {
    List<Color> usedColors = empires.stream().map(Empire::getColor).collect(toList());
    Optional<Color> color = COLORS.stream().filter(c -> !usedColors.contains(c)).findFirst();
    if (color.isPresent())
      return color.get();
    else {
      // must use an existing color
      return COLORS.get(new Random().nextInt(COLORS.size()));
    }
  }

  private final Random rand = new Random();

  public Culture guessCulture() {
    Map<Culture, Integer> usedCultures = cultures.values().stream().distinct()
        .collect(toMap(c -> c, c -> (int) empires.stream().filter(e -> e.culture == c).count()));
    List<Culture> list = cultures
        .values()
        .stream()
        .sorted(
            (c1, c2) -> -Integer.compare(c1.cities.size - usedCultures.get(c1), c2.cities.size - usedCultures.get(c2)))
        .limit(3).collect(toList());
    if (list.isEmpty())
      throw new IllegalStateException("No remaining culture!");
    return list.get(abs(rand.nextInt() % list.size()));
  }
} public final class GameMap {

  public final Terrain[][] map;

  private final Entity[][] entityByCoord;

  private final MapPosition[][] posByCoord;

  private final Influence[][] influenceByCoord;

  public final int width, height;

  /** Represents the whole map as a single image. */
  public final Texture texture;

  public final Empire[] empires;

  public GameMap(Terrain[][] map, Collection<Empire> empires) {
    this.map = map;
    this.empires = empires.toArray(new Empire[0]);
    width = map.length;
    height = map[0].length;

    entityByCoord = new Entity[width][height];
    influenceByCoord = new Influence[width][height];
    posByCoord = new MapPosition[width][height];

    Pixmap pixmap = new Pixmap(width, height, Pixmap.Format.RGBA8888);

    for (int x = 0; x < width; x++) {
      for (int y = 0; y < height; y++) {
        MapPosition pos = posByCoord[x][y] = new MapPosition(x, y);
        influenceByCoord[x][y] = new Influence(pos, map[x][y]);
        pixmap.setColor(map[x][y].getColor());
        pixmap.drawPixel(x, y);
      }
    }

    texture = new Texture(pixmap);
    pixmap.dispose();
  }

  public Entity getEntityAt(int x, int y) {
    if (isOnMap(x, y))
      return entityByCoord[x][y];
    else
      return null;
  }

  public Entity getEntityAt(MapPosition pos) {
    return getEntityAt(pos.x, pos.y);
  }

  public boolean hasEntity(MapPosition pos) {
    return getEntityAt(pos) != null;
  }

  public Terrain getTerrainAt(MapPosition pos) {
    return getTerrainAt(pos.x, pos.y);
  }

  public Terrain getTerrainAt(int x, int y) {
    if (isOnMap(x, y))
      return map[x][y];
    else
      throw new IllegalStateException("(" + x + ", " + y + ") is outside map boundaries.");
  }

  public Influence getInfluenceAt(MapPosition pos) {
    return getInfluenceAt(pos.x, pos.y);
  }

  public Influence getInfluenceAt(int x, int y) {
    if (isOnMap(x, y))
      return influenceByCoord[x][y];
    else
      throw new IllegalStateException("(" + x + ", " + y + ") is outside map boundaries.");
  }

  public boolean isOnMap(MapPosition p) {
    return isOnMap(p.x, p.y);
  }

  public boolean isOnMap(int x, int y) {
    return x >= 0 && x < posByCoord.length && y >= 0 && y < posByCoord[0].length;
  }

  public MapPosition getPositionAt(int x, int y) {
    if (isOnMap(x, y))
      return posByCoord[x][y];
    else
      throw new IllegalStateException("(" + x + ", " + y + ") is outside map boundaries.");
  }

  public void setEntity(Entity e, int x, int y) {
    entityByCoord[x][y] = e;
  }

  public void setEntity(Entity e, MapPosition p) {
    setEntity(e, p.x, p.y);
  }

  public void moveEntity(Entity e, MapPosition from, MapPosition to) {
    entityByCoord[from.x][from.y] = null;
    entityByCoord[to.x][to.y] = e;
  }

  public Array<MapPosition> getNeighbors(int x, int y, int n) {
    Array<MapPosition> coordinates = new Array<MapPosition>();
    int min;
    int myrow;
    for (int row = y - n; row < y + n + 1; row++) {
      min = MyMath.min(2 * (row - y + n), n, -2 * (row - y - n) + 1);
      for (int col = x - min; col < x + min + 1; col++) {
        if ((col < 0) || (col >= width))
          continue;
        if (x == col && y == row)
          continue;
        else if (x % 2 == 0)
          myrow = 2 * y - row;
        else
          myrow = row;
        if ((myrow < 0) || (myrow >= height))
          continue;
        coordinates.add(new MapPosition(col, myrow));
      }
    }
    return coordinates;
  }

  public Array<MapPosition> getNeighbors(MapPosition pos) {
    return getNeighbors(pos.x, pos.y, 1);
  }

} public final class Influence implements Iterable<IntIntMap.Entry> {

  public final MapPosition position;

  public final Terrain terrain;

  /** Entity id to influence score. */
  private final IntIntMap influence = new IntIntMap();

  /**
   * Entity id to influence delta from last turn. Only for display when starting
   * a new turn, player needs to know how things evolve.
   */
  private final IntIntMap influenceDelta = new IntIntMap();

  /**
   * Entity id to influence target for next turn. Used to compute the increment
   * at each turn.
   */
  private final IntIntMap influenceTarget = new IntIntMap();

  private int mainInfluence = 0;

  private int mainInfluenceSource = -1;

  private int secondInfluenceDiff = 0;

  public Influence(MapPosition position, Terrain terrain) {
    this.position = position;
    this.terrain = terrain;
  }

  /** Set the source influence to the tile. */
  public void setInfluence(Entity source, int inf) {
    influence.put(source.getId(), inf);
    recomputeMain();
  }

  /** Move the source influence on the tile. */
  public void moveInfluence(Entity from, Entity to) {
    influence.getAndIncrement(to.getId(), 0, influence.remove(from.getId(), 0));
    recomputeMain();
  }

  /** Remove the source influence from the tile. */
  public void removeInfluence(Entity source) {
    influence.remove(source.getId(), 0);
    influenceDelta.remove(source.getId(), 0);
    recomputeMain();
  }

  public Iterable<IntIntMap.Entry> getDelta() {
    return influenceDelta;
  }

  public int getDelta(Entity source) {
    return influenceDelta.get(source.getId(), 0);
  }

  public void clearInfluenceTarget() {
    influenceTarget.clear();
  }

  public void increaseTarget(Entity source, int target) {
    influenceTarget.getAndIncrement(source.getId(), 0, target);
  }

  /** Make each participant influence closer to target and compute delta. */
  public void computeNewInfluence() {
    influenceDelta.clear();

    // make sure all involved empires have a target influence
    for (Entry e : influence) {
      if (!influenceTarget.containsKey(e.key))
        influenceTarget.put(e.key, 0);
    }

    // then compute the delta and apply it
    for (Entry e : influenceTarget) {
      final int current = influence.get(e.key, 0);
      final int target = e.value;
      int delta = 0;
      if (target > current)
        delta = max(1, (target - current) / 10);
      else if (target < current)
        delta = -max(1, (current - target) / 10);

      if (delta != 0) {
        // do nothing if same obviously
        influence.put(e.key, max(0, current + delta));
        influenceDelta.put(e.key, delta);
      }
    }
    recomputeMain();
  }

  private void recomputeMain() {
    int secondInfluence = Integer.MIN_VALUE;
    mainInfluence = terrain.moveCost() - 1;
    mainInfluenceSource = -1;
    if (influence.size > 0) {
      for (Iterator<Entry> it = influence.iterator(); it.hasNext();) {
        Entry e = it.next();
        if (e.value > mainInfluence) {
          secondInfluence = mainInfluence;
          mainInfluenceSource = e.key;
          mainInfluence = e.value;
        } else if (e.value <= 0) {
          it.remove();
        } else if (e.value > secondInfluence) {
          secondInfluence = e.value;
        }
      }
    }
    if (secondInfluence > 0)
      secondInfluenceDiff = mainInfluence - secondInfluence;
  }

  public int getInfluence(Entity e) {
    return influence.get(e.getId(), 0);
  }

  public int getSecondInfluenceDiff() {
    return secondInfluenceDiff;
  }

  public int getMainInfluenceSource() {
    return mainInfluenceSource;
  }

  public Entity getMainInfluenceSource(World world) {
    if (hasMainInfluence())
      return world.getEntity(mainInfluenceSource);
    else
      return null;
  }

  public int requiredInfluence(Entity e) {
    if (isMainInfluencer(e))
      return 0;
    int current = influence.get(e.getId(), 0);
    return max(terrain.moveCost(), mainInfluence + 1) - current;
  }

  public boolean isMainInfluencer(Entity e) {
    return e.getId() == mainInfluenceSource;
  }

  public boolean hasMainInfluence() {
    return mainInfluenceSource >= 0;
  }

  public boolean hasInfluence(Entity source) {
    return influence.get(source.getId(), 0) > 0;
  }

  public int getMaxInfluence() {
    int max = 0;
    for (Entry e : influence)
      if (e.value > max)
        max = e.value;
    return max(max, terrain.moveCost());
  }

  public int getTotalInfluence() {
    int total = 0;
    for (Entry e : influence)
      total += e.value;
    return max(total, terrain.moveCost());
  }

  @Override
  public Iterator<Entry> iterator() {
    return influence.iterator();
  }

  @Override
  public int hashCode() {
    return position.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    else
      return (obj instanceof Influence) && position.equals(((Influence) obj).position);
  }

  @Override
  public String toString() {
    return terrain + influence.toString();
  }

} public final class MapPosition extends Component {

  public final int x, y;

  public MapPosition(int x, int y) {
    this.x = x;
    this.y = y;
  }

  @Override
  public int hashCode() {
    return x + y * 31;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this)
      return true;
    else if (obj instanceof MapPosition) {
      MapPosition p = (MapPosition) obj;
      return x == p.x && y == p.y;
    }
    return false;
  }

  @Override
  public String toString() {
    return "(" + x + "," + y + ")";
  }
}
 public final class CameraMovementSystem extends VoidEntitySystem {

  private float t;

  /** Camera position start and destination. */
  private float x0, x1, y0, y1;

  private float Ax, Ay;

  private final OrthographicCamera camera;

  private float T;

  public boolean active;

  public CameraMovementSystem(OrthographicCamera camera) {
    super();
    this.camera = camera;
    active = false;
  }

  public void move(MapPosition target) {
    Vector3 position = camera.position;
    x0 = position.x;
    y0 = position.y;
    FloatPair p = MapTools.world2window(target);
    this.x1 = p.x;
    this.y1 = p.y;
    t = 0;
    MapPosition startWorld = MapTools.libgdx2world(x0, y0);

    // d is used to calculate how long it will take to get to the target tile.
    // If it is close, d is small - if it is far, d is large
    // Very close by, d is similar to how many tiles away it is
    // For longer distances, it grows as sqrt(distance)
    float d = (float) Math.sqrt(MapTools.distance(startWorld, target) + 0.25) - 0.5f;

    // start with a base of 0.4 seconds, then add d seconds every 4 tiles
    T = 0.4f + d / 4f;
    Ax = (x1 - x0) / T;
    Ay = (y1 - y0) / T;
    active = true;
  }

  @Override
  protected void processSystem() {
    if (!active)
      return;

    t = min(t + world.getDelta(), T);
    // TODO increase speed from start, reduce when approaching end, a curb like:
    // http://1.bp.blogspot.com/-ps-1MpKTgbc/UXTSzyOGhKI/AAAAAAAAAGc/9Kef1mD-Plk/s1600/camera.png
    // maybe use this formula:
    // http://www.wolframalpha.com/input/?i=x+%2F+sqrt%281+%2B+x^2%29
    camera.position.set(x0 + Ax * t, y0 + Ay * t, 0);

    if (t >= T) {
      active = false;
    }
  }
} @Wire
public final class CounterRenderSystem extends AbstractRendererSystem {

  private static final int SIZE = 32;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<Counter> counters;

  private final BitmapFont font;

  private final Map<Color, Texture> textures = new HashMap<>();

  @SuppressWarnings("unchecked")
  public CounterRenderSystem(Assets assets, OrthographicCamera camera, SpriteBatch batch) {
    super(Aspect.getAspectForAll(MapPosition.class, Counter.class), camera, batch);

    font = assets.getFont(14);
    //font.setUseIntegerPositions(false);
  }

  @Override
  protected void inserted(Entity e) {
    super.inserted(e);

    Counter counter = counters.get(e);
    Texture texture = textures.get(counter.backColor);
    if (texture == null)
      textures.put(counter.backColor, texture = createTexture(counter.backColor));
    counter.background = texture;
  }

  private static Texture createTexture(Color color) {
    Pixmap pm = new Pixmap(SIZE, SIZE, Format.RGB888);
    pm.setColor(color);
    pm.fillRectangle(0, 0, SIZE, SIZE);
    Texture texture = new Texture(pm);
    pm.dispose();
    return texture;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity e : entities)
      process(e);
  }

  private void process(Entity e) {
    MapPosition position = positions.get(e);

    // If sprite is off-screen, don't bother drawing it!
    if (!isOnScreen(position))
      return;

    Counter counter = counters.get(e);

    FloatPair drawPosition = MapTools.world2window(position);
    float posX = drawPosition.x - (SIZE / 2);
    float posY = drawPosition.y - (SIZE / 2);

    batch.setColor(Color.WHITE);
    batch.draw(counter.background, posX, posY);

    posX += 2;
    posY += SIZE - 2;

    font.setColor(counter.textColor);
    font.draw(batch, Integer.toString(counter.value), posX, posY);
  }
} @Wire
public final class DestinationRenderSystem extends EntitySystem {

  private ComponentMapper<Empire> empires;

  private ComponentMapper<Destination> destinations;

  private ComponentMapper<MapPosition> positions;

  private final AtlasRegion flagTexture;

  private final ShapeRenderer renderer;

  private final OrthographicCamera camera;

  private final SpriteBatch batch;

  public DestinationRenderSystem(OrthographicCamera camera, SpriteBatch batch) {
    super(Aspect.getAspectForAll(Destination.class));
    this.camera = camera;
    this.batch = batch;
    this.renderer = new ShapeRenderer();

    flagTexture = new TextureAtlas(Gdx.files.internal("textures/characters.atlas"), Gdx.files.internal("textures"))
        .findRegion("flag");
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    batch.setProjectionMatrix(camera.combined);
    batch.begin();
    for (Entity e : entities)
      processTargetsFlags(e);
    batch.end();
    batch.setColor(1f, 1f, 1f, 1f);

    renderer.setProjectionMatrix(camera.combined);
    renderer.begin(ShapeType.Line);
    for (Entity e : entities)
      processPathLines(e);
    renderer.end();
  }

  private void processTargetsFlags(Entity e) {
    Destination dest = destinations.get(e);

    // use source color
    Color c = batch.getColor().cpy();
    batch.setColor(empires.get(e).color);
    // draw the flag for influencing
    if (dest.target != null)
      draw(flagTexture, dest.target);
    // revert to previous (may be it is the last movement?)
    batch.setColor(c);
  }

  private void processPathLines(Entity e) {
    Destination dest = destinations.get(e);

    // draw the path line
    if (dest.path != null) {
      renderer.setColor(empires.get(e).color);
      MapPosition start = positions.get(e);
      for (MapPosition next : dest.path) {
        FloatPair startScreen = MapTools.world2window(start);
        FloatPair nextScreen = MapTools.world2window(next);
        renderer.line(startScreen.x, startScreen.y, nextScreen.x, nextScreen.y);
        start = next;
      }
    }
  }

  private void draw(AtlasRegion reg, MapPosition p) {
    FloatPair position = MapTools.world2window(p);
    batch.draw(reg, position.x - reg.getRegionWidth() / 2, position.y - reg.getRegionHeight() / 2);
  }
} @Wire
public final class DiplomacyRenderSystem extends AbstractRendererSystem {

  /** Arrow height in pixels at standard zoom. */
  private static final int ARROW_HEIGHT = 4;

  /** Do not mask terrain with arrow. */
  private static final float ALPHA = 0.5f;

  private ComponentMapper<Empire> empires;

  private ComponentMapper<Diplomacy> relations;

  private ComponentMapper<MapPosition> positions;

  private final float textureRatio;

  private final Texture arrow;

  public DiplomacyRenderSystem(OrthographicCamera camera, SpriteBatch batch) {
    super(with(Diplomacy.class), camera, batch);

    arrow = new Texture("textures/arrow.png");
    arrow.setWrap(TextureWrap.Repeat, TextureWrap.ClampToEdge);

    textureRatio = arrow.getHeight() / ARROW_HEIGHT;
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  static final class Link {

    final MapPosition start;

    final MapPosition end;

    final Color color;

    Link(MapPosition start, MapPosition end, Color color) {
      this.start = start;
      this.end = end;
      this.color = color;
    }

  }

  private Link createLink(Entity start, Entity end) {
    return new Link(positions.get(start), positions.get(end), empires.get(end).color);
  }

  private final List<Link> links = new ArrayList<>();

  /**
   * Collect the borders for every influence source. Should be done only when
   * the borders changed, meaning after processing a turn.
   */
  public void preprocess() {
    links.clear();

    ImmutableBag<Entity> players = getActives();

    for (int i = 0; i < players.size(); i++) {
      Entity e1 = players.get(i);
      Diplomacy r1 = relations.get(e1);
      for (int j = i + 1; j < players.size(); j++) {
        Entity e2 = players.get(j);
        Diplomacy r2 = relations.get(e2);

        if (r1.getRelationWith(e2) == State.TRIBUTE)
          links.add(createLink(e1, e2));
        else if (r2.getRelationWith(e1) == State.TRIBUTE)
          links.add(createLink(e2, e1));
      }
    }
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    Color c = batch.getColor().cpy();

    // draw all the pre-processed links
    for (Link l : links) {
      // use source color
      batch.setColor(l.color);

      FloatPair start = MapTools.world2window(l.start);
      FloatPair end = MapTools.world2window(l.end);

      batch.draw(arrow, vertices(start, end, l.color), 0, 20);
    }

    // revert to previous (may be it is the last source?)
    batch.setColor(c);
  }

  private static final Vector2 tmp = new Vector2();

  /**
   * Grossly inspired by
   * {@link ShapeRenderer#rectLine(float, float, float, float, float)} and
   * {@link SpriteBatch#draw(Texture, float, float, float, float)}.
   */
  private float[] vertices(FloatPair start, FloatPair end, Color color) {
    // http://stackoverflow.com/questions/11954964/libgdx-spritebatch-draw-specifying-4-vertices
    // https://gist.github.com/mattdesl/4255476
    float c = Color.toFloatBits(color.r, color.g, color.b, ALPHA);

    float x1 = start.x;
    float y1 = start.y;
    float x2 = end.x;
    float y2 = end.y;

    tmp.set(y2 - y1, x1 - x2);
    float length = tmp.len();
    Vector2 t = tmp.nor();
    float tx = t.x * ARROW_HEIGHT;
    float ty = t.y * ARROW_HEIGHT;

    final float u1 = 0;
    final float v1 = 1;
    // number of times it is repeated, have it a bit longer than high
    final float u2 = (length / arrow.getWidth()) * textureRatio / 2;
    final float v2 = 0;

    float[] vertices = new float[20];
    vertices[X1] = x1 + tx;
    vertices[Y1] = y1 + ty;
    vertices[C1] = c;
    vertices[U1] = u1;
    vertices[V1] = v1;

    vertices[X2] = x1 - tx;
    vertices[Y2] = y1 - ty;
    vertices[C2] = c;
    vertices[U2] = u1;
    vertices[V2] = v2;

    vertices[X4] = x2 + tx;
    vertices[Y4] = y2 + ty;
    vertices[C4] = c;
    vertices[U4] = u2;
    vertices[V4] = v1;

    vertices[X3] = x2 - tx;
    vertices[Y3] = y2 - ty;
    vertices[C3] = c;
    vertices[U3] = u2;
    vertices[V3] = v2;

    return vertices;
  }

} @Wire
public final class FadingMessageRenderSystem extends EntityProcessingSystem {

  private ComponentMapper<MutableMapPosition> mmpm;

  private ComponentMapper<FadingMessage> fmm;

  private final Assets assets;

  private final SpriteBatch batch;

  private final OrthographicCamera camera;

  private final BitmapFont font;

  private final GlyphLayout layout  = new GlyphLayout();

  @SuppressWarnings("unchecked")
  public FadingMessageRenderSystem(Assets assets, OrthographicCamera camera, SpriteBatch batch) {
    super(Aspect.getAspectForAll(MutableMapPosition.class, FadingMessage.class));
    this.assets = assets;
    this.batch = batch;
    this.camera = camera;

    this.font = assets.getFont(14);
  }

  public void createFadingIcon(Icon icon, Color color, MapPosition pos, float duration) {
    EntityFactory.createFadingTileIcon(world, assets.getTexture(icon), color, pos.x, pos.y, duration);
  }

  @Override
  protected void begin() {
    batch.setProjectionMatrix(camera.combined);
    batch.begin();
    batch.setColor(1, 1, 1, 1);
  }

  @Override
  protected void process(Entity e) {
    MutableMapPosition position = mmpm.get(e);
    FadingMessage message = fmm.get(e);
    FloatPair drawPosition = MapTools.world2window(position.x, position.y);

    if (message.label != null)
      drawLabel(drawPosition, message);

    if (message.icon != null)
      drawIcon(drawPosition, message);

    position.x += message.vx * world.getDelta();
    position.y += message.vy * world.getDelta();
    message.currentTime += world.getDelta();

    if (message.currentTime >= message.duration)
      e.deleteFromWorld();
  }

  private void drawLabel(FloatPair drawPosition, FadingMessage message) {
    Color color = message.color;
    font.setColor(color.r, color.g, color.b, 1f - message.currentTime / message.duration);
    layout.setText(font, message.label);

    float posX = drawPosition.x - layout.width / 2;
    float posY = drawPosition.y;
    font.draw(batch, layout, posX, posY);
  }

  private void drawIcon(FloatPair drawPosition, FadingMessage message) {
    float posX = drawPosition.x - (message.icon.getRegionWidth() / 2);
    float posY = drawPosition.y;

    Color color = message.color;
    Color oldColor = batch.getColor().cpy();
    batch.setColor(color.r, color.g, color.b, 1f - message.currentTime / message.duration);
    batch.draw(message.icon, posX, posY);
    batch.setColor(oldColor);
  }

  @Override
  protected void end() {
    batch.end();
  }

} @Wire
public final class InfluenceRenderSystem extends AbstractRendererSystem {

  private ComponentMapper<Empire> empires;

  private final GameMap map;

  private final EnumMap<Border, AtlasRegion> borderTexture = new EnumMap<>(Border.class);

  private final AtlasRegion blank;

  public InfluenceRenderSystem(OrthographicCamera camera, SpriteBatch batch, GameMap map) {
    super(with(InfluenceSource.class), camera, batch);
    this.map = map;

    TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("textures/maptiles.atlas"), Gdx.files.internal("textures"));

    blank = atlas.findRegion("hex_blank");

    borderTexture.put(Border.BOTTOM_LEFT, atlas.findRegion("border_bottom_left"));
    borderTexture.put(Border.BOTTOM_RIGHT, atlas.findRegion("border_bottom_right"));
    borderTexture.put(Border.BOTTOM, atlas.findRegion("border_bottom"));
    borderTexture.put(Border.TOP_LEFT, atlas.findRegion("border_top_left"));
    borderTexture.put(Border.TOP_RIGHT, atlas.findRegion("border_top_right"));
    borderTexture.put(Border.TOP, atlas.findRegion("border_top"));
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  private final Map<Empire, List<InfluenceBorder>> borders = new HashMap<>();

  private static final class InfluenceBorder {

    private final Border[] borders;

    private final MapPosition position;

    public InfluenceBorder(MapPosition position, Border ... borders) {
      this.position = position;
      this.borders = borders;
    }

  }

  private final Map<MapPosition, Color> colors = new HashMap<>();

  private volatile boolean displayColoredInfluence = true;

  /**
   * Collect the borders for every influence source. Should be done only when
   * the borders changed, meaning after processing a turn.
   */
  public void preprocess() {
    borders.clear();
    colors.clear();
    List<Border> tmp = new ArrayList<>();
    for (int x = 0; x < map.map.length; x++) {
      for (int y = 0; y < map.map[0].length; y++) {
        Influence inf = map.getInfluenceAt(x, y);
        Entity source = inf.getMainInfluenceSource(world);
        if (source != null) {
          Empire empire = empires.get(source);

          // collect borders
          for (Border b : Border.values()) {
            MapPosition neighbor = b.getNeighbor(x, y);
            Influence neighborTile = map.getInfluenceAt(neighbor);
            if (neighborTile == null || empire != getMainEmpire(neighborTile))
              tmp.add(b);
          }
          if (!tmp.isEmpty()) {
            List<InfluenceBorder> list = borders.get(empire);
            if (list == null)
              borders.put(empire, list = new ArrayList<>());
            list.add(new InfluenceBorder(map.getPositionAt(x, y), tmp.toArray(new Border[tmp.size()])));
            tmp.clear();
          }

          // collect color
          Color c = empire.color;
          colors.put(inf.position, new Color(c.r, c.g, c.b, min(0.8f,inf.getMaxInfluence() / 300f)));
        }
      }
    }
  }

  private Empire getMainEmpire(Influence tile) {
    Entity main = tile.getMainInfluenceSource(world);
    return main == null ? null : empires.get(main);
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    Color c = batch.getColor().cpy();

    // draw all the pre-processed borders
    for (Empire empire : borders.keySet()) {
      // use source color
      batch.setColor(empire.color);

      for (InfluenceBorder ib : borders.get(empire))
        for (Border b : ib.borders)
          draw(borderTexture.get(b), ib.position);
    }

    if (displayColoredInfluence)
      for (Entry<MapPosition, Color> e : colors.entrySet()) {
        batch.setColor(e.getValue());
        draw(blank, e.getKey());
      }

    // revert to previous (may be it is the last source?)
    batch.setColor(c);
  }

  private void draw(AtlasRegion reg, MapPosition p) {
    FloatPair position = MapTools.world2window(p);
    batch.draw(reg, position.x - reg.getRegionWidth() / 2, position.y - reg.getRegionHeight() / 2);
  }

  public void displayColoredInfluence(boolean b) {
    displayColoredInfluence = b;
  }

  public boolean displayColoredInfluence() {
    return displayColoredInfluence;
  }
} @Wire
public final class NotificationsSystem extends VoidEntitySystem {

  private static final Comparator<Notification> INFO_FIRST = new Comparator<Notification>() {
    @Override
    public int compare(Notification o1, Notification o2) {
      if (o1.requiresAction()) {
        if (o2.requiresAction())
          return o1.msg.compareTo(o2.msg);
        else
          return 1;
      } else {
        if (o2.requiresAction()) return -1;
        else
          return o1.msg.compareTo(o2.msg);
      }
    }
  };

  /** Notifications registered for next turn. */
  private final List<Notification> notifications = new ArrayList<>();

  /** Notifications for current turn. */
  private final List<Notification> current = new ArrayList<>();

  private final Assets icons;

  public NotificationsSystem(Assets icons) {
    this.icons = icons;
  }

  @Override
  protected void processSystem() {
    current.clear();
    current.addAll(notifications);
    notifications.clear();
  }

  public List<Notification> getNotifications() {
    current.removeIf(n -> n.discard != null && n.discard.canDiscard());
    current.sort(INFO_FIRST);
    return Collections.unmodifiableList(current);
  }

  public void addNotification(Runnable action, Condition discard, Assets.Icon type, String msg, Object ... args) {
    if (args == null || args.length == 0)
      notifications.add(new Notification(icons.getDrawable(type), action, discard, msg));
    else
      notifications.add(new Notification(icons.getDrawable(type), action, discard, format(msg, args)));
  }

  @FunctionalInterface
  public interface Condition {
    boolean canDiscard();
  }

  public static final class Notification {

    public final String msg;

    public final Drawable type;

    public final Runnable action;

    private final Condition discard;

    public Notification(Drawable type, Runnable action, Condition discard, String msg) {
      this.type = type;
      this.action = action;
      this.discard = discard;
      this.msg = msg;
    }

    public boolean requiresAction() {
      return discard != null && !discard.canDiscard();
    }

  }

  public boolean canFinishTurn() {
    for (Notification n : current)
      if (n.requiresAction())
        return false;
    return true;
  }

  public void discard(Notification n) {
    if (!n.requiresAction())
      current.remove(n);
  }

} @Wire
public final class SpriteRenderSystem extends AbstractRendererSystem {

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<Sprite> sprites;

  private ComponentMapper<SpriteAnimation> animations;

  private final TextureAtlas atlas;

  private final List<Entity> sortedEntities = new ArrayList<>();

  public SpriteRenderSystem(OrthographicCamera camera, SpriteBatch batch) {
    super(with(MapPosition.class, Sprite.class), camera, batch);

    atlas = new TextureAtlas(Gdx.files.internal("textures/characters.atlas"), Gdx.files.internal("textures"));
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> entities) {
    for (Entity e : sortedEntities)
      process(e);
  }

  private void process(Entity e) {
    if (positions.has(e)) {
      MapPosition position = positions.getSafe(e);

      // If sprite is off-screen, don't bother drawing it!
      if (!isOnScreen(position))
        return;

      Sprite sprite = sprites.get(e);

      TextureRegion spriteRegion = sprite.region;
      batch.setColor(sprite.color);

      int width = spriteRegion.getRegionWidth();
      int height = spriteRegion.getRegionHeight();

      sprite.region.setRegion(sprite.x, sprite.y, width, height);

      FloatPair drawPosition = MapTools.world2window(position);
      float posX = drawPosition.x - ((spriteRegion.getRegionWidth() / 2) * sprite.scaleX);
      float posY = drawPosition.y - ((spriteRegion.getRegionHeight() / 2) * sprite.scaleY);

      batch.draw(spriteRegion, posX, posY, 0, 0, spriteRegion.getRegionWidth(), spriteRegion.getRegionHeight(),
          sprite.scaleX, sprite.scaleY, sprite.rotation);
    }
  }

  @Override
  protected void inserted(Entity entity) {
    sortedEntities.add(entity);
    Sprite sprite = sprites.get(entity);
    initSprite(sprite);

    sortedEntities.sort(comparing(e -> sprites.get(e).layer));
  }

  public void setSprite(Entity e, String spriteName) {
    Sprite sprite = sprites.get(e);
    sprite.name = spriteName;
    initSprite(sprite);
  }

  private void initSprite(Sprite sprite) {
    TextureRegion reg = atlas.findRegion(sprite.name);
    sprite.region = reg;
    sprite.x = reg.getRegionX();
    sprite.y = reg.getRegionY();
    sprite.width = reg.getRegionWidth();
    sprite.height = reg.getRegionHeight();
  }

  @Override
  protected void removed(Entity e) {
    sortedEntities.remove(e);
  }
} @Wire
public final class TextBoxRenderSystem extends AbstractRendererSystem {

  private static final int PAD = 2;

  private ComponentMapper<MapPosition> positions;

  private ComponentMapper<TextBox> boxes;

  private ComponentMapper<Empire> empires;

  private final List<Entity> entities = new ArrayList<>();

  private final BitmapFont font;

  public TextBoxRenderSystem(Assets assets, OrthographicCamera camera, SpriteBatch batch) {
    super(with(MapPosition.class, TextBox.class), camera, batch);

    font = assets.getFont(16);
  }

  @Override
  protected void inserted(Entity e) {
    entities.add(e);
  }

  @Override
  protected void removed(Entity e) {
    entities.remove(e);
  }

  @Override
  protected boolean checkProcessing() {
    return true;
  }

  /** Update the text for all the entities. */
  public void preprocess() {
    for (Entity e : entities) {
      preprocess(e);
    }
  }

  private Texture preprocess(Entity e) {
    TextBox box = boxes.get(e);
    box.text = box.generator.get();

    Empire empire = empires.get(e);

    GlyphLayout bounds = font.getCache().setText(box.text, 0, 0);
    Pixmap pm = new Pixmap((int) bounds.width + PAD * 2, (int) bounds.height + PAD * 2, Pixmap.Format.RGBA8888);
    pm.setColor(empire.backColor.r, empire.backColor.g, empire.backColor.b, 0.5f);
    pm.fill();
    pm.setColor(empire.color);
    pm.drawRectangle(0, 0, pm.getWidth(), pm.getHeight());
    box.texture = new Texture(pm);
    box.color = empire.color;

    return box.texture;
  }

  @Override
  protected void processEntities(ImmutableBag<Entity> list) {
    for (Entity e : entities)
      process(e);
  }

  private void process(Entity e) {
    if (positions.has(e)) {
      MapPosition position = positions.getSafe(e);

      // If sprite is off-screen, don't bother drawing it!
      if (!isOnScreen(position))
        return;

      TextBox box = boxes.get(e);
      Texture texture = box.texture;
      if (texture == null)
        texture = preprocess(e);

      int width = texture.getWidth();
      int height = texture.getHeight();

      FloatPair drawPosition = MapTools.world2window(position);
      float posX = drawPosition.x - (width / 2);
      float posY = drawPosition.y + height;

      batch.draw(texture, posX, posY);

      posX += PAD;
      posY += texture.getHeight() - (PAD / 2);

      font.setColor(box.color);
      font.draw(batch, box.text, posX, posY);
    }
  }
} public final class Description extends Component {

  public String desc;

  public final AtlasRegion texture;

  public Description(String desc) {
    this(desc, null);
  }

  public Description(String desc, AtlasRegion texture) {
    this.desc = desc;
    this.texture = texture;
  }

  @Override
  public String toString() {
    return desc;
  }

} public class MapHighlighter extends AbstractRenderer {

  private final AtlasRegion highlight;

  private float t;

  private float r, g, b, a;

  private final BitmapFont font;

  public MapHighlighter(Assets assets, OrthographicCamera camera, SpriteBatch batch) {
    super(camera, batch);

    TextureAtlas atlas = new TextureAtlas(Gdx.files.internal("textures/maptiles.atlas"), Gdx.files.internal("textures"));
    highlight = atlas.findRegion("hex_blank");
    font = assets.getFont(10);
    font.setColor(Color.BLACK);

    t = 0;
  }

  public void render(Map<MapPosition, String> tiles) {
    if (tiles == null || tiles.size() < 1)
      return;

    // Get bottom left and top right coordinates of camera viewport and convert
    // into grid coordinates for the map
    int x0 = MathUtils.floor(camera.frustum.planePoints[0].x / MapTools.col_multiple) - 1;
    int y0 = MathUtils.floor(camera.frustum.planePoints[0].y / MapTools.row_multiple);
    int x1 = MathUtils.floor(camera.frustum.planePoints[2].x / MapTools.col_multiple);
    int y1 = MathUtils.floor(camera.frustum.planePoints[2].y / MapTools.row_multiple);

    begin();
    batch.setColor(r, g, b, a / 8 * (7 + MathUtils.cos(8 * t)));

    for (Entry<MapPosition, String> e : tiles.entrySet()) {
      MapPosition tile = e.getKey();
      if (tile.x < x0 || tile.x > x1 || tile.y < y0 || tile.y > y1)
        continue;
      FloatPair coords = MapTools.world2window(tile);
      batch.draw(highlight,
              coords.x - (highlight.getRegionWidth() / 2),
              coords.y - (highlight.getRegionHeight() / 2));

      // then draw the remaining cost for each cell
      String msg = e.getValue();
      if (msg != null)
        font.draw(batch, msg, coords.x, coords.y);
    }

    t += Gdx.graphics.getDeltaTime();

    end();
  }

  public void setColor(float r, float g, float b, float a) {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
} public class MapRenderer extends AbstractRenderer {

  private final TextureAtlas atlas;

  private final Array<AtlasRegion> tilesTextures;

  private final GameMap map;

  public static final String TEXTURE_NAME = "hex";

  public MapRenderer(OrthographicCamera camera, SpriteBatch batch, GameMap map) {
    super(camera, batch);
    this.map = map;

    atlas = new TextureAtlas(Gdx.files.internal("textures/maptiles.atlas"), Gdx.files.internal("textures"));
    tilesTextures = atlas.findRegions(TEXTURE_NAME);
  }

  public AtlasRegion getTexture(Terrain t) {
    return tilesTextures.get(t.getTexture());
  }

  public void render() {
    begin();

    // Get bottom left and top right coordinates of camera viewport and convert
    // into grid coordinates for the map
    int x0 = MathUtils.floor(camera.frustum.planePoints[0].x / MapTools.col_multiple) - 1;
    int y0 = MathUtils.floor(camera.frustum.planePoints[0].y / MapTools.row_multiple) - 1;
    int x1 = MathUtils.floor(camera.frustum.planePoints[2].x / MapTools.col_multiple) + 1;
    int y1 = MathUtils.floor(camera.frustum.planePoints[2].y / MapTools.row_multiple) + 1;

    // Restrict the grid coordinates to realistic values
    if (x0 % 2 == 1)
      x0 -= 1;
    if (x0 < 0)
      x0 = 0;
    if (x1 > map.map.length)
      x1 = map.map.length;
    if (y0 < 0)
      y0 = 0;
    if (y1 > map.map[0].length)
      y1 = map.map[0].length;

    TextureRegion reg;

    // Loop over everything in the window to draw
    for (int row = y0; row < y1; row++) {
      for (int col = x0; col < x1; col++) {
        reg = tilesTextures.get(map.map[col][row].getTexture());
        FloatPair position = MapTools.world2window(col, row);
        batch.draw(reg, position.x - reg.getRegionWidth() / 2, position.y - reg.getRegionHeight() / 2);
      }
    }

    end();
  }

} public class InputManager {

  private static final Logger log = LoggerFactory.getLogger(InputManager.class);

  final Stage stage;

  private final OverworldSelectorController select;

  private final OverworldDragController drag;

  final OverworldFlagController flag;

  public final MenuBuilder menuBuilder;

  private final InputMultiplexer manager;

  private final OverworldScreen screen;

  private final World world;

  Entity selectedEntity;

  public InputManager(Assets assets, OrthographicCamera camera, World world, OverworldScreen screen, Stage stage, GameMap map) {
    this.world = world;
    this.screen = screen;
    this.stage = stage;

    selectedEntity = null;

    select = new OverworldSelectorController(camera, world, map, screen, this);
    drag = new OverworldDragController(camera);
    flag = new OverworldFlagController(camera, screen, this);

    manager = new InputMultiplexer(stage, drag, select);

    menuBuilder = new MenuBuilder(assets, stage, world, map, screen, this);

    Gdx.input.setInputProcessor(manager);
  }

  public void setEnabled(boolean enabled) {
    if (enabled)
      Gdx.input.setInputProcessor(manager);
    else
      Gdx.input.setInputProcessor(null);
  }

  public void reloadMenus() {
    Actor keyboardFocus = stage.getKeyboardFocus();
    Actor scrollFocus = stage.getScrollFocus();

    menuBuilder.clearSubMenus();
    menuBuilder.buildTurnMenu();
    menuBuilder.buildIndicationMenu();
    menuBuilder.buildEmpireMenu();
    menuBuilder.buildNotificationMenu();
    menuBuilder.buildEventsMenu(selectedEntity);
    if (screen.selectedTile != null)
      menuBuilder.buildSelectionMenu(screen.selectedTile, selectedEntity);

    stage.setKeyboardFocus(keyboardFocus);
    stage.setScrollFocus(scrollFocus);
  }

  public void appendInputSystems(InputProcessor ... processors) {
    for (InputProcessor p : processors)
      manager.addProcessor(p);
  }

  public void setInputSystems(InputProcessor ... processors) {
    manager.setProcessors(new Array<>(processors));
  }

  public void prependInputSystems(InputProcessor ... processors) {
    Array<InputProcessor> arr = new Array<>();
    arr.addAll(processors);
    arr.addAll(manager.getProcessors());
    manager.setProcessors(arr);
  }

  public void removeInputSystems(InputProcessor ... processors) {
    for (InputProcessor p : processors)
      manager.removeProcessor(p);
  }

  public void select(MapPosition coords, Entity entity, boolean flagIfMoveable) {
    // user clicked on the map :-)
    screen.selectedTile = coords;
    EntityFactory.createClick(world, coords.x, coords.y, 0.1f, 4f);

    // Now select the current entity (may be null)
    selectedEntity = entity;

    if (entity != null)
      log.debug("Selected {}: {}", coords, entity.getComponent(Description.class));
    else
      log.debug("Selected {}: no entity", coords);

    // Put up a menu for the selected entity
    menuBuilder.clearSubMenus();
    menuBuilder.buildSelectionMenu(coords, entity);
    menuBuilder.buildEventsMenu(entity);

    if (flagIfMoveable && entity != null && entity.getComponent(Destination.class) != null
    // player cannot control AI empires
        && entity.getComponent(AIControlled.class) == null) {
      screen.highlightFlagRange(entity);
      prependInputSystems(flag);
    } else {
      // Make sure we drop any of the highlighted tiles
      screen.stopHighlighing();
    }

    // Need to keep the focus on the map
    stage.setScrollFocus(null);
    stage.setKeyboardFocus(null);
  }

}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.galvarez.ttw.screens.*;
import com.galvarez.ttw.utils.Assets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.artemis.Entity;
import com.artemis.World;
import com.artemis.managers.PlayerManager;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.galvarez.ttw.EntityFactory;
import com.galvarez.ttw.ThingsThatWereGame;
import com.galvarez.ttw.model.AIArmyCommandSystem;
import com.galvarez.ttw.model.AIArmyMovementSystem;
import com.galvarez.ttw.model.AIDiplomaticSystem;
import com.galvarez.ttw.model.AIDiscoverySystem;
import com.galvarez.ttw.model.AISourceDestinationSystem;
import com.galvarez.ttw.model.ArmiesSystem;
import com.galvarez.ttw.model.DestinationSystem;
import com.galvarez.ttw.model.DiplomaticSystem;
import com.galvarez.ttw.model.DisasterSystem;
import com.galvarez.ttw.model.DiscoverySystem;
import com.galvarez.ttw.model.EffectsSystem;
import com.galvarez.ttw.model.EventsSystem;
import com.galvarez.ttw.model.InfluenceSystem;
import com.galvarez.ttw.model.PoliciesSystem;
import com.galvarez.ttw.model.RevoltSystem;
import com.galvarez.ttw.model.ScoreSystem;
import com.galvarez.ttw.model.SpecialDiscoveriesSystem;
import com.galvarez.ttw.model.components.InfluenceSource;
import com.galvarez.ttw.model.data.Empire;
import com.galvarez.ttw.model.data.SessionSettings;
import com.galvarez.ttw.model.map.GameMap;
import com.galvarez.ttw.model.map.Influence;
import com.galvarez.ttw.model.map.MapPosition;
import com.galvarez.ttw.rendering.CameraMovementSystem;
import com.galvarez.ttw.rendering.CounterRenderSystem;
import com.galvarez.ttw.rendering.DestinationRenderSystem;
import com.galvarez.ttw.rendering.DiplomacyRenderSystem;
import com.galvarez.ttw.rendering.FadingMessageRenderSystem;
import com.galvarez.ttw.rendering.InfluenceRenderSystem;
import com.galvarez.ttw.rendering.NotificationsSystem;
import com.galvarez.ttw.rendering.SpriteRenderSystem;
import com.galvarez.ttw.rendering.TextBoxRenderSystem;
import com.galvarez.ttw.rendering.components.Description;
import com.galvarez.ttw.rendering.map.MapHighlighter;
import com.galvarez.ttw.rendering.map.MapRenderer;
import com.galvarez.ttw.screens.overworld.controls.InputManager;
package com.galvarez.ttw.screens.overworld;




public final class OverworldScreen extends AbstractScreen {

  private static final Logger log = LoggerFactory.getLogger(OverworldScreen.class);

  public final GameMap map;

  private final SpriteRenderSystem spriteRenderSystem;

  private final CounterRenderSystem counterRenderSystem;

  private final DiplomacyRenderSystem diplomacyRenderSystem;

  private final InfluenceRenderSystem influenceRenderSystem;

  private final TextBoxRenderSystem textBoxRenderSystem;

  private final DestinationRenderSystem destinationRenderSystem;

  private final FadingMessageRenderSystem fadingMessageRenderSystem;

  private final EffectsSystem effectsSystem;

  private final EventsSystem eventsSystem;

  private final DiscoverySystem discoverySystem;

  private final PoliciesSystem policiesSystem;

  private final DiplomaticSystem diplomaticSystem;

  private final InfluenceSystem influenceSystem;

  final RevoltSystem revoltSystem;

  private final DisasterSystem disasterSystem;

  private final ArmiesSystem armiesSystem;

  private final DestinationSystem destinationSystem;

  private final ScoreSystem scoreSystem;

  private final AISourceDestinationSystem aiDestination;

  private final AIDiscoverySystem aiDiscovery;

  private final AIDiplomaticSystem aiDiplomacy;

  private final AIArmyCommandSystem aiCommand;

  private final AIArmyMovementSystem aiArmies;

  final MapRenderer mapRenderer;

  private final MapHighlighter mapHighlighter;

  private final Map<MapPosition, String> highlightedTiles = new HashMap<>();

  private boolean renderHighlighter;

  private final List<String> indications = new ArrayList<>();

  private final InputManager inputManager;

  /** Stage to display the HUD. */
  private final Stage stage;

  private final CameraMovementSystem cameraMovementSystem;

  private boolean firstShow = true;

  private int turnNumber = 0;

  public MapPosition selectedTile;

  private final PauseMenuScreen pauseScreen;

  private final DiplomacyMenuScreen diplomacyScreen;

  private final DiscoveryMenuScreen discoveryScreen;

  private final AskDiscoveryScreen askDiscoveryScreen;

  private final AskEventScreen askEventScreen;

  private final PoliciesMenuScreen policiesScreen;

  private final ScoresMenuScreen scoresScreen;

  private final ArmiesMenuScreen armiesScreen;

  private final TutorialMenuScreen tutorialScreen;

  private final NotificationsSystem notificationsSystem;

  public Entity player;

  private final List<Entity> empires;

  public OverworldScreen(ThingsThatWereGame game, SpriteBatch batch, World world, SessionSettings settings) {
    super(game, world, batch);

    cameraMovementSystem = world.setSystem(new CameraMovementSystem(camera));

    stage = new Stage(new ScreenViewport(), new SpriteBatch());

    map = new GameMap(settings.mapType.get().algo.getMapData(settings.map), settings.empires);

    notificationsSystem = world.setSystem(new NotificationsSystem(game.assets), true);
    eventsSystem = world.setSystem(new EventsSystem(), true);
    effectsSystem = world.setSystem(new EffectsSystem(), true);
    world.setSystem(new SpecialDiscoveriesSystem(this), true);
    discoverySystem = world.setSystem(new DiscoverySystem(settings, map, this), true);
    policiesSystem = world.setSystem(new PoliciesSystem(), true);
    diplomaticSystem = world.setSystem(new DiplomaticSystem(this, settings.startWithDiplomacy.get()), true);
    influenceSystem = world.setSystem(new InfluenceSystem(map), true);
    revoltSystem = world.setSystem(new RevoltSystem(map, settings, this), true);
    disasterSystem = world.setSystem(new DisasterSystem(this), true);
    armiesSystem = world.setSystem(new ArmiesSystem(map, this), true);
    destinationSystem = world.setSystem(new DestinationSystem(map, this), true);
    scoreSystem = world.setSystem(new ScoreSystem(settings, this), true);
    aiDestination = world.setSystem(new AISourceDestinationSystem(map, this), true);
    aiDiscovery = world.setSystem(new AIDiscoverySystem(), true);
    aiDiplomacy = world.setSystem(new AIDiplomaticSystem(map), true);
    aiCommand = world.setSystem(new AIArmyCommandSystem(map, this), true);
    aiArmies = world.setSystem(new AIArmyMovementSystem(map, this), true);
    influenceRenderSystem = world.setSystem(new InfluenceRenderSystem(camera, batch, map), true);
    diplomacyRenderSystem = world.setSystem(new DiplomacyRenderSystem(camera, batch), true);
    textBoxRenderSystem = world.setSystem(new TextBoxRenderSystem(game.assets, camera, batch), true);
    destinationRenderSystem = world.setSystem(new DestinationRenderSystem(camera, batch), true);
    spriteRenderSystem = world.setSystem(new SpriteRenderSystem(camera, batch), true);
    counterRenderSystem = world.setSystem(new CounterRenderSystem(game.assets, camera, batch), true);
    fadingMessageRenderSystem = world.setSystem(new FadingMessageRenderSystem(game.assets, camera, batch), true);

    world.initialize();
    empires = fillWorldWithEntities();
    discoverySystem.process();
    policiesSystem.process();
    diplomaticSystem.process();
    influenceSystem.process();
    revoltSystem.process();
    disasterSystem.process();
    armiesSystem.process();
    destinationSystem.process();
    scoreSystem.process();
    diplomacyRenderSystem.preprocess();
    influenceRenderSystem.preprocess();
    textBoxRenderSystem.preprocess();
    notificationsSystem.process();
    log.info("The world is initialized");

    inputManager = new InputManager(game.assets, camera, world, this, stage, map);
    inputManager.menuBuilder.buildTurnMenu();
    inputManager.menuBuilder.buildNotificationMenu();
    inputManager.menuBuilder.buildEmpireMenu();

    world.setManager(new PlayerManager());

    mapRenderer = new MapRenderer(camera, batch, map);
    mapHighlighter = new MapHighlighter(game.assets, camera, batch);

    renderHighlighter = false;

    tutorialScreen = new TutorialMenuScreen(game, world, batch, this);
    pauseScreen = new PauseMenuScreen(game, world, batch, this);
    diplomacyScreen = new DiplomacyMenuScreen(game, world, batch, this, empires, player, diplomaticSystem);
    discoveryScreen = new DiscoveryMenuScreen(game, world, batch, this, player, discoverySystem);
    askDiscoveryScreen = new AskDiscoveryScreen(game, world, batch, this, player, discoverySystem);
    askEventScreen = new AskEventScreen(game, world, batch, this);
    policiesScreen = new PoliciesMenuScreen(game, world, batch, this, player, policiesSystem, effectsSystem,
        revoltSystem);
    scoresScreen = new ScoresMenuScreen(game, world, batch, this, scoreSystem);
    armiesScreen = new ArmiesMenuScreen(game, world, batch, this, player, armiesSystem);
  }

  @Override
  public void show() {
    super.show();

    inputManager.setEnabled(true);
  }

  @Override
  public void render(float delta) {
    super.render(delta);

    if (firstShow) {
      if (selectedTile != null)
        cameraMovementSystem.move(selectedTile);
      else
        cameraMovementSystem.move(new MapPosition(map.width / 2, map.height / 2));
      firstShow = false;
    }

    mapRenderer.render();
    diplomacyRenderSystem.process();
    influenceRenderSystem.process();
    destinationRenderSystem.process();
    counterRenderSystem.process();
    spriteRenderSystem.process();
    textBoxRenderSystem.process();

    if (renderHighlighter) {
      mapHighlighter.render(highlightedTiles);
    }

    // render AFTER map was drawn
    fadingMessageRenderSystem.process();

    stage.act(delta);
    stage.draw();
  }

  @Override
  public void resize(int width, int height) {
    super.resize(width, height);

    // update map camera and keep same center
    Vector3 pos = camera.position.cpy();
    camera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
    camera.position.set(pos);

    // update GUI view
    stage.getViewport().update(width, height, true);
    /*
     * The GUI use a ScreenViewport, meaning it won't scale when screen size
     * change. This is fine because we don't want the GUI size to change,
     * becoming zoomed in and ugly or zoomed and unreadable. However it has a
     * small side effect: the existing menu were placed according to the
     * vertical stage size. The stage size changed with the screen (game window)
     * one. So we must recompute the GUI elements coordinates. The simlest way
     * to do it is to recreate the menu.
     */
    inputManager.reloadMenus();
  }

  @Override
  public void dispose() {
    stage.dispose();
  }

  private List<Entity> fillWorldWithEntities() {
    // add the entity for players cities
    List<Entity> list = new ArrayList<>();
    for (Empire empire : map.empires)
      list.add(createEmpire(empire.newCityName(), empire));

    selectedTile = player.getComponent(MapPosition.class);

    // You have to process the world once to get all the entities loaded up with
    // the "Stats" component - I'm not sure why, but if you don't, the bag of
    // entities that turnManagementSystem gets is empty?
    world.process();

    return list;
  }

  private Entity createEmpire(String name, Empire empire) {
    int x, y;
    do {
      x = MathUtils.random(map.width - 4) + 2;
      y = MathUtils.random(map.height - 4) + 2;
    } while (!map.getTerrainAt(x, y).canStart()
    // avoid having two cities on the same tile
        || map.getEntityAt(x, y) != null
        // or on neighbor tiles
        || hasNeighbourCity(x, y));
    Entity entity = EntityFactory.createEmpire(world, x, y, name, empire);
    if (!empire.isComputerControlled())
      player = entity;
    map.setEntity(entity, x, y);
    InfluenceSource source = entity.getComponent(InfluenceSource.class);
    source.influencedTiles.add(map.getPositionAt(x, y));
    for (MapPosition p : map.getNeighbors(x, y, 1)) {
      if (map.isOnMap(p)) {
        Influence inf = map.getInfluenceAt(p);
        inf.setInfluence(entity, inf.getMaxInfluence());
        source.influencedTiles.add(p);
      }
    }
    log.info("Created {} for empire {}", name, empire);
    return entity;
  }

  private boolean hasNeighbourCity(int x, int y) {
    for (MapPosition pos : map.getNeighbors(x, y, 2))
      if (map.getEntityAt(pos.x, pos.y) != null)
        return true;
    return false;
  }

  private void processTurn() {
    log.info("Process new turn");
    long start = System.currentTimeMillis();

    // If they don't have AI, give control to the human player
    inputManager.setEnabled(true);
    stage.setKeyboardFocus(null);
    stage.setScrollFocus(null);

    // compute AI before 'really' processing turn
    aiDestination.process();
    aiDiscovery.process();
    aiDiplomacy.process();
    aiCommand.process();
    // make sure armies created by command are assigned a destination
    aiArmies.process();

    discoverySystem.process();
    policiesSystem.process();
    diplomaticSystem.process();
    armiesSystem.process();
    destinationSystem.process();
    influenceSystem.process();
    revoltSystem.process();
    disasterSystem.process();
    scoreSystem.process();
    eventsSystem.process();

    world.setDelta(0);
    world.process();
    
    updateRendering();
    notificationsSystem.process();
    turnNumber++;

    log.debug("Processed turn #{} in {}ms", turnNumber, System.currentTimeMillis() - start);
  }
  
  public void updateRendering() {
    diplomacyRenderSystem.preprocess();
    influenceRenderSystem.preprocess();
    textBoxRenderSystem.preprocess();    
  }

  public void flag(Entity source, int x, int y) {
    destinationSystem.computePath(source, new MapPosition(x, y));
  }

  public void highlightFlagRange(Entity source) {
    highlightedTiles.clear();
    for (MapPosition tile : destinationSystem.getTargetTiles(source))
      highlightedTiles.put(tile, null);
    renderHighlighter = true;
    mapHighlighter.setColor(0f, 0f, 0.2f, 0.3f);
    indications.add("Select destination in highlighted tiles for " + source.getComponent(Description.class));