Thursday, May 16, 2013

User Interface: Menus with scene2dui and skins - Level 4

The code from this post is available here.

Now that we're done with the AI (actually, I hope you're not done, but at least you're on your own from here on out) It was time to return to human players.  Before, they could move and attack, and that was about it, but the enemy AI could cast healing spells, shoot bow-and-arrows, etc!  Kinda one sided, don't you think?

The bulk of this update will cover a much more extensive menu system for controlling PCs.  It turns out my old post on menus has garnered a bit of attention (it even got a link on the official libgdx external tutorials page!), which is actually kind of sad because the menus we produced there SUCKED!  They had dull gray buttons, and that was about it.

For comparison, here's what we ended with last time


Compared with what we have now:


In the after shot, notice the pleasantly textured backgrounds, crisp beveled edges, and cascading design.  The first menu is for basic options, the second comes up if they click "Action", giving them the option to attack, or use abilities from either their "Knight" skills, or "Healer" skills, or to use an item, while at the same time graying out the original menu (and disabling it).  The third menu has come up by clicking on "Healer", bringing up a list of known healer spells (yes, I know, they're all the same - I was busy enough as it was!)  Also notice that the ability menu can be scrolled, though the scrollbars will gradually fade to completely transparent if they're not being used.


What you can't see is that at any time the player can hit "ESC" to close a particular menu and refocus on the previous one (if such a previous one exists).  Furthermore, once you select an ability and target a cell with it, a dialog box appears asking you to confirm that you want to do it (perhaps someday it will give some statistics about the action).


Also, once an action has been taken, or if something costs too much MP, that menu item is grayed out and disabled.


Okay, to get from the hideous, disgusting "before" menu, to the lovely, refined "after" menu, we'll begin with a look at skins.  The libgdx-users wiki has an entry on skins, which is unfortunately a little out of date.  The big thing it gets wrong any more is the distinction between "resources" and "styles".  A more up-to-date example is in the libgdx tests directory, uiskin.json.  The first two things it defines are resources - a font and a bunch of colors - after which it defines a bunch of styles.  But there's no header "distinction" between them - you just plop them all down.  If you follow the pattern in that file, you'll do fine!

Now there are really only 3 types of resources you can define - Fonts, Colors, and TintedDrawables (all of which you can read about at the official libgdx page for skins - the first link of the previous paragraph).  Other than that, you define widget styles.  But which widgets can you make styles for?  Which ones do you need?  What can you specify in the skin file?  Where do the images for it all come from?  How do you design those images?

Let's start with what you can do.  libgdx offers a bunch of widgets in the scene2dui package, each of which can be customized a little bit.  Some of them have subclasses called _____Style.  So for Label, you also need a LabelStyle, for a Button you have ButtonStyle, etc.  Not  everything has a style - for instance there is no TableStyle.  And the closest thing Dialog has to a style is WindowStyle (Dialog extends Window).  All of the _____Style's can be described in the skin, and that's it.  In my last post on this, we instantiated all those styles manually, which takes a lot of time and space.  Skins let us describe them all once, and then if we want a particular LabelStyle we can just call
skin.get("particular_label_style_I_want", LabelStyle.class);
We can define any number of LabelStyles in the Skin, and they are each referenced by a string we give it.  If you don't pass skin.get() a String value, it will assume the String you mean is "default".  To see what LabelStyle properties you can specify in the Skin, let's pull up Eclipse (or your favorite code completing software).

First instantiate a new LabelStyle variable:
LabelStyle ls = new LabelStyle()
On the next line, just start with
ls.
and see what comes up.  3 things jump out at me: a Drawable called "background", a BitmapFont called "font", and a Color called "fontColor".  These are the things you can specify in the skin.  Say we wanted to make two types of Labels - one with black text and one with gray text, with a background image called "bg.png" and font "myFont.fnt".  Here's what such a skin might look like:
{
 com.badlogic.gdx.graphics.g2d.BitmapFont: {
  default-font: { file: myFont.fnt }
 },
 com.badlogic.gdx.graphics.Color: {
  black: { a: 1, b: 0, g: 0, r: 0 },
  gray:  { a: 1, b: 0.5, g: 0.5, r: 0.5 }
 },
 com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: {
  default: { background: bg, font: default-font, fontColor: black },
  otherone: { background: bg,
     font: default-font,
     fontColor: gray }
 }
}
Now I have two LabelStyles, one referenced by "default", the other referenced by "otherone".  If I want to instantiate a Label will this style, I can just say any of the following:
Label l1 = new Label("H", skin);
Label l2 = new Label("H", skin, "default");
Label l3 = new Label("H", skin.get(LabelStyle.class));
Label l4 = new Label("H", skin.get("default",LabelStyle.class));

Label l5 = new Label("H", skin, "otherone");
Label l6 = new Label("H", skin.get("otherone",LabelStyle.class));
All the labels here just have the text "H".  Labels 1-4 will all look the same - in fact all those constructors will ultimately call the constructor used in l4.  Labels 5-6 will have gray text, but otherwise be the same.  The skin.get() command actually returns a _____Style (of whatever type is given in the 2nd argument).

Well, this is almost good, but how does the skin file know where bg.png is?  Especially because it only calls it "bg" (without the .png extension).  And what about the font?

For all the images, we'll use the TexturePacker2 class.  A long time ago we made a class called ImagePacker which would be responsible for running libgdx's TexturePacker2.  Let's say in the GameXYZ-desktop project I have a folder called "textures", with a subfolder called "uiskin" in which I place all the images associated with the skin.  In our case, that's just bg.png, but in most cases you'll have a lot of things.  Then we call
TexturePacker2.process(settings, "textures/uiskin", "resources/uiskin", "uiskin");
which grabs everything in "textures/uiskin" and packs it into an atlas in "resources/uiskin", called "uiskin.png" and "uiskin.atlas".

Remember, this is handy for now, but in your final delivered product you don't want to run the ImagePacker to make the atlas everytime they run the program - just deliver it with the atlas already made.

In that same "resources/uiskin" directory, add the myFont.png and myFont.fnt files (made by the Hiero tool we talked about last time), as well as our uiskin.json file.  Then, when we instantiate the Skin, call
FileHandle skinFile = new FileHandle("resources/uiskin/uiskin.json");
Skin skin = new Skin(skinFile);
Then, because the atlas is also called "uiskin", our Skin will instantly know how to find "bg.png" when you just say "bg".  And because our .fnt file is also in there, if we say "file: myFont.fnt" - once again our skin can find it!

Now let's think about what widgets we need for our menu system, and how to configure them in our Skin.  Like before, our general "Menu" will consist of a ScrollPane which scrolls a Table, which has a bunch of Buttons, where each Button has a Label in it (I opted not to use TextButton for this because TextButton aligns the Label in its center, whereas I wanted it lined up along the left - and try as I might I couldn't force TextButton to line it all up like that).  So we might need Styles for each of them.  Let's look at what we can do for each - again using Eclipse's autocomplete to help us out:
ScrollPane:
  • Drawable background
  • Drawable corner
  • Drawable hScroll
  • Drawable hScrollKnob
  • Drawable vScroll
  • Drawable vScrollKnob
Button:
  • Drawable checked
  • Drawable checkedOver
  • Drawable disabled
  • Drawable down
  • Drawable over
  • float pressedOffsetX
  • float pressedOffsetY
  • float unpressedOffsetX
  • float unpressedOffsetY
  • Drawable up
Label:
  • Drawable background
  • BitmapFont font
  • Color fontColor
First, the ScrollPane.  If we set the background using the ScrollPane, it doesn't actually scroll with the contents - the table/buttons/labels will all scroll over the background, but I want the background following it.  I also don't want the user scrolling horizontally, so we can skip all that crap.  That pretty much leaves us with vScroll (what the full vertical scrollbar will look like) and vScrollKnob (what the movable piece in the middle of the bar looks like).  I just made some tiny little images for these:


