mirror of
https://github.com/collinsmith/riiablo.git
synced 2025-02-21 20:18:14 +07:00
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:
parent
aae12329c1
commit
d480d2566b
14
core/src/gdx/diablo/ai/AI.java
Normal file
14
core/src/gdx/diablo/ai/AI.java
Normal 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) {}
|
||||
}
|
79
core/src/gdx/diablo/ai/Npc.java
Normal file
79
core/src/gdx/diablo/ai/Npc.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user