Integrated basic NPC AI

Added basic AI support along with NPC AI which uses built in paths/actions
NPCs will choose a random path after each arrival after their action finishes
Added support for per-entity run/walk speed along with a toggle
Added Map as a param for static/monster Entity creation along with Map.Zone
Fixed some debug drawing for entity paths
This commit is contained in:
Collin Smith 2019-02-22 00:37:44 -08:00
parent aae12329c1
commit d480d2566b
8 changed files with 208 additions and 31 deletions

View File

@ -0,0 +1,14 @@
package gdx.diablo.ai;
import gdx.diablo.entity.Monster;
public abstract class AI {
protected Monster entity;
public AI(Monster entity) {
this.entity = entity;
}
public void update(float delta) {}
}

View File

@ -0,0 +1,79 @@
package gdx.diablo.ai;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import org.apache.commons.lang3.ArrayUtils;
import gdx.diablo.entity.Monster;
import gdx.diablo.map.DS1;
public class Npc extends AI {
private static final String TAG = "Npc";
int targetId = ArrayUtils.INDEX_NOT_FOUND;
float actionTimer = 0;
boolean actionPerformed = false;
public Npc(Monster entity) {
super(entity);
}
public void update(float delta) {
Vector3 target = entity.target();
if (target.equals(Vector3.Zero) || (entity.position().epsilonEquals(target) && !entity.targets().hasNext())) {
DS1.Path path = entity.object.path;
if (targetId == ArrayUtils.INDEX_NOT_FOUND) {
targetId = 0;
} else if (actionTimer > 0) {
actionTimer -= delta;
actionPerformed = actionTimer < 0;
return;
} else if (actionPerformed) {
actionPerformed = false;
targetId = MathUtils.random(path.numPoints - 1);
} else {
entity.setMode("NU");
actionTimer = action(path.points[targetId].action);
actionPerformed = actionTimer < 0;
return;
}
//entity.setMode("WL");
DS1.Path.Point dst = path.points[targetId];
entity.setPath(entity.map, new Vector3(dst.x, dst.y, 0));
}
}
private float action(int action) {
// path.actions == look at nearest player, chill, hold time, quest?
// 1 = 4 second hold
// 2 = 6 second hold
// 3 = 4 second hold
// 4 = special action at end of 10 second warriv jamella spell
// 4 = special action at end of 8 second charsi jamella book
// 4? fara = 5 seconds
// 4 == S1
// 5 == S2
// etc
switch (action) {
case 1:
case 3:
return 4;
case 2:
return 6;
// TODO: play anim only once, after timer, ending the action
case 4:
entity.setMode("S1");
return 10;
case 5:
entity.setMode("S2");
return 10;
default:
Gdx.app.error(TAG, "Unknown action index: " + action);
return 4;
}
}
}

View File