The width is the width that will actually be used, but the height will be stretched to whatever size it needs (these are quite scaled up - I mean the actual file width will be used).  Now we can specify this in our uiskin.json file like so:
{
 com.badlogic.gdx.scenes.scene2d.ui.ScrollPane$ScrollPaneStyle: {
  default: { vScroll: scrollBar, vScrollKnob: scrollKnob }
 }
}
I also don't want the Buttons to specify the background.  It would be way too repeated if every button had the same background image.  Maybe when they click on it, the button can be highlighted (we can even just reuse our scrollBar.png image for this!), but in general we don't want to do background here.  I do however like the pressedOffsets, so the Button Skin will look like this:
{
 com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle: {
  default: { pressedOffsetX: 1,
    pressedOffsetY: -1,
    down: scrollBar}
 }
}
The font I've been using is irisUPC.  We want the default color to be black, and the inactive color to be gray, so for the Label we can say:
{
 com.badlogic.gdx.graphics.g2d.BitmapFont: { default-font: { file: irisUPC.fnt } },
 com.badlogic.gdx.graphics.Color: {
  black: { a: 1, b: 0, g: 0, r: 0 },
  gray:  { a: 1, b: 0.5, g: 0.5, r: 0.5 }
 },
 com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: {
  default: { font: default-font, fontColor: black },
  inactive: { font: default-font, fontColor: gray }
 }
}
Putting it all together, our uiskin.json file will now look like this:
{
 com.badlogic.gdx.graphics.g2d.BitmapFont: { default-font: { file: irisUPC.fnt } },
 com.badlogic.gdx.graphics.Color: {
  black: { a: 1, b: 0, g: 0, r: 0 },
  gray:  { a: 1, b: 0.5, g: 0.5, r: 0.5 }
 },
 com.badlogic.gdx.scenes.scene2d.ui.ScrollPane$ScrollPaneStyle: {
  default: { vScroll: scrollBar, vScrollKnob: scrollKnob }
 },
 com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: {
  default: { font: default-font, fontColor: black },
  inactive: { font: default-font, fontColor: gray }
 },
 com.badlogic.gdx.scenes.scene2d.ui.Button$ButtonStyle: {
  default: { pressedOffsetX: 1,
    pressedOffsetY: -1,
    down: scrollBar}
 }
}
The most trouble you'll probably have working with these is making sure all your brackets and commas line up right.

Of course, this is no good, we still need to have our background!  None of these options was appealing for the background - really the only appealing option is the Table, and there is no TableStyle.  For the menu texture, I used turbulent noise and an edge detector to make a 128 x 128 repeatable Texture.  (The code for this is included in the Google Code repository - check the "textures" package).


I put this image in my "textures/uiskin" directory to be packed with the rest, then in my Table I call
table.setBackground(skin.getTiledDrawable("menuTexture"));
TiledDrawable makes it repeat the background over and over to fill the Table.  If the Table is 1.5 x's as wide as the Drawable, it will Draw it once, then another half (but it will stop there).  HOWEVER, if your table is 1/2 the size as your Drawable, it will enlarge the Table to where it can draw the ENTIRE Drawable once.  In this case you'll have to be careful to keep the ScrollPane from scrolling beyond the bounds of the table you wanted.


Unfortunately, this menu ends very abruptly.  It has no edge or frame.

This makes me think of the NinePatch, which is an image where part of it (a frame) does not stretch as you resize it, but the center part (the main background) does stretch.  We don't want our inner part to stretch - we want it to tile like it's doing here, and unfortunately the NinePatch can't do that.  My workaround is kind of hackish - but who cares.  It works!

I created a class called FramedMenu which has a ScrollPane, Table, and Image.  The ScrollPane and Table are constructed like we've talked about, and the Image holds a NinePatch for the frame.  Based on the size of the ScrollPane, I will stretch the frame to cover it.  I chose to draw the Frame on top of the ScrollPane, and give the Frame a solid back border, a whitish/semi-transparent edge along the inside, and a blackish/semi-transparent shadow on the outside.  Here's my frame image:
The black pixels on the border indicate that the single transparent pixel within their "crosshairs" should be stretched.  NinePatch images don't need to be very big for just this reason.

Now FINALLY, let's look at some code.  Here's my FramedMenu.java class:
package com.blogspot.javagamexyz.gamexyz.ui;

import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Button.ButtonStyle;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;

public class FramedMenu {

 private Image frame;
 private ScrollPane scrollPane;
 private Table table;
 private Skin skin;
 // max's represent the largest we want the menu getting
 private float maxHeight, maxWidth;
 
 // fontHeight and rows are used to estimate how tall our table is (frustratingly, 
 // table.getHeight() always gives me 0.0)
 private float fontHeight;
 private int rows; 
 
 // The parent menu is the one we will focus on if the user closes this one
 private FramedMenu parent;
 
 public FramedMenu(Skin skin, float preferredWidth, float preferredHeight) {
  this(skin, preferredWidth, preferredHeight, null);
 }
 
 public FramedMenu(Skin skin, float maxWidth, float maxHeight, FramedMenu parent) {
  this.skin = skin;
  
  table = new Table();
  
  this.maxHeight = maxHeight;
  this.maxWidth = maxWidth;
  
  fontHeight = skin.getFont("default-font").getCapHeight() + 14;
  rows = 0;
  this.parent = parent;
 }
 
 // Adds a button to the menu
 public void addButton(String label, ChangeListener listener, boolean active) {
  addButton(label, "", listener, active);
 }
 
 // Adds a button to the menu, with a secondary label (like MP cost) aligned to the right
 public void addButton(String label, String secondaryLabel, ChangeListener listener, boolean active) {
  LabelStyle style;
  if (active) style = skin.get(LabelStyle.class);
  else style = skin.get("inactive",LabelStyle.class);
  
  Label l = new Label(label, style);
  
  Button b = new Button(skin.get(ButtonStyle.class));
  b.addListener(listener);
  b.setDisabled(!active);

  b.add(l).left().expandX();
  b.add(new Label(secondaryLabel, style)).padRight(15f);

  table.add(b).left().padLeft(5f).expandX().fillX();
  table.row();
  
  rows++;  
 }

 // Adds the frame and scrollpane to the specified stage at the specified location.
 // Sizes the scrollpane to the (estimated) table size, up to a maximum given
 // in the constructor.
 public void addToStage(final Stage stage, float x, float y) {
  scrollPane = new ScrollPane(table, skin);
  frame = new Image(skin.getPatch("frame"));
  
  // If the user presses "ESC", close this menu and focus on the "parent"
  stage.setKeyboardFocus(scrollPane);
  scrollPane.addListener(new InputListener() {
   public boolean keyDown(InputEvent event, int keycode) {
    if (keycode == 131) { //escape
     // If this menu is invisible, don't do anything
     if (!frame.isVisible()) return false;
     
     // If there is a parent, get rid of this
     // menu and focus on it
     if (parent != null) {
      stage.setKeyboardFocus(parent.scrollPane);
      parent.enable();
      clear();
     }
     // Otherwise this must be the last one, so just clear it all
     else {
      stage.clear();
     }
    }
    return true;
   }
  });
  
  // Go ahead and add them to the stage
  stage.addActor(scrollPane);
  stage.addActor(frame);
  
  // If the table does not fill our maximum size, resize it to our
  // estimated height, and disable scrolling both x and y
  if (rows*fontHeight < maxHeight) {
   scrollPane.setScrollingDisabled(true, true);
   scrollPane.setHeight(rows*fontHeight);
  }
  
  // Otherwise, it's bigger than our maximum size, so we need to
  // enable vertical scrolling, and set the height to our max.
  else {
   scrollPane.setScrollingDisabled(true, false);
   scrollPane.setHeight(maxHeight);
  }
  
  // For now, no matter what, the width is set to maxWidth
  scrollPane.setWidth(maxWidth);
  
  table.setBackground(skin.getTiledDrawable("menuTexture"));
  
  // Move the table to the far left of the scrollPane
  table.left();
  
  // Prevent the scrollPane from scrolling (and snapping back) beyond the scroll limits
  scrollPane.setOverscroll(false, false);
  scrollPane.setFillParent(false);
  // If y is negative, center the scrollPane vertically on the stage
  if (y < 0) scrollPane.setY((stage.getHeight() - scrollPane.getHeight())/2f);
  else scrollPane.setY(y - scrollPane.getHeight());
  // If x is negative, do likewise
  if (x < 0) scrollPane.setX((stage.getWidth() - scrollPane.getWidth())/2f);
  else scrollPane.setX(x);

  // Make sure we can't touch the frame - that would make the scrollPane 
  // inaccessible
  frame.setTouchable(Touchable.disabled);

  // Now set the Frame's position and size based on the scrollPane's stuff
  frame.setX(scrollPane.getX()-1);
  frame.setY(scrollPane.getY()-3);
  frame.setWidth(maxWidth + 4);
  frame.setHeight(scrollPane.getHeight() + 4);
  
  // In case they became invisible earlier, make them visible now
  scrollPane.setVisible(true);
  frame.setVisible(true);
 }
 
 // Wipe all the buttons off, and remove widgets from stage (and reset row count)
 public void clear() {
  table.clear();
  table.setColor(1f, 1f, 1f, 1);
  if (scrollPane != null) scrollPane.remove();
  if (frame != null) frame.remove();
  rows = 0;
 }
 
 public float getY() {
  return scrollPane.getY();
 }
 
 // Make it untouchable, and gray/transparent it out
 public void disable() {
  scrollPane.setTouchable(Touchable.disabled);
  table.setColor(0.7f, 0.7f, 0.7f, 0.7f);
 }
 
