Monday, April 22, 2013

Combat and Turn Management - Level 3

This started out as a nice, short, update.  But I kept adding stuff which messed with what I already had, so it kind of grew.  You can check out all the code at the repository - I probably won't put it all here verbatim: there were a lot of changes, many of which were totally cosmetic.

Last time we made a menu which users could select move, and I want to add more controls now.  This update will introduce a basic combat system, and simple turn management, plus a few jazzy effects.

Before we really get into that, there are a few organizational changes in OverworldScreen worth mentioning
  1. In the constructor, I separate code out into a few different void methods so it's a little easier to find what I'm looking for.
  2. I also keep instances of each of the Controllers in memory, so I don't have to set the input processor to a new _____Controller every time.
  3. On that note, I had to split up the drag functionality, and the character select functionality from what used to be called the OverworldDefaultController.  It's been replaced with OverworldDragController and OverworldSelectorController (I had to do this because I needed the attack controller to fit between them)
  4. I also added a "remove" method for the input multiplexor (I know it has a built in one, but for now I feel more like forcing everything to interface with it through the helper methods I made.
  5. Because it's written with libgdx, it has the potential to be ported to Android (and even iPhone now).  Consequently, not everybody will have a mouse (or touchscreen) and I started experimenting with a cursor that users can move around with arrow keys.  One problem with that is that each cell has 6 neighbors, but you only have 4 arrow keys.  The cursor, for now, just looks like an unanimated knight who starts at (0,0), and you can move it around to see how it works.  I don't like it right now, particularly when you need to go up/left or down/right.  I have plans to fix it someday though.
Here's an overview of the topics we'll cover now:
  • Combat System
    • Components: Stats, Damage
    • Systems: DamageSystem
    • Controller: OverworldAttackController
  • Damage Label
    • Components: FadingMessage
    • Systems: FadingMessageRenderSystem
  • Turn Management
    • Systems: TurnManagementSystem
  • Camera Movement
    • Systems: CameraMovementSystem

Combat System

For combat to make any sense, characters must now have things like strength and health.  I made a Stats component which looks like this:
package com.blogspot.javagamexyz.gamexyz.components;

import com.artemis.Component;
import com.badlogic.gdx.math.MathUtils;

public class Stats extends Component {
 
 public int level, xp, next_xp;
 
 private int strength, strength_modifier;
 private int intelligence, intelligence_modifier;
 private int speed, speed_modifier;
 private int agility, agility_modifier;
 private int charisma, charisma_modifier;
 private int hardiness, hardiness_modifier;
 private int resistance, resistance_modifier;
 
 public int health, maxHealth, maxHealth_modifier;
 public int magic, maxMagic, maxMagic_modifier;
 
 public String name;
 
 public int actionPoints;
 
 public Stats() {
  level = 1;
  xp = 0;
  next_xp = 100;
  
  strength = 15 + MathUtils.random(-3, 3);
  intelligence = 15 + MathUtils.random(-3, 3);
  speed = 15 + MathUtils.random(-3, 3);
  agility = 15 + MathUtils.random(-3, 3);
  charisma = 15 + MathUtils.random(-3, 3);
  hardiness = 15 + MathUtils.random(-3, 3);
  resistance = 15 + MathUtils.random(-3, 3);
  
  health = maxHealth = (int)((5*hardiness + 4*strength + 2*resistance) / 11);
  magic = maxMagic = (int)((5*intelligence + 2*resistance) / 7);
  
  strength_modifier = intelligence_modifier = speed_modifier = agility_modifier = 
    charisma_modifier = hardiness_modifier = resistance_modifier = maxHealth_modifier =
    maxMagic_modifier = 0;
  
  name = names[MathUtils.random(names.length-1)];
  
  actionPoints = 0;
  
 }
 
 private final String[] names = {"Rodann","Ranlan","Luhiri","Serl","Polm","Boray","Ostan","Inaes"};
 
 public int getAgility() {
  return agility + agility_modifier;
 }
 
 public int getHardiness() {
  return hardiness + hardiness_modifier;
 }
 
 public int getStrength() {
  return strength + strength_modifier;
 }
 
 public int getCharisma() {
  return charisma + charisma_modifier;
 }
 
 public int getIntelligence() {
  return intelligence + intelligence_modifier;
 }
 
 public int getResistance() {
  return resistance + resistance_modifier;
 }
 
 public int getSpeed() {
  return speed + speed_modifier;
 }
}


It permits storage of base stats (which are mostly totally self explanatory), plus modifiers which might come from spells, equipment, etc.  The list of names was just a short list taken from the Fantasy Name Generator over at RinkWorks.com.  HP and MP are derived stats calculated as a weighted average of a few different stats.  Right now when an entity gets created with new stats, they are all random ints from 12 to 18.

The actionPoints stat will be used to determine who gets to move, and will be discussed in more detail further down this post under Turn Management.

In OverworldScreen, I added a method "selectedAttack()" and "selectedWait()" just like we had "selectedMove()".  When they choose "Attack", selectedAttack() puts a new Controller in the mix, OverworldAttackController:

public void selectedAttack() {
  if (attacked) return;
  setInputSystems(controllerDrag,controllerAttack,controllerSelector);
  highlightedCells = MapTools.getNeighbors(activeEntityCell.x, activeEntityCell.y);
  renderAttackRange = true;
  handleStage = false;
  stage.clear();
 }

 public void selectedWait() {
  setInputSystems(controllerDrag, controllerSelector);
  processTurn();
  handleStage = false;
  stage.clear();
  selectedEntity = -1;
  
  moved = attacked = false;
 }


All this really does is highlight the cells that the player can attack.  We set the controllers we want - again, order is important!  First and foremost they can drag the screen, secondly they can attack, thirdly they can select another entity (perhaps to check its stats).

We also get the neighbors of activeEntityCell (which is a pair that is determined during the turn management part of things) to find the attackable range, set a flag renderAttackRange to true, quit handling the stage and clear it off.

In render(), we have to actually highlight these cells
  if (renderAttackRange) {
   mapHighlighter.render(highlightedCells,0.5f,0f,0f,0.3f);
  }



But most of the real magic happens over in OverworldAttackController
package com.blogspot.javagamexyz.gamexyz.screens.control.overworld;

import com.artemis.Entity;
import com.artemis.World;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.blogspot.javagamexyz.gamexyz.components.Damage;
import com.blogspot.javagamexyz.gamexyz.components.Stats;
import com.blogspot.javagamexyz.gamexyz.custom.Pair;
import com.blogspot.javagamexyz.gamexyz.maps.GameMap;
import com.blogspot.javagamexyz.gamexyz.maps.MapTools;
import com.blogspot.javagamexyz.gamexyz.screens.OverworldScreen;

public class OverworldAttackController implements InputProcessor {
 
 private OrthographicCamera camera;
 private World world;
 private GameMap gameMap;
 private OverworldScreen screen;
 
 public OverworldAttackController(OrthographicCamera camera, World world, GameMap gameMap, OverworldScreen screen) {
  this.camera = camera;
  this.world = world;
  this.gameMap = gameMap;
  this.screen = screen;
 }

 @Override
 public boolean keyDown(int keycode) {
  // TODO Auto-generated method stub
  return false;
 }

 @Override
 public boolean keyUp(int keycode) {
  // TODO Auto-generated method stub
  return false;
 }

 @Override
 public boolean keyTyped(char character) {
  // TODO Auto-generated method stub
  return false;
 }

 @Override
 public boolean touchDown(int screenX, int screenY, int pointer, int button) {
  // TODO Auto-generated method stub
  return false;
 }

 @Override
 public boolean touchUp(int screenX, int screenY, int pointer, int button) {
  Pair coords = MapTools.window2world(Gdx.input.getX(), Gdx.input.getY(), camera);
  int entityId = gameMap.getEntityAt(coords.x, coords.y);
  
  // Did they click within the attackable range, and a real entity?
  if (screen.highlightedCells.contains(coords, false) && entityId > -1) {
   
   Entity source = world.getEntity(screen.selectedEntity);
   Entity target = world.getEntity(entityId);
   
   Stats stats = source.getComponent(Stats.class);
   Damage damage = new Damage(stats);
   
   target.addComponent(damage);
   target.changedInWorld();
   
   // Tell the screen that this entity has attacked this turn
   screen.attacked = true;
  }

  // Wherever they clicked, they are now done with the "attacking" aspect of things
  screen.highlightedCells = null;
  screen.renderAttackRange = false;
  screen.selectedEntity = -1;
  screen.removeInputSystems(this);
  return true;
 }

 @Override
 public boolean touchDragged(int screenX, int screenY, int pointer) {
  // TODO Auto-generated method stub
  return false;
 }

 @Override
 public boolean mouseMoved(int screenX, int screenY) {
  // TODO Auto-generated method stub
  return false;
 }

 @Override
 public boolean scrolled(int amount) {
  // TODO Auto-generated method stub
  return false;
 }
}


The only thing we really want to process is a touchUp(), where we get the cell they clicked, and the entity at that cell (or -1 if there is no entity).  Then if they clicked within the attackable range, and on a real entity, we define source and target entities (source is the attacker, target is the attackee).

Then we read the stats from the attacker (remember, this isn't an "EntitySystem" so we can't use the @Mapper) and pass it to a new Component called Damage.  The idea here is that entities with the Damage component will get processed by the DamageSystem, which will then doll out damage appropriately.  I wanted the actual damage calculations to happen outside of the controller, in their own system.

Then, no matter where they clicked, we're done rendering the attack stuff.

The damage component is pretty simple for now:
package com.blogspot.javagamexyz.gamexyz.components;

import com.artemis.Component;

public class Damage extends Component {

 public int power;
 public int accuracy;
 public Stats stats;
 
 public Damage(Stats stats) {
  this.stats = stats;
  power = stats.getStrength();
  accuracy = stats.getAgility();
 }
}
I know it's redundant, but since I'm sure it will be heavily tweaked later on for different types of attacks, I'm leaving it like this for now.

The DamageSystem extends EntityProcessingSystem and looks like this:
package com.blogspot.javagamexyz.gamexyz.systems;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.annotations.Mapper;
import com.artemis.systems.EntityProcessingSystem;
import com.badlogic.gdx.math.MathUtils;
import com.blogspot.javagamexyz.gamexyz.EntityFactory;
import com.blogspot.javagamexyz.gamexyz.components.Damage;
import com.blogspot.javagamexyz.gamexyz.components.MapPosition;
import com.blogspot.javagamexyz.gamexyz.components.Stats;
import com.blogspot.javagamexyz.gamexyz.maps.GameMap;
import com.blogspot.javagamexyz.gamexyz.utils.MyMath;

public class DamageSystem extends EntityProcessingSystem {
 @Mapper ComponentMapper<Damage> dm;
 @Mapper ComponentMapper<Stats> sm;
 @Mapper ComponentMapper<MapPosition> mpm;
 
 private GameMap gameMap;
 
 @SuppressWarnings("unchecked")
 public DamageSystem(GameMap gameMap) {
  super(Aspect.getAspectForAll(Damage.class, Stats.class));
  this.gameMap = gameMap;
 }

 @Override
 protected void process(Entity e) {
  Damage damage = dm.get(e);
  Stats stats = sm.get(e);
  MapPosition position = mpm.getSafe(e); // Useful for displaying damage on screen 
  
  // Did the blow hit?
  if (damage.accuracy - stats.getAgility() + MathUtils.random(-1,4) > 0) {
   
   // Compute how much damage it did
   int dmg = MyMath.max(damage.power - stats.getHardiness() / 2 + MathUtils.random(-5,5), 1);
   
   // Update the target's health accordingly
   stats.health -= dmg;
   System.out.println(damage.stats.name + " HIT " + stats.name + "! Damage " + dmg + "\t\t Health: " + stats.health + "/" + stats.maxHealth);
   
   // If the target had a MapPosition, create a damage label showing how much damage was done
   if (position != null) {
    EntityFactory.createDamageLabel(world, ""+dmg, position.x, position.y).addToWorld();
   }
  }
 
  else { // Otherwise they missed
   System.out.println(damage.stats.name + " MISSED " + stats.name +"!");
   
   // Create a damage label of "Miss" to add insult to injury
   if (position != null) {
    EntityFactory.createDamageLabel(world, "MISS", position.x, position.y).addToWorld();
   }
  }
  
  // We've processed the damage, now it's done
  e.removeComponent(damage);
  e.changedInWorld();
 }
 
 @Override
 protected void removed(Entity e) {
  // This is called after the damage gets removed
  // We want to see if the target died in the process
  Stats stats = sm.get(e);
  if (stats.health <= 0) {
   // If so, it's toast!
   gameMap.removeEntity(e.getId());
   world.deleteEntity(e); 
  }
 }
 
 @Override
 protected boolean checkProcessing() {
  return true;
 }
}

It processes entities that have both damage and stats (I figure if they have no stats, they won't be getting damaged!).  In process(), we get the Damage and Stats, and also (if applicable) the MapPosition.  This last part is useful for actually rendering the damage dealt on the screen.

First we need to see if they actually hit them.  My check here is if source_accuracy - target_agility + rand(-1,4) > 0, then they hit.  The damage  dealt = source_power - target_hardiness + rand(-5,5).  If that damage is less than 1, they just deal 1 damage instead.  We update the target's health, and if appropriate, add a damage label.

If the attack missed, so we just add a "missed" label.

At the end, we have finished processing this damage, so we remove it (which calls the removed() method).  In this method, we check to see if the damage killed the entity.  If so, remove it from the world.

Damage Label - Fading Message

The damage label is totally cosmetic, but it's pretty cool.  Let's take a look at it in EntityFactory
 public static Entity createDamageLabel(World world, String label, float x, float y) {
  Entity e = world.createEntity();
  
  e.addComponent(new MapPosition(x,y));
  e.addComponent(new FadingMessage(label,1.2f,0f,1.3f));
  
  return e;
 }
Here we make an entity with MapPosition and a new component FadingMessage.  FadingMessage is a general component I created for giving the player a... well... a fading message.  The arguments are a String for the actual label, a float for the duration (how long it takes to fade away), and floats for horizontal and vertical velocity to it can move.  For our case, it lasts 1.2 seconds, and has a vertical velocity of 1.3 cells per second (notice that's not pixels / seconds, but literally 1.3 hex cells / second).

Here's the code for FadingMessage (which will get processed AND rendered by FadingMessageRenderSystem)
package com.blogspot.javagamexyz.gamexyz.components;

import com.artemis.Component;

public class FadingMessage extends Component {

 public String label;
 public float duration, currentTime;
 public float vx, vy;
 
 public FadingMessage(String label, float duration) {
  this(label,duration,0,0);
 }
 
 public FadingMessage(String label, float duration, float vx, float vy) {
  this.label = label;
  this.duration = duration;
  this.vx = vx;
  this.vy = vy;
  currentTime = 0f;
 } 
}
Nothing too surprising, it just holds all that info, plus currentTime which stores how long it has been alive.
package com.blogspot.javagamexyz.gamexyz.systems;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.annotations.Mapper;
import com.artemis.systems.EntityProcessingSystem;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.blogspot.javagamexyz.gamexyz.components.FadingMessage;
import com.blogspot.javagamexyz.gamexyz.components.MapPosition;
import com.blogspot.javagamexyz.gamexyz.custom.FloatPair;
import com.blogspot.javagamexyz.gamexyz.maps.MapTools;

public class FadingMessageRenderSystem extends EntityProcessingSystem {
 @Mapper ComponentMapper<MapPosition> mpm;
 @Mapper ComponentMapper<FadingMessage> fmm;

 private BitmapFont font;
 private SpriteBatch batch;
 private OrthographicCamera camera;
 
 
 @SuppressWarnings("unchecked")
 public FadingMessageRenderSystem(OrthographicCamera camera, SpriteBatch batch) {
  super(Aspect.getAspectForAll(MapPosition.class, FadingMessage.class));
  this.batch = batch;
  this.camera = camera;
 }

 @Override
 protected void initialize() {
  Texture fontTexture = new Texture(Gdx.files.internal("fonts/normal_0.png"));
  fontTexture.setFilter(TextureFilter.Linear, TextureFilter.MipMapLinearLinear);
  TextureRegion fontRegion = new TextureRegion(fontTexture);
  font = new BitmapFont(Gdx.files.internal("fonts/normal.fnt"), fontRegion, false);
  font.setUseIntegerPositions(false);
 }

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

 @Override
 protected void process(Entity e) {
  MapPosition position = mpm.get(e);
  FadingMessage message = fmm.get(e);
 
  FloatPair drawPosition = MapTools.world2window(position.x, position.y);
  float posX = drawPosition.x - message.label.length() * font.getSpaceWidth();
  float posY = drawPosition.y;
  
  font.setColor(1, 1, 1, 1 - message.currentTime / message.duration);
  font.draw(batch, message.label, posX, posY);

  position.x += message.vx * world.getDelta();
  position.y += message.vy * world.getDelta();
  message.currentTime += world.getDelta();
  
  if (message.currentTime >= message.duration) e.deleteFromWorld();
 }
 
 @Override
 protected void end() {
  batch.end();
 }
}
Here we have a RenderSystem which is actually an EntityProcessingSystem again, so I modeled it after them.  It has its camera and batch, plus a font.  In process() it gets the position and does its best to center it (posX = drawPosition.x - message.label.length() * font.getSpaceLength()).  The color is set to white, and the alpha goes from 1 to 0 from start to finish.  The position is updated (remember, that's not screen position, that's world position - as in what cell it occupies) and currentTime is updated.  Then, if currentTime exceeds duration, the message gets deleted from the world.

So that's all pretty cool - you can now attack someone and have it deal damage (potentially killing them), and the damage is displayed in what I think is a pretty attractive floating/fading message.  Of course, there's nothing stopping you from attacking AGAIN AND AGAIN AND AGAIN until they're dead.  Obviously in a game you only get to attack once, and then it's the next character's turn.

To facilitate that, I created flags in OverworldScreen boolean attacked, boolean moved.  They are initialized to false, and in selectedAttack() and selectedMove(), we first check them:
public void selectedMove() {
 if (moved) return;
 ...
}

public void selectedAttack() {
 if (attacked) return;
 ...
}

In the controllers, if the player successfully moves or attacks, they are then set to true.  Then, in a new method, selectedWait(), we resent them both to false and "processTurn()".  processTurn() is a method which helps establish whose turn it is to move.
 public void processTurn() {
  turnManagementSystem.process();
  activeEntity = unitOrder.get(0);
  activeEntityCell = gameMap.getCoordinatesFor(activeEntity);
  if (activeEntityCell != null) cameraMovementSystem.move(activeEntityCell.x, activeEntityCell.y);
 }

The first bit, turnManagementSystem.process() comes up with a list representing the order units are going to be moving in.  It assumes nothing ever changes - like no units will die - so it must be run each turn to account for updates.  turnManagementSystem.process() updates an Array of ints in OverworldScreen called unitOrder (the order in which units will get their turns).  The next bit, activeEntity = unitOrder.get(0) gets the first unit from the list (the one about to go) and activeEntityCell (which we referenced earlier) gets set here.  The last bit, cameraMovementSystem, moves the camera to be centered on the new active entity (we'll talk about it next).

TurnManagementSystem

Before looking at the code, let's discuss how it works.  The idea is that each unit has something called Action Points.  Every turn, a unit's action points are incremented by their speed, and once it hits 100, that unit gets a turn.  But what if nobody has reached 100 action points?

In this case, we ask "How many turns will each player need before reaching 100 points?"  This can be calculated by taking (100 - actionPoints) / speed.  (Note: If an entity has over 100 action points, this will return a negative number of turns - in this case we just want to make it say 0).

Then, we look at the player with the fewest turns needed before reaching 100 action points.  We just "skip" those turns, and instead of incrementing everyone's action points by their speed, we increment it by speed*turnsSkipped.

We also need to remember who went, because they should have their action points reset to 0 next time.

Now let's look at how it works in an example case.  We have two units: A and B.  A has speed 5, B has speed 4.  To start with, both have 0 action points.  On the first turn, we calculate that A needs (100-0)/5 = 20 turns, and B needs (100-0)/4 = 25 turns.  Because A is closer to moving, we "skip" 20 turns, bringing them up to 100 action points, and B up to 4*20 = 80 action points.  When A moves,
their action points ought to reset to 0, then we repeat.

A now needs (100-0)/5 = 20 turns, but B only needs (100-80)/4 = 5 turns.  So we skip those 5 turns, giving B 100 action points, and A 5*5 = 25 action points.  B moves and is reduced to 0 points.

Now A needs (100-25)/5 = 15, B needs (100-0)/4 = 25 turns.  We skip 15, and A is at 100, B is at 4*15 = 60.  A moves and is reduced to 0 points.

Okay, well that's all well and good, but it may also be nice to project out the next x turns to let the player know who's moving when.  We can do that too.  In the step where we figure out who moves next, we can just repeat this process over and over again, pretending like we're updating units' action points, and simulating what it will really look like.  So without further ado, here's TurnManagementSystem:
package com.blogspot.javagamexyz.gamexyz.systems;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.EntitySystem;
import com.artemis.annotations.Mapper;
import com.artemis.utils.ImmutableBag;
import com.badlogic.gdx.utils.Array;
import com.blogspot.javagamexyz.gamexyz.components.Stats;
import com.blogspot.javagamexyz.gamexyz.utils.MyMath;

public class TurnManagementSystem extends EntitySystem {
 @Mapper ComponentMapper<Stats> sm;

 private Array<Integer> unitOrder;
 private Array<Sorter> sorter;
 
 @SuppressWarnings("unchecked")
 public TurnManagementSystem(Array<Integer> unitOrder) {
  super(Aspect.getAspectForAll(Stats.class));
  
  this.unitOrder = unitOrder;
  sorter = new Array<Sorter>();
 }

 @Override
 protected void processEntities(ImmutableBag<Entity> entities) { 
  // Get the ID of the last entity to move - because they will get reset to 0
  // To be safe, first assume that this is the first turn, so that oldEntity = -1
  int oldEntity = -1;
  
  // We'll use this to store how many turns got skipped for the next "real" turn
  float turnsSkipped = 1f;
  
  // Then, if there is a list for unitOrder, the first entity was the one that
  // moved last turn
  if (unitOrder.size > 0) oldEntity = unitOrder.get(0);
  
  // Now we just clear the unit list because it needs to be recalculated
  unitOrder.clear();
  sorter.clear();
  
  // add the entity to the sorter array
  for (int i=0; i<entities.size(); i++) {
   Entity e = entities.get(i);
   Stats stats = sm.get(e);
   
   // Earlier we stored who moved last turn as oldEntity.  Evidently they
   // just moved, so we'll reset their actionPoints to 0.
   if (e.getId() == oldEntity) stats.actionPoints = 0;
   
   sorter.add(new Sorter(e.getId(), stats.actionPoints, stats.getSpeed(), stats.getAgility()));
  }
  
  // Come up with a list of the next 30 entities to move
  for (int i = 0; i < 30; i++) {
   
   // Sort the list based on turnsSkipped
   sorter.sort();
   
   // The first unit in the sorted list is the next unit to get a turn
   unitOrder.add(sorter.get(0).id);
   
   // In case this is the 1st time we're going through the loop, that means
   // we're looking at the unit that actually gets to move this turn.  Note
   // how many turns it had to skip, because that's what we will ACTUALLY
   // use to increment unit's actionPoints.
   if (i == 0) turnsSkipped = sorter.get(0).turnsSkipped;
   
   // Update everyone's actionPoints
   for (int index = 1; index < sorter.size; index++) {
    Sorter s = sorter.get(index);
    s.actionPoints += (int)(sorter.get(0).turnsSkipped * s.speed);
    s.calculateTurnsSkipped();
   }
   
   // The first character in the array just had a turn (real or inferred),
   // so we'll set their actionPoints to 0. 
   sorter.get(0).actionPoints = 0;
   sorter.get(0).calculateTurnsSkipped();
  }
  
  // Now we've made a list of the next 30 moves, but we didn't actually update
  // any of the real entity's action points (that was all projecting into the
  // future).  Now we'll increment them all based on the turnsSkipped of the
  // unit that actually gets to move (
  for (int i=0; i<entities.size(); i++) {
   Entity e = entities.get(i);   
   Stats stats = sm.get(e);
   stats.actionPoints += stats.getSpeed() * turnsSkipped;
  }
 }
 
 @Override
 protected boolean checkProcessing() {
  return true;
 }
  
 private static class Sorter implements Comparable<Sorter> {

  public Sorter(int id, int actionPoints, int speed, int agility) {
   this.id = id;
   this.actionPoints = actionPoints;
   this.speed = speed;
   this.agility = agility;
   calculateTurnsSkipped();
  }
  
  int actionPoints;
  int speed;
  int agility;
  int id;
  float turnsSkipped;
  
  @Override
  public int compareTo(Sorter other) {
   
   // First try comparing how many "turns" each unit has to wait before its next turn
   if (turnsSkipped > other.turnsSkipped) return 1;
   if (turnsSkipped < other.turnsSkipped) return -1;
   
   // They are equal, try speed next
   if (speed < other.speed) return 1;
   if (speed > other.speed) return -1;
   
   // Speed failed, so let's check agility
   if (agility < other.agility) return 1;
   if (agility > other.agility) return -1;
   
   // Barring all that, screw it
   return 0;
  }
  
  public void calculateTurnsSkipped() {
   turnsSkipped = (float)MyMath.max(0, 100-actionPoints)/(float)speed;
  }
  
 }
}

Notice it extends EntitySystem, and processes all entities in the world with Stats.  In its constructor, it gets passed the Array<Integer> unitOrder from OverworldScreen, and since Java arguments get passed by reference, anything that happens here happens in OverworldScreen too.

I wanted to use Array.sort(), which means I needed a temporary class which implements Comparable to hold the relevant data.  I (in a most uninspired decision) called this class Sorter, and the Array which will hold this stuff to sort is Array<Sorter> sorter.  It holds information I think could be relevant to deciding who goes first.  In compareTo(), we first compare turnsSkipped.  If that's equal, we compare speed (the faster one gets to go first).  If that's equal too, as a last resort we compare agility.  If those are all equal, we admit the characters are equal and so the .sort() method just gives preference to whichever one it sees first.

No let's look at processEntities().  The first thing we do is check to see if there is already a unitList.  If there isn't, it means this is the first time we're running this.  If there is, it means we ran it once before (to line everyone up at first), but since then somebody has moved.  We need to remember who that was, so we store that in oldEntity.

Next we loop over the entities to put them in Array<Sorter> sorter.  In this loop, we grab oldEntity (the one which moved last turn) and set their actionPoints to 0.

We then simulate the next 30 "real" turns.  If we're going through the loop the first time, that means it represents the upcoming turn, so we specifically store how many turns had to be skipped to get here.  Other than that, we update all the "dummy" Sorters' actionPoints, and do it again and again.  After we get out of that, we need to process the "real" turn, so we loop over the entities again and update all their actionPoints based on the turnsSkipped for the next turn.

CameraMovementSystem

Now, characters take their turn in which they can move once, attack once, and that's it.  Of course, it can be a HUGE pain in the butt to actually figure out whose turn it is!!!  I made a system to smoothly move the camera to new locations.  My rough idea is that I wanted it to move slowly for a second, speed up to some maximum speed halfway there, then slow down again to a nice, gentle stop.  In other words, I wanted each coordinate to look something like this:
I just needed to know how to adjust the speed to make sure it gets from point A to point B correctly.  For that I had to design a crappy heuristic for how long I wanted the transition to take before I could do that - I won't talk about it too much, but it's short for nearby cells, longer for farther away cells, but as you get farther and farther, there's an economy of scale where moving 30 cells takes less than twice the time to move 15 cells.

I decided I would always try to process CameraMovementSystem, but it would just return unless it had a destination.  The biggest problem I ran into was determining which cell the camera started out focused on.  At first I used window2world(x,y,camera), but that doesn't actually work.  That get's the cell a user clicks on from "window space", but those coordinates have to be unprojected by the camera to fall into the actual coordinate system for the game (not just the coordinate system for the window).  But the camera's position isn't in window space, it's in the natural coordinate system for the game - what I will now call "libgdx space".  The code for libgdx2world(float x, float y) is the exact same as window2world(float x, float y, camera), but it doesn't use a camera to unproject the coordinates into libgdx space - it assumes the coordinates are already there.

Here's the code for CameraMovementSystem:
package com.blogspot.javagamexyz.gamexyz.systems;

import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.blogspot.javagamexyz.gamexyz.custom.FloatPair;
import com.blogspot.javagamexyz.gamexyz.custom.Pair;
import com.blogspot.javagamexyz.gamexyz.maps.MapTools;

public class CameraMovementSystem {
 private float t;
 private float  x0, x1, y0, y1;
 private OrthographicCamera camera;
 private float T;
 private boolean active;
 
 public CameraMovementSystem(OrthographicCamera camera) {
  this.camera = camera;
  active = false;
 }
 
 public void move(int x1, int y1) {
  Vector3 position = camera.position;
  x0 = position.x;
  y0 = position.y;
  FloatPair p = MapTools.world2window(x1, y1);
  this.x1 = p.x;
  this.y1 = p.y;
  t=0;
  Pair start = MapTools.libgdx2world(x0, y0);
  
  // d is used to calculate how long it will take to get to the target cell.
  // If it is close, d is small - if it is far, d is large
  // Very close by, d is similar to how many cells away it is
  // For longer distances, it grows as sqrt(distance)
  float d = (float)Math.sqrt(MapTools.distance(start.x, start.y, x1, y1) + 0.25) - 0.5f;
  T = 0.4f + d/4f;
  active = true;
 }
 
 public void process(float delta) {
  if (!active) return;
  
  float vx, vy;
  float Ax = 6*(x1-x0)/(T*T);
  float Ay = 6*(y1-y0)/(T*T);
  vx = Ax*(t)*(1-t/T);
  vy = Ay*(t)*(1-t/T);
  
  camera.translate(new Vector2(vx*delta,vy*delta));
  
  t += delta;
  if (t > T) {
   active = false;
  }
 }
}

In OverworldScreen, you just need to make sure to initialize it and process it every time.  I decided to call cameraMovementSystem.move(x,y) every time I process a turn to focus on the new character, as well as every time the player moves a character, to focus on their destination.  I did the first in a method called processTurn() in OverworldScreen which basically runs turnManagementSystem, stores the ID and location of the active entity, then runs cameraMovementSystem.move().

So that's it for this time - there was a little code I didn't exactly specify but I think it should be clear when/where/how to process these new parts.  But as a reminder, be sure to check out the code from the repository to stay up to date.

You have gained 200 XP.  Progress to Level 4: 450 / 700

2 comments:

  1. I like this site/blog.

    Do you have your source code on any platform (github, google code)? I would like to see it completely or maybe someone can help you with it.

    ReplyDelete
    Replies
    1. Peter,

      I'm glad you like the site!

      I do have it hosted on Google Code - you can check it out here:
      https://code.google.com/p/javagamexyz/

      The most recent version is under the 2013-05-05 tag, but you can also check out older versions.

      Thanks

      Delete