@ -31,6 +31,7 @@ import gdx.diablo.map.DS1;
import gdx.diablo.map.DT1.Tile;
import gdx.diablo.map.Map;
import gdx.diablo.map.MapGraph;
import gdx.diablo.map.MapRenderer;
import gdx.diablo.widget.Label;
public class Entity {
@ -128,6 +129,10 @@ public class Entity {
Vector3 velocity = new Vector3();
float angle = MathUtils.PI * 3 / 2;
boolean running = false;
float walkSpeed = 6;
float runSpeed = 9;
Animation animation;
public boolean over = true;
Label label;
@ -136,13 +141,13 @@ public class Entity {
MapGraph.MapGraphPath path = new MapGraph.MapGraphPath();
Iterator<MapGraph.Point2> targets = Collections.emptyIterator();
public static Entity create(DS1 ds1, DS1.Object obj) {
public static Entity create(Map map, DS1 ds1, DS1.Object obj) {
final int type = obj.type;
switch (type) {
case DS1.Object.DYNAMIC_TYPE:
return Monster.create(ds1, obj);
return Monster.create(map, ds1, obj);
case DS1.Object.STATIC_TYPE:
return StaticEntity.create(ds1, obj);
return StaticEntity.create(map, ds1, obj);
default:
throw new AssertionError("Unexpected type: " + type);
}
@ -232,6 +237,32 @@ public class Entity {
return path;
}
public Iterator<MapGraph.Point2> targets() {
return targets;
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean b) {
if (running != b) {
running = b;
}
}
public void setWalkSpeed(float speed) {
if (walkSpeed != speed) {
walkSpeed = speed;
}
}
public void setRunSpeed(float speed) {
if (runSpeed != speed) {
runSpeed = speed;
}
}
public void setPath(Map map, Vector3 dst) {
setPath(map, dst, -1);
}
@ -264,14 +295,13 @@ public class Entity {
if (position.epsilonEquals(target)) {
if (!targets.hasNext()) {
path.clear();
setMode("NU");
if (mode.equalsIgnoreCase(running ? "RN" : "WL")) setMode("NU");
return;
}
}
setMode("RN");
//float targetLen = target.len();
float speed = 9f * 2f;
setMode(running ? "RN" : "WL");
float speed = (running ? walkSpeed + runSpeed : walkSpeed);
float distance = speed * delta;
float traveled = 0;
while (traveled < distance) {
@ -410,13 +440,17 @@ public class Entity {
public void drawDebug(ShapeRenderer shapes) {
drawDebugStatus(shapes);
drawDebugTarget(shapes);
if (DEBUG_TARGET) drawDebugTarget(shapes);
}
public void drawDebugStatus(ShapeRenderer shapes) {
float x = +(position.x * Tile.SUBTILE_WIDTH50) - (position.y * Tile.SUBTILE_WIDTH50);
float y = -(position.x * Tile.SUBTILE_HEIGHT50) - (position.y * Tile.SUBTILE_HEIGHT50);
shapes.setColor(Color.WHITE);
MapRenderer.drawDiamond(shapes, x - Tile.SUBTILE_WIDTH50, y - Tile.SUBTILE_HEIGHT50, Tile.SUBTILE_WIDTH, Tile.SUBTILE_HEIGHT);
//shapes.ellipse(x - Tile.SUBTILE_WIDTH50, y - Tile.SUBTILE_HEIGHT50, Tile.SUBTILE_WIDTH, Tile.SUBTILE_HEIGHT);
final float R = 32;
shapes.setColor(Color.RED);
shapes.line(x, y, x + MathUtils.cos(angle) * R, y + MathUtils.sin(angle) * R);

View File

@ -10,27 +10,36 @@ import com.badlogic.gdx.utils.Align;
import org.apache.commons.lang3.StringUtils;
import gdx.diablo.Diablo;
import gdx.diablo.ai.AI;
import gdx.diablo.ai.Npc;
import gdx.diablo.codec.excel.MonStats;
import gdx.diablo.codec.excel.MonStats2;
import gdx.diablo.graphics.PaletteIndexedBatch;
import gdx.diablo.map.DS1;
import gdx.diablo.map.DT1.Tile;
import gdx.diablo.map.Map;
public class Monster extends Entity {
private static final String TAG = "Monster";
DS1.Object object;
MonStats.Entry monstats;
MonStats2.Entry monstats2;
public final Map map;
public final DS1.Object object;
public final MonStats.Entry monstats;
public final MonStats2.Entry monstats2;
public Monster(DS1.Object object, MonStats.Entry monstats) {
AI ai;
public Monster(Map map, DS1.Object object, MonStats.Entry monstats) {
super(monstats.Code, EntType.MONSTER);
this.map = map;
this.object = object;
this.monstats = monstats;
this.monstats2 = Diablo.files.monstats2.get(monstats.MonStatsEx);
setName(monstats.NameStr);
setWeaponClass(monstats2.BaseW);
setMode(monstats.spawnmode.isEmpty() ? "NU" : monstats.spawnmode);
setWalkSpeed(monstats.Velocity);
setRunSpeed(monstats.Run);
for (int i = 0; i < monstats2.ComponentV.length; i++) {
String ComponentV = monstats2.ComponentV[i];
if (!ComponentV.isEmpty()) {
@ -41,7 +50,7 @@ public class Monster extends Entity {
}
}
public static Monster create(DS1 ds1, DS1.Object obj) {
public static Monster create(Map map, DS1 ds1, DS1.Object obj) {
assert obj.type == DS1.Object.DYNAMIC_TYPE;
String id = Diablo.files.obj.getType1(ds1.getAct(), obj.id);
@ -49,29 +58,38 @@ public class Monster extends Entity {
Gdx.app.debug(TAG, "Monster: " + monstats);
if (monstats == null) return null; // TODO: Which ones fall under this case? Some static entities did, none here yet in testing.
//if (!object.Draw) return null; // TODO: Not yet
return new Monster(obj, monstats);
Monster monster = new Monster(map, obj, monstats);
if (monstats.AI.equalsIgnoreCase("Npc")) {
monster.ai = new Npc(monster);
}
return monster;
}
@Override
public void drawDebugPath(PaletteIndexedBatch batch, ShapeRenderer shapes) {
DS1.Path path = object.path;
if (path == null) return;
float p1x = +(position.x * Tile.SUBTILE_WIDTH50) - (position.y * Tile.SUBTILE_WIDTH50);
float p1y = -(position.x * Tile.SUBTILE_HEIGHT50) - (position.y * Tile.SUBTILE_HEIGHT50);
DS1.Path.Point point;
float p1x = 0, p1y = 0;
float p2x = 0, p2y = 0;
for (int i = 0; i < path.numPoints; i++) {
DS1.Path.Point point = path.points[i];
for (int i = 0; i < path.numPoints; i++, p1x = p2x, p1y = p2y) {
point = path.points[i];
if (p1x == 0 && p1y == 0) {
p2x = +(point.x * Tile.SUBTILE_WIDTH50) - (point.y * Tile.SUBTILE_WIDTH50);
p2y = -(point.x * Tile.SUBTILE_HEIGHT50) - (point.y * Tile.SUBTILE_HEIGHT50);
continue;
}
p2x = +(point.x * Tile.SUBTILE_WIDTH50) - (point.y * Tile.SUBTILE_WIDTH50);
p2y = -(point.x * Tile.SUBTILE_HEIGHT50) - (point.y * Tile.SUBTILE_HEIGHT50);
shapes.setColor(Color.PURPLE);
shapes.rectLine(p1x, p1y, p2x, p2y, 2);
p1x = p2x;
p1y = p2y;
}
if (path.numPoints > 1) {
DS1.Path.Point point = path.points[0];
point = path.points[0];
p1x = +(point.x * Tile.SUBTILE_WIDTH50) - (point.y * Tile.SUBTILE_WIDTH50);
p1y = -(point.x * Tile.SUBTILE_HEIGHT50) - (point.y * Tile.SUBTILE_HEIGHT50);
shapes.setColor(Color.PURPLE);
@ -85,7 +103,7 @@ public class Monster extends Entity {
shapes.setColor(Color.WHITE);
shapes.rect(p1x - HALF_BOX, p1y - HALF_BOX, BOX_SIZE, BOX_SIZE);
for (int i = 0; i < path.numPoints; i++) {
DS1.Path.Point point = path.points[i];
point = path.points[i];
p1x = +(point.x * Tile.SUBTILE_WIDTH50) - (point.y * Tile.SUBTILE_WIDTH50);
p1y = -(point.x * Tile.SUBTILE_HEIGHT50) - (point.y * Tile.SUBTILE_HEIGHT50);
shapes.setColor(Color.WHITE);
@ -96,7 +114,7 @@ public class Monster extends Entity {
batch.begin();
batch.setShader(null);
for (int i = 0; i < path.numPoints; i++) {
DS1.Path.Point point = path.points[i];
point = path.points[i];
p1x = +(point.x * Tile.SUBTILE_WIDTH50) - (point.y * Tile.SUBTILE_WIDTH50);
p1y = -(point.x * Tile.SUBTILE_HEIGHT50) - (point.y * Tile.SUBTILE_HEIGHT50);
Diablo.fonts.consolas16.draw(batch, Integer.toString(point.action), p1x, p1y - BOX_SIZE, 0, Align.center, false);
@ -119,4 +137,10 @@ public class Monster extends Entity {
if (!monstats2.isSel) return false;
return super.contains(coords);
}
@Override
public void update(float delta) {
if (ai != null) ai.update(delta);
super.update(delta);
}
}

View File

@ -71,6 +71,9 @@ public class Player extends Entity {
public Player(D2S d2s) {
super(Diablo.files.PlrType.get(d2s.charClass).Token, EntType.PLAYER);
setMode("TN");
setWalkSpeed(6);
setRunSpeed(9);
setRunning(true);
stats = new D2SStats(d2s);
loadEquipped(d2s.items.equipped);
@ -80,6 +83,9 @@ public class Player extends Entity {
public Player(String name, int classId) {
super(Diablo.files.PlrType.get(classId).Token, EntType.PLAYER);
setMode("TN");
setWalkSpeed(6);
setRunSpeed(9);
setRunning(true);
stats = new StatsImpl(name, classId);
}

View File

@ -9,6 +9,7 @@ import gdx.diablo.codec.excel.Objects;
import gdx.diablo.graphics.PaletteIndexedBatch;
import gdx.diablo.map.DS1;
import gdx.diablo.map.DT1.Tile;
import gdx.diablo.map.Map;
public class StaticEntity extends Entity {
private static final String TAG = "StaticEntity";
@ -24,7 +25,7 @@ public class StaticEntity extends Entity {
init();
}
public static StaticEntity create(DS1 ds1, DS1.Object obj) {
public static StaticEntity create(Map map, DS1 ds1, DS1.Object obj) {
assert obj.type == DS1.Object.STATIC_TYPE;
int id = Diablo.files.obj.getType2(ds1.getAct(), obj.id);

View File

@ -513,14 +513,14 @@ public class Map implements Disposable {
}
Zone addZone(Levels.Entry level, int diff, int gridSizeX, int gridSizeY) {
Zone zone = new Zone(level, diff, gridSizeX, gridSizeY);
Zone zone = new Zone(this, level, diff, gridSizeX, gridSizeY);
if (DEBUG_ZONES) Gdx.app.debug(TAG, zone.toString());
zones.add(zone);
return zone;
}
Zone addZone(Levels.Entry level, int gridSizeX, int gridSizeY, int gridsX, int gridsY) {
Zone zone = new Zone(level, gridSizeX, gridSizeY, gridsX, gridsY);
Zone zone = new Zone(this, level, gridSizeX, gridSizeY, gridsX, gridsY);
if (DEBUG_ZONES) Gdx.app.debug(TAG, zone.toString());
zones.add(zone);
return zone;
@ -569,6 +569,7 @@ public class Map implements Disposable {
int tx, ty;
int tilesX, tilesY;
Map map;
Levels.Entry level;
LvlTypes.Entry type;
Preset presets[][];
@ -581,7 +582,8 @@ public class Map implements Disposable {
/**
* Constructs a zone using sizing info from levels.txt
*/
Zone(Levels.Entry level, int diff, int gridSizeX, int gridSizeY) {
Zone(Map map, Levels.Entry level, int diff, int gridSizeX, int gridSizeY) {
this.map = map;
this.level = level;
this.type = Diablo.files.LvlTypes.get(level.LevelType);
this.gridSizeX = gridSizeX;
@ -601,7 +603,8 @@ public class Map implements Disposable {
/**
* Constructs a zone using custom sizing info
*/
Zone(Levels.Entry level, int gridSizeX, int gridSizeY, int gridsX, int gridsY) {
Zone(Map map, Levels.Entry level, int gridSizeX, int gridSizeY, int gridsX, int gridsY) {
this.map = map;
this.level = level;
this.type = Diablo.files.LvlTypes.get(level.LevelType);
this.gridSizeX = gridSizeX;
@ -624,7 +627,7 @@ public class Map implements Disposable {
if (entities == EMPTY_ARRAY) entities = new Array<>();
for (int i = 0; i < ds1.numObjects; i++) {
DS1.Object obj = ds1.objects[i];
Entity entity = Entity.create(ds1, obj);
Entity entity = Entity.create(map, ds1, obj);
if (entity == null) continue;
entity.position().set(x + obj.x, y + obj.y, 0);
entities.add(entity);

View File

@ -39,6 +39,7 @@ public class MapRenderer {
private static final boolean DEBUG_MOUSE = DEBUG && true;
private static final boolean DEBUG_PATHS = DEBUG && true;
private static final boolean DEBUG_POPPADS = DEBUG && !true;
private static final boolean DEBUG_ENTITIES = DEBUG && true;
public static boolean RENDER_DEBUG_SUBTILE = DEBUG_SUBTILE;
public static boolean RENDER_DEBUG_TILE = DEBUG_TILE;
@ -434,6 +435,11 @@ public class MapRenderer {
Vector3 position = entity.position();
if ((stx <= position.x && position.x < stx + Tile.SUBTILE_SIZE)
&& (sty <= position.y && position.y < sty + Tile.SUBTILE_SIZE)) {
entity.update(Gdx.graphics.getDeltaTime());
if (!entity.position().epsilonEquals(entity.target())) {
entity.setAngle(angle(entity.position(), entity.target()));
}
entity.draw(batch);
}
}
@ -490,6 +496,16 @@ public class MapRenderer {
drawDiamond(shapes, spx, spy, Tile.SUBTILE_WIDTH, Tile.SUBTILE_HEIGHT);
}
if (DEBUG_ENTITIES) {
Map.Zone zone = map.getZone(stx, sty);
if (zone != null) {
// TODO: limit range
for (Entity entity : zone.entities) {
entity.drawDebug(shapes);
}
}
}
if (RENDER_DEBUG_PATHS)
drawDebugPaths(batch, shapes);
@ -921,7 +937,7 @@ public class MapRenderer {
shapes.set(ShapeRenderer.ShapeType.Line);
}
private static void drawDiamond(ShapeRenderer shapes, float x, float y, int width, int height) {
public static void drawDiamond(ShapeRenderer shapes, float x, float y, int width, int height) {
int hw = width >>> 1;
int hh = height >>> 1;
shapes.line(x , y + hh , x + hw , y + height);