 // Re-enable 
 public void enable() {
  scrollPane.setTouchable(Touchable.enabled);
  table.setColor(1,1,1,1);
 }
 
 // Make invisible or visible
 public void setVisible(boolean visible) {
  if (frame == null) return;
  frame.setVisible(visible);
  scrollPane.setVisible(visible);
 }
 
 // Let someone else know who your parent is - currently used in MenuBuilder
 public FramedMenu getParent () {
  return parent;
 }
}
Most of this stuff is self-explanatory, but let's talk about a few things.  First, the menu gets a preferred height called maxHeight in the constructor.  If I just made everything have this height, then tables with only a few buttons would have a lot of wasted space.  To get around this, I want to use table.getHeight(), but unfortunately that seems broken (always gives me a 0).  So instead I use rows (where I keep track of how many rows the table has) and fontHeight to estimate how tall the table is - but note, this is not particularly accurate right now.

Next, look at addButton() starting on line 57.  This method is used to create a menu option that has a name (like "Group Heal") and a secondary label, like the MP cost or something.  Now, Button actually extends Table, so we can use the Table alignment methods to layout our button.  Line 68 adds the first label.  .left() tells the label to move to the left of the button.  .expandX() tells the cell that the label is in to take up the ENTIRE width of the button (or at least as much as it can take - because as we'll see there will be another label in a cell that gets a teency bit).  Line 69 then adds the secondary label.  .padRight(15f) tells it to have a 15 pixel border to the right of the label.

Line 71 then adds the button to the table.  .left() again puts it to the left, .padLeft(5f) gives it a small padding on the left though, .expandX() makes the table cell that the button is being placed in take up the entire width of the table, then .fillX() tells the button widget to fill its entire cell (i.e. the entire row width).  This makes it so the button takes up the whole width of the table, so the user won't have to click directly on the text "Move" to move, but can click anywhere in that table row.

Next let's look at Line 80: addToStage().  This is where we actually instantiate the ScrollPane and Image (I wanted to do it earlier, but I ran into the weirdest dang sizing problems, and finally just gave up!)  Line 85 makes it so the ScrollPane will receive keyboard input.  Someday users may be able to press arrow keys to navigate the ScrollPane, but for now I just let them hit "escape" to close out of that menu.  I do this by adding a new InputListener() on line 86.

Big thing to notice: the InputListener stuff will look a lot like the InputProcessor stuff we did before (touchUp, touchDown, dragged, mouseMoved, etc...) but it's actually all a little bit different.  All in all it works relatively similarly though.  Here, if they hit key 131 (escape) it closes the menu (unless the menu is invisible, which happens sometimes).

Lines 114-124 are where we determine how tall to make the ScrollPane, based on the row/fontHeight stuff we talked about earlier.  On 132 we make sure the table is pushed all the way to the left within the ScrollPane.  135-142 position the ScrollPane.  It set it so that negative positions make it center within the stage.

On 146 we make the Frame untouchable.  Because the frame is drawn on top of the scrollPane, if it were touchable, it would steal all the touch events from the ScrollPane.  The frames position and size are set so that it perfectly frames the scrollPane.

Now for some helper methods.  We can clear(), which clears the table and removes the scrollPane and frame from the stage.  We can disable, which is what grays out the menus we don't want our user interacting with, or we can just make it invisible (which also makes it untouchable).

Now, we know there are really only 3 menus we will be displaying for now: Move/Action/etc..., Attack/Item/etc... and Ability1/Ability2/etc...  To handle these, I modified the CRAP out of our old MenuBuilder class.
package com.blogspot.javagamexyz.gamexyz.ui;

import com.artemis.Entity;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.Align;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Array;
import com.blogspot.javagamexyz.gamexyz.abilities.Action2;
import com.blogspot.javagamexyz.gamexyz.components.Stats;
import com.blogspot.javagamexyz.gamexyz.screens.control.overworld.InputManager;
import com.blogspot.javagamexyz.gamexyz.screens.control.overworld.MenuProcessor;

public class MenuBuilder {

 private InputManager inputManager;
 private MenuProcessor menuProcessor;
 private Stage stage;
 private Skin skin;
 
 private FramedMenu turnMenu;
 private FramedMenu actionMenu;
 private FramedMenu abilityMenu;
 private FramedMenu statsMenu;

 public MenuBuilder(InputManager inputManager, MenuProcessor menuProcessor, Stage stage) {
  this.inputManager = inputManager;
  this.menuProcessor = menuProcessor;
  this.stage = stage;

  FileHandle skinFile = new FileHandle("resources/uiskin/uiskin.json");
  skin = new Skin(skinFile);
  
  turnMenu = new FramedMenu(skin, 128, 128);
  actionMenu = new FramedMenu(skin, 128, 128, turnMenu);
  abilityMenu = new FramedMenu(skin, 230, 200, actionMenu);
  statsMenu = new FramedMenu(skin, 128, 128);
 }

 public void buildTurnMenu(final Entity e) {
  turnMenu.clear();
  
  // Move button
  ChangeListener moveListener = new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    menuProcessor.move();
   }
  };
  turnMenu.addButton("Move", moveListener, inputManager.canMove());
  
  // Action button
  ChangeListener actionListener = new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    buildActionMenu(e, 30, turnMenu.getY());
   }
  };
  turnMenu.addButton("Action", actionListener, inputManager.canAct());
  
  // Wait button
  ChangeListener waitListener = new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    menuProcessor.selectWait();
   }
  };
  turnMenu.addButton("Wait", waitListener, true);
  
  // Stats button
  ChangeListener statListener = new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    float w = 200f;
    float h = 100f;
    float x = (stage.getWidth() - w)/2f;
    float y = (stage.getHeight() - h)/2f;
    buildDialog("", "Status is not yet implemented", x, y, w, h, new TextButton("OK",skin));
   }
  };
  turnMenu.addButton("Status", statListener, true);
  
  turnMenu.addToStage(stage, 30, stage.getHeight() - 30);
 }
  
 public void buildActionMenu(Entity e, float x, float y) {
  actionMenu.clear();
  actionMenu.getParent().disable();
  
  // Stat based actions
  final Stats stats = e.getComponent(Stats.class);
  if (stats != null) {
   
   // Attack
   if (stats.regularAttack != null) {
    ChangeListener attack = new ChangeListener() {
     @Override
     public void changed(ChangeEvent event, Actor actor) {
      menuProcessor.action(stats.regularAttack);
     }
    };
    actionMenu.addButton(stats.regularAttack.name, attack, inputManager.canAct());
   }
   
   // Primary class
   if (stats.primaryClass != null) {
    ChangeListener primary = new ChangeListener() {
     @Override
     public void changed(ChangeEvent event, Actor actor) {
      buildAbilityMenu(stats.primaryClass.actions, stats.magic);
     }
    };
    actionMenu.addButton(stats.primaryClass.name, primary, inputManager.canAct());
   }
   
   // Secondary class
   if (stats.secondaryClass != null) {
    ChangeListener secondary = new ChangeListener() {
     @Override
     public void changed(ChangeEvent event, Actor actor) {
      buildAbilityMenu(stats.secondaryClass.actions, stats.magic);
     }
    };
    actionMenu.addButton(stats.secondaryClass.name, secondary, inputManager.canAct());
   }
  }
  
  // Item
  ChangeListener item = new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    float w = 200f;
    float h = 100f;
    float x = (stage.getWidth() - w)/2f;
    float y = (stage.getHeight() - h)/2f;
    buildDialog("", "Items have not yet been implemented", x, y, w, h, new TextButton("OK",skin));
   }
  };
  actionMenu.addButton("Item", item, inputManager.canAct());
  
  actionMenu.addToStage(stage, 30, turnMenu.getY()-5);
 }
 
 private void buildAbilityMenu(Array<Action2> actions, int mp) {
  abilityMenu.clear();
  abilityMenu.getParent().disable();
  
  // Loop through all the actions
  for (final Action2 action : actions) {
   // If they click it, process that action
   ChangeListener listener = new ChangeListener() {
    @Override
    public void changed(ChangeEvent event, Actor actor) {
     menuProcessor.action(action);
    }
   };
   // If it has an MP cost, display it as a secondary label
   // The button is active if the character has not already attacked,
   // and if the MP cost is affordable
   if (action.mpCost > 0) abilityMenu.addButton(action.name, ""+action.mpCost, listener, inputManager.canAct() && (action.mpCost < mp));
   else abilityMenu.addButton(action.name, listener, inputManager.canAct() && (action.mpCost < mp));
  }
  
  abilityMenu.addToStage(stage, -1, -1);
 }
 
 public void buildStatsMenu(Entity e) {
  statsMenu.clear();
  ChangeListener listener = new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    float w = 200f;
    float h = 100f;
    float x = (stage.getWidth() - w)/2f;
    float y = (stage.getHeight() - h)/2f;
    buildDialog("", "Status is not yet implemented", x, y, w, h, new TextButton("OK",skin));
   }
  };
  statsMenu.addButton("Status", listener, true);
  statsMenu.addToStage(stage, 30, stage.getHeight()-30);
 }
 
 public void buildDialog(String title, String message, float x, float y, float width, float height, Button... buttons) {
  FramedDialog fd = new FramedDialog(skin, title, message, width, height);
  for (Button b : buttons) {
   b.align(Align.center);
   fd.addButton(b);
  }  
  fd.addToStage(stage, x, y);
 }
 
 public TextButton getTextButton(String text, ChangeListener listener) {
  TextButton button = new TextButton(text, skin);
  if (listener != null) button.addListener(listener);
  return button;
 }
 
 public void setMenusVisible(boolean visible) {
  turnMenu.setVisible(visible);
  actionMenu.setVisible(visible);
  abilityMenu.setVisible(visible);
  statsMenu.setVisible(visible);
 }
}
Let's go ahead and decompose what this says.  First, it has FramedMenus for everything (plus one for stats, which we won't really talk about yet).  In the constructor I actually initialize the Skin.  Let's focus on the turnMenu for now.

