package com.galvarez.ttw.model; import static java.lang.Math.abs; import static java.lang.Math.max; import static java.lang.Math.min; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.artemis.Aspect; import com.artemis.ComponentMapper; import com.artemis.Entity; import com.artemis.EntitySystem; import com.artemis.annotations.Wire; import com.artemis.utils.ImmutableBag; import com.badlogic.gdx.utils.IntIntMap; import com.badlogic.gdx.utils.IntIntMap.Entry; import com.badlogic.gdx.utils.ObjectIntMap; import com.galvarez.ttw.EntityFactory; import com.galvarez.ttw.model.DiplomaticSystem.Action; import com.galvarez.ttw.model.DiplomaticSystem.State; import com.galvarez.ttw.model.components.Army; import com.galvarez.ttw.model.components.ArmyCommand; import com.galvarez.ttw.model.components.Diplomacy; import com.galvarez.ttw.model.components.InfluenceSource; import com.galvarez.ttw.model.components.InfluenceSource.Modifiers; import com.galvarez.ttw.model.data.Empire; 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.model.map.MapTools.Border; import com.galvarez.ttw.model.map.Terrain; import com.galvarez.ttw.rendering.components.Description; /** * This classes computes the influence from the different sources (i.e. cities) * and update it every turn. * <p> * The game is centered on the influence idea. It starts from a source with a * certain power and flow on the neighboring map tiles. The throughput depends * on the source power, distance to the source and terrain cost/difficulty. When * multiple sources influence the same tile, it belongs to the source with the * highest influence score. For ease of understanding, influence is expressed as * percentages. * <p> * Ideally the influence progression should be: * <ul> * <li>at a constant pace and continuous from a turn to the next (for instance * one tile per turn) * <li>with decreasing values from the civilized center to the wild or disputed * border * <li>on a small scale so that close influence sources can fight for tiles * <p> * The model that works the best seems to be a linear interpolation of the * target influence. The target influence is computed as a waterfall algorithm * from the source. * * @author Guillaume Alvarez */ @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.height; x++) for (int y = 0; y < map.width; 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.height; x++) for (int y = 0; y < map.width; 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); } }