v0.9.4: added a new rare enemy: spectral necromancers

This commit is contained in:
Evan Debenham
2021-07-10 20:02:24 -04:00
parent 65b3f2cdb0
commit 92f190d04c
7 changed files with 315 additions and 51 deletions

View File

@@ -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 necromancers 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 781 B

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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<Integer> 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());
}
}

View File

@@ -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 );

View File

@@ -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();
}
}
}
}