On line 45, the first thing I do is clear out the old turnMenu.  If I don't do this, it will still have all the buttons it originally had, so when I add more, it will now just have duplicates.  Lines 48-54 are where I build the "Move" button.  The ChangeListener is called when the button is activated, and the resulting code is handled elsewhere in menuProcessor.move(), but suffice it to say, it opens up the movement range and lets the player move.

Line 54 adds it to the menu using the .addButton() method from FramedMenu.  It just goes through and does similar things for each of the other buttons.  The "Action" button, though, has its code all right here.  If you click it, it immediately builds the actionMenu at coordinates (30, turnMenu.getY()).  The coordinates used for drawing the menus base it on the top left corner, and it actually gets drawn a few pixels down, so this command just puts the actionMenu right below the turnMenu.

Line 87 actually adds the turnMenu to the stage.

Let's look down at line 148 for the abilityMenu.  At the start, we don't just clear the abilityMenu, we also disable the parent menu (the actionMenu).  We likewise did this when we built the actionMenu (disabled the turnMenu).  turnMenu doesn't have a parent, so we didn't worry about it there.  Then we just loop over all the actions, and build buttons accordingly.  If an action has an MP cost, we add a secondary label with that MP cost.  If an action costs too much MP, or the user has already acted this turn (though, in this case, we shouldn't be here anyway) the button will be disabled (we pass "false" to the addButton method).

The other "Framed" thing we make is a FramedDialog - a popup window with a row of buttons on the bottom.  When a dialog pops up, it becomes the ONLY thing you can click on, so other menus are disabled (though not grayed out, but that's okay with me).  Given a set of Buttons, lines 187-194 will build a FramedDialog, for which the code is here:
package com.blogspot.javagamexyz.gamexyz.ui;

import static com.badlogic.gdx.scenes.scene2d.actions.Actions.fadeOut;
import static com.badlogic.gdx.scenes.scene2d.actions.Actions.sequence;

import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.Dialog;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.Align;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;

public class FramedDialog {

 private Dialog dialog;
 private Image frame;
 private Skin skin;
 float width, height;
 
 public FramedDialog(Skin skin, String title, String message, float width, float height) {
  this.skin = skin;
  this.width = width;
  this.height = height;
  
  dialog = new Dialog(title,skin);
  dialog.setBackground(skin.getTiledDrawable("menuTexture"));
  dialog.getContentTable().defaults().expandX().fillX();
  dialog.getButtonTable().defaults().width(50).fillX();
  
  Label label = new Label(message, skin);
  label.setAlignment(Align.center);
  label.setWrap(true);
  
  dialog.text(label);
  
  frame = new Image(skin.getPatch("frame"));
 }
 
 public void addButton(String text, ChangeListener changeListener) {
  TextButton button = new TextButton(text, skin);
  button.addListener(changeListener);
  
  button.addListener(new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    frame.addAction(sequence(fadeOut(Dialog.fadeDuration, Interpolation.fade), Actions.removeActor()));
   }
  });
  dialog.button(button);
 }
 
 public void addButton(Button button) {
  button.addListener(new ChangeListener() {
   @Override
   public void changed(ChangeEvent event, Actor actor) {
    frame.addAction(sequence(fadeOut(Dialog.fadeDuration, Interpolation.fade), Actions.removeActor()));
   }
  });
  dialog.button(button);
 }
 
 public void addToStage(Stage stage, float x, float y) {
  stage.addActor(dialog);
  stage.addActor(frame);
  
  dialog.setX(x);
  dialog.setY(y);
  dialog.setWidth(width);
  dialog.setHeight(height);
  frame.setX(x-1);
  frame.setY(y-3);
  frame.setWidth(width + 4);
  frame.setHeight(height + 4);
  
  frame.setTouchable(Touchable.disabled);
 }
}
It's similar in spirit to the menu (though quite a bit simpler!).  A Dialog is a Window with a table for content up top, and a table for Buttons on the bottom.  When any button is pressed, the Dialog will disappear (actually, it gradually fades out, so we had to match that with the frame).  In our constructor, we tell the ContentTable's cells to, by default, fill the whole thing (expandX()), and have the widgets inside those cells also fill the whole thing (fillX()).  Also by default, we want our Buttons to have a width of 50, so we set the ButtonTable default cell width to 50, and tell the buttons to fill their cells.

We also tell our Label to be centrally aligned, and enable textWrap.  When we add a Button, here I'm actually fine using TextButton (because I want the text centered anyway).  We also add a ChangeListener to all buttons so that when they are pressed, the frame will fade out with the dialog.  With that, we've got a FramedDialog.

To integrate all this into the larger context of my game, I created a few new classes:
  • InputManager
  • MenuProcesser
  • CharacterClassFactory
    • Subclass - CharacterClass
As well as modifying all the controllers, and adding a bit to the Stats component.

In the interest of NOT making this update too much longer, and because the additions are fairly minor, we'll cover these next bits in a LOT less detail.  All the code for these can be found at the Google Code repository here.

First, I moved all input controls (like the controllers, multiplexer, etc...) away from OverworldScreen and put them in InputManager.  I also got rid of the boolean handleScreen - I just handle it all the time.  It doesn't cost too much for that wasted cycle when it's empty.

I moved all the "selectedMove()" kind of stuff to MenuProcesser.  They don't immediately call stage.clear() either - instead they just make the menu's invisible.  This way, if someone is selecting where to attack, and they click out of the range, it doesn't just reset them to a blank screen, I can bring their old menus right back up.

Most of the interplay between the controllers and the menus is based on trial-and-error what do I want it doing?  For instance, everything the user tries to do gives them a "confirm" dialog box.  I know it's kind of annoying - especially regarding "Move", but it works for now.  When they choose an attack, and click on a cell to focus it on, not only does the confirm box come up, but also it highlights the cells which will be affected.  When they choose "Move" and click on a cell to move to, a "Ghost" entity will show up there, showing them exactly where they'll actually move.  You can glance through all the controllers to see how it was all done.

I will look at CharacterClassFactory though.
package com.blogspot.javagamexyz.gamexyz.abilities;

import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;

public class CharacterClassFactory {
 
 private static CharacterClass knight_instance, archer_instance, wizard_instance, healer_instance;
 
 public static CharacterClass knight() {
  if (knight_instance == null) {
   knight_instance = new CharacterClass("Knight", "A badass sword user");
   knight_instance.actions.add(ActionFactory.spinAttack("Spin Attack", 6, 90));
   knight_instance.actions.add(ActionFactory.cure("First Aid", 2, 0, 0, 1));
   knight_instance.actions.add(ActionFactory.cure("First Aid", 2, 0, 0, 1));
   knight_instance.actions.add(ActionFactory.cure("First Aid", 2, 0, 0, 1));
   knight_instance.actions.add(ActionFactory.cure("First Aid", 2, 0, 0, 1));
  }
  return knight_instance;
 }
 
 public static CharacterClass archer() {
  if (archer_instance == null) {
   archer_instance = new CharacterClass("Archer", "Shoots stuff");
   archer_instance.actions.add(ActionFactory.physicalAttack("Long Range Shot", 6, 70, 5));
   archer_instance.actions.add(ActionFactory.magicAttack("Fire Arrow", 4, 0, 90, 3, 2));
  }
  return archer_instance;
 }
 
 public static CharacterClass wizard() {
  if (wizard_instance == null) {
   wizard_instance = new CharacterClass("Wizard", "Casts spells");
   wizard_instance.actions.add(ActionFactory.magicAttack("Magic Missle", 6, 3, 80, 4, 1));
   wizard_instance.actions.add(ActionFactory.magicAttack("Fireball", 10, 8, 90, 4, 2));
   wizard_instance.actions.add(ActionFactory.magicAttack("Explosion", 7, 5, 80, 4, 2));
  }
  return wizard_instance;
 }
 
