From 92f190d04cef41b761983ee150fbfb528f35bfe7 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Sat, 10 Jul 2021 20:02:24 -0400 Subject: [PATCH] v0.9.4: added a new rare enemy: spectral necromancers --- .../assets/messages/actors/actors.properties | 3 + core/src/main/assets/sprites/necromancer.png | Bin 448 -> 781 bytes .../actors/mobs/Bestiary.java | 2 + .../actors/mobs/Necromancer.java | 100 +++++++------- .../actors/mobs/SpectralNecromancer.java | 129 +++++++++++++++++ .../actors/mobs/Wraith.java | 2 +- .../sprites/SpectralNecromancerSprite.java | 130 ++++++++++++++++++ 7 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/SpectralNecromancer.java create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/SpectralNecromancerSprite.java 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 9fa626cd8e37d529db3df80d398396f5e1391400..97030e0ddfc4fff02c60c18818b937bdcf548f5b 100644 GIT binary patch delta 761 zcmVo5$1SFd;P`u;C_5(G0wfJjdWzqx+dDqE`48)}*+ zvs+X_aC@BW4Af5q(&Zy*)qq6<;yjT4e-0qt@Qo5+=6*U5 zT#0c35Q_ki01o2H*Ar&e2jY1Sgg2sJ06f5e0H`0aS;5N1GX*jv03Arb00MCk7>ZV_ z(Dfa_0+}%fq8)P_0KsPI=NteKe}%)4ZiUz9VvVmrULSOeARh|a`vmwnw zuchCJD?tJLO&)M1zNsOzH}dyC+2xh!yb-5=OSv<*8&G=f{rE~CkxmJqf5G#)exJ^K z(NKLnoS|EB=r4c{mMX6~m0!Ic;j0J9DN3E&{Ed_7@i zeITBvKxiYn20#N02!Q$#OM79jTs%`C0|F3%bPga82Z5pJwF+I|0W6R^b0FF=#{m%R ro_@{&0AZMz^??vbSPy_dZr_4`8r*19DrlO700000NkvXXu0mjf{QprX delta 425 zcmV;a0apHv2EYR)iBL{Q4GJ0x0000DNk~Le0004G0000G1Oos70MqdpZ~y=R9+4#? ze|zMw%m4rY0d!JMQvg8b*k%9#0Z>UqK~#7Fy_11P20;+VuOmKy?mGlHqkV%OAnQ;- zVsrqa7l`i#me>FS^|tNo-e23eO>w&eKezWE^D}n4P#440MUt>k$f-Qow$84JA}v2? zH89WVyzXmI$93dro}ZwkiJZuWtqYNr#lP<;}rpq^x?`JFjp^k0WRL02vDx(jinRszfZ=$T# z|EMf+W*0wWu&1ED^P0UU%6RZjCV_IEls&QV@S2n_lPZY3#6?*?CFnf~cii(AWun*0 T`XxE#00000NkvXXu0mjf$#uh* 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(); + } + } + } + +}