v0.8.0: AI improvements (primarily pathfinding):
- removed path lookaheads, characters now only check the next tile for validity - the hero is now interrupted if their path is broken and replaced by a much longer one - added pathfinding functionality to ignore characters - significantly improved hunting enemy pathfinding when a route is blocked - improved enemy logic for switching targets if current one is unreachable
This commit is contained in:
parent
5206141aeb
commit
0653d0d1f2
|
@ -808,7 +808,7 @@ public class Dungeon {
|
||||||
BArray.setFalse(passable);
|
BArray.setFalse(passable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PathFinder.Path findPath(Char ch, int from, int to, boolean pass[], boolean[] visible ) {
|
public static PathFinder.Path findPath(Char ch, int to, boolean[] pass, boolean[] vis, boolean chars) {
|
||||||
|
|
||||||
setupPassable();
|
setupPassable();
|
||||||
if (ch.flying || ch.buff( Amok.class ) != null) {
|
if (ch.flying || ch.buff( Amok.class ) != null) {
|
||||||
|
@ -821,19 +821,21 @@ public class Dungeon {
|
||||||
BArray.and( pass, Dungeon.level.openSpace, passable );
|
BArray.and( pass, Dungeon.level.openSpace, passable );
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Char c : Actor.chars()) {
|
if (chars) {
|
||||||
if (visible[c.pos]) {
|
for (Char c : Actor.chars()) {
|
||||||
passable[c.pos] = false;
|
if (vis[c.pos]) {
|
||||||
|
passable[c.pos] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PathFinder.find( from, to, passable );
|
return PathFinder.find( ch.pos, to, passable );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int findStep(Char ch, int from, int to, boolean pass[], boolean[] visible ) {
|
public static int findStep(Char ch, int to, boolean[] pass, boolean[] visible, boolean chars ) {
|
||||||
|
|
||||||
if (Dungeon.level.adjacent( from, to )) {
|
if (Dungeon.level.adjacent( ch.pos, to )) {
|
||||||
return Actor.findChar( to ) == null && (pass[to] || Dungeon.level.avoid[to]) ? to : -1;
|
return Actor.findChar( to ) == null && (pass[to] || Dungeon.level.avoid[to]) ? to : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -848,17 +850,19 @@ public class Dungeon {
|
||||||
BArray.and( pass, Dungeon.level.openSpace, passable );
|
BArray.and( pass, Dungeon.level.openSpace, passable );
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Char c : Actor.chars()) {
|
if (chars){
|
||||||
if (visible[c.pos]) {
|
for (Char c : Actor.chars()) {
|
||||||
passable[c.pos] = false;
|
if (visible[c.pos]) {
|
||||||
|
passable[c.pos] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PathFinder.getStep( from, to, passable );
|
return PathFinder.getStep( ch.pos, to, passable );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int flee( Char ch, int cur, int from, boolean pass[], boolean[] visible ) {
|
public static int flee( Char ch, int from, boolean[] pass, boolean[] visible, boolean chars ) {
|
||||||
|
|
||||||
setupPassable();
|
setupPassable();
|
||||||
if (ch.flying) {
|
if (ch.flying) {
|
||||||
|
@ -871,14 +875,16 @@ public class Dungeon {
|
||||||
BArray.and( pass, Dungeon.level.openSpace, passable );
|
BArray.and( pass, Dungeon.level.openSpace, passable );
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Char c : Actor.chars()) {
|
if (chars) {
|
||||||
if (visible[c.pos]) {
|
for (Char c : Actor.chars()) {
|
||||||
passable[c.pos] = false;
|
if (visible[c.pos]) {
|
||||||
|
passable[c.pos] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
passable[cur] = true;
|
passable[ch.pos] = true;
|
||||||
|
|
||||||
return PathFinder.getStepBack( cur, from, passable );
|
return PathFinder.getStepBack( ch.pos, from, passable );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1156,15 +1156,8 @@ public class Hero extends Char {
|
||||||
else if (path.getLast() != target)
|
else if (path.getLast() != target)
|
||||||
newPath = true;
|
newPath = true;
|
||||||
else {
|
else {
|
||||||
//looks ahead for path validity, up to length-1 or 2.
|
if (!Dungeon.level.passable[path.get(0)] || Actor.findChar(path.get(0)) != null) {
|
||||||
//Note that this is shorter than for mobs, so that mobs usually yield to the hero
|
newPath = true;
|
||||||
int lookAhead = (int) GameMath.gate(0, path.size()-1, 2);
|
|
||||||
for (int i = 0; i < lookAhead; i++){
|
|
||||||
int cell = path.get(i);
|
|
||||||
if (!Dungeon.level.passable[cell] || (fieldOfView[cell] && Actor.findChar(cell) != null)) {
|
|
||||||
newPath = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1179,7 +1172,12 @@ public class Hero extends Char {
|
||||||
passable[i] = p[i] && (v[i] || m[i]);
|
passable[i] = p[i] && (v[i] || m[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
path = Dungeon.findPath(this, pos, target, passable, fieldOfView);
|
PathFinder.Path newpath = Dungeon.findPath(this, target, passable, fieldOfView, true);
|
||||||
|
if (newpath != null && path != null && newpath.size() > 2*path.size()){
|
||||||
|
path = null;
|
||||||
|
} else {
|
||||||
|
path = newpath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path == null) return false;
|
if (path == null) return false;
|
||||||
|
|
|
@ -49,7 +49,6 @@ public class Ghoul extends Mob {
|
||||||
|
|
||||||
SLEEPING = new Sleeping();
|
SLEEPING = new Sleeping();
|
||||||
WANDERING = new Wandering();
|
WANDERING = new Wandering();
|
||||||
HUNTING = new Hunting();
|
|
||||||
state = SLEEPING;
|
state = SLEEPING;
|
||||||
|
|
||||||
properties.add(Property.UNDEAD);
|
properties.add(Property.UNDEAD);
|
||||||
|
@ -203,54 +202,6 @@ public class Ghoul extends Mob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO currently very similar to super.Hunting and is largely a stop-gap, need to refactor
|
|
||||||
private class Hunting extends Mob.Hunting {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean act( boolean enemyInFOV, boolean justAlerted ) {
|
|
||||||
enemySeen = enemyInFOV;
|
|
||||||
if (enemyInFOV && !isCharmedBy( enemy ) && canAttack( enemy )) {
|
|
||||||
|
|
||||||
return doAttack( enemy );
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if (enemyInFOV) {
|
|
||||||
target = enemy.pos;
|
|
||||||
} else if (enemy == null) {
|
|
||||||
state = WANDERING;
|
|
||||||
target = Dungeon.level.randomDestination( Ghoul.this );
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int oldPos = pos;
|
|
||||||
if (target != -1 && getCloser( target )) {
|
|
||||||
|
|
||||||
spend( 1 / speed() );
|
|
||||||
return moveSprite( oldPos, pos );
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
Ghoul partner = (Ghoul) Actor.findById( partnerID );
|
|
||||||
if (!enemyInFOV) {
|
|
||||||
spend( TICK );
|
|
||||||
sprite.showLost();
|
|
||||||
state = WANDERING;
|
|
||||||
target = Dungeon.level.randomDestination( Ghoul.this );
|
|
||||||
|
|
||||||
//try to move closer to partner if they can't move to hero
|
|
||||||
} else if (partner != null && getCloser(partner.pos)) {
|
|
||||||
spend( 1 / speed() );
|
|
||||||
return moveSprite( oldPos, pos );
|
|
||||||
} else {
|
|
||||||
spend( TICK );
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GhoulLifeLink extends Buff{
|
public static class GhoulLifeLink extends Buff{
|
||||||
|
|
||||||
private Ghoul ghoul;
|
private Ghoul ghoul;
|
||||||
|
|
|
@ -377,21 +377,21 @@ public abstract class Mob extends Char {
|
||||||
//shorten for a closer one
|
//shorten for a closer one
|
||||||
if (Dungeon.level.adjacent(target, pos)) {
|
if (Dungeon.level.adjacent(target, pos)) {
|
||||||
path.add(target);
|
path.add(target);
|
||||||
//extend the path for a further target
|
//extend the path for a further target
|
||||||
} else {
|
} else {
|
||||||
path.add(last);
|
path.add(last);
|
||||||
path.add(target);
|
path.add(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (!path.isEmpty()) {
|
} else {
|
||||||
//if the new target is simply 1 earlier in the path shorten the path
|
//if the new target is simply 1 earlier in the path shorten the path
|
||||||
if (path.getLast() == target) {
|
if (path.getLast() == target) {
|
||||||
|
|
||||||
//if the new target is closer/same, need to modify end of path
|
//if the new target is closer/same, need to modify end of path
|
||||||
} else if (Dungeon.level.adjacent(target, path.getLast())) {
|
} else if (Dungeon.level.adjacent(target, path.getLast())) {
|
||||||
path.add(target);
|
path.add(target);
|
||||||
|
|
||||||
//if the new target is further away, need to extend the path
|
//if the new target is further away, need to extend the path
|
||||||
} else {
|
} else {
|
||||||
path.add(last);
|
path.add(last);
|
||||||
path.add(target);
|
path.add(target);
|
||||||
|
@ -404,39 +404,65 @@ public abstract class Mob extends Char {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//checks if the next cell along the current path can be stepped into
|
||||||
if (!newPath) {
|
if (!newPath) {
|
||||||
//looks ahead for path validity, up to length-1 or 4, but always at least 1.
|
int nextCell = path.removeFirst();
|
||||||
int lookAhead = (int)GameMath.gate(1, path.size()-1, 4);
|
if (!Dungeon.level.passable[nextCell]
|
||||||
for (int i = 0; i < lookAhead; i++) {
|
|| (!flying && Dungeon.level.avoid[nextCell])
|
||||||
int cell = path.get(i);
|
|| (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[nextCell])
|
||||||
if (!Dungeon.level.passable[cell]
|
|| Actor.findChar(nextCell) != null) {
|
||||||
|| (!flying && Dungeon.level.avoid[target])
|
|
||||||
|| (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[cell])
|
newPath = true;
|
||||||
|| (fieldOfView[cell] && Actor.findChar(cell) != null)) {
|
//If the next cell on the path can't be moved into, see if there is another cell that could replace it
|
||||||
newPath = true;
|
if (!path.isEmpty()) {
|
||||||
break;
|
for (int i : PathFinder.NEIGHBOURS8) {
|
||||||
|
if (Dungeon.level.adjacent(pos, nextCell + i) && Dungeon.level.adjacent(nextCell + i, path.getFirst())) {
|
||||||
|
if (Dungeon.level.passable[nextCell+i]
|
||||||
|
&& (flying || !Dungeon.level.avoid[nextCell+i])
|
||||||
|
&& (!Char.hasProp(this, Char.Property.LARGE) || Dungeon.level.openSpace[nextCell+i])
|
||||||
|
&& Actor.findChar(nextCell+i) == null){
|
||||||
|
path.addFirst(nextCell+i);
|
||||||
|
newPath = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path.addFirst(nextCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//generate a new path
|
||||||
|
if (newPath) {
|
||||||
|
//If we aren't hunting, always take a full path
|
||||||
|
PathFinder.Path full = Dungeon.findPath(this, target, Dungeon.level.passable, fieldOfView, true);
|
||||||
|
if (state != HUNTING){
|
||||||
|
path = full;
|
||||||
|
} else {
|
||||||
|
//otherwise, check if other characters are forcing us to take a very slow route
|
||||||
|
// and don't try to go around them yet in response, basically assume their blockage is temporary
|
||||||
|
PathFinder.Path ignoreChars = Dungeon.findPath(this, target, Dungeon.level.passable, fieldOfView, false);
|
||||||
|
if (full == null || full.size() > 2*ignoreChars.size()){
|
||||||
|
//check if first cell of shorter path is valid. If it is, use new shorter path. Otherwise do nothing and wait.
|
||||||
|
path = ignoreChars;
|
||||||
|
if (!Dungeon.level.passable[ignoreChars.getFirst()]
|
||||||
|
|| (!flying && Dungeon.level.avoid[ignoreChars.getFirst()])
|
||||||
|
|| (Char.hasProp(this, Char.Property.LARGE) && !Dungeon.level.openSpace[ignoreChars.getFirst()])
|
||||||
|
|| Actor.findChar(ignoreChars.getFirst()) != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path = full;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPath) {
|
if (path != null) {
|
||||||
path = Dungeon.findPath(this, pos, target,
|
step = path.removeFirst();
|
||||||
Dungeon.level.passable,
|
} else {
|
||||||
fieldOfView);
|
|
||||||
}
|
|
||||||
|
|
||||||
//if hunting something, don't follow a path that is extremely inefficient
|
|
||||||
//FIXME this is fairly brittle, primarily it assumes that hunting mobs can't see through
|
|
||||||
// permanent terrain, such that if their path is inefficient it's always because
|
|
||||||
// of a temporary blockage, and therefore waiting for it to clear is the best option.
|
|
||||||
if (path == null ||
|
|
||||||
(state == HUNTING && path.size() > Math.max(9, 2*Dungeon.level.distance(pos, target)))) {
|
|
||||||
//
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
step = path.removeFirst();
|
|
||||||
}
|
}
|
||||||
if (step != -1) {
|
if (step != -1) {
|
||||||
move( step );
|
move( step );
|
||||||
|
@ -451,9 +477,7 @@ public abstract class Mob extends Char {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int step = Dungeon.flee( this, pos, target,
|
int step = Dungeon.flee( this, target, Dungeon.level.passable, fieldOfView, true );
|
||||||
Dungeon.level.passable,
|
|
||||||
fieldOfView );
|
|
||||||
if (step != -1) {
|
if (step != -1) {
|
||||||
move( step );
|
move( step );
|
||||||
return true;
|
return true;
|
||||||
|
@ -845,6 +869,17 @@ public abstract class Mob extends Char {
|
||||||
return moveSprite( oldPos, pos );
|
return moveSprite( oldPos, pos );
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
//if moving towards an enemy isn't possible, try to switch targets to another enemy that is closer
|
||||||
|
Char oldEnemy = enemy;
|
||||||
|
enemy = null;
|
||||||
|
enemy = chooseEnemy();
|
||||||
|
if (enemy != null && enemy != oldEnemy){
|
||||||
|
return act(enemyInFOV, justAlerted);
|
||||||
|
} else {
|
||||||
|
enemy = oldEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
spend( TICK );
|
spend( TICK );
|
||||||
if (!enemyInFOV) {
|
if (!enemyInFOV) {
|
||||||
sprite.showLost();
|
sprite.showLost();
|
||||||
|
|
|
@ -170,13 +170,13 @@ public class NewDM300 extends Mob {
|
||||||
if (Dungeon.level.adjacent(pos, Dungeon.hero.pos)){
|
if (Dungeon.level.adjacent(pos, Dungeon.hero.pos)){
|
||||||
canReach = true;
|
canReach = true;
|
||||||
} else {
|
} else {
|
||||||
canReach = (Dungeon.findStep(this, pos, Dungeon.hero.pos, Dungeon.level.openSpace, fieldOfView) != -1);
|
canReach = (Dungeon.findStep(this, Dungeon.hero.pos, Dungeon.level.openSpace, fieldOfView, true) != -1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Dungeon.level.adjacent(pos, enemy.pos)){
|
if (Dungeon.level.adjacent(pos, enemy.pos)){
|
||||||
canReach = true;
|
canReach = true;
|
||||||
} else {
|
} else {
|
||||||
canReach = (Dungeon.findStep(this, pos, enemy.pos, Dungeon.level.openSpace, fieldOfView) != -1);
|
canReach = (Dungeon.findStep(this, enemy.pos, Dungeon.level.openSpace, fieldOfView, true) != -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -121,9 +121,7 @@ public class Piranha extends Mob {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int step = Dungeon.findStep( this, pos, target,
|
int step = Dungeon.findStep( this, target, Dungeon.level.water, fieldOfView, true );
|
||||||
Dungeon.level.water,
|
|
||||||
fieldOfView );
|
|
||||||
if (step != -1) {
|
if (step != -1) {
|
||||||
move( step );
|
move( step );
|
||||||
return true;
|
return true;
|
||||||
|
@ -134,9 +132,7 @@ public class Piranha extends Mob {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean getFurther( int target ) {
|
protected boolean getFurther( int target ) {
|
||||||
int step = Dungeon.flee( this, pos, target,
|
int step = Dungeon.flee( this, target, Dungeon.level.water, fieldOfView, true );
|
||||||
Dungeon.level.water,
|
|
||||||
fieldOfView );
|
|
||||||
if (step != -1) {
|
if (step != -1) {
|
||||||
move( step );
|
move( step );
|
||||||
return true;
|
return true;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user