 public static CharacterClass healer() {
  if (healer_instance == null) {
   healer_instance = new CharacterClass("Healer", "...heals?");
   healer_instance.actions.add(ActionFactory.cure("Heal", 5, 5, 4, 1));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
   healer_instance.actions.add(ActionFactory.cure("Group Heal", 5, 10, 4, 2));
  }
  return healer_instance;
 }

 public static class CharacterClass {
  // To become this class, you need certain level proficiency in these other classes...
  public ObjectMap<CharacterClass,Integer> requirements;
  
  // Each class has a list of actions they can learn, a name, and a description
  public Array<Action2> actions;
  public String name;
  public String description;
  
  public CharacterClass(String name, String description) {
   this.name = name;
   this.description = description;
   actions = new Array<Action2>();
   requirements = new ObjectMap<CharacterClass, Integer>();
  }
 }
}
We really only ever need a single instance of a CharacterClass, so we make them all static.  The Factory just returns references to those static instances.  If the instance is null, create it, otherwise just return the already created one.  CharacterClass has an ObjectMap which can someday hold requirements - for instance, maybe you must be a level 3 knight, level 5 archer, and level 5 thief to become an Assassin.  It also has an array of Action2s that can be learned by that class (for now, they just instantly know them all).  You can see I gave Healers a LOT of the same skill, over and over again (just to test the ScrollPane design).

Lastly, in Stats, people now have a CharacterClass primaryClass and CharacterClass secondaryClass, as well as an Action2 regularAttack.  In the future, I'd like to make regularAttack be obtained from the weapon currently equipped.

Alright!  Woo-hoo!  This can actually be played now, almost like a real game.  Add some enemies with AI, a few of your own people, make some new abilities and it works alright.  Notice a few things though:
  1. The "Confirm" dialog box always pops up right in the middle.  This is annoying if it blocks the thing you're doing from visibility.
  2. If you make the screen much larger, the menus look dinky and silly.  For Android development that's not at all a problem, but for desktop it matters.  People like their fullscreen.
These are issues that I don't care to deal with now - and maybe not ever in these tutorials.  We'll see.  I'm much more interested in creating an inventory/equipment system, a character leveling system, a few more classes (plus character management like changing a class to something else), and save/load ability.  And probably some other stuff.

But for now I think we're doing alright.  Remember to check out the code!

You have gained 300 XP.  Progress to Level 5: 550/850

Sunday, May 5, 2013

Artificial Intelligence Part II (Level 4)

In today's update we'll work on our AI to produce more complex behavior.  For starters, entities will be placed on teams and should not attack their own teammates (unless a lot of enemies happen to be caught in the crossfire too), and if there are no enemies in the near proximity, they will pick an enemy based on who is nearby and a high value target.

Furthermore, entities will come with customized abilities - including single range melee attacks (like a sword), long range single target attacks (like a bow or magic spell), long range multi-target attacks (like a bigger spell), or single/multi-target cure spells.  Spells will cost MP (which is tentatively figured into their calculation of what to do).

To see a glimpse of how it all looks, I tried to capture a video using CamStudio and the  Xvid mpeg-4 codec (supposedly a setup which yields smooth desktop videos) but it still came out pretty laggy.  Just bare in mind the actual game usually gets 400-500 fps (obviously silly - I need to limit fps at some point) on my little laptop.  Camstudio, apparently, does not.  The code has also been uploaded to the Google Code repository here.

Let's dive in!

First, I needed to assign entities to teams.  Artemis managers seemed like a good place to start, and TeamManager sounded just right.  It turned out that PlayerManager was closer to what I wanted, but really neither were perfect.  I wanted something which knew not only which team an entity was on, but could also return a list of all the teams, so I made my own class (which was based mostly on the TeamManager code, but because it's philosophically more like the PlayerManager I called it PlayerManager2).
package com.blogspot.javagamexyz.gamexyz;

import java.util.HashMap;
import java.util.Map;

import com.artemis.Entity;
import com.artemis.Manager;
import com.artemis.utils.Bag;
import com.artemis.utils.ImmutableBag;


/**
 * Designates player teams (e.g. Human, AI_Enemy, AI_Ally, etc...)
 * Based off of Artemis: TeamManager.java by Arni Arent
 */
public class PlayerManager2 extends Manager {
 private Map<String, Bag<Entity>> entitiesByPlayer;
 private Map<Integer, String> playerByEntity;
 private Bag<String> players;

 public PlayerManager2() {
  entitiesByPlayer = new HashMap<String, Bag<Entity>>();
  playerByEntity = new HashMap<Integer, String>();
  players = new Bag<String>();
 }
 
 public ImmutableBag<String> getPlayers() {
  return players;
 }
 
 @Override
 protected void initialize() {
 }
 
 public String getPlayer(int entityId) {
  return playerByEntity.get(entityId);
 }
 
 public void setPlayer(Entity e, String player) {
  removeFromPlayer(e);
  
  playerByEntity.put(e.getId(), player);
  
  Bag<Entity> entities = entitiesByPlayer.get(player);
  if(entities == null) {
   entities = new Bag<Entity>();
   entitiesByPlayer.put(player, entities);
   players.add(player);
  }
  entities.add(e);
 }
 
 public ImmutableBag<Entity> getEntities(String player) {
  return entitiesByPlayer.get(player);
 }
 
 public void removeFromPlayer(Entity e) {
  String player = playerByEntity.remove(e.getId());
  if(player != null) {
   Bag<Entity> entities = entitiesByPlayer.get(player);
   if(entities != null) {
    entities.remove(e);
   }
  }
 }
 
 @Override
 public void deleted(Entity e) {
  removeFromPlayer(e);
 }
}

The big thing I would note about managers is that you DEFINITELY need to remember to include the public void deleted(Entity e) method.  I didn't have this at first (because it wasn't in TeamManager - which is really based off of having a PlayerManager anyway) and it caused big trouble.

Teams (which are here referenced as Players) are denoted by Strings, so you could have a "Red" player, "Blue" player, "Human" player, etc.  To assign an entity to a "Player", in EntityFactory just say:
world.getManager(PlayerManager2.class).setPlayer(e, Players.Blue);

I made a class which would just hold constant Player names in EntityFactory.
public static class Players {
 public static final String Human = "HUMAN_TEAM";
 public static final String Computer = "COMPUTER_TEAM";
 public static final String Blue = "BLUE_TEAM";
 public static final String Red = "RED_TEAM";
 public static final String Green = "GREEN_TEAM";
 public static final String Yellow = "YELLOW_TEAM";
 public static final String Purple = "PURPLE_TEAM";
 public static final String Teal = "TEAL_TEAM";
}

What team an entity is on should influence the scorePlan() method from last time, such that hitting an ally with a damage (or cure) attack decreases (or increases) the plan score, whereas they will do the opposite to enemies.  The PlayerManager2 allows any number of players, so to make things simple a given entity will just have two lists: allies and enemies.  Whether or not some of their enemies are also enemies with each other won't be considered here.  Below we'll look at how to compute scores using team information.

First things first, I want to come up with a measure to score how important a given entity is.  On the surface, a good guess would be that entity's level, but consider that a high level enemy who is nowhere near your team members shouldn't really be considered that big a threat (at least not yet)

To account for that, I "divide" an entity's level by its "average distance" from its enemies.  To see how this works, imagine there are 4 teams: A, B, C, and D, and that somebody on team B is taking their turn now.  They compile both a list of enemies (everyone from teams A, C, and D) - let's say there are 7 enemies - and a list of allies (everyone else from team B) - let's say there are 3 allies (including the active entity).

From that we build a 3x7 2D array in which we store the distance from each enemy to each ally:

1234567
1
2
3

This distance is not based on the actual mobility of anyone - instead it is raw distance, so 5 cells over deep water is indistinguishable from 5 cells of grassy plains.

Actually, I don't just store the distance, I store (sort of) the "inverse" of the distance.  For distances 0 through 4, the entry is just 1/1, for distances 5-9, the entry is 1/2, 10-14 = 1/3, 15-29=1/4, etc...

Then we compute a weighted average of the "inverse distance", weighted by the level of the ally each distance is measured to.  For instance, let's say that enemy 5 is level 2, and its distance from each ally is given below:

8 cells from Ally 1 (level 3)
12 cells from Ally 2 (level 2)
5 cells from Ally 3 (level 2)

The inverse distance entries would then be 1/2, 1/3, 1/2 respectively.  So the weighted average inverse distances would be

(3 * 1/2) + (2 * 1/3) + (2 * 1/2) / (3 + 2 + 2) = 0.45

Finally we will take this score and multiply it by Enemy 5's level, to get a final score of 0.45 * 2 = 0.9.

