diff --git a/core/src/main/assets/ghoul.png b/core/src/main/assets/ghoul.png
new file mode 100644
index 000000000..ac64eef5c
Binary files /dev/null and b/core/src/main/assets/ghoul.png differ
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java
index 0cd629b61..f9aa1373d 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java
@@ -102,6 +102,7 @@ public class Assets {
public static final String SLIME = "slime.png";
public static final String SNAKE = "snake.png";
public static final String NECRO = "necromancer.png";
+ public static final String GHOUL = "ghoul.png";
public static final String ITEMS = "items.png";
public static final String TERRAIN_FEATURES = "terrain_features.png";
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java
new file mode 100644
index 000000000..a5226d287
--- /dev/null
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java
@@ -0,0 +1,140 @@
+/*
+ * Pixel Dungeon
+ * Copyright (C) 2012-2015 Oleg Dolya
+ *
+ * Shattered Pixel Dungeon
+ * Copyright (C) 2014-2019 Evan Debenham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ */
+
+package com.shatteredpixel.shatteredpixeldungeon.actors.mobs;
+
+import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
+import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
+import com.shatteredpixel.shatteredpixeldungeon.effects.Pushing;
+import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
+import com.shatteredpixel.shatteredpixeldungeon.sprites.GhoulSprite;
+import com.watabou.utils.Bundle;
+import com.watabou.utils.Random;
+
+import java.util.ArrayList;
+
+//TODO some aspects of existing enemy AI make these really wonky. Need to address that.
+public class Ghoul extends Mob {
+
+ {
+ spriteClass = GhoulSprite.class;
+
+ HP = HT = 50;
+ defenseSkill = 18;
+
+ EXP = 5;
+ maxLvl = 20;
+
+ SLEEPING = new Sleeping();
+ WANDERING = new Wandering();
+ state = SLEEPING;
+
+ properties.add(Property.UNDEAD);
+ }
+
+ private int partnerID = -1;
+ private static final String PARTNER_ID = "partner_id";
+
+ @Override
+ public void storeInBundle( Bundle bundle ) {
+ super.storeInBundle( bundle );
+ bundle.put( PARTNER_ID, partnerID );
+ }
+
+ @Override
+ public void restoreFromBundle( Bundle bundle ) {
+ super.restoreFromBundle( bundle );
+ partnerID = bundle.getInt( PARTNER_ID );
+ }
+
+ @Override
+ protected boolean act() {
+ //create a child
+ if (partnerID == -1){
+
+ ArrayList candidates = new ArrayList<>();
+
+ int[] neighbours = {pos + 1, pos - 1, pos + Dungeon.level.width(), pos - Dungeon.level.width()};
+ for (int n : neighbours) {
+ if (Dungeon.level.passable[n] && Actor.findChar( n ) == null) {
+ candidates.add( n );
+ }
+ }
+
+ if (!candidates.isEmpty()){
+ Ghoul child = new Ghoul();
+ child.partnerID = this.id();
+ this.partnerID = child.id();
+ if (state != SLEEPING) {
+ child.state = child.WANDERING;
+ }
+
+ child.pos = Random.element( candidates );
+
+ Dungeon.level.occupyCell(child);
+
+ GameScene.add( child );
+ if (sprite.visible) {
+ Actor.addDelayed( new Pushing( child, pos, child.pos ), -1 );
+ }
+ }
+
+ }
+ return super.act();
+ }
+
+ private class Sleeping extends Mob.Sleeping {
+ @Override
+ public boolean act( boolean enemyInFOV, boolean justAlerted ) {
+ Ghoul partner = (Ghoul) Actor.findById( partnerID );
+ if (partner != null && partner.state != partner.SLEEPING){
+ state = WANDERING;
+ target = partner.pos;
+ return true;
+ } else {
+ return super.act( enemyInFOV, justAlerted );
+ }
+ }
+ }
+
+ private class Wandering extends Mob.Wandering {
+
+ @Override
+ protected boolean continueWandering() {
+ enemySeen = false;
+
+ Ghoul partner = (Ghoul) Actor.findById( partnerID );
+ if (partner != null && (partner.state != partner.WANDERING || Dungeon.level.distance( pos, partner.target) > 1)){
+ target = partner.pos;
+ int oldPos = pos;
+ if (getCloser( target )){
+ spend( 1 / speed() );
+ return moveSprite( oldPos, pos );
+ } else {
+ spend( TICK );
+ return true;
+ }
+ } else {
+ return super.continueWandering();
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java
index 6e78a0e3a..63285a740 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java
@@ -750,37 +750,49 @@ public abstract class Mob extends Char {
public boolean act( boolean enemyInFOV, boolean justAlerted ) {
if (enemyInFOV && (justAlerted || Random.Float( distance( enemy ) / 2f + enemy.stealth() ) < 1)) {
- enemySeen = true;
-
- notice();
- alerted = true;
- state = HUNTING;
- target = enemy.pos;
-
- if (Dungeon.isChallenged( Challenges.SWARM_INTELLIGENCE )) {
- for (Mob mob : Dungeon.level.mobs) {
- if (Dungeon.level.distance(pos, mob.pos) <= 8 && mob.state != mob.HUNTING) {
- mob.beckon( target );
- }
- }
- }
+ return noticeEnemy();
} else {
- enemySeen = false;
-
- int oldPos = pos;
- if (target != -1 && getCloser( target )) {
- spend( 1 / speed() );
- return moveSprite( oldPos, pos );
- } else {
- target = Dungeon.level.randomDestination();
- spend( TICK );
- }
+ return continueWandering();
}
+ }
+
+ protected boolean noticeEnemy(){
+ enemySeen = true;
+
+ notice();
+ alerted = true;
+ state = HUNTING;
+ target = enemy.pos;
+
+ if (Dungeon.isChallenged( Challenges.SWARM_INTELLIGENCE )) {
+ for (Mob mob : Dungeon.level.mobs) {
+ if (Dungeon.level.distance(pos, mob.pos) <= 8 && mob.state != mob.HUNTING) {
+ mob.beckon( target );
+ }
+ }
+ }
+
return true;
}
+
+ protected boolean continueWandering(){
+ enemySeen = false;
+
+ int oldPos = pos;
+ if (target != -1 && getCloser( target )) {
+ spend( 1 / speed() );
+ return moveSprite( oldPos, pos );
+ } else {
+ target = Dungeon.level.randomDestination();
+ spend( TICK );
+ }
+
+ return true;
+ }
+
}
protected class Hunting implements AiState {
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/GhoulSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/GhoulSprite.java
new file mode 100644
index 000000000..794443e5d
--- /dev/null
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/GhoulSprite.java
@@ -0,0 +1,52 @@
+/*
+ * Pixel Dungeon
+ * Copyright (C) 2012-2015 Oleg Dolya
+ *
+ * Shattered Pixel Dungeon
+ * Copyright (C) 2014-2019 Evan Debenham
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ */
+
+package com.shatteredpixel.shatteredpixeldungeon.sprites;
+
+import com.shatteredpixel.shatteredpixeldungeon.Assets;
+import com.watabou.noosa.TextureFilm;
+
+//TODO currently just a recolored monk sprite
+public class GhoulSprite extends MobSprite {
+
+ public GhoulSprite() {
+ super();
+
+ texture( Assets.GHOUL );
+
+ TextureFilm frames = new TextureFilm( texture, 15, 14 );
+
+ idle = new Animation( 6, true );
+ idle.frames( frames, 1, 0, 1, 2 );
+
+ run = new Animation( 15, true );
+ run.frames( frames, 11, 12, 13, 14, 15, 16 );
+
+ attack = new Animation( 12, false );
+ attack.frames( frames, 3, 4, 3, 4 );
+
+ die = new Animation( 15, false );
+ die.frames( frames, 1, 7, 8, 8, 9, 10 );
+
+ play( idle );
+ }
+
+}
diff --git a/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties b/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties
index c814a1980..7e03a1b6e 100644
--- a/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties
+++ b/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties
@@ -494,6 +494,9 @@ actors.mobs.eye.desc=Evil Eyes are floating balls of pent up demonic energy. Whi
actors.mobs.fetidrat.name=fetid rat
actors.mobs.fetidrat.desc=Something is clearly wrong with this rat. Its greasy black fur and rotting skin are very different from the healthy rats you've seen previously. Its pale green eyes make it seem especially menacing.\n\nThe rat carries a cloud of horrible stench with it, it's overpoweringly strong up close.\n\nDark ooze dribbles from the rat's mouth, it eats through the floor but seems to dissolve in water.
+actors.mobs.ghoul.name=dwarven ghoul
+actors.mobs.ghoul.desc=TODO
+
actors.mobs.gnoll.name=gnoll scout
actors.mobs.gnoll.desc=Gnolls are hyena-like humanoids. They dwell in sewers and dungeons, venturing up to raid the surface from time to time. Gnoll scouts are regular members of their pack, they are not as strong as brutes and not as intelligent as shamans.