diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DwarfKing.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DwarfKing.java index b30fb06ce..e08eeb30a 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DwarfKing.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DwarfKing.java @@ -22,25 +22,325 @@ 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.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LifeLink; +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.effects.particles.ElmoParticle; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.ShadowParticle; +import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.DriedRose; +import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; +import com.shatteredpixel.shatteredpixeldungeon.levels.NewCityBossLevel; +import com.shatteredpixel.shatteredpixeldungeon.mechanics.Ballistica; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.sprites.KingSprite; +import com.shatteredpixel.shatteredpixeldungeon.ui.BossHealthBar; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +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 com.watabou.utils.Reflection; + +import java.util.ArrayList; +import java.util.HashSet; -//TODO currently just regular DK but with no summoning ability public class DwarfKing extends Mob { + //TODO decide on final stats, implement later phases { spriteClass = KingSprite.class; - HP = HT = 1; + HP = HT = 300; + EXP = 40; + defenseSkill = 25; + + properties.add(Property.BOSS); + properties.add(Property.UNDEAD); } - protected boolean canTryToSummon() { + @Override + public int damageRoll() { + return Random.NormalIntRange( 25, 40 ); + } + + @Override + public int attackSkill( Char target ) { + return 32; + } + + @Override + public int drRoll() { + return Random.NormalIntRange(0, 14); + } + + @Override + protected boolean act() { + if (buffs(Summoning.class).size() < 1) { + + if (enemy != null + && Dungeon.level.adjacent(pos, enemy.pos) && teleportSubject()){ + spend(TICK); + return true; + } else if (lifeLinkSubject()) { + spend(TICK); + return true; + } else { + summonSubject(); + } + + } + return super.act(); + } + + private boolean summonSubject(){ + Summoning s = new Summoning(); + s.pos = ((NewCityBossLevel)Dungeon.level).getSummoningPos(); + if (s.pos == -1) return false; + switch (Random.Int(6)){ + default: + s.summon = Ghoul.class; + break; + case 0: + s.summon = Monk.class; + break; + case 1: + s.summon = Warlock.class; + break; + } + s.delay = 5; + s.attachTo(this); + return true; + } + + private HashSet getSubjects(){ + HashSet subjects = new HashSet<>(); + for (Mob m : Dungeon.level.mobs){ + if (m.alignment == alignment && (m instanceof Ghoul || m instanceof Monk || m instanceof Warlock)){ + subjects.add(m); + } + } + return subjects; + } + + private boolean lifeLinkSubject(){ + Mob furthest = null; + + for (Mob m : getSubjects()){ + boolean alreadyLinked = false; + for (LifeLink l : m.buffs(LifeLink.class)){ + if (l.object == id()) alreadyLinked = true; + } + if (!alreadyLinked) { + if (furthest == null || Dungeon.level.distance(pos, furthest.pos) < Dungeon.level.distance(pos, m.pos)){ + furthest = m; + } + } + } + + if (furthest != null) { + Buff.append(furthest, LifeLink.class, 100f).object = id(); + Buff.append(this, LifeLink.class, 100f).object = furthest.id(); + yell(Messages.get(this, "lifelink_" + Random.IntRange(1, 2))); + sprite.parent.add(new Beam.HealthRay(sprite.destinationCenter(), furthest.sprite.destinationCenter())); + return true; + + } return false; } + private boolean teleportSubject(){ + Mob furthest = null; + + for (Mob m : getSubjects()){ + //TODO avoid warlocks? + if (furthest == null || Dungeon.level.distance(pos, furthest.pos) < Dungeon.level.distance(pos, m.pos)){ + furthest = m; + } + } + + if (furthest != null){ + + float bestDist; + int bestPos = pos; + + Ballistica trajectory = new Ballistica(enemy.pos, pos, Ballistica.STOP_TARGET); + int targetCell = trajectory.path.get(trajectory.dist+1); + //if the position opposite the direction of the hero is open, go there + if (Actor.findChar(targetCell) == null && !Dungeon.level.solid[targetCell]){ + bestPos = targetCell; + + //Otherwise go to the neighbour cell that's open and is furthest + } else { + bestDist = Dungeon.level.trueDistance(pos, enemy.pos); + + for (int i : PathFinder.NEIGHBOURS8){ + if (Actor.findChar(pos+i) == null + && !Dungeon.level.solid[pos+1] + && Dungeon.level.trueDistance(pos+i, enemy.pos) > bestDist){ + bestPos = pos+i; + bestDist = Dungeon.level.trueDistance(pos+i, enemy.pos); + } + } + } + + Actor.add(new Pushing(this, pos, bestPos)); + pos = bestPos; + + //find closest cell that's adjacent to enemy, place subject there + bestDist = Dungeon.level.trueDistance(enemy.pos, pos); + bestPos = enemy.pos; + for (int i : PathFinder.NEIGHBOURS8){ + if (Actor.findChar(enemy.pos+i) == null + && !Dungeon.level.solid[enemy.pos+1] + && Dungeon.level.trueDistance(enemy.pos+i, pos) < bestDist){ + bestPos = enemy.pos+i; + bestDist = Dungeon.level.trueDistance(enemy.pos+i, pos); + } + } + + if (bestPos != enemy.pos) ScrollOfTeleportation.appear(furthest, bestPos); + yell(Messages.get(this, "teleport_" + Random.IntRange(1, 2))); + return true; + } + return false; + } + + @Override + public void notice() { + super.notice(); + if (!BossHealthBar.isAssigned()) { + BossHealthBar.assignBoss(this); + yell(Messages.get(this, "notice")); + for (Char ch : Actor.chars()){ + if (ch instanceof DriedRose.GhostHero){ + GLog.n("\n"); + ((DriedRose.GhostHero) ch).sayBoss(); + } + } + } + } + @Override public void die(Object cause) { super.die(cause); Dungeon.level.unseal(); + + for (Mob m : Dungeon.level.mobs.toArray(new Mob[0])){ + if (m instanceof Ghoul || m instanceof Monk || m instanceof Warlock){ + m.die(null); + } + } } + + public static class Summoning extends Buff { + + private int delay; + private int pos; + private Class summon; + + private Emitter particles; + + public int getPos() { + return pos; + } + + @Override + public boolean act() { + delay--; + + if (delay <= 0){ + + if (summon == Warlock.class){ + particles.burst(ShadowParticle.CURSE, 10); + Sample.INSTANCE.play(Assets.SND_CURSED); + } else if (summon == Monk.class){ + particles.burst(ElmoParticle.FACTORY, 10); + Sample.INSTANCE.play(Assets.SND_BURNING); + } else { + particles.burst(Speck.factory(Speck.BONE), 10); + Sample.INSTANCE.play(Assets.SND_BONES); + } + particles = null; + + if (Actor.findChar(pos) != null){ + ArrayList candidates = new ArrayList<>(); + for (int i : PathFinder.NEIGHBOURS8){ + if (Dungeon.level.passable[pos+i] && Actor.findChar(pos+i) == null){ + candidates.add(pos+i); + } + } + if (!candidates.isEmpty()){ + pos = Random.element(candidates); + } + } + + if (Actor.findChar(pos) == null) { + Mob m = Reflection.newInstance(summon); + if (m instanceof Ghoul) { + ((Ghoul) m).setSolo(); + } + m.pos = pos; + m.maxLvl = -2; + GameScene.add(m); + m.state = m.HUNTING; + } else { + Char ch = Actor.findChar(pos); + ch.damage(Random.NormalIntRange(20, 40), summon); + } + + detach(); + } + + spend(TICK); + return true; + } + + @Override + public void fx(boolean on) { + if (on && particles == null) { + particles = CellEmitter.get(pos); + + if (summon == Warlock.class){ + particles.pour(ShadowParticle.UP, 0.1f); + } else if (summon == Monk.class){ + particles.pour(ElmoParticle.FACTORY, 0.1f); + } else { + particles.pour(Speck.factory(Speck.RATTLE), 0.1f); + } + + } else if (!on && particles != null) { + particles.on = false; + } + } + + private static final String DELAY = "delay"; + private static final String POS = "pos"; + private static final String SUMMON = "summon"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(DELAY, delay); + bundle.put(POS, pos); + bundle.put(SUMMON, summon); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + delay = bundle.getInt(DELAY); + pos = bundle.getInt(POS); + summon = bundle.getClass(SUMMON); + } + } + } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java index 51b2c1191..9559da13f 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Ghoul.java @@ -95,6 +95,10 @@ public class Ghoul extends Mob { partnerID = bundle.getInt( PARTNER_ID ); timesDowned = bundle.getInt( TIMES_DOWNED ); } + + public void setSolo(){ + partnerID = -2; + } @Override protected boolean act() { diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCityBossLevel.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCityBossLevel.java index 6277bd0b0..190b5fad5 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCityBossLevel.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCityBossLevel.java @@ -46,6 +46,9 @@ import com.watabou.utils.Point; import com.watabou.utils.Random; import com.watabou.utils.Rect; +import java.util.ArrayList; +import java.util.HashSet; + public class NewCityBossLevel extends Level { { @@ -53,6 +56,9 @@ public class NewCityBossLevel extends Level { color2 = 0xf2f2f2; } + private static int WIDTH = 15; + private static int HEIGHT = 48; + private static final Rect entry = new Rect(1, 37, 14, 48); private static final Rect arena = new Rect(1, 25, 14, 38); private static final Rect end = new Rect(0, 0, 15, 22); @@ -60,6 +66,16 @@ public class NewCityBossLevel extends Level { private static final int bottomDoor = 7 + (arena.bottom-1)*15; private static final int topDoor = 7 + arena.top*15; + private static final int[] pedestals = new int[4]; + + static{ + Point c = arena.center(); + pedestals[0] = c.x-3 + (c.y-3) * WIDTH; + pedestals[1] = c.x+3 + (c.y-3) * WIDTH; + pedestals[2] = c.x+3 + (c.y+3) * WIDTH; + pedestals[3] = c.x-3 + (c.y+3) * WIDTH; + } + private ImpShopRoom impShop; @Override @@ -90,7 +106,7 @@ public class NewCityBossLevel extends Level { @Override protected boolean build() { - setSize(15, 48); + setSize(WIDTH, HEIGHT); //entrance room Painter.fill(this, entry, Terrain.WALL); @@ -124,10 +140,10 @@ public class NewCityBossLevel extends Level { Painter.set(this, c.x+3, c.y, Terrain.STATUE); Painter.set(this, c.x+4, c.y, Terrain.STATUE); - Painter.set(this, c.x-3, c.y-3, Terrain.PEDESTAL); - Painter.set(this, c.x+3, c.y-3, Terrain.PEDESTAL); - Painter.set(this, c.x+3, c.y+3, Terrain.PEDESTAL); - Painter.set(this, c.x-3, c.y+3, Terrain.PEDESTAL); + Painter.set(this, pedestals[0], Terrain.PEDESTAL); + Painter.set(this, pedestals[1], Terrain.PEDESTAL); + Painter.set(this, pedestals[2], Terrain.PEDESTAL); + Painter.set(this, pedestals[3], Terrain.PEDESTAL); Painter.set(this, c.x, arena.top, Terrain.LOCKED_DOOR); @@ -174,6 +190,37 @@ public class NewCityBossLevel extends Level { return true; } + //returns a random pedestal that doesn't already have a summon inbound on it + public int getSummoningPos(){ + Mob king = getKing(); + HashSet summons = king.buffs(DwarfKing.Summoning.class); + ArrayList positions = new ArrayList<>(); + for (int pedestal : pedestals) { + boolean clear = true; + for (DwarfKing.Summoning s : summons) { + if (s.getPos() == pedestal) { + clear = false; + break; + } + } + if (clear) { + positions.add(pedestal); + } + } + if (positions.isEmpty()){ + return -1; + } else { + return Random.element(positions); + } + } + + private Mob getKing(){ + for (Mob m : mobs){ + if (m instanceof DwarfKing) return m; + } + return null; + } + @Override protected void createMobs() { } 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 b67b3ed0f..a3a18f3d7 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,19 @@ actors.mobs.newdm300.desc=The DM-300 is the largest and most powerful 'defense m actors.mobs.newdm300.desc_supercharged=DM-300 is currently charged full of electrical energy, In this form DM-300 cannot be damaged and moves at double speed! Additionally, its drill now spins fast enough for it to _tunnel through solid rock,_ though it moves much more slowly when doing this.\n\nAttacking DM-300 directly is pointless while it is supercharged, but _something in the area must be providing it with this energy,_ destroying that may weaken it. actors.mobs.newdm300$fallingrocks.desc=Loose rocks are tumbling down from the ceiling here, it looks like its about to collapse! +actors.mobs.dwarfking.name=Dwarf King +actors.mobs.dwarfking.notice=How dare you! You have no idea what you're interfering with! +actors.mobs.dwarfking.lifelink_1=I need of your essence, slave! +actors.mobs.dwarfking.lifelink_2=Bleed for me, slave! +actors.mobs.dwarfking.teleport_1=Deal with them, slave! +actors.mobs.dwarfking.teleport_2=Keep them busy, slave! +actors.mobs.dwarfking.wave_1=Enough! Kill them my slaves! +actors.mobs.dwarfking.wave_2=More! Bleed for your king! +actors.mobs.dwarfking.wave_3=Useless! KILL THEM NOW! +actors.mobs.dwarfking.enraged=You cannot kill me %s, I AM IMMORTAL! +actors.mobs.dwarfking.dieing=No! You can't do this... you have no idea what lies below... +actors.mobs.dwarfking.dead=You've... Doomed us all... + actors.mobs.elemental$fire.name=fire elemental actors.mobs.elemental$fire.desc=Elementals are chaotic creatures that are often created when powerful occult magic isn't properly controlled. Elementals have minimal intelligence, and are usually associated with a particular type of magic.\n\nFire elementals are a common type of elemental which deals damage with fiery magic. They will set their target ablaze with melee attacks, and can occasionally shoot bolts of fire as well. actors.mobs.elemental$newbornfire.name=newborn fire elemental