diff --git a/android/src/main/assets/necromancer.png b/android/src/main/assets/necromancer.png
new file mode 100644
index 000000000..86ab5026e
Binary files /dev/null and b/android/src/main/assets/necromancer.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 bfa280187..26d11f65c 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java
@@ -101,6 +101,7 @@ public class Assets {
public static final String GUARDIAN = "guardian.png";
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 ITEMS = "items.png";
public static final String TERRAIN_FEATURES = "terrain_features.png";
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java
index ff89f5094..a2553eb7d 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java
@@ -90,7 +90,7 @@ public class Dungeon {
//Health potion sources
//enemies
SWARM_HP,
- GUARD_HP,
+ NECRO_HP,
BAT_HP,
WARLOCK_HP,
SCORPIO_HP,
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Adrenaline.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Adrenaline.java
index b0fc044c4..f8f70f596 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Adrenaline.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Adrenaline.java
@@ -29,6 +29,8 @@ public class Adrenaline extends FlavourBuff {
{
type = buffType.POSITIVE;
+
+ announced = true;
}
public static final float DURATION = 10f;
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Bestiary.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Bestiary.java
index 2524a3964..ef483e625 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Bestiary.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Bestiary.java
@@ -78,17 +78,19 @@ public class Bestiary {
Shaman.class,
Guard.class));
case 8:
- //3x skeleton, 1x thief, 2x shaman, 2x guard
- return new ArrayList<>(Arrays.asList(Skeleton.class, Skeleton.class, Skeleton.class,
+ //2x skeleton, 1x thief, 2x shaman, 2x guard, 1x necromancer
+ return new ArrayList<>(Arrays.asList(Skeleton.class, Skeleton.class,
Thief.class,
Shaman.class, Shaman.class,
- Guard.class, Guard.class));
+ Guard.class, Guard.class,
+ Necromancer.class));
case 9: case 10:
- //3x skeleton, 1x thief, 2x shaman, 3x guard
- return new ArrayList<>(Arrays.asList(Skeleton.class, Skeleton.class, Skeleton.class,
+ //1x skeleton, 1x thief, 2x shaman, 2x guard, 2x necromancer
+ return new ArrayList<>(Arrays.asList(Skeleton.class,
Thief.class,
Shaman.class, Shaman.class,
- Guard.class, Guard.class, Guard.class));
+ Guard.class, Guard.class,
+ Necromancer.class, Necromancer.class));
// Caves
case 11:
@@ -180,9 +182,6 @@ public class Bestiary {
return;
// Prison
- case 6:
- if (Random.Float() < 0.2f) rotation.add(Shaman.class);
- return;
case 8:
if (Random.Float() < 0.02f) rotation.add(Bat.class);
return;
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Necromancer.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Necromancer.java
new file mode 100644
index 000000000..39ea94771
--- /dev/null
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Necromancer.java
@@ -0,0 +1,303 @@
+/*
+ * 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.Assets;
+import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
+import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
+import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Adrenaline;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Barrier;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Corruption;
+import com.shatteredpixel.shatteredpixeldungeon.effects.Beam;
+import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter;
+import com.shatteredpixel.shatteredpixeldungeon.effects.Pushing;
+import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
+import com.shatteredpixel.shatteredpixeldungeon.items.Item;
+import com.shatteredpixel.shatteredpixeldungeon.items.potions.PotionOfHealing;
+import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation;
+import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
+import com.shatteredpixel.shatteredpixeldungeon.sprites.NecromancerSprite;
+import com.watabou.noosa.audio.Sample;
+import com.watabou.noosa.particles.Emitter;
+import com.watabou.utils.Bundle;
+import com.watabou.utils.PathFinder;
+import com.watabou.utils.Random;
+
+import java.util.ArrayList;
+
+public class Necromancer extends Mob {
+
+ {
+ spriteClass = NecromancerSprite.class;
+
+ HP = HT = 35;
+ defenseSkill = 11;
+
+ EXP = 7;
+ maxLvl = 14;
+
+ loot = new PotionOfHealing();
+ lootChance = 0.2f; //see createloot
+
+ properties.add(Property.UNDEAD);
+
+ HUNTING = new Hunting();
+ }
+
+ private boolean summoning = false;
+ private Emitter summoningEmitter = null;
+ private int summoningPos = -1;
+
+ private NecroSkeleton mySkeleton;
+ private int storedSkeletonID = -1;
+
+ @Override
+ public void updateSpriteState() {
+ super.updateSpriteState();
+
+ if (summoning && summoningEmitter == null){
+ summoningEmitter = CellEmitter.get( summoningPos );
+ summoningEmitter.pour(Speck.factory(Speck.RATTLE), 0.2f);
+ ((NecromancerSprite)sprite).charge( summoningPos );
+ }
+ }
+
+ @Override
+ public int drRoll() {
+ return Random.NormalIntRange(0, 5);
+ }
+
+ @Override
+ public void rollToDropLoot() {
+ lootChance *= ((6f - Dungeon.LimitedDrops.NECRO_HP.count) / 6f);
+ super.rollToDropLoot();
+ }
+
+ @Override
+ protected Item createLoot(){
+ Dungeon.LimitedDrops.NECRO_HP.count++;
+ return super.createLoot();
+ }
+
+ private static final String SUMMONING = "summoning";
+ private static final String SUMMONING_POS = "summoning_pos";
+ private static final String MY_SKELETON = "my_skeleton";
+
+ @Override
+ public void storeInBundle(Bundle bundle) {
+ super.storeInBundle(bundle);
+ bundle.put( SUMMONING, summoning);
+ if (summoning){
+ bundle.put( SUMMONING_POS, summoningPos);
+ }
+ if (mySkeleton != null){
+ bundle.put( MY_SKELETON, mySkeleton.id() );
+ }
+ }
+
+ @Override
+ public void restoreFromBundle(Bundle bundle) {
+ super.restoreFromBundle(bundle);
+ summoning = bundle.getBoolean( SUMMONING );
+ if (summoning){
+ summoningPos = bundle.getInt( SUMMONING_POS );
+ }
+ if (bundle.contains( MY_SKELETON )){
+ storedSkeletonID = bundle.getInt( MY_SKELETON );
+ }
+ }
+
+ private class Hunting extends Mob.Hunting{
+
+ @Override
+ public boolean act(boolean enemyInFOV, boolean justAlerted) {
+ enemySeen = enemyInFOV;
+
+ if (storedSkeletonID != -1){
+ Actor ch = Actor.findById(storedSkeletonID);
+ storedSkeletonID = -1;
+ if (ch instanceof NecroSkeleton){
+ mySkeleton = (NecroSkeleton) ch;
+ }
+ }
+
+ if (summoning){
+
+ //push anything on summoning spot away, to the furthest valid cell
+ if (Actor.findChar(summoningPos) != null) {
+ int pushPos = pos;
+ for (int c : PathFinder.NEIGHBOURS8) {
+ if (Actor.findChar(summoningPos + c) == null
+ && Dungeon.level.passable[summoningPos + c]
+ && Dungeon.level.trueDistance(pos, summoningPos + c) > Dungeon.level.trueDistance(pos, pushPos)) {
+ pushPos = summoningPos + c;
+ }
+ }
+
+ //push enemy, or wait a turn if there is no valid pushing position
+ if (pushPos != pos) {
+ Char ch = Actor.findChar(summoningPos);
+ Actor.addDelayed( new Pushing( ch, ch.pos, pushPos ), -1 );
+
+ ch.pos = pushPos;
+ Dungeon.level.occupyCell(ch );
+
+ } else {
+ spend(TICK);
+ return true;
+ }
+ }
+
+ summoning = false;
+
+ mySkeleton = new NecroSkeleton();
+ mySkeleton.pos = summoningPos;
+ GameScene.add( mySkeleton );
+ Sample.INSTANCE.play(Assets.SND_BONES);
+ summoningEmitter.burst( Speck.factory( Speck.RATTLE ), 5 );
+ sprite.idle();
+
+ if (buff(Corruption.class) != null){
+ Buff.affect(mySkeleton, Corruption.class);
+ }
+
+ spend(TICK);
+ return true;
+ }
+
+ if (mySkeleton != null &&
+ (!mySkeleton.isAlive()
+ || !Dungeon.level.mobs.contains(mySkeleton)
+ || mySkeleton.alignment != alignment)){
+ mySkeleton = null;
+ }
+
+ //if enemy is seen, and enemy is within range, and we haven no skeleton, summon a skeleton!
+ if (enemySeen && Dungeon.level.distance(pos, enemy.pos) <= 4 && mySkeleton == null){
+
+ summoningPos = -1;
+ for (int c : PathFinder.NEIGHBOURS8){
+ if (Actor.findChar(enemy.pos+c) == null
+ && Dungeon.level.passable[enemy.pos+c]
+ && fieldOfView[enemy.pos+c]
+ && Dungeon.level.trueDistance(pos, enemy.pos+c) < Dungeon.level.trueDistance(pos, summoningPos)){
+ summoningPos = enemy.pos+c;
+ }
+ }
+
+ if (summoningPos != -1){
+
+ summoning = true;
+ summoningEmitter = CellEmitter.get(summoningPos);
+ summoningEmitter.pour(Speck.factory(Speck.RATTLE), 0.2f);
+
+ ((NecromancerSprite)sprite).charge(summoningPos);
+
+ spend(TICK);
+ } else {
+ //wait for a turn
+ spend(TICK);
+ }
+
+ return true;
+ //otherwise, if enemy is seen, and we have a skeleton...
+ } else if (enemySeen && mySkeleton != null){
+
+ target = enemy.pos;
+
+ if (!fieldOfView[mySkeleton.pos]){
+
+ //if the skeleton is not next to the enemy
+ //teleport them to the closest spot next to the enemy that can be seen
+ if (!Dungeon.level.adjacent(mySkeleton.pos, enemy.pos)){
+ int telePos = -1;
+ for (int c : PathFinder.NEIGHBOURS8){
+ if (Actor.findChar(enemy.pos+c) == null
+ && Dungeon.level.passable[enemy.pos+c]
+ && fieldOfView[enemy.pos+c]
+ && Dungeon.level.trueDistance(pos, enemy.pos+c) < Dungeon.level.trueDistance(pos, telePos)){
+ telePos = enemy.pos+c;
+ }
+ }
+
+ if (telePos != -1){
+ sprite.zap(telePos);
+ ScrollOfTeleportation.appear(mySkeleton, telePos);
+ mySkeleton.teleportSpend();
+ }
+ }
+
+ } else {
+
+ //heal skeleton first
+ if (mySkeleton.HP < mySkeleton.HT){
+
+ sprite.zap(mySkeleton.pos);
+ sprite.parent.add(new Beam.HealthRay(sprite.center(), mySkeleton.sprite.center()));
+
+ int healRoll = Random.NormalIntRange(5, 8);
+ mySkeleton.HP = Math.min(mySkeleton.HP + healRoll, mySkeleton.HT);
+ mySkeleton.sprite.emitter().burst( Speck.factory( Speck.HEALING ), 1 );
+
+ //otherwise give it adrenaline
+ } else if (mySkeleton.buff(Adrenaline.class) == null) {
+
+ sprite.zap(mySkeleton.pos);
+ sprite.parent.add(new Beam.HealthRay(sprite.center(), mySkeleton.sprite.center()));
+
+ Buff.affect(mySkeleton, Adrenaline.class, 3f);
+ }
+ }
+
+ spend(TICK);
+ return true;
+
+
+ //otherwise, default to regular hunting behaviour
+ } else {
+ return super.act(enemyInFOV, justAlerted);
+ }
+ }
+ }
+
+ public static class NecroSkeleton extends Skeleton {
+
+ {
+ state = WANDERING;
+
+ //no loot or exp
+ maxLvl = -5;
+
+ //15/25 health to start
+ HP = 15;
+ }
+
+ private void teleportSpend(){
+ spend(TICK);
+ }
+
+ //TODO sometimes skeleton can get blocked behind necromancer, might want to address that
+
+ }
+}
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Speck.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Speck.java
index 2c51eef7a..e549de881 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Speck.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Speck.java
@@ -224,7 +224,7 @@ public class Speck extends Image {
case RATTLE:
lifespan = 0.5f;
- speed.set( 0, -200 );
+ speed.set( 0, -100 );
acc.set( 0, -2 * speed.y / lifespan );
angle = Random.Float( 360 );
angularSpeed = 360;
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/NecromancerSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/NecromancerSprite.java
new file mode 100644
index 000000000..c2fb28220
--- /dev/null
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/NecromancerSprite.java
@@ -0,0 +1,70 @@
+/*
+ * 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 placeholder graphics atm
+public class NecromancerSprite extends MobSprite {
+
+ private Animation charging;
+
+ public NecromancerSprite(){
+ super();
+
+ texture( Assets.NECRO );
+ TextureFilm film = new TextureFilm( texture, 16, 16 );
+
+ idle = new Animation( 1, true );
+ idle.frames( film, 0, 0, 0, 1, 0, 0, 0, 0, 1 );
+
+ run = new Animation( 10, true );
+ run.frames( film, 0, 2, 3, 4, 0 );
+
+ zap = new Animation( 5, false );
+ zap.frames( film, 5, 1 );
+
+ charging = new Animation( 5, true );
+ charging.frames( film, 1, 5 );
+
+ die = new Animation( 10, false );
+ die.frames( film, 6, 7, 8, 9 );
+
+ attack = zap.clone();
+
+ idle();
+ }
+
+ public void charge( int pos ){
+ turnTo(ch.pos, pos);
+ play(charging);
+ }
+
+ @Override
+ public void onComplete(Animation anim) {
+ super.onComplete(anim);
+ if (anim == zap){
+ 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 e97de89eb..e98bee446 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
@@ -517,6 +517,9 @@ actors.mobs.king$undead.desc=These undead dwarves, risen by the will of the King
actors.mobs.mimic.name=mimic
actors.mobs.mimic.desc=Mimics are magical creatures which can take any shape they wish. In dungeons they almost always choose a shape of a treasure chest, because they know how to beckon an adventurer.
+actors.mobs.necromancer.name=necromancer
+actors.mobs.necromancer.desc=These apprentice dark mages have flocked to the prison, as it is the perfect place to practise their evil craft.\n\nNecromancers will summon and empower skeletons to fight for them. Killing the necromancer will also kill the skeleton it summons.
+
actors.mobs.mob.died=You hear something die in the distance.
actors.mobs.mob.rage=#$%^
actors.mobs.mob.exp=%+dEXP