Friday, April 26, 2013

Artificial Intelligence (Level 3)

It seems a little silly trying to tackle AI at level 3, but I felt like it was the next part that needed to be built.

As I started on this part, I realized that AI was going to require at least 2 major parts:
  1. The ability for the computer to decide what a good move is
  2. The ability for the computer to execute that move, in an attractive way. 
That 2nd point makes me think again of Final Fantasy Tactics - consider, for instance, this video:
When it's the enemy's turn, it doesn't just instantly flash to the end, it shows it progress gradually.  It's like the enemy decides where to move first, then moves, then spends a second deciding where to attack.  And you see the full range of displays: the movement range is highlighted, even  though the computer doesn't need to "see" it.  The cell they select is highlighted.  There are pauses in between segments.  It all flows quite nicely.

In this article I will mostly tackle the 2nd point, though the computer will decide what to do, there decision will be pretty lame.  But it serves as a framework to expand later.

Part 1 - Deciding on a plan
Entities which are computer controlled will get a new Component called AI.  AI will have a field of type Plan.  Plan will contain the information about what the entity will do:
  • Where will it move?
  • Where will it attack?
  • What attack will it be using?
  • Will it move first, or will it attack first and then move?
  • How "good" is this plan?
Plan will also implement Comparable so we can generate an Array of Plans, then sort them based on their score to find the best one.

To determine the best Plan, the computer will loop over all possible combinations of what it can do.  For now let's juts assume that they entity will move first, and act second.  Basically, we must loop over:
  • All possible cells you can move to
  • All possible actions you can do
  • All cells you can target with that action
    • This may be a single cell, or it may hit a whole field of cells centered on a particular one
Each of these describes one possible Plan - find some way to score them (this is what we are totally going to skimp on).  Then sort the list of Plans and grab the best one.  For some semi-pseudo-code, we want it to look like this:

Array<Plan> plans = new Array<plan>();

moveFirst = true;

