package com.galvarez.ttw.model;
import static java.lang.Math.min;
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
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.Entry;
import com.badlogic.gdx.utils.IntMap;
import com.badlogic.gdx.utils.ObjectFloatMap;
import com.galvarez.ttw.EntityFactory;
import com.galvarez.ttw.model.EventsSystem.EventHandler;
import com.galvarez.ttw.model.components.AIControlled;
import com.galvarez.ttw.model.components.Diplomacy;
import com.galvarez.ttw.model.components.Discoveries;
import com.galvarez.ttw.model.components.InfluenceSource;
import com.galvarez.ttw.model.components.Research;
import com.galvarez.ttw.model.data.Discovery;
import com.galvarez.ttw.model.data.Empire;
import com.galvarez.ttw.model.data.Policy;
import com.galvarez.ttw.model.data.SessionSettings;
import com.galvarez.ttw.model.map.GameMap;
import com.galvarez.ttw.model.map.MapPosition;
import com.galvarez.ttw.model.map.Terrain;
import com.galvarez.ttw.rendering.IconsSystem.Type;
import com.galvarez.ttw.rendering.NotificationsSystem;
import com.galvarez.ttw.rendering.components.Description;
import com.galvarez.ttw.rendering.components.Name;
import com.galvarez.ttw.screens.overworld.OverworldScreen;
/**
* For every empire, compute the new discovery.
* <p>
* Every turn a certain progress is made toward discovery is made, depending on
* its capital power and influence.
* </p>
*
* @author Guillaume Alvarez
*/
@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;
}
Collections.sort(possible, new Comparator<Research>() {
@Override
public int compare(Research d1, Research d2) {
// reverse order: greatest first
return -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 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, Type.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, Type.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);
}
}