We do this calculation for all enemies, and likewise for all allies (weighted average inverse distance from all enemies).  This score represents in some sense which enemies pose the greatest threat to your allies, and which allies pose the greatest threat to your enemies.  Find the greatest enemy score, and the greatest ally score, and normalize all enemies and allies by division, and all enemies and allies are now on a scale from 0 to 1 in terms of importance.

Actually, to make other things simpler, ally scores are made to be negative, so really their scores range from 0 (worst) to -1 (best).

Here's the code that gets this all done. GroupAI.java:

package com.blogspot.javagamexyz.gamexyz.AI;

import com.artemis.Entity;
import com.artemis.World;
import com.artemis.utils.Bag;
import com.artemis.utils.ImmutableBag;
import com.badlogic.gdx.utils.ObjectMap;
import com.blogspot.javagamexyz.gamexyz.PlayerManager2;
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;

public class GroupAI {

 private PlayerManager2 playerManager;
 
 public ObjectMap<Integer,Float> entityScores;

 private float[][] invDistance;
 
 private GameMap gameMap;
 
 private Bag<Entity> allies;
 private Bag<Entity> enemies;
 private int[] allyLevels;
 private int[] enemyLevels;
 private int step;
 
 public GroupAI(World world, GameMap gameMap) {
  if (world == null) System.out.println("WTF");
  playerManager = world.getManager(PlayerManager2.class);
  entityScores = new ObjectMap<Integer,Float>();
  this.gameMap = gameMap;
  step = 0;
  allies = new Bag<Entity>();
  enemies = new Bag<Entity>();
 }
 
 public boolean processGroup(int entityId) {
  String group = playerManager.getPlayer(entityId);
  ImmutableBag<String> players = playerManager.getPlayers();
  
  if (step == 0) {
   // Load enemies and allies + levels
   
   // Reset stuff from previous runs
   allies.clear();
   enemies.clear();
   entityScores.clear();
   
   // First load all the entities
   for (int i = 0; i < players.size(); i++) {
    // Load allies
    if (players.get(i).compareTo(group) == 0) {
     allies.addAll(playerManager.getEntities(players.get(i)));
    }
    // Load enemies
    else {
     enemies.addAll(playerManager.getEntities(players.get(i)));
    }
   }
   
   // If there are no more enemies, this whole thing becomes kind of silly
   if (enemies.size() == 0) {
    for (int i = 0; i < allies.size(); i++) {
     entityScores.put(allies.get(i).getId(), -1f);
    }
    return true;
   }

   // Next store their levels
   allyLevels = new int[allies.size()];
   enemyLevels = new int[enemies.size()];
   for (int i = 0; i < allyLevels.length; i++) {
    allyLevels[i] = allies.get(i).getComponent(Stats.class).level;
   }
   for (int i = 0; i < enemyLevels.length; i++) {
    enemyLevels[i] = enemies.get(i).getComponent(Stats.class).level;
   }
   
   step++;
   return false;
  }
  
  else if (step == 1) {
   // Compute the distance from each enemy to each ally
   
   invDistance = new float[enemies.size()][allies.size()];
   Pair enemyPos, allyPos;
   
   // Loop over all enemies and allies, get distance between them
   for (int i = 0; i < enemies.size(); i++) {
    enemyPos = gameMap.getCoordinatesFor(enemies.get(i).getId());
    
    for (int j = 0; j < allies.size(); j++) {
     allyPos = gameMap.getCoordinatesFor(allies.get(j).getId());
     
     invDistance[i][j] = 1f / (1 + MapTools.distance(enemyPos.x, enemyPos.y, allyPos.x, allyPos.y) / 5);
    }
   }

   step++;
   return false;
  }
   
  else if (step == 2) {
   // Compute the weighted scores
   float sum;
   float weightSum;
   
   /*
    * Compute a weighted average for the "threat" posed by
    * each enemy (the average level your own allies, weighted
    * by their distance from each particular enemy)
    * For a given ally, the farther it is from the enemy, the less
    * it contributes to the enemy's score.
    * 
    * For 2 allies the same distance away, the higher level one
    * is deemed to contribute more to the enemies threat.
    * 
    * It may make sense to someday incorporate additional information,
    * like a more injured ally also makes the enemy seem more dangerous.
    * 
    * Also, it's possible to do this iteratively, i.e., say that the first
    * estimate of everyone's importance is their level, then we run through
    * this once to get a refined estimate of their importance, then run it
    * again, etc... until it converges to something?  Maybe, I'll consider
    * this in more detail another time.
   */
   
   for (int i = 0; i < enemies.size(); i++) {
    sum = 0f;
    weightSum = 0f;
    
    for (int j = 0; j < allies.size(); j++) {
     sum += invDistance[i][j] * allyLevels[j];
     weightSum += allyLevels[j];
    }
    entityScores.put(enemies.get(i).getId(), enemyLevels[i] * sum / weightSum); 
   }
   
   // Do the same for an allies.  This effectively measures the threat that
   // your enemy will detect from your allies.  The ones your enemies want dead
   // should be the ones you want to take care of!
   
   for (int i = 0; i < allies.size(); i++) {
    sum = 0f;
    weightSum = 0f;
    
    for (int j = 0; j < enemies.size(); j++) {
     sum += invDistance[j][i] * enemyLevels[j];
     weightSum += enemyLevels[j];
    }
    entityScores.put(allies.get(i).getId(), -1f*allyLevels[i] * sum / weightSum); 
   }
   
   step++;
   return false;
  }
  else {
   // Normalize the scores - all enemies (and allies) on range from 0 to 1 (or -1)
   
   float bestEnemy=0;
   float bestAlly=0;
   float score;
   
   for (int x : entityScores.keys()) {
    score = entityScores.get(x);
    if (score > 0) { //Enemy
     if (score > bestEnemy) bestEnemy = score;
    }
    else if (score < 0) { // Ally
     if (score < bestAlly) bestAlly = score;
    }
   }
   
   for (int x : entityScores.keys()) {
    score = entityScores.get(x);
    if (score > 0) entityScores.put(x, score/bestEnemy);
    else entityScores.put(x, -1f*score/bestAlly);
   }
   
   step = 0;
   return true;
  }
 }

 public ImmutableBag<Integer> getEnemies(Entity e) {
  String player = playerManager.getPlayer(e.getId());
  ImmutableBag<String> players = playerManager.getPlayers();
  Bag<Integer> enemies = new Bag<Integer>();
  for (int i = 0; i < players.size(); i++) {
   if (player.compareTo(players.get(i)) == 0) continue;
   else {
    ImmutableBag<Entity> enemyPlayer = playerManager.getEntities(players.get(i)); 
    for (int j = 0; j < enemyPlayer.size(); j++) enemies.add(enemyPlayer.get(j).getId());
   }
  }
  return enemies;
 }
}

The main stuff comes in .process() - you can see that once again we break it into steps, each of which does a particular part of the task:
  • Step 0 - Load all enemy and ally levels into Arrays
  • Step 1 - Fill in the inverse distance matrix
  • Step 2 - Compute the weighted scores
  • Step 3 - Normalize all enemy and ally scores based on the best enemy, and best ally