for (Pair moveTarget : reachableCells) {
 for (Action action : availableActions) {
  for (Pair actionTarget : cellsWithinActionsRange) {
   Array<pair> field = getFieldFor(actionTarget, action);
   score = scorePlan(moveTarget, action, field);
   plans.add(new Plan(moveFirst, moveTarget, actionTarget, action, score);
  }
 }
}

plans.sort();
plan = plans.get(0);

(Note, in practice we will also want to consider that they can act first, then move later - the loop would look quite similar but there are some additional complications that might arise from this).

Let's look at these and figure out which ones we already have the foundation for, and which ones need to be developed.
  • reachableCells - DONE
  • Action and availableActions - We will need to design these
  • actionTarget and cellsWithinActionsRange - should be easy to implement if we give Action something called range
  • getFieldFor - in the simplest case, let's say that each action hits just the target cell, or the target cell + it's neighbors, or the target cell + 2 sets of neighbors, etc.  If we give Action something called "field" where field=1 means just the center, field=2 means the center+neighbors, etc, this should be easy too!
  • scorePlan - ????????
So to make this work, we need to develop a class called Action with data like range and field, and also some details of what the action does.

To accomplish this, I made a class called Action2 (Action is a class used in libgdx, so I went with Action2 to avoid any confusion) and a component called Abilities which holds the different actions an entity can do.
package com.blogspot.javagamexyz.gamexyz.abilities;

public class Action2 {
 public int range; // How far from can this ability be targeted
 public int field; // How large an area does it hit (1 = single cell, 2 = middle+neighbors, etc)
 public float baseSucessProbability; // How likely is it to hit, on its own
 public float damage; // How much damage does it do, on its own
}

package com.blogspot.javagamexyz.gamexyz.components;

public class Abilities extends Component {
 
 Action2 attack_sword;
 Action2 attack_bow;
 
 public Abilities() {
  
  attack_sword = new Action2();
  attack_sword.damage = 4;
  attack_sword.baseSucessProbability = 0.9f;
  attack_sword.range = 1;
  attack_sword.field = 1;
  
  
  attack_bow = new Action2();
  attack_bow.damage = 3;
  attack_bow.baseSucessProbability = 0.7f;
  attack_bow.range = 4;
  attack_bow.field = 1;
  
 }
 
 public Array<action2> getAbilities() {
  Array<action2> abilities = new Array<action2>();
  abilities.add(attack_sword);
  abilities.add(attack_bow);
  return abilities;
 }
}

Each entity will have 2 attacks: sword and bow.  The sword has a range and field of 1, a 90% chance of success, and does 4 damage.  The bow has a range of 4, a field of 1, a 70% chance of success, and does 3 damage.

For now it's kind of silly, but getAbilities() returns an Array containing those two actions.  With that, we can build our Plan class
package com.blogspot.javagamexyz.gamexyz.AI;

import com.blogspot.javagamexyz.gamexyz.abilities.Action2;
import com.blogspot.javagamexyz.gamexyz.custom.Pair;


public class Plan implements Comparable<Plan> {
 
 public boolean moveFirst, moveDecided, moveDone, actionDone;
 public Action2 action;
 public Pair moveTarget, actionTarget;
 public int score;
 
 public Plan(boolean moveFirst, Pair moveTarget, Pair actionTarget, Action2 action, int score) {
  this.moveFirst = moveFirst;
  this.moveTarget = moveTarget;
  this.actionTarget = actionTarget;
  this.action = action;
  this.score = score;
  
  moveDecided = moveDone = actionDone = false;
 }
 
 @Override
 public int compareTo(Plan p) {
  if (score < p.score) return 1; // p was better
  else if (score > p.score) return -1; // p was worse
  return 0; // they were equal
 }

}

moveFirst is the boolean which, again, lets us know what we're doing first (in the loop I outlined above, everything was predicated upon moving first).  It also has an Action2, a Pair indicating where it will move, and a Pair indicating where the action will be targeted.  The other booleans will come up later.  moveDone and actionDone are to help us time things smoothly (part 2), and moveDecided will be used for when we act first, move second.

Also, notice it implements Comparable so that we can sort it to find which is the best plan.  In reality, if we are strictly going with the BEST plan, then we don't need to sort it, we just need to find the maximum.  But to shake things up later, we may want to pick one of the best strategies at random, so that we are harder to predict - in this case we may as well sort it.

Okay, now let's look at the AI component.  It should hold a Plan, and probably a timer (so we can time the execution of the plan).  Now, we don't want all of the entities with AI processing all the time, just on their own turn.  This could be accomplished at least two ways I can think of:
  1. Give each AI a boolean flag for being active or not.  Then, each turn during processTurn(), check to see if the next entity has AI - if so, set active to true.  At the end of that entity's turn, before they call processTurn() again, set their AI to inactive.
  2. Have a dummy component (CPUControllable?) to indicate that the Entity is controlled by the computer, and each turn during processTurn() check to see if the it has CPUControllable, and if so, add a new AI component.  Then, at the end of their turn, remove the AI component.
I went with option #1, but I can see pros and cons with each way.  Here is my AI component:
package com.blogspot.javagamexyz.gamexyz.components;

import com.artemis.Component;
import com.blogspot.javagamexyz.gamexyz.AI.Plan;

public class AI extends Component {

 public Plan plan;
 public float timer;
 public boolean active;
 
 public boolean planDone;
 
 public void begin() {
  timer = 0;
  active = true;
  planDone = false;
 } 
}

The first 3 things we already talked about.  We'll talk about planDone soon, but it becomes true as soon as the computer has figured out the plan.  I kind of broke the whole "component-as-data-storage-only" paradigm - but it makes it easy for when I want to start someone's AI (i.e. in processTurn()).

Now we need to look at AISystem.  It is responsible for a lot of different things, primarily deciding what Plan to do, but it's also in charge of pacing.  We don't want to do everything every time, so it has a series of flags/stops along the way.

public class AISystem extends EntityProcessingSystem {

 @Mapper ComponentMapper<AI> AIm;
 @Mapper ComponentMapper<Stats> sm;
 @Mapper ComponentMapper<Movable> mm;
 @Mapper ComponentMapper<Abilities> am;
 
 private OverworldScreen screen;
 private GameMap gameMap;
 private ActionSequencer sequencer;
 
 private boolean startMove, startAction;
 
 @SuppressWarnings("unchecked")
 public AISystem(OverworldScreen screen, GameMap gameMap) {
  super(Aspect.getAspectForAll(AI.class, Stats.class, Movable.class, Abilities.class));
  
  this.screen = screen;
  this.gameMap = gameMap;
  
  startMove = true;
  startAction = true;
 }

 @Override
 protected void process(Entity e) {
  
  AI ai = AIm.get(e);
  if (!ai.active) return;
  
  if (!ai.planDone) decidePlan(e, ai);
  
  else {
   
   // If moveFirst
   if (ai.plan.moveFirst) {
    
    // Move
    if (!ai.plan.moveDone) {
     if (startMove) {
      ai.timer = 0;
      startMove = false;
     }
     sequencer.move(gameMap, screen, ai.timer);
    }
    
    // Act
    else if (!ai.plan.actionDone) {
     if (startAction) {
      ai.timer = 0;
      startAction = false;
     }
     sequencer.act(gameMap, screen, ai.timer);
    }
   }
   
   // Else, actFirst
   else {
    
    // Act
    if (!ai.plan.actionDone) {
     if (startAction) {
      ai.timer = 0;
      startAction = false;
     }
     sequencer.act(gameMap, screen, ai.timer);
    }
    
    // Decide move location
    else if (!ai.plan.moveDecided) decideMovement(e, ai.plan);
    
    // Begin moving to target location
    else if (!ai.plan.moveDone) {
     if (startMove) {
      ai.timer = 0;
      startMove = false;
     }
     sequencer.move(gameMap, screen, ai.timer);
    }
    
   }
  }
  
  // Don't increment your counter while the camera is moving 
  // This let's us focus on the character for a second (or so)
  // before they start acting.
  if (screen.cameraMoving()) return;
  ai.timer += world.getDelta();
  
  // If everything is done, process the turn
  if (ai.plan.actionDone && ai.plan.moveDone) {
   ai.active = false;
   startMove = startAction = true;
   screen.processTurn();
  }
 }
First things first, for now it's kind of awkward, but I'm telling it to process things not just with AI, but with Stats,Movable, and Abilities (I don't even use stats yet).

ActionSequencer is a class we'll discuss next, which I use to actually pace things (Part 2).  It has two methods right now: move() and act().  They, as you might guess, control the pacing of what those ought to look like.

In process() it first checks to see that this particular AI is active.  If not, it just quits.  Next it checks to see if it has already determined the plan.  This check prevents us from determining the plan every time.  Also, and I think this is a decent strength here, it may take a while to determine the plan.  Even if it takes 1 second, this would be no big deal to the player - unless the graphics locked up for that whole second, which is exactly what would happen if we required the plan to be determined in a single go.  We don't worry about it yet, but if our needs expand, this will help us so that every time AI is processed, it can refine the plan a little bit more until it has decided, at which point it is ready to act.

If the plan is decided, we begin enacting it.  Here's a plain outline of what this section says:
  • if moveFirst
    • move
    • act
  • else
    • act
    • determine where to move
    • move
The "determine where to move" step is important.  We don't want the computer deciding where to move until it has to.  Say they expected their attack to kill the enemy, but missed?  Should they stick with the plan where they thought the enemy would be dead?  This uncertainty is what will make scoring plans where the player acts first extra difficult.  How do you rate the advantage you get by having a move left over?  I think it depends on how good your potential moves look, but I haven't even touched that stuff yet.

Remember again, this thing will be processed hundreds to thousands of times.  You don't want to act until you're done moving (or vice versa), and you don't want to determine the plan thousands of times, nor where to move.  That is the purpose of those flags, plan.moveDone and plan.actionDone.  It will do those things until they are done, then no more.  startMove and startAct are flags that tell us that we are on the very first loop where we move (or act).  This is helpful for us to reset the timer (so those will execute appropriately).  Other than that, we ask our ActionSequencer to do the appropriate action.

After all that (starting around line 87) we need to increment the timer.  However, I don't want the timer moving while the camera is still on its way to focus on the enemy - otherwise they may have already begun their turn and done things we didn't see.  So I only update the timer when the camera is not moving.

Lastly, if both actionDone and moveDone are true, then we have completed the whole turn (timing and all): deactivate the AI, reset the AI system, and processTurn().

There are still a few things I left vague.  For instance, on line 31 I call some mystery method decidePlan(), and on line 70 I call decideMovement().  We already went over the pseudo-code for decidePlan() at the beginning of this article, and for now, decideMovement() just picks a random cell from reachableCells.  Here they are:
 private void decidePlan(Entity e, AI ai) {
  Pair pos = gameMap.getCoordinatesFor(e.getId());
  Movable movable = mm.get(e);
  Abilities abilities = am.get(e);
  
  Array<Pair> reachableCells = gameMap.pathFinder.getReachableCells(pos.x, pos.y, movable);
  Array<Action2> availableActions = abilities.getAbilities(); 
  
  Array<Plan> plans = new Array<Plan>();
  
  
  
  // Loop over all possible plans where the entity moves, then acts
  // If the action doesn't hit anyone, skip that plan
  boolean moveFirst = true;
  for (Pair moveTarget : reachableCells) {
   for (Action2 action : availableActions) {
    for (Pair actionTarget : MapTools.getNeighbors(moveTarget.x, moveTarget.y, action.range)) {
     Array<Pair> field = MapTools.getNeighbors(actionTarget.x, actionTarget.y, action.field-1);
     field.add(actionTarget);
     if (!gameMap.containsEntitiesOtherThan(field,e.getId())) continue;
     int score = scorePlan(moveTarget, action, field);
     plans.add(new Plan(moveFirst, moveTarget, actionTarget, action, score));
    }
   }
  }
  
  // If there were no good plans there, add at least one plan
  // where you move at random, and do nothing
  if (plans.size == 0) plans.add(new Plan(moveFirst, reachableCells.get(MathUtils.random(reachableCells.size-1)), null, null, 0));
  
  // Now loop over all possible plans where the entity doesn't
  // move anywhere, so they still have their move stored up
  moveFirst = false;
  for (Action2 action : availableActions) {
   for (Pair actionTarget : MapTools.getNeighbors(pos.x, pos.y, action.range)) {
    Array<Pair> field = MapTools.getNeighbors(actionTarget.x, actionTarget.y, action.field-1);
    field.add(actionTarget); 
    if (!gameMap.containsEntitiesOtherThan(field,e.getId())) continue;
    int score = scorePlan(pos, action, field);
    plans.add(new Plan(moveFirst, pos, actionTarget, action, score));
    
   }
  }
  
  plans.sort();
  ai.plan = plans.get(0);
  ai.planDone = true;
  sequencer = new ActionSequencer(ai.plan, e);
 }
 private void decideMovement(Entity e, Plan plan) {
  Movable movable = mm.get(e);
  Pair pos = gameMap.getCoordinatesFor(e.getId());
  Array<Pair> reachableCells = gameMap.pathFinder.getReachableCells(pos.x, pos.y, movable);
  plan.moveTarget = reachableCells.get(MathUtils.random(reachableCells.size-1));
  plan.moveDecided = true;
 }
decidePlan has a few very cheap hacks for now.  For instance, on lines 21 and 39, I say that if that particular move/act combo has no entities in the action field, skip that plan (this way I don't have to bother scoring or sorting it).  Even worse, I had to look for targets other than the acting entity - just think, if they move one cell over and say "aha, there's someone right back where I moved from - I'll attack them!" it would be pretty silly.  containsEntitiesOtherThan() is pretty simple - just check the field to see if anyone is there, but don't count it if it's the entity whose ID is being passed.

In case they don't find any actions that work, on line 30 we give them a dummy option to move to some random location and do nothing (with a default score of 0).

The next loop is over just the actions, because they will decide where to move later.  Again, I don't know how to handle score discrepancies between these two types of Plans.  The ability to move later is worth something... probably.  But what?

After that we sort the plans, grab the best one, and tell our AISystem that were are done making the plan.  We also create a new ActionSequencer to handle this plan (we'll get to that soon).

There's still some cop-out: on lines 22 and 40 I magically call some scorePlan() method.  For now, this is as simple as can be:

Go with whichever plan has the higher potential for damage.  Don't care who you're attacking.  Don't care how safe you are.  Don't care 'bout nuthin.  In practice, this means that everyone will try to move toward and attack anyone at random who is in some range.  They prefer sword (because it's more powerful), but they'll do bow if that's all they have.  There's nothing that says if they go for bow range to try to come in close for sword, nor to keep a distance.  They just choose some random cell where they can attack from - and if it's possible, it will be right next to them for some swift sword justice.

decideMove(), like I said before, doesn't factor in anything smart.  It just picks some random place to move.

That's our AISystem.  We'll talk about the ActionSequencer in a second, but first let's look at how we want to implement our AISystem.  It's just another Artemis system, and we don't want to update it manually (it runs continuously through the enemy's turn) so just add it like a regular System.  In OverworldScreen.processTurn(), we need to check the incoming players status: is it controlled by AI?  If so, I'd like to disable player control entirely, and begin the AI processing.
 public void processTurn() {
  turnManagementSystem.process();
  activeEntity = unitOrder.get(0);
  activeEntityCell = gameMap.getCoordinatesFor(activeEntity);
  
  // As long as that entity has a location, focus the camera on them
  if (activeEntityCell != null) {
   cameraMovementSystem.move(activeEntityCell.x, activeEntityCell.y);
  }
  
  // Try to get the next entity's AI
  AI ai = world.getEntity(activeEntity).getComponent(AI.class);
  
  // If they don't have AI, give control to the human player
  if (ai == null) Gdx.input.setInputProcessor(inputSystem);
  
  // Otherwise take control away, and begin that AI's processing.
  else {
   Gdx.input.setInputProcessor(null);
   ai.begin();
  }
Whew, we're coming down to the end.  That's how the AISystems gets integrated into the rest of the game.  Now we just need to talk about the ActionSequencer (plus the few things it messed up).

The idea for ActionSequencer is to break things down into a few discrete steps.  For instance, to handle move, we want to do a few things:
  1. Highlight the movable range
  2. Specially highlight the cell they choose to move to
  3. Begin the actual movement (remember, the camera will be moving during this period too, so the timer won't be ticking until it stops)
  4. Unhighlight the target cell
  5. Finish (wrap up anything else that needs to happen at the end)
Each of these steps should last a particular amount of time, before moving on to the next one.  I broke each step into a series of if (step == 0) { ... } else if (step == 1) { ... }, etc...

Now, for each of these, the code only needs to run once (for instance, in step 1 you must figure out the reachableCells, then tell OverworldScreen to highlight them), so within each step, we do step++.  To stop it from running directly into the next step, we also monitor the AI timer to keep it from getting ahead of itself.  Here's how I did it:
package com.blogspot.javagamexyz.gamexyz.AI;

import com.artemis.Entity;
import com.badlogic.gdx.utils.Array;
import com.blogspot.javagamexyz.gamexyz.components.Damage;
import com.blogspot.javagamexyz.gamexyz.components.Movable;
import com.blogspot.javagamexyz.gamexyz.components.Movement;
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 ActionSequencer {
 
 Plan plan;
 Entity e;
 
 private int step;
 private float m_t;
 private float a_t;
 
 private float[] moveTimer;
 private float[] actTimer;
 
 public ActionSequencer(Plan plan, Entity e) {
  this.plan = plan;
  this.e = e;
  
  float t1,t2,t3,t4;
  
  step = 0;
 
  // Move timing constants
  m_t = 0.75f; // Time to wait upon focus, before showing movement range
  t1 = 1f;
  t2 = 0.5f;
  t3 = 0.05f;
  t4 = 0.25f;
  moveTimer = new float[]{t1, t2, t3, t4};
  
  // Act timing constants
  a_t = 0.75f; // Time to wait upon focus, before showing action range
  t1 = 1f;  // Highlight attackable range
  t2 = 0.6f;  // Highlight target cell
  t3 = 0.75f;  // Linger to watch damage hit
  actTimer = new float[]{t1, t2, t3};
 }
...
}
This thing has immediate access to the Plan, and the entity who is executing it.  It has an int step to keep track of which step it's on (it will only ever either be moving or acting, so we only need one step counter).  The moveTimer holds the duration for each step.  Look at lines 34-40 in the constructor.

m_t will be the "accumulated" timer, but for now, it just holds how long the camera should wait before showing the movement range (remember, the timer isn't running at all while the camera is moving, so this is 0.75 seconds after the camera has settled on the entity).  t1 says that we will then highlight reachableCells for 1 second.  t2 is how long for the next step, etc.  An array, moveTimer is created which holds those values and can conveniently be referenced by moveTimer[step] to get the appropriate time.

Below all that, we actually have the move() method:
 public void move(GameMap gameMap, OverworldScreen screen, float time) {
  
  if (time < m_t) return;
  
  // Step 1 - Highlight the reachable cells
  if (step == 0) {
   Movable movable = e.getComponent(Movable.class);
   
   Pair pos = gameMap.getCoordinatesFor(e.getId());
   
   Array<Pair> reachableCells = gameMap.pathFinder.getReachableCells(pos.x, pos.y, movable);
   screen.highlightedCells = reachableCells;
   screen.highlightMovementRange();
   
   m_t += moveTimer[step];
   step++;
   
   return;
  }
  
  // Step 2 - Highlight the cell we will move to
  else if (step == 1) {
   screen.highlightedCells.clear();
   screen.setHighlightColor(0.05f, 0.05f, 0.2f, 0.8f);
   screen.highlightedCells.add(plan.moveTarget);
   
   m_t += moveTimer[step];
   step++;
   
   return;
  }
  
  // Step 3 - Begin moving to target cell
  else if (step == 2) {
   Movable movable = e.getComponent(Movable.class);
   
   Pair pos = gameMap.getCoordinatesFor(e.getId());
   
   e.addComponent(new Movement(gameMap.pathFinder.findPath(pos.x, pos.y, plan.moveTarget.x, plan.moveTarget.y, movable, false)));
   e.changedInWorld();
   
   m_t += moveTimer[step];
   step++;
   
   return;
  }
  
  // Step 4 - We have arrived (plus a brief waiting period), unhighlight the cell
  else if (step == 3) {
   screen.renderHighlighter = false;
   
   m_t += moveTimer[step];
   step++;
   
   return;
  }
  
  // Finished: Tell the plan that the "move" is done 
  else {
   plan.moveDone = true;
   m_t = 0;
   step = 0;
  }
  
 }
The first thing (line 3) is the most important part.  If the AI timer has not exceeded the time limit we have set, then don't even bother doing any more here.  This is how we control to make sure we don't jump ahead to steps inappropriately.  In the Step 1 block, we get the reachableCells and tell the OverworldScreen to highlight them (I slightly fudged with the way highlighting works - we'll talk about it at the very end).

Step 2 clears the highlightedCells and replaces them with the moveTarget, along with a less transparent, darker blue color.

Step 3 actually adds the Movement component.

Step 4 stops the highlighter.

Notice that in each step, we increment m_t by the timer for that step.  This way, we are always getting caught by that line 3 code.  At first, it needs timer to exceed 0.75.  But after that, it must exceed 1.75 (1 second beyond what it already had - as added on in line 27).  So each step calls m_t += moveTimer[step], and each step also calls step++.  Those two lines keep the flow going.

After that, we're done, so we call plan.moveDone (which helps control how AISystem knows where to delegate its attention) .

Here's act():
 public void act(GameMap gameMap, OverworldScreen screen, float time) {
  if (plan.action == null) {
   plan.actionDone = true;
   System.err.println("action = null");
   return;
  }
  if (time < a_t) return;
  
  // Step 1 - Highlight attackable range
  if (step == 0) {
   Pair pos = gameMap.getCoordinatesFor(e.getId());
   screen.highlightedCells = MapTools.getNeighbors(pos.x, pos.y, plan.action.range);
   screen.highlightAttackRange();
   
   a_t += actTimer[step];
   step++;
  }
  
  // Step 2 - Highlight the target cell (the whole field)
  else if (step == 1) {
   screen.highlightedCells = MapTools.getNeighbors(plan.actionTarget.x, plan.actionTarget.y, plan.action.field-1);
   screen.highlightedCells.add(plan.actionTarget);
   screen.setHighlightColor(0.2f, 0.05f, 0.05f, 0.6f);
   
   a_t += actTimer[step];
   step++;
  }
  
  // Step 3 - Add damage and unhighlight target cells
  else if (step == 2) {
   Damage damage = new Damage(e.getComponent(Stats.class),plan.action);
   int entityId;
   Array<Pair> field = MapTools.getNeighbors(plan.actionTarget.x, plan.actionTarget.y, plan.action.field-1);
   field.add(plan.actionTarget);
   for (Pair target : field) {
    entityId = gameMap.getEntityAt(target.x, target.y);
    screen.addComponent(damage, entityId);
   }
   
   screen.renderHighlighter = false;
   
   a_t += actTimer[step];
   step++;
  }
  
  // Finished
  else {
   plan.actionDone = true;
   a_t = 0;
  }
 }
It basically works the same way as move(), but instead of moving, it handles acting.  To add the damage component, it needs access to the target entity, but it only has the ID.  It could get the entity from the World, but it doesn't have that either.  So I made some generally accessible method on OverworldScreen called addComponent() which will try to add a component to a particular entity.

Here's the list of silly methods in OverworldScreen I had to add at the end to make some of this come together:
 public boolean cameraMoving() {
  return cameraMovementSystem.active;
 }
 
 public void setHighlightColor(float r, float g, float b, float a) {
  mapHighlighter.setColor(r,g,b,a);
 }
 
 public void highlightMovementRange() {
  renderHighlighter = true;
  setHighlightColor(0f, 0f, 0.2f, 0.3f);
 }
 
 public void highlightAttackRange() {
  renderHighlighter = true;
  setHighlightColor(0.5f, 0f, 0f, 0.3f);
 }
 
 public void addComponent(Component component, int entityId) {
  if (entityId < 0) {
   System.err.println("No entity");
   return;
  }
  Entity e = world.getEntity(entityId);
  e.addComponent(component);
  e.changedInWorld();
 }
I replaced renderAttackRange and renderMovementRange with a general renderHighlighter (it never worked well highlighting both anyway, because they both worked off of the same timer which then went on double speed).  I also gave mapHighlighter a setColor method, and it in general remembers what color it's supposed to be working on.

I also felt like I had to tweak the Damage component a little bit, but with very minor changes.  Really this should be another update, but here it is in craptacular form now:
package com.blogspot.javagamexyz.gamexyz.components;

import com.artemis.Component;
import com.blogspot.javagamexyz.gamexyz.abilities.Action2;

public class Damage extends Component {

 public float baseDamage;
 public float baseAccuracy;
 
 public int power;
 public int accuracy;
 public Stats stats;
 
 public Damage(Stats stats) {
  this.stats = stats;
  power = stats.getStrength();
  accuracy = stats.getAgility();
  
  baseDamage = power;
  baseAccuracy = accuracy;
 }
 
 public Damage(Stats stats, Action2 action) {
  this.stats = stats;
  this.baseDamage = action.damage;
  this.baseAccuracy = action.baseSucessProbability;
  accuracy = (int)(baseAccuracy * 100);
  power = (int)(3*baseDamage);
 }
}
Holy crap that was a lot!  I hope I got it all (or at least most of it so that it will make sense).  Now you can make some computer controlled entities that will duke it out in a Battle Royale!  And you can mix it up with a few of your own characters - really the sky is the limit!

I'd encourage you all to think about how you actually want to tweak the decidePlan() and scorePlan() methods.  They are surely crap right now (I didn't really plan on making them perfect just yet though).  But whatever I come up with probably won't be that great in the end either, so this is definitely a place for your own logic to shine through!  What do you think is important for the computer to consider when deciding on a good plan?

You have gained 250 XP.  Progress to Level 4: 700/700
DING!  You have advanced to Level 4, congratulations!

3 comments:

  1. JavaGameXYZ, thanks fo this blog! Hope you won't abandon it in a month or two. I am working on a goldbox-like rpg (still have problem with drawing 16x16\32x32 textures, though; I am not a designer), and find your posts very helpful.
    Keep up the good work!

    ReplyDelete
    Replies
    1. Hi Sir Caneghem, thanks for the comment. I'm glad you find the tutorials helpful, and good luck resolving your graphics troubles. If you have any questions I can /try/ to help. I plan to keep addressing comments as often as I can, but I'm starting a Ph.D. program in August and probably won't have much time to devote to new posts on the blog.

      The goal was never to build a FULL awesome game, but rather show how you can build individual pieces and put them together into a game framework. For instance, I won't add more classes/abilities than I have. I am just wrapping up a section on the character status screen / inventory / learning abilities / changing class / etc. After that I'll probably add graphical and audio effects for attacks, etc. I think at that point I'll be done creating new content for the blog.

      But, anybody who has followed through it should be able to modify / create / do whatever they want to expand it into a full fledged GAME. That was always the goal: create a tutorial which is detailed enough, and goes into complicated enough topics, that people really can build their own game.

      Anyway, I'm glad you're enjoying it, and I hope you succeed with your goldbox rpg!

      Delete
  2. hey bro, great job with this tutorials!

    ReplyDelete