diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index 005d0b3e7..53b35a7ff 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -1067,6 +1067,9 @@ actors.mobs.snake.name=sewer snake actors.mobs.snake.hint=Try using the examine button on a snake to learn how to counter them. actors.mobs.snake.desc=These oversized serpents are capable of quickly slithering around blows, making them quite hard to hit. Magical attacks or surprise attacks are capable of catching them off-guard however.\n\nYou can perform a surprise attack by attacking while out of the snake's vision. One way is to let a snake chase you through a doorway and then _strike just as it moves into the door._ +actors.mobs.spectralnecromancer.name=spectral necromancer +actors.mobs.spectralnecromancer.desc=While Skeletons are a necromancer’s bread and butter, some prefer to look into other less corporeal minions. Spectral necromancers have chosen wraiths as their minion of choice!\n\nIndividual wraiths aren't as strong as skeletons, but these necromancers aren't afraid to summon a bunch of them! + actors.mobs.spinner.name=cave spinner actors.mobs.spinner.desc=These greenish furry cave spiders try to avoid direct combat. Instead they prefer to wait in the distance while their victim struggles in their excreted cobweb, slowly dying from their venomous bite. They are capable of shooting their webs great distances, and will try to block whatever path their prey is taking. diff --git a/core/src/main/assets/sprites/necromancer.png b/core/src/main/assets/sprites/necromancer.png index 9fa626cd8..97030e0dd 100644 Binary files a/core/src/main/assets/sprites/necromancer.png and b/core/src/main/assets/sprites/necromancer.png differ 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 ac45d4ae0..b164ef86d 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 @@ -220,6 +220,8 @@ public class Bestiary { cl = CausticSlime.class; } else if (cl == Thief.class) { cl = Bandit.class; + } else if (cl == Necromancer.class){ + cl = SpectralNecromancer.class; } else if (cl == Brute.class) { cl = ArmoredBrute.class; } else if (cl == DM200.class) { 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 index 94510f93e..d01019ea0 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Necromancer.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Necromancer.java @@ -63,7 +63,7 @@ public class Necromancer extends Mob { public boolean summoning = false; public int summoningPos = -1; - private boolean firstSummon = true; + protected boolean firstSummon = true; private NecroSkeleton mySkeleton; private int storedSkeletonID = -1; @@ -176,6 +176,54 @@ public class Necromancer extends Mob { next(); } + + public void summonMinion(){ + 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.openSpace[summoningPos + c] || !hasProp(Actor.findChar(summoningPos), Property.LARGE)) + && 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 { + + Char blocker = Actor.findChar(summoningPos); + if (blocker.alignment != alignment){ + blocker.damage( Random.NormalIntRange(2, 10), this ); + } + + spend(TICK); + return; + } + } + + summoning = firstSummon = false; + + mySkeleton = new NecroSkeleton(); + mySkeleton.pos = summoningPos; + GameScene.add( mySkeleton ); + Dungeon.level.occupyCell( mySkeleton ); + ((NecromancerSprite)sprite).finishSummoning(); + + if (buff(Corruption.class) != null){ + Buff.affect(mySkeleton, Corruption.class); + } + for (Buff b : buffs(ChampionEnemy.class)){ + Buff.affect( mySkeleton, b.getClass()); + } + } private class Hunting extends Mob.Hunting{ @@ -192,55 +240,7 @@ public class Necromancer extends Mob { } 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.openSpace[summoningPos + c] || !hasProp(Actor.findChar(summoningPos), Property.LARGE)) - && 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 { - - Char blocker = Actor.findChar(summoningPos); - if (blocker.alignment != alignment){ - blocker.damage( Random.NormalIntRange(2, 10), this ); - } - - spend(TICK); - return true; - } - } - - summoning = firstSummon = false; - - mySkeleton = new NecroSkeleton(); - mySkeleton.pos = summoningPos; - GameScene.add( mySkeleton ); - Dungeon.level.occupyCell( mySkeleton ); - ((NecromancerSprite)sprite).finishSummoning(); - - if (buff(Corruption.class) != null){ - Buff.affect(mySkeleton, Corruption.class); - } - for (Buff b : buffs(ChampionEnemy.class)){ - Buff.affect( mySkeleton, b.getClass()); - } - - spend(TICK); + summonMinion(); return true; } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/SpectralNecromancer.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/SpectralNecromancer.java new file mode 100644 index 000000000..0b052f6c2 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/SpectralNecromancer.java @@ -0,0 +1,129 @@ +package com.shatteredpixel.shatteredpixeldungeon.actors.mobs; + +import com.shatteredpixel.shatteredpixeldungeon.Dungeon; +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.ChampionEnemy; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Corruption; +import com.shatteredpixel.shatteredpixeldungeon.effects.Pushing; +import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfRemoveCurse; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.NecromancerSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.SpectralNecromancerSprite; +import com.watabou.utils.Bundle; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; + +import java.util.ArrayList; +import java.util.Collections; + +public class SpectralNecromancer extends Necromancer { + + { + spriteClass = SpectralNecromancerSprite.class; + } + + private ArrayList wraithIDs = new ArrayList<>(); + + @Override + protected boolean act() { + if (summoning && state != HUNTING){ + summoning = false; + if (sprite instanceof SpectralNecromancerSprite) { + ((SpectralNecromancerSprite) sprite).cancelSummoning(); + } + } + return super.act(); + } + + @Override + public void rollToDropLoot() { + if (Dungeon.hero.lvl > maxLvl + 2) return; + + super.rollToDropLoot(); + + int ofs; + do { + ofs = PathFinder.NEIGHBOURS8[Random.Int(8)]; + } while (Dungeon.level.solid[pos + ofs] && !Dungeon.level.passable[pos + ofs]); + Dungeon.level.drop( new ScrollOfRemoveCurse(), pos + ofs ).sprite.drop( pos ); + } + + @Override + public void die(Object cause) { + for (int ID : wraithIDs){ + Actor a = Actor.findById(ID); + if (a instanceof Wraith){ + ((Wraith) a).die(null); + } + } + + super.die(cause); + } + + private static final String WRAITH_IDS = "wraith_ids"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + int[] wraithIDArr = new int[wraithIDs.size()]; + int i = 0; for (Integer val : wraithIDs){ wraithIDArr[i] = val; i++; } + bundle.put(WRAITH_IDS, wraithIDArr); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + Collections.addAll(wraithIDs, bundle.getInt(WRAITH_IDS)); + } + + @Override + public void summonMinion() { + 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.openSpace[summoningPos + c] || !hasProp(Actor.findChar(summoningPos), Property.LARGE)) + && 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 { + + Char blocker = Actor.findChar(summoningPos); + if (blocker.alignment != alignment){ + blocker.damage( Random.NormalIntRange(2, 10), this ); + } + + spend(TICK); + return; + } + } + + summoning = firstSummon = false; + + Wraith wraith = Wraith.spawnAt(summoningPos); + wraith.adjustStats(0); + Dungeon.level.occupyCell( wraith ); + ((SpectralNecromancerSprite)sprite).finishSummoning(); + + if (buff(Corruption.class) != null){ + Buff.affect(wraith, Corruption.class); + } + for (Buff b : buffs(ChampionEnemy.class)){ + Buff.affect( wraith, b.getClass()); + } + wraithIDs.add(wraith.id()); + } +} diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Wraith.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Wraith.java index 3b86d300f..4f839d82c 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Wraith.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Wraith.java @@ -100,7 +100,7 @@ public class Wraith extends Mob { } public static Wraith spawnAt( int pos ) { - if (!Dungeon.level.solid[pos] && Actor.findChar( pos ) == null) { + if ((!Dungeon.level.solid[pos] || Dungeon.level.passable[pos]) && Actor.findChar( pos ) == null) { Wraith w = new Wraith(); w.adjustStats( Dungeon.depth ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/SpectralNecromancerSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/SpectralNecromancerSprite.java new file mode 100644 index 000000000..a166747bf --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/SpectralNecromancerSprite.java @@ -0,0 +1,130 @@ +package com.shatteredpixel.shatteredpixeldungeon.sprites; + +import com.shatteredpixel.shatteredpixeldungeon.Assets; +import com.shatteredpixel.shatteredpixeldungeon.Dungeon; +import com.shatteredpixel.shatteredpixeldungeon.actors.Char; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Necromancer; +import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.ShadowParticle; +import com.watabou.noosa.TextureFilm; +import com.watabou.noosa.audio.Sample; +import com.watabou.noosa.particles.Emitter; + +public class SpectralNecromancerSprite extends MobSprite { + + private Animation charging; + private Emitter summoningBones; + + //TODO sprite is still a bit of a WIP + public SpectralNecromancerSprite(){ + super(); + + texture( Assets.Sprites.NECRO ); + TextureFilm film = new TextureFilm( texture, 16, 16 ); + + int c = 16; + + idle = new Animation( 1, true ); + idle.frames( film, c+0, c+0, c+0, c+1, c+0, c+0, c+0, c+0, c+1 ); + + run = new Animation( 8, true ); + run.frames( film, c+0, c+0, c+0, c+2, c+3, c+4 ); + + zap = new Animation( 10, false ); + zap.frames( film, c+5, c+6, c+7, c+8 ); + + charging = new Animation( 5, true ); + charging.frames( film, c+7, c+8 ); + + die = new Animation( 10, false ); + die.frames( film, c+9, c+10, c+11, c+12 ); + + attack = zap.clone(); + + idle(); + } + + @Override + public void link(Char ch) { + super.link(ch); + if (ch instanceof Necromancer && ((Necromancer) ch).summoning){ + zap(((Necromancer) ch).summoningPos); + } + } + + @Override + public void update() { + super.update(); + if (summoningBones != null && ((Necromancer) ch).summoningPos != -1){ + summoningBones.visible = Dungeon.level.heroFOV[((Necromancer) ch).summoningPos]; + } + } + + @Override + public void die() { + super.die(); + if (summoningBones != null){ + summoningBones.on = false; + } + } + + @Override + public void kill() { + super.kill(); + if (summoningBones != null){ + summoningBones.killAndErase(); + } + } + + public void cancelSummoning(){ + if (summoningBones != null){ + summoningBones.on = false; + } + } + + public void finishSummoning(){ + if (summoningBones.visible) { + Sample.INSTANCE.play(Assets.Sounds.CURSED); + summoningBones.burst(ShadowParticle.CURSE, 5); + } else { + summoningBones.on = false; + } + idle(); + } + + public void charge(){ + play(charging); + } + + @Override + public void zap(int cell) { + super.zap(cell); + if (ch instanceof Necromancer && ((Necromancer) ch).summoning){ + if (summoningBones != null){ + summoningBones.on = false; + } + summoningBones = CellEmitter.get(((Necromancer) ch).summoningPos); + summoningBones.pour(ShadowParticle.MISSILE, 0.1f); + summoningBones.visible = Dungeon.level.heroFOV[((Necromancer) ch).summoningPos]; + if (visible || summoningBones.visible ) Sample.INSTANCE.play( Assets.Sounds.CHARGEUP, 1f, 0.8f ); + } + } + + @Override + public void onComplete(Animation anim) { + super.onComplete(anim); + if (anim == zap){ + if (ch instanceof Necromancer){ + if (((Necromancer) ch).summoning){ + charge(); + } else { + ((Necromancer)ch).onZapComplete(); + idle(); + } + } else { + idle(); + } + } + } + +}