We'll discuss the getEnemies() method later, but first let's see how we can incorporate these scores into AISystem.  Previously, we split the AISystem into a series of steps - this GroupAI should count as another step which must be completed before moving on to deciding the plan.  The plan we choose should somehow depend on the importance of various entities.  To do this, we just make a new flag called groupAiDone, initialize it to false, and set it to true once the groupAI.process() has completed all its steps.  This is easy to check, because groupAI.process() returns false, until it's done at which point it returns true.
@Override
protected void process(Entity e) {
  
 AI ai = AIm.get(e);
 if (!ai.active) return;
 
 if (!groupAiDone) {
  groupAiDone = groupAI.processGroup(e.getId());
 }
  
 else if (!ai.planDone) decidePlan(e, ai);
  
 else { ...

Now we should use the entityScores to help us decide our plans, let's consider more carefully how scorePlan ought to work.  In general, different actions ought to have their scores calculated differently - for instance healing actions vs damage actions vs status ailment actions, etc, and should separately consider the caster (for whom there might be an MP cost or something) and the targets.  I offloaded this scoring to each individual Action2 using a ScoreCalculator (which we'll discuss later).  For now, recall that allies have a negative "importance" score, and enemies have a positive score.

To score the plan as a whole, we'll take the score from the ScoreCalculator and multiply it by the entityScore.  Thus, beneficial actions ought to have a negative score (so that when you cast it on an ally, (negative) x (negative) = positive score), and attack actions ought to have a positive score.
private int scorePlan(Entity e, Plan plan) {
 Action2 action = plan.action;
 
 // Get the stats of the entity doing this action
 Stats source = sm.get(e);
 
 // If the action costs too much, we won't even consider this plan
 if (source.magic < action.mpCost) return 0;
 
 // get the target field of this action
 Array<Pair> field = action.fieldCalculator.getField(plan.actionTarget, action);
 
 // Start a bag of stats of all the entities who will be targeted in this attack, and their IDs 
 Bag<Stats> targetBag = new Bag<Stats>();
 Bag<Integer> targetIds = new Bag<Integer>();
 int targetId;
 
 // Loop over all cells in the field
 for (Pair cell : field) {
  targetId = gameMap.getEntityAt(cell.x, cell.y);
  
  // If there is an entity at that cell, add it to the targetBag and targetIds bag 
  if (targetId > -1) {
   Entity target = world.getEntity(targetId);
   targetBag.add(sm.get(target));
   targetIds.add(targetId);
   plan.targetEntities.add(target);
  }
 }
 
 // If there are no targets, return 0
 if (targetBag.size() == 0) return 0;
 
 // Calculate the score of this action for each target in the target bag, as well as the
 // cost to the caster (attacker)
 ImmutableBag<Float> scoreBag = action.scoreCalculator.calculateScore(source, targetBag, action);
 
 // Null means that the action exceeded the casters MP, so this action gets a 0 score
 if (scoreBag == null) return 0;
 
 // scoreBag(0) is the cost to the caster, then multiplied by casters importance
 float score = scoreBag.get(0) * groupAI.entityScores.get(e.getId());
 
 // Add to that the scores*importance for all targets of the spell
 for (int i = 1; i < scoreBag.size(); i++) {
  score += scoreBag.get(i) * groupAI.entityScores.get(targetIds.get(i-1));
 }
 
 // Now we multiply by 100 and make it an int
 plan.score = (int)(100*score);
 
 // A negative score means something bad happened, so we don't really want to bother
 // with this plan.  Otherwise, if the score is positive, and we are acting first
 // (which means we get to move later) we should get a bonus of 5 points (to indicate
 // that there is some benefit to moving after we act)
 if (!plan.moveFirst && plan.score > 0) plan.score+=5; 
 return (int)plan.score;
}

The first thing I did was make sure the Action2's mpCost doesn't exceed the caster's MP.  If it does, the plan gets a score of 0.  Beyond that, I made the ScoreCalculator ask for everything up front - the caster's Stats and all the targets' Stats, so we had to make a Bag (I have typically been using the libgdx Array, but I just thought I'd go with the Artemis Bag this time) to hold all the targets' Stats.  Along with this, I made a Bag of target IDs so we could multiply the the Action2 score by the groupAI entityScore.

After we get the Action2 scores, we check to see if it's null (this would indicate that the action couldn't be done in this situation, for whatever reason).  If it's good, ScoreCalculator should return a Bag where the first element is the cost to the caster, and each subsequent element is the score for each target (in order).  We want to add these all up, multiplied by the entity's importance.  I then multiply it by 100 and return.

Action2

Let's take a closer look at the updated Action2 now.  It had to be extremely general so that different actions could do very different things.  Each action has an integer for mpCost, range, field, baseProbability (0% to 100%), and strength.  But in addition to that, they have special methods for process(), calculateScore(), calculateField(), and calculateRange().  The last 2 are useful for attacks that hit a series of cells in a straight line, or perhaps that can only hit targets 3-4 cells away, but not less.

In Action2 I defined interfaces for each of those methods, and each action will have instances of them:

package com.blogspot.javagamexyz.gamexyz.abilities;

import com.artemis.Entity;
import com.artemis.utils.Bag;
import com.artemis.utils.ImmutableBag;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
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.MapTools;

public class Action2 {
  
 public int mpCost, range, field, baseProbability, strength;
 
 public ActionProcessor actionProcessor;
 public ScoreCalculator scoreCalculator;
 public FieldCalculator fieldCalculator;
 public RangeCalculator rangeCalculator;
 
 public interface ActionProcessor {
  public void process(Entity source, Array<Entity> targets, Action2 action);
 }
 
 public interface ScoreCalculator {
  public ImmutableBag<Float> calculateScore(Stats source, ImmutableBag<Stats> target, Action2 action);
 }
 
 public interface FieldCalculator {
  public Array<Pair> getField(Pair target, Action2 action);
 }
 
 public interface RangeCalculator {
  public Array<Pair> getRange(Pair source, Action2 action);
 }
}


I also built two constructors for Action2: one of which requires that the instance variables be passed in:

public Action2(int strength, int mpCost, int baseProbability, int range, int field,
  ActionProcessor actionProcessor,
  ScoreCalculator scoreCalculator,
  FieldCalculator fieldCalculator,
  RangeCalculator rangeCalculator) {
 this.strength = strength;
 this.baseProbability = baseProbability;
 this.mpCost = mpCost;
 this.range = range;
 this.field = field;
 this.actionProcessor = actionProcessor;
 this.scoreCalculator = scoreCalculator;
 this.fieldCalculator = fieldCalculator;
 this.rangeCalculator = rangeCalculator;
}

The other builds default interfaces based on mpCost, etc...

public Action2(int strength, int mpCost, int baseProbability, int range, int field) {
 this.mpCost = mpCost;
 this.range = range;
 this.field = field;
 this.strength = strength;
 this.baseProbability = baseProbability;
 
 actionProcessor = new ActionProcessor() {
  @Override
  public void process(Entity sourceE, Array<Entity> targets, Action2 action) {
   Stats source = sourceE.getComponent(Stats.class);
   source.magic -= action.mpCost;
   boolean hitOnce = false;
   
   for (Entity targetE : targets) {
    Stats target = targetE.getComponent(Stats.class);
    int damage = 0;
    int probability = action.baseProbability; // +source.agility() - target.agility() +blah blah blah...
    if (MathUtils.random(100) < probability) { //HIT
     if (!hitOnce) {
      source.xp += 10;
      hitOnce = true;
     }
     damage = (int)(MathUtils.random(0.8f,1.2f)*(action.strength + source.getStrength() - target.getHardiness()));
     if (damage < 1) damage = 1;
    }

    targetE.addComponent(new Damage(damage));
    targetE.changedInWorld();
   }
  }
 };
 
 scoreCalculator = new ScoreCalculator() {
  @Override
  public ImmutableBag<Float> calculateScore(Stats source, ImmutableBag<Stats> targets, Action2 action) {    
   // If we can't even cast it, then don't bother
   int MP = source.magic;
   if (action.mpCost > MP) return null;
   
   Bag<Float> scoreBag = new Bag<Float>();
   
   // Get the cost to the source
   scoreBag.add(0.1f*(float)action.mpCost / (float)MP);
   
   // Get the scores for each target
   for (int i = 0; i < targets.size(); i++) {
    Stats target = targets.get(i);
    int HP = target.health;
    int damage = (int)(MathUtils.random(0.8f,1.2f)*(action.strength + source.getStrength() - target.getHardiness()));
    if (damage < 1) damage = 1;
    scoreBag.add((float)action.baseProbability/100f * (float)Math.min(damage, HP) / (float)HP);
   }
   
   return scoreBag;
  }
 };
 
 fieldCalculator = new FieldCalculator() {
  @Override
  public Array<Pair> getField(Pair target, Action2 action) {
   Array<Pair> field = MapTools.getNeighbors(target.x, target.y, action.field-1);
   field.add(target);
   return field;
  }
 };
 
 rangeCalculator = new RangeCalculator() {
  @Override
  public Array<Pair> getRange(Pair source, Action2 action) {
   return MapTools.getNeighbors(source.x, source.y, action.range);
  }
 };
}

The default ActionProcessor reduces the caster's MP by mpCost, then goes through each entity in the target list.  If a random roll successfully hits them, the caster gets 10 xp (not that this does anything, and it only happens for the 1st target hit - subsequent hits from the same attack don't do diddly), and the action calculates how much damage is dealt (minimum of 1) and adds a Damage component to the target.  Since all the probabilities and stats have been taken into account, this new Damage component only carries how much damage was dealt, and is immediately delivered by the DamageSystem.  If damage=0, the DamageSystem will interpret it as a miss, damage < 0 is interpreted as a cure.  (Scroll down a bit to see the updated Damage and DamageSystem)

The default ScoreCalculator first checks that it can be cast (i.e. that it has enough MP in this case, but there may later be other restrictions).  The first score is equal to 1/10th of the fraction of remaining MP the action costs.  Thus, if an entity has 20 MP and checks an action which costs 12 MP, 1/10th of 12/20 = 0.06, which is what we take the "cost" to be.  Remember, that will be multiplied by the caster's "importance", which is negative, so ultimately it detracts from the plan's score.

Beyond that, each target's score becomes the expected value of what percent of the target's remaining HP will be lost.  Thus if a target has 20 HP, and will be dealt 13 damage with probability = 90%, the score is 0.90 * 13/20 = 0.9 * 0.65 = 0.585.  If it would deal 100 damage, the score is not 0.9 * 100/20 - it really only gets the benefit of 0.9 * 20/20 = 0.9 * 1 = 0.9.  For now, I'm just using baseProbability, but in reality it should be based also on the target's agility, etc...

The default FieldCalculator does exactly what we used to expect - it gets all the neighbors out to the field-1, and adds the central tile as well.

The default RangeCalcuator gets all cells a distance range away from the source, without including the source's own cell.

Aside: Updated Damage and DamageSystem
package com.blogspot.javagamexyz.gamexyz.components;

import com.artemis.Component;

public class Damage extends Component {

 public int damage;
 
 public Damage(int damage) {
  this.damage = damage;
 }
 
}

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 
  
  if (damage.damage > 0) { // Successful attack
  
   // Update the target's health
   stats.health -= damage.damage;
  
   // Display a message
   if (position != null) {
    EntityFactory.createDamageLabel(world, ""+damage.damage, position.x, position.y).addToWorld();
   }
  }
  
  else if (damage.damage < 0) { // cure
   int cureAmt = MyMath.min(-1*damage.damage,stats.maxHealth-stats.health);
   stats.health += cureAmt;
   if (position != null) {
    EntityFactory.createDamageLabel(world, "+"+cureAmt, position.x, position.y).addToWorld();
   }
  }
 
  else { // Otherwise they missed   
   // Create a damage label of "MISS"
   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();
 }
 ...
}

To quickly create an Action2, I made an ActionFactory.

Right now it can make 3 types of actions - regular physical attacks, magic attacks (which cost MP, but are still based on the strength and hardiness stats because I was too lazy to make it better), and cure spells.  I made general cure and spell calculators which can be referenced from the ActionFactory.

Lastly, I tweaked decideMovement() which we use when we don't have an action (which implies that there are no interesting entity's around, so we need to hunt one down).  Using a similar method to groupAI, it looks at all enemies and rates them on their attractiveness to pursue, then finds the shortest path to them, and goes as far along that path as possible.

It bases its decision on who to chase on their proximity, their entityScore, and the number of other enemies within 6 cells of it.  It doesn't count allies within 6 cells, just enemies (using the getEnemies() method from groupAI to tell the difference).

/*
 * Call this when you have no good plans in your personal range.
 * Try to find an enemy to hunt down based on how far it is from you,
 * how powerful it is, and how many other enemies are near to it.
 */
private Pair decideMovement(Entity e) {
 Pair pos = gameMap.getCoordinatesFor(e.getId());
 Movable movable = mm.get(e);
 Array<Pair> reachableCells = gameMap.pathFinder.getReachableCells(pos.x, pos.y, movable);
 ImmutableBag<Integer> enemies = groupAI.getEnemies(e);
 if (enemies.size() == 0) return reachableCells.get(MathUtils.random(reachableCells.size-1));
 
 // The best enemy you are considering chasing and its score
 int targetEnemy = -1;
 float bestScore = 0f;
 
 // The current enemy you are checking out and its score
 int id;
 float score;
 
 // How far away is the enemy?  How many enemies are within a small radius of it?
 int distance, count;
 
 for (int i = 0; i < enemies.size(); i++) {
  count = 1;
  Pair target = gameMap.getCoordinatesFor(enemies.get(i));
  distance = MapTools.distance(pos.x, pos.y, target.x, target.y);
  for (Pair cell : MapTools.getNeighbors(target.x, target.y, 6)) {
   id = gameMap.getEntityAt(cell.x, cell.y);
   if (!enemies.contains(id)) continue;
   count++;
  }
  
  score = groupAI.entityScores.get(enemies.get(i)) * count / (1 + distance / 5);
  if (score > bestScore) {
   bestScore = score;
   targetEnemy = enemies.get(i);
  }
 }
 
 if (targetEnemy > -1) {
  Pair target = gameMap.getCoordinatesFor(targetEnemy);
  Path path = gameMap.pathFinder.findPath(pos.x, pos.y, target.x, target.y, movable, true);
  for (int i = 0; i < path.getLength(); i++) {
   Step step = path.getStep(i);
   Pair p = new Pair(step.getX(),step.getY());
   if (reachableCells.contains(p, false)) return p;
  }
 }
 return reachableCells.get(MathUtils.random(reachableCells.size-1)); 
}

With this, enemies that are far away from one another may decide to pursue each other if nobody else is around.

To finish up, and to give this a visual representation, I made a set of character graphics for Archer, Wizard, Fighter, and Healer.

In EntityFactory I can now createRed, which creates a random NPC for the Red player, and createBlue which creates a random NPC for the blue team.
public static Entity createBlue(World world, int x, int y, GameMap gameMap) {
 Entity e = world.createEntity();
 
 e.addComponent(new MapPosition(x,y));
 gameMap.addEntity(e.getId(), x, y);
 
 Sprite sprite = new Sprite("cylinder");
 sprite.r = 0;
 sprite.g = 0;
 sprite.b = 0.4f;
 sprite.a = 1f;
 sprite.rotation = 0f;
 sprite.scaleX = 1f;
 sprite.scaleY = 1f;
 e.addComponent(sprite);
 
 e.addComponent(new Movable(10f, 0.14f));
 
 Abilities abilities = new Abilities();
 int abilityRoll = MathUtils.random(0,3);
 if (abilityRoll == 0) { 
  sprite.name = "fighter";
  abilities.actions.add(ActionFactory.physicalAttack(5, 85, 1));
 }
 else if (abilityRoll == 1) {
  sprite.name = "archer";
  abilities.actions.add(ActionFactory.physicalAttack(4, 70, 4));
  abilities.actions.add(ActionFactory.physicalAttack(2, 80, 1));
 }
 else if (abilityRoll == 2) {
  sprite.name = "wizard";
  abilities.actions.add(ActionFactory.magicAttack(3, 4, 80, 3, 2));
  abilities.actions.add(ActionFactory.magicAttack(4, 4, 90, 3, 1));
  abilities.actions.add(ActionFactory.physicalAttack(2, 70, 1));
 }
 else {//if (abilityRoll == 3) {
  sprite.name = "healer";
  abilities.actions.add(ActionFactory.cure(4, 4, 3, 1));
  abilities.actions.add(ActionFactory.cure(3, 6, 3, 2));
  abilities.actions.add(ActionFactory.physicalAttack(1, 65, 1));
 }
 e.addComponent(abilities);
 e.addComponent(new Stats());
 e.addComponent(new AI());
 world.getManager(PlayerManager2.class).setPlayer(e, Players.Blue);
 
 return e;
}

public static Entity createRed(World world, int x, int y, GameMap gameMap) {
 Entity e = world.createEntity();
 
 e.addComponent(new MapPosition(x,y));
 gameMap.addEntity(e.getId(), x, y);
 
 Sprite sprite = new Sprite("cylinder");
 sprite.r = 0.4f;
 sprite.g = 0;
 sprite.b = 0f;
 sprite.a = 1f;
 sprite.rotation = 0f;
 sprite.scaleX = 1f;
 sprite.scaleY = 1f;
 e.addComponent(sprite);
 
 e.addComponent(new Movable(10f, 0.14f));
 
 Abilities abilities = new Abilities();
 int abilityRoll = MathUtils.random(0,3);
 if (abilityRoll == 0) { 
  sprite.name = "fighter";
  abilities.actions.add(ActionFactory.physicalAttack(5, 85, 1));
 }
 else if (abilityRoll == 1) {
  sprite.name = "archer";
  abilities.actions.add(ActionFactory.physicalAttack(4, 70, 4));
  abilities.actions.add(ActionFactory.physicalAttack(2, 80, 1));
 }
 else if (abilityRoll == 2) {
  sprite.name = "wizard";
  abilities.actions.add(ActionFactory.magicAttack(3, 4, 80, 3, 2));
  abilities.actions.add(ActionFactory.magicAttack(4, 4, 90, 3, 1));
  abilities.actions.add(ActionFactory.physicalAttack(2, 70, 1));
 }
 else {//if (abilityRoll == 3) {
  sprite.name = "healer";
  abilities.actions.add(ActionFactory.cure(4, 4, 3, 1));
  abilities.actions.add(ActionFactory.cure(3, 6, 3, 2));
  abilities.actions.add(ActionFactory.physicalAttack(1, 65, 1));
 }
 e.addComponent(abilities);
 e.addComponent(new Stats());
 e.addComponent(new AI());
 world.getManager(PlayerManager2.class).setPlayer(e, Players.Red);
 
 return e;
}

Now we have two teams which have specialized units which fight to the death, relatively intelligently!  I think it's pretty awesome!  You can check out all this code from the repository here.

You have gained 250 XP.  Progress to Level 5: 250/850

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!