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 int invisible = 0;
//these are relative to the hero
public enum Alignment{
ENEMY,
NEUTRAL,
ALLY
}
public Alignment alignment;
public int viewDistance = 8;
protected boolean[] fieldOfView = null;

View File

@ -21,6 +21,7 @@
package com.shatteredpixel.shatteredpixeldungeon.actors.buffs;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator;
@ -33,6 +34,16 @@ public class Corruption extends Buff {
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
public boolean act() {
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.
alignment = Alignment.ALLY;
}
public static final int MAX_LEVEL = 30;
@ -988,7 +990,7 @@ public class Hero extends Char {
Mob target = null;
for (Mob m : Dungeon.level.mobs) {
if (fieldOfView[ m.pos ] && m.hostile) {
if (fieldOfView[ m.pos ] && m.alignment == Alignment.ENEMY) {
visible.add(m);
if (!visibleEnemies.contains( m )) {
newMob = true;

View File

@ -127,9 +127,13 @@ public class Bee extends Mob {
//find all mobs near the pot
HashSet<Char> enemies = new HashSet<>();
for (Mob mob : Dungeon.level.mobs)
if (!(mob instanceof Bee) && Dungeon.level.distance(mob.pos, potPos) <= 3 && (mob.hostile || mob.ally))
for (Mob mob : Dungeon.level.mobs) {
if (!(mob instanceof Bee)
&& Dungeon.level.distance(mob.pos, potPos) <= 3
&& mob.alignment != Alignment.NEUTRAL) {
enemies.add(mob);
}
}
//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);

View File

@ -62,6 +62,8 @@ public abstract class Mob extends Char {
{
name = Messages.get(this, "name");
actPriority = 2; //hero gets priority over mobs.
alignment = Alignment.ENEMY;
}
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;
public boolean hostile = true;
public boolean ally = false;
private static final String STATE = "state";
private static final String SEEN = "seen";
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
if ( enemy == null || !enemy.isAlive() || state == WANDERING)
newEnemy = true;
//We are corrupted, and current enemy is either the hero or another corrupted character.
else if (buff(Corruption.class) != null && (enemy == Dungeon.hero || enemy.buff(Corruption.class) != null))
//We are an ally, and current enemy is another ally.
else if (alignment == Alignment.ALLY && enemy.alignment == Alignment.ALLY)
newEnemy = true;
//We are amoked and current enemy is the 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<>();
//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...
} else if ( buff(Amok.class) != null) {
if ( buff(Amok.class) != null) {
//try to find an enemy mob to attack first.
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);
if (enemies.size() > 0) return Random.element(enemies);
if (enemies.isEmpty()) {
//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)
if (mob.alignment == Alignment.ALLY && mob != this && fieldOfView[mob.pos])
enemies.add(mob);
//and add the hero to the list of targets.
if (enemies.isEmpty()) {
//try to find the hero third
if (fieldOfView[Dungeon.hero.pos]) {
enemies.add(Dungeon.hero);
}
}
}
//go after the closest enemy, preferring the hero if two are equidistant
//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;
for (Char curr : enemies){
if (closest == null
@ -255,7 +262,6 @@ public abstract class Mob extends Char {
}
}
return closest;
}
} else
@ -538,7 +544,7 @@ public abstract class Mob extends Char {
if (Dungeon.hero.isAlive()) {
if (hostile) {
if (alignment == Alignment.ENEMY) {
Statistics.enemiesSlain++;
Badges.validateMonstersSlain();
Statistics.qualifiedForNoKilling = false;
@ -566,7 +572,7 @@ public abstract class Mob extends Char {
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;
if (properties.contains(Property.BOSS)) rolls = 15;
else if (properties.contains(Property.MINIBOSS)) rolls = 5;
@ -723,6 +729,10 @@ public abstract class Mob extends Char {
if (enemyInFOV) {
target = enemy.pos;
} else if (enemy == null) {
state = WANDERING;
target = Dungeon.level.randomDestination();
return true;
}
int oldPos = pos;
@ -732,9 +742,7 @@ public abstract class Mob extends Char {
return moveSprite( oldPos, pos );
} else {
spend( TICK );
if (!enemyInFOV) {
sprite.showLost();
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.buffs.Burning;
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.MirrorSprite;
import com.watabou.utils.Bundle;
import java.util.HashSet;
public class MirrorImage extends NPC {
{
spriteClass = MirrorSprite.class;
alignment = Alignment.ALLY;
state = HUNTING;
}
@ -93,32 +91,6 @@ public class MirrorImage extends NPC {
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
public CharSprite sprite() {
CharSprite s = super.sprite();

View File

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

View File

@ -378,13 +378,11 @@ public class DriedRose extends Artifact {
flying = true;
ally = true;
alignment = Alignment.ALLY;
WANDERING = new Wandering();
HUNTING = new Hunting();
state = WANDERING;
enemy = null;
state = HUNTING;
//after hero, but before mobs
actPriority = 1;
@ -470,11 +468,12 @@ public class DriedRose extends Artifact {
|| !enemy.isAlive()
|| !Dungeon.level.mobs.contains(enemy)
|| Dungeon.level.distance(enemy.pos, Dungeon.hero.pos) > 8
|| enemy.alignment != Alignment.ENEMY
|| state == WANDERING) {
HashSet<Mob> enemies = new HashSet<>();
for (Mob mob : Dungeon.level.mobs) {
if (mob.hostile
if (mob.alignment == Alignment.ENEMY
&& fieldOfView[mob.pos]
&& Dungeon.level.distance(mob.pos, Dungeon.hero.pos) <= 8
&& mob.state != mob.PASSIVE) {
@ -676,12 +675,14 @@ public class DriedRose extends Artifact {
enemySeen = true;
notice();
alerted = true;
state = HUNTING;
target = enemy.pos;
} else {
enemySeen = false;
sprite.hideLost();
int oldPos = pos;
//always move towards the hero when wandering
@ -702,40 +703,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.
//************************************************************************************

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.LockedFloor;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile;
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
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++) {
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") );
return;
}

View File

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

View File

@ -227,7 +227,7 @@ public class CavesBossLevel extends Level {
for (Mob m : mobs){
//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.sprite.place(m.pos);
break;

View File

@ -198,7 +198,7 @@ public class CityBossLevel extends Level {
for (Mob m : mobs){
//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.sprite.place(m.pos);
break;

View File

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

View File

@ -287,7 +287,7 @@ public class PrisonBossLevel extends Level {
for (Mob m : mobs){
//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.sprite.place(m.pos);
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
for (Mob m : mobs){
if (m.ally) {
if (m.alignment == Char.Alignment.ALLY) {
m.pos += 9 + 3 * 32;
m.sprite().place(m.pos);
}
@ -386,7 +386,7 @@ public class PrisonBossLevel extends Level {
//remove all mobs, but preserve allies
ArrayList<Mob> allies = new ArrayList<>();
for(Mob m : mobs.toArray(new Mob[0])){
if (m.ally){
if (m.alignment == Char.Alignment.ALLY){
allies.add(m);
mobs.remove(m);
}