From a871261185b20558170a818d209b5945c34065b8 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Tue, 17 Sep 2019 18:48:28 -0400 Subject: [PATCH] v0.7.5: added necromancers --- android/src/main/assets/necromancer.png | Bin 0 -> 3298 bytes .../shatteredpixeldungeon/Assets.java | 1 + .../shatteredpixeldungeon/Dungeon.java | 2 +- .../actors/buffs/Adrenaline.java | 2 + .../actors/mobs/Bestiary.java | 17 +- .../actors/mobs/Necromancer.java | 303 ++++++++++++++++++ .../shatteredpixeldungeon/effects/Speck.java | 2 +- .../sprites/NecromancerSprite.java | 70 ++++ .../messages/actors/actors.properties | 3 + 9 files changed, 389 insertions(+), 11 deletions(-) create mode 100644 android/src/main/assets/necromancer.png create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Necromancer.java create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/NecromancerSprite.java diff --git a/android/src/main/assets/necromancer.png b/android/src/main/assets/necromancer.png new file mode 100644 index 0000000000000000000000000000000000000000..86ab5026edf6d22665110fd41ce7199c7ec0f530 GIT binary patch literal 3298 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.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