v0.6.2: overhauled enemy AI behaviour with respect to alignment

This commit is contained in:
Evan Debenham 2017-09-19 01:43:18 -04:00
parent ddaa763a86
commit 925d737d93
14 changed files with 103 additions and 133 deletions

View File

@ -77,6 +77,14 @@ public abstract class Char extends Actor {
public boolean flying = false; public boolean flying = false;
public int invisible = 0; public int invisible = 0;
//these are relative to the hero
public enum Alignment{
ENEMY,
NEUTRAL,
ALLY
}
public Alignment alignment;
public int viewDistance = 8; public int viewDistance = 8;
protected boolean[] fieldOfView = null; protected boolean[] fieldOfView = null;

View File

@ -21,6 +21,7 @@
package com.shatteredpixel.shatteredpixeldungeon.actors.buffs; package com.shatteredpixel.shatteredpixeldungeon.actors.buffs;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator; import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator;
@ -32,7 +33,17 @@ public class Corruption extends Buff {
} }
private float buildToDamage = 0f; private float buildToDamage = 0f;
@Override
public boolean attachTo(Char target) {
if (super.attachTo(target)){
target.alignment = Char.Alignment.ALLY;
return true;
} else {
return false;
}
}
@Override @Override
public boolean act() { public boolean act() {
buildToDamage += target.HT/200f; buildToDamage += target.HT/200f;

View File

@ -131,6 +131,8 @@ public class Hero extends Char {
{ {
actPriority = 0; //acts at priority 0, baseline for the rest of behaviour. actPriority = 0; //acts at priority 0, baseline for the rest of behaviour.
alignment = Alignment.ALLY;
} }
public static final int MAX_LEVEL = 30; public static final int MAX_LEVEL = 30;
@ -988,7 +990,7 @@ public class Hero extends Char {
Mob target = null; Mob target = null;
for (Mob m : Dungeon.level.mobs) { for (Mob m : Dungeon.level.mobs) {
if (fieldOfView[ m.pos ] && m.hostile) { if (fieldOfView[ m.pos ] && m.alignment == Alignment.ENEMY) {
visible.add(m); visible.add(m);
if (!visibleEnemies.contains( m )) { if (!visibleEnemies.contains( m )) {
newMob = true; newMob = true;

View File

@ -127,9 +127,13 @@ public class Bee extends Mob {
//find all mobs near the pot //find all mobs near the pot
HashSet<Char> enemies = new HashSet<>(); HashSet<Char> enemies = new HashSet<>();
for (Mob mob : Dungeon.level.mobs) for (Mob mob : Dungeon.level.mobs) {
if (!(mob instanceof Bee) && Dungeon.level.distance(mob.pos, potPos) <= 3 && (mob.hostile || mob.ally)) if (!(mob instanceof Bee)
&& Dungeon.level.distance(mob.pos, potPos) <= 3
&& mob.alignment != Alignment.NEUTRAL) {
enemies.add(mob); enemies.add(mob);
}
}
//pick one, if there are none, check if the hero is near the pot, go for them, otherwise go for nothing. //pick one, if there are none, check if the hero is near the pot, go for them, otherwise go for nothing.
if (enemies.size() > 0) return Random.element(enemies); if (enemies.size() > 0) return Random.element(enemies);

View File

@ -62,6 +62,8 @@ public abstract class Mob extends Char {
{ {
name = Messages.get(this, "name"); name = Messages.get(this, "name");
actPriority = 2; //hero gets priority over mobs. actPriority = 2; //hero gets priority over mobs.
alignment = Alignment.ENEMY;
} }
private static final String TXT_DIED = "You hear something died in the distance"; private static final String TXT_DIED = "You hear something died in the distance";
@ -92,9 +94,6 @@ public abstract class Mob extends Char {
protected static final float TIME_TO_WAKE_UP = 1f; protected static final float TIME_TO_WAKE_UP = 1f;
public boolean hostile = true;
public boolean ally = false;
private static final String STATE = "state"; private static final String STATE = "state";
private static final String SEEN = "seen"; private static final String SEEN = "seen";
private static final String TARGET = "target"; private static final String TARGET = "target";
@ -194,8 +193,8 @@ public abstract class Mob extends Char {
//we have no enemy, or the current one is dead //we have no enemy, or the current one is dead
if ( enemy == null || !enemy.isAlive() || state == WANDERING) if ( enemy == null || !enemy.isAlive() || state == WANDERING)
newEnemy = true; newEnemy = true;
//We are corrupted, and current enemy is either the hero or another corrupted character. //We are an ally, and current enemy is another ally.
else if (buff(Corruption.class) != null && (enemy == Dungeon.hero || enemy.buff(Corruption.class) != null)) else if (alignment == Alignment.ALLY && enemy.alignment == Alignment.ALLY)
newEnemy = true; newEnemy = true;
//We are amoked and current enemy is the hero //We are amoked and current enemy is the hero
else if (buff( Amok.class ) != null && enemy == Dungeon.hero) else if (buff( Amok.class ) != null && enemy == Dungeon.hero)
@ -205,47 +204,55 @@ public abstract class Mob extends Char {
HashSet<Char> enemies = new HashSet<>(); HashSet<Char> enemies = new HashSet<>();
//if the mob is corrupted...
if ( buff(Corruption.class) != null) {
//look for enemy mobs to attack, which are also not corrupted
for (Mob mob : Dungeon.level.mobs)
if (mob != this && fieldOfView[mob.pos] && mob.hostile && mob.buff(Corruption.class) == null)
enemies.add(mob);
if (enemies.size() > 0) return Random.element(enemies);
//otherwise go for nothing
return null;
//if the mob is amoked... //if the mob is amoked...
} else if ( buff(Amok.class) != null) { if ( buff(Amok.class) != null) {
//try to find an enemy mob to attack first. //try to find an enemy mob to attack first.
for (Mob mob : Dungeon.level.mobs) for (Mob mob : Dungeon.level.mobs)
if (mob != this && fieldOfView[mob.pos] && mob.hostile) if (mob.alignment == Alignment.ENEMY && mob != this && fieldOfView[mob.pos])
enemies.add(mob); enemies.add(mob);
if (enemies.size() > 0) return Random.element(enemies);
//try to find ally mobs to attack second.
for (Mob mob : Dungeon.level.mobs)
if (mob != this && fieldOfView[mob.pos] && mob.ally)
enemies.add(mob);
if (enemies.size() > 0) return Random.element(enemies);
//if there is nothing, go for the hero
else return Dungeon.hero;
} else {
//try to find ally mobs to attack.
for (Mob mob : Dungeon.level.mobs)
if (mob != this && fieldOfView[mob.pos] && mob.ally)
enemies.add(mob);
//and add the hero to the list of targets.
enemies.add(Dungeon.hero);
//go after the closest enemy, preferring the hero if two are equidistant if (enemies.isEmpty()) {
//try to find ally mobs to attack second.
for (Mob mob : Dungeon.level.mobs)
if (mob.alignment == Alignment.ALLY && mob != this && fieldOfView[mob.pos])
enemies.add(mob);
if (enemies.isEmpty()) {
//try to find the hero third
if (fieldOfView[Dungeon.hero.pos]) {
enemies.add(Dungeon.hero);
}
}
}
//if the mob is an ally...
} else if ( alignment == Alignment.ALLY ) {
//look for hostile mobs that are not passive to attack
for (Mob mob : Dungeon.level.mobs)
if (mob.alignment == Alignment.ENEMY
&& fieldOfView[mob.pos]
&& mob.state != mob.PASSIVE)
enemies.add(mob);
//if the mob is an enemy...
} else if (alignment == Alignment.ENEMY) {
//look for ally mobs to attack
for (Mob mob : Dungeon.level.mobs)
if (mob.alignment == Alignment.ALLY && fieldOfView[mob.pos])
enemies.add(mob);
//and look for the hero
if (fieldOfView[Dungeon.hero.pos]) {
enemies.add(Dungeon.hero);
}
}
//neutral character in particular do not choose enemies.
if (enemies.isEmpty()){
return null;
} else {
//go after the closest potential enemy, preferring the hero if two are equidistant
Char closest = null; Char closest = null;
for (Char curr : enemies){ for (Char curr : enemies){
if (closest == null if (closest == null
@ -255,7 +262,6 @@ public abstract class Mob extends Char {
} }
} }
return closest; return closest;
} }
} else } else
@ -538,7 +544,7 @@ public abstract class Mob extends Char {
if (Dungeon.hero.isAlive()) { if (Dungeon.hero.isAlive()) {
if (hostile) { if (alignment == Alignment.ENEMY) {
Statistics.enemiesSlain++; Statistics.enemiesSlain++;
Badges.validateMonstersSlain(); Badges.validateMonstersSlain();
Statistics.qualifiedForNoKilling = false; Statistics.qualifiedForNoKilling = false;
@ -566,7 +572,7 @@ public abstract class Mob extends Char {
Dungeon.level.drop( loot , pos ).sprite.drop(); Dungeon.level.drop( loot , pos ).sprite.drop();
} }
if (hostile && Dungeon.hero.lvl <= maxLvl + 2){ if (alignment == Alignment.ENEMY && Dungeon.hero.lvl <= maxLvl + 2){
int rolls = 1; int rolls = 1;
if (properties.contains(Property.BOSS)) rolls = 15; if (properties.contains(Property.BOSS)) rolls = 15;
else if (properties.contains(Property.MINIBOSS)) rolls = 5; else if (properties.contains(Property.MINIBOSS)) rolls = 5;
@ -723,18 +729,20 @@ public abstract class Mob extends Char {
if (enemyInFOV) { if (enemyInFOV) {
target = enemy.pos; target = enemy.pos;
} else if (enemy == null) {
state = WANDERING;
target = Dungeon.level.randomDestination();
return true;
} }
int oldPos = pos; int oldPos = pos;
if (target != -1 && getCloser( target )) { if (target != -1 && getCloser( target )) {
spend( 1 / speed() ); spend( 1 / speed() );
return moveSprite( oldPos, pos ); return moveSprite( oldPos, pos );
} else { } else {
spend( TICK ); spend( TICK );
if (!enemyInFOV) { if (!enemyInFOV) {
sprite.showLost(); sprite.showLost();
state = WANDERING; state = WANDERING;

View File

@ -27,18 +27,16 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.VenomGas; import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.VenomGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Burning; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Burning;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.sprites.MirrorSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.MirrorSprite;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
import java.util.HashSet;
public class MirrorImage extends NPC { public class MirrorImage extends NPC {
{ {
spriteClass = MirrorSprite.class; spriteClass = MirrorSprite.class;
alignment = Alignment.ALLY;
state = HUNTING; state = HUNTING;
} }
@ -93,32 +91,6 @@ public class MirrorImage extends NPC {
return damage; return damage;
} }
protected Char chooseEnemy() {
if (enemy == null || !enemy.isAlive()) {
HashSet<Mob> enemies = new HashSet<>();
for (Mob mob : Dungeon.level.mobs) {
if (mob.hostile
&& fieldOfView[mob.pos]
&& mob.state != mob.PASSIVE) {
enemies.add(mob);
}
}
//go for closest enemy
Char closest = null;
for (Char curr : enemies){
if (closest == null
|| Dungeon.level.distance(pos, curr.pos) < Dungeon.level.distance(pos, closest.pos)){
closest = curr;
}
}
return closest;
}
return enemy;
}
@Override @Override
public CharSprite sprite() { public CharSprite sprite() {
CharSprite s = super.sprite(); CharSprite s = super.sprite();

View File

@ -33,7 +33,7 @@ public abstract class NPC extends Mob {
HP = HT = 1; HP = HT = 1;
EXP = 0; EXP = 0;
hostile = false; alignment = Alignment.NEUTRAL;
state = PASSIVE; state = PASSIVE;
} }

View File

@ -378,13 +378,11 @@ public class DriedRose extends Artifact {
flying = true; flying = true;
ally = true; alignment = Alignment.ALLY;
WANDERING = new Wandering(); WANDERING = new Wandering();
HUNTING = new Hunting();
state = WANDERING; state = HUNTING;
enemy = null;
//after hero, but before mobs //after hero, but before mobs
actPriority = 1; actPriority = 1;
@ -470,11 +468,12 @@ public class DriedRose extends Artifact {
|| !enemy.isAlive() || !enemy.isAlive()
|| !Dungeon.level.mobs.contains(enemy) || !Dungeon.level.mobs.contains(enemy)
|| Dungeon.level.distance(enemy.pos, Dungeon.hero.pos) > 8 || Dungeon.level.distance(enemy.pos, Dungeon.hero.pos) > 8
|| enemy.alignment != Alignment.ENEMY
|| state == WANDERING) { || state == WANDERING) {
HashSet<Mob> enemies = new HashSet<>(); HashSet<Mob> enemies = new HashSet<>();
for (Mob mob : Dungeon.level.mobs) { for (Mob mob : Dungeon.level.mobs) {
if (mob.hostile if (mob.alignment == Alignment.ENEMY
&& fieldOfView[mob.pos] && fieldOfView[mob.pos]
&& Dungeon.level.distance(mob.pos, Dungeon.hero.pos) <= 8 && Dungeon.level.distance(mob.pos, Dungeon.hero.pos) <= 8
&& mob.state != mob.PASSIVE) { && mob.state != mob.PASSIVE) {
@ -676,12 +675,14 @@ public class DriedRose extends Artifact {
enemySeen = true; enemySeen = true;
notice(); notice();
alerted = true;
state = HUNTING; state = HUNTING;
target = enemy.pos; target = enemy.pos;
} else { } else {
enemySeen = false; enemySeen = false;
sprite.hideLost();
int oldPos = pos; int oldPos = pos;
//always move towards the hero when wandering //always move towards the hero when wandering
@ -701,40 +702,6 @@ public class DriedRose extends Artifact {
} }
} }
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;
}
int oldPos = pos;
if (enemyInFOV && getCloser( target )) {
spend( 1 / speed() );
return moveSprite( oldPos, pos );
} else {
//don't lose a turn due to the transition, immediately act instead
state = WANDERING;
return state.act( false, justAlerted );
}
}
}
}
//************************************************************************************ //************************************************************************************
//This is a bunch strings & string arrays, used in all of the sad ghost's voice lines. //This is a bunch strings & string arrays, used in all of the sad ghost's voice lines.

View File

@ -29,7 +29,6 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invisibility; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invisibility;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile; import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile;
import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation;
@ -118,7 +117,7 @@ public class LloydsBeacon extends Artifact {
for (int i = 0; i < PathFinder.NEIGHBOURS8.length; i++) { for (int i = 0; i < PathFinder.NEIGHBOURS8.length; i++) {
Char ch = Actor.findChar(hero.pos + PathFinder.NEIGHBOURS8[i]); Char ch = Actor.findChar(hero.pos + PathFinder.NEIGHBOURS8[i]);
if (ch != null && !(ch instanceof Mob && !((Mob) ch).hostile)) { if (ch != null && ch.alignment == Char.Alignment.ENEMY) {
GLog.w( Messages.get(this, "creatures") ); GLog.w( Messages.get(this, "creatures") );
return; return;
} }

View File

@ -23,12 +23,10 @@ package com.shatteredpixel.shatteredpixeldungeon.items.wands;
import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap;
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char; import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Charm; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Charm;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Corruption;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
import com.shatteredpixel.shatteredpixeldungeon.effects.Beam; import com.shatteredpixel.shatteredpixeldungeon.effects.Beam;
import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter;
@ -48,6 +46,7 @@ import com.shatteredpixel.shatteredpixeldungeon.plants.Plant;
import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet;
import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap;
import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
import com.watabou.noosa.audio.Sample; import com.watabou.noosa.audio.Sample;
import com.watabou.utils.Bundle; import com.watabou.utils.Bundle;
@ -83,8 +82,8 @@ public class WandOfTransfusion extends Wand {
processSoulMark(ch, chargesPerCast()); processSoulMark(ch, chargesPerCast());
//heals an ally, or charmed/corrupted enemy //heals an ally, or a charmed enemy
if (((Mob) ch).ally || ch.buff(Charm.class) != null || ch.buff(Corruption.class) != null){ if (ch.alignment == Char.Alignment.ALLY || ch.buff(Charm.class) != null){
int missingHP = ch.HT - ch.HP; int missingHP = ch.HT - ch.HP;
//heals 30%+3%*lvl missing HP. //heals 30%+3%*lvl missing HP.

View File

@ -227,7 +227,7 @@ public class CavesBossLevel extends Level {
for (Mob m : mobs){ for (Mob m : mobs){
//bring the first ally with you //bring the first ally with you
if (m.ally){ if (m.alignment == Char.Alignment.ALLY){
m.pos = Dungeon.hero.pos + (Random.Int(2) == 0 ? +1 : -1); m.pos = Dungeon.hero.pos + (Random.Int(2) == 0 ? +1 : -1);
m.sprite.place(m.pos); m.sprite.place(m.pos);
break; break;

View File

@ -198,7 +198,7 @@ public class CityBossLevel extends Level {
for (Mob m : mobs){ for (Mob m : mobs){
//bring the first ally with you //bring the first ally with you
if (m.ally){ if (m.alignment == Char.Alignment.ALLY){
m.pos = Dungeon.hero.pos + (Random.Int(2) == 0 ? +1 : -1); m.pos = Dungeon.hero.pos + (Random.Int(2) == 0 ? +1 : -1);
m.sprite.place(m.pos); m.sprite.place(m.pos);
break; break;

View File

@ -503,7 +503,7 @@ public abstract class Level implements Bundlable {
protected boolean act() { protected boolean act() {
int count = 0; int count = 0;
for (Mob mob : mobs.toArray(new Mob[0])){ for (Mob mob : mobs.toArray(new Mob[0])){
if (mob.hostile) count++; if (mob.alignment == Char.Alignment.ENEMY) count++;
} }
if (count < nMobs()) { if (count < nMobs()) {

View File

@ -287,7 +287,7 @@ public class PrisonBossLevel extends Level {
for (Mob m : mobs){ for (Mob m : mobs){
//bring the first ally with you //bring the first ally with you
if (m.ally){ if (m.alignment == Char.Alignment.ALLY){
m.pos = 5 + 25 * 32; //they should immediately walk out of the door m.pos = 5 + 25 * 32; //they should immediately walk out of the door
m.sprite.place(m.pos); m.sprite.place(m.pos);
break; break;
@ -340,7 +340,7 @@ public class PrisonBossLevel extends Level {
//if any allies are left over, move them along the same way as the hero //if any allies are left over, move them along the same way as the hero
for (Mob m : mobs){ for (Mob m : mobs){
if (m.ally) { if (m.alignment == Char.Alignment.ALLY) {
m.pos += 9 + 3 * 32; m.pos += 9 + 3 * 32;
m.sprite().place(m.pos); m.sprite().place(m.pos);
} }
@ -386,7 +386,7 @@ public class PrisonBossLevel extends Level {
//remove all mobs, but preserve allies //remove all mobs, but preserve allies
ArrayList<Mob> allies = new ArrayList<>(); ArrayList<Mob> allies = new ArrayList<>();
for(Mob m : mobs.toArray(new Mob[0])){ for(Mob m : mobs.toArray(new Mob[0])){
if (m.ally){ if (m.alignment == Char.Alignment.ALLY){
allies.add(m); allies.add(m);
mobs.remove(m); mobs.remove(m);
} }