From 32a755cfeedd71b2dcb24626991c1e1b15569fe7 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 17 Jan 2020 22:58:01 -0500 Subject: [PATCH] v0.8.0: redesigned the DM-300 boss fight --- .../main/assets/custom_tiles/caves_boss.png | Bin 0 -> 4715 bytes core/src/main/assets/pylon.png | Bin 0 -> 254 bytes .../shatteredpixeldungeon/Assets.java | 2 + .../shatteredpixeldungeon/Dungeon.java | 3 +- .../actors/mobs/NewDM300.java | 586 +++++++++++++ .../actors/mobs/Pylon.java | 189 +++++ .../effects/particles/EarthParticle.java | 5 +- .../effects/particles/SparkParticle.java | 7 +- .../items/wands/WandOfBlastWave.java | 6 +- .../shatteredpixeldungeon/levels/Level.java | 6 +- .../levels/NewCavesBossLevel.java | 786 ++++++++++++++++++ .../shatteredpixeldungeon/levels/Terrain.java | 2 +- .../sprites/DM300Sprite.java | 112 ++- .../sprites/PylonSprite.java | 93 +++ .../messages/actors/actors.properties | 21 + .../messages/levels/levels.properties | 12 +- 16 files changed, 1815 insertions(+), 15 deletions(-) create mode 100644 core/src/main/assets/custom_tiles/caves_boss.png create mode 100644 core/src/main/assets/pylon.png create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Pylon.java create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCavesBossLevel.java create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PylonSprite.java diff --git a/core/src/main/assets/custom_tiles/caves_boss.png b/core/src/main/assets/custom_tiles/caves_boss.png new file mode 100644 index 0000000000000000000000000000000000000000..69cf318af99301cfa93b484922e8c87deaf6bed3 GIT binary patch literal 4715 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 b?Me5z;o{|pjzip^NPxvfE0gjI?hpP4nqhFr literal 0 HcmV?d00001 diff --git a/core/src/main/assets/pylon.png b/core/src/main/assets/pylon.png new file mode 100644 index 0000000000000000000000000000000000000000..abc42afaae6df05f9fe0aee678e02dc17f624067 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvl>na**Z=?j@7S^V>eVyx@oCo9 zj{jd*-<`oQYu3!#+J^l65`X_NRaMKRLd{DK)Ap4~_TaymR+978G?@19iT zJLDkH`f&f0U)$6dyyuE-&c4JoNh-ut$lt?)SzIAxae&OkvqmRP4lb&AH2aCX5$n{M zYEx^Lty(o@Lx6Xdkl3srYlOwZCYf>ORK8d+gYS=uNPMFF$Akl#Zywml^6rRT=l#IW ze)@#`=kCuPZnq^CEa*=!Nx0ko@u`6Km7lBkGFnR*L~1Nuk_mJZgQu&X%Q~loCIB3r BWOo1n literal 0 HcmV?d00001 diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java index 5e4a8e910..6eed43e6f 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Assets.java @@ -106,6 +106,7 @@ public class Assets { public static final String RIPPER = "ripper.png"; public static final String SPAWNER = "spawner.png"; public static final String DM100 = "dm100.png"; + public static final String PYLON = "pylon.png"; public static final String ITEMS = "items.png"; public static final String TERRAIN_FEATURES = "terrain_features.png"; @@ -133,6 +134,7 @@ public class Assets { public static final String PRISON_QUEST = "custom_tiles/prison_quests.png"; public static final String PRISON_EXIT_OLD = "custom_tiles/prison_exit_old.png"; public static final String PRISON_EXIT_NEW = "custom_tiles/prison_exit_new.png"; + public static final String CAVES_BOSS = "custom_tiles/caves_boss.png"; public static final String BUFFS_SMALL = "buffs.png"; public static final String BUFFS_LARGE = "large_buffs.png"; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java index a35e0290d..869d9417b 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java @@ -53,6 +53,7 @@ import com.shatteredpixel.shatteredpixeldungeon.levels.HallsLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.LastLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.LastShopLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.Level; +import com.shatteredpixel.shatteredpixeldungeon.levels.NewCavesBossLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.NewPrisonBossLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.PrisonLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.SewerBossLevel; @@ -263,7 +264,7 @@ public class Dungeon { level = new CavesLevel(); break; case 15: - level = new CavesBossLevel(); + level = new NewCavesBossLevel(); break; case 16: case 17: diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java new file mode 100644 index 000000000..a7181abeb --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/NewDM300.java @@ -0,0 +1,586 @@ +/* + * 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.Badges; +import com.shatteredpixel.shatteredpixeldungeon.Dungeon; +import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; +import com.shatteredpixel.shatteredpixeldungeon.actors.Char; +import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob; +import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Barrier; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis; +import com.shatteredpixel.shatteredpixeldungeon.effects.BlobEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.EarthParticle; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SparkParticle; +import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.DriedRose; +import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.LloydsBeacon; +import com.shatteredpixel.shatteredpixeldungeon.items.quest.MetalShard; +import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfBlastWave; +import com.shatteredpixel.shatteredpixeldungeon.levels.Level; +import com.shatteredpixel.shatteredpixeldungeon.levels.NewCavesBossLevel; +import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; +import com.shatteredpixel.shatteredpixeldungeon.mechanics.Ballistica; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.DM300Sprite; +import com.shatteredpixel.shatteredpixeldungeon.ui.BossHealthBar; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.Camera; +import com.watabou.noosa.audio.Sample; +import com.watabou.utils.Bundle; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; +import com.watabou.utils.RectF; + +public class NewDM300 extends Mob { + + { + //TODO improved sprite + spriteClass = DM300Sprite.class; + + HP = HT = 300; + EXP = 30; + defenseSkill = 15; + + properties.add(Property.BOSS); + properties.add(Property.INORGANIC); + properties.add(Property.LARGE); + } + + @Override + public int damageRoll() { + return Random.NormalIntRange( 15, 25 ); + } + + @Override + public int attackSkill( Char target ) { + return 20; + } + + @Override + public int drRoll() { + return Random.NormalIntRange(0, 10); + } + + public int pylonsActivated = 0; + public boolean supercharged = false; + public boolean chargeAnnounced = false; + + private int turnsSinceLastAbility = -1; + private int abilityCooldown = Random.NormalIntRange(MIN_COOLDOWN, MAX_COOLDOWN); + + private static final int MIN_COOLDOWN = 5; + private static final int MAX_COOLDOWN = 9; + + private int lastAbility = 0; + private static final int NONE = 0; + private static final int GAS = 1; + private static final int ROCKS = 2; + + private static final String PYLONS_ACTIVATED = "pylons_activated"; + private static final String SUPERCHARGED = "supercharged"; + private static final String CHARGE_ANNOUNCED = "charge_announced"; + + private static final String TURNS_SINCE_LAST_ABILITY = "turns_since_last_ability"; + private static final String ABILITY_COOLDOWN = "ability_cooldown"; + + private static final String LAST_ABILITY = "last_ability"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(PYLONS_ACTIVATED, pylonsActivated); + bundle.put(SUPERCHARGED, supercharged); + bundle.put(CHARGE_ANNOUNCED, chargeAnnounced); + bundle.put(TURNS_SINCE_LAST_ABILITY, turnsSinceLastAbility); + bundle.put(ABILITY_COOLDOWN, abilityCooldown); + bundle.put(LAST_ABILITY, lastAbility); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + pylonsActivated = bundle.getInt(PYLONS_ACTIVATED); + supercharged = bundle.getBoolean(SUPERCHARGED); + chargeAnnounced = bundle.getBoolean(CHARGE_ANNOUNCED); + turnsSinceLastAbility = bundle.getInt(TURNS_SINCE_LAST_ABILITY); + abilityCooldown = bundle.getInt(ABILITY_COOLDOWN); + lastAbility = bundle.getInt(LAST_ABILITY); + + if (turnsSinceLastAbility != -1){ + BossHealthBar.assignBoss(this); + if (!supercharged && pylonsActivated == 2) BossHealthBar.bleed(true); + } + } + + @Override + protected boolean act() { + GameScene.add(Blob.seed(pos, 0, FallingRocks.class)); + GameScene.add(Blob.seed(pos, 0, ToxicGas.class)); + + //ability logic only triggers if DM is not supercharged + if (!supercharged){ + if (turnsSinceLastAbility >= 0) turnsSinceLastAbility++; + + //in case DM-300 hasn't been able to act yet + if (fieldOfView == null || fieldOfView.length != Dungeon.level.length()){ + fieldOfView = new boolean[Dungeon.level.length()]; + Dungeon.level.updateFieldOfView( this, fieldOfView ); + } + + //determine if DM can reach its enemy + boolean canReach; + if (enemy == null){ + if (Dungeon.level.adjacent(pos, Dungeon.hero.pos)){ + canReach = true; + } else { + canReach = (Dungeon.findStep(this, pos, Dungeon.hero.pos, Dungeon.level.openSpace, fieldOfView) != -1); + } + } else { + if (Dungeon.level.adjacent(pos, enemy.pos)){ + canReach = true; + } else { + canReach = (Dungeon.findStep(this, pos, enemy.pos, Dungeon.level.openSpace, fieldOfView) != -1); + } + } + + if (state != HUNTING){ + if (Dungeon.hero.invisible <= 0 && canReach){ + beckon(Dungeon.hero.pos); + } + } else { + + if (!canReach){ + + if (enemy == null) enemy = Dungeon.hero; + + if (fieldOfView[enemy.pos] && turnsSinceLastAbility >= MIN_COOLDOWN){ + + lastAbility = GAS; + turnsSinceLastAbility = 0; + spend(TICK); + + GLog.w(Messages.get(this, "vent")); + if (sprite != null && (sprite.visible || enemy.sprite.visible)) { + sprite.zap(enemy.pos); + return false; + } else { + ventGas(enemy); + Sample.INSTANCE.play(Assets.SND_PUFF); + return true; + } + + } + + } else { + if (turnsSinceLastAbility > abilityCooldown) { + + if (lastAbility == NONE) { + //50/50 either ability + lastAbility = Random.Int(2) == 0 ? GAS : ROCKS; + } else if (lastAbility == GAS) { + //more likely to use rocks + lastAbility = Random.Int(4) == 0 ? GAS : ROCKS; + } else { + //more likely to use gas + lastAbility = Random.Int(4) != 0 ? GAS : ROCKS; + } + + //doesn't spend a turn if enemy is at a distance + if (Dungeon.level.adjacent(pos, enemy.pos)){ + spend(TICK); + } + + turnsSinceLastAbility = 0; + abilityCooldown = Random.NormalIntRange(MIN_COOLDOWN, MAX_COOLDOWN); + + if (lastAbility == GAS) { + GLog.w(Messages.get(this, "vent")); + if (sprite != null && (sprite.visible || enemy.sprite.visible)) { + sprite.zap(enemy.pos); + return false; + } else { + ventGas(enemy); + Sample.INSTANCE.play(Assets.SND_PUFF); + return true; + } + } else { + GLog.w(Messages.get(this, "rocks")); + if (sprite != null && (sprite.visible || enemy.sprite.visible)) { + ((DM300Sprite)sprite).slam(enemy.pos); + return false; + } else { + dropRocks(enemy); + Sample.INSTANCE.play(Assets.SND_PUFF); + return true; + } + } + } + } + } + } else { + + if (!chargeAnnounced){ + yell(Messages.get(this, "supercharged")); + chargeAnnounced = true; + } + + if (state == WANDERING && Dungeon.hero.invisible <= 0){ + beckon(Dungeon.hero.pos); + state = HUNTING; + enemy = Dungeon.hero; + } + + } + + return super.act(); + } + + @Override + public void move(int step) { + super.move(step); + + Camera.main.shake( supercharged ? 3 : 1, 0.25f ); + + if (Dungeon.level.map[step] == Terrain.INACTIVE_TRAP && state == HUNTING) { + + //don't gain energy from cells that are energized + if (NewCavesBossLevel.PylonEnergy.volumeAt(pos, NewCavesBossLevel.PylonEnergy.class) > 0){ + return; + } + + if (Dungeon.level.heroFOV[step]) { + if (buff(Barrier.class) == null) { + GLog.w(Messages.get(this, "shield")); + } + Sample.INSTANCE.play(Assets.SND_LIGHTNING); + sprite.emitter().start(SparkParticle.STATIC, 0.05f, 20); + } + + Buff.affect(this, Barrier.class).setShield( 30 + (HT - HP)/10); + + } + } + + @Override + public float speed() { + return super.speed() * (supercharged ? 2 : 1); + } + + @Override + public void notice() { + super.notice(); + if (!BossHealthBar.isAssigned()) { + BossHealthBar.assignBoss(this); + turnsSinceLastAbility = 0; + yell(Messages.get(this, "notice")); + for (Char ch : Actor.chars()){ + if (ch instanceof DriedRose.GhostHero){ + GLog.n("\n"); + ((DriedRose.GhostHero) ch).sayBoss(); + } + } + } + } + + public void onZapComplete(){ + ventGas(enemy); + next(); + } + + public void ventGas( Char target ){ + + int gasVented = 0; + + Ballistica trajectory = new Ballistica(pos, target.pos, Ballistica.STOP_TARGET); + + for (int i : trajectory.subPath(0, trajectory.dist)){ + GameScene.add(Blob.seed(i, 20, ToxicGas.class)); + gasVented += 20; + } + + GameScene.add(Blob.seed(trajectory.collisionPos, 100, ToxicGas.class)); + + if (gasVented < 250){ + int toVentAround = (int)Math.ceil((250 - gasVented)/8f); + for (int i : PathFinder.NEIGHBOURS8){ + GameScene.add(Blob.seed(pos+i, toVentAround, ToxicGas.class)); + } + + } + + } + + public void onSlamComplete(){ + dropRocks(enemy); + next(); + } + + public void dropRocks( Char target ) { + + int rockCenter = target.pos; + + if (Dungeon.level.adjacent(pos, target.pos)){ + int oppositeAdjacent = target.pos + (target.pos - pos); + Ballistica trajectory = new Ballistica(target.pos, oppositeAdjacent, Ballistica.MAGIC_BOLT); + WandOfBlastWave.throwChar(target, trajectory, 2, false); + if (target == Dungeon.hero){ + Dungeon.hero.interrupt(); + } + rockCenter = trajectory.path.get(Math.min(trajectory.dist, 2)); + } + + //pick an adjacent cell to the hero as a safe cell. This cell is less likely to be in a wall or containing hazards + int safeCell; + do { + safeCell = rockCenter + PathFinder.NEIGHBOURS8[Random.Int(8)]; + } while (safeCell == pos + || (Dungeon.level.solid[safeCell] && Random.Int(2) == 0) + || (Blob.volumeAt(safeCell, NewCavesBossLevel.PylonEnergy.class) > 0 && Random.Int(2) == 0)); + + int start = rockCenter - Dungeon.level.width() * 3 - 3; + int pos; + for (int y = 0; y < 7; y++) { + pos = start + Dungeon.level.width() * y; + for (int x = 0; x < 7; x++) { + if (!Dungeon.level.solid[pos] && pos != safeCell && Random.Int(Dungeon.level.distance(rockCenter, pos)) == 0) { + GameScene.add(Blob.seed(pos, 1, FallingRocks.class)); + } + //add rock cell to pos, if it is not solid, and isn't the safecell + pos++; + } + } + + } + + @Override + public void damage(int dmg, Object src) { + if (supercharged){ + dmg = 0; + } + + super.damage(dmg, src); + + LockedFloor lock = Dungeon.hero.buff(LockedFloor.class); + if (lock != null && !isImmune(src.getClass())) lock.addTime(dmg); + + int threshold = HT/3 * (2- pylonsActivated); + + if (HP < threshold){ + HP = threshold; + supercharge(); + } + + } + + public void supercharge(){ + supercharged = true; + ((NewCavesBossLevel)Dungeon.level).activatePylon(); + pylonsActivated++; + + spend(3f); + yell(Messages.get(this, "charging")); + sprite.showStatus(CharSprite.POSITIVE, Messages.get(this, "immune")); + chargeAnnounced = false; + } + + public boolean isSupercharged(){ + return supercharged; + } + + public void loseSupercharge(){ + supercharged = false; + sprite.resetColor(); + + if (pylonsActivated < 2){ + yell(Messages.get(this, "charge_lost")); + } else { + yell(Messages.get(this, "pylons_destroyed")); + BossHealthBar.bleed(true); + } + } + + @Override + public boolean isAlive() { + return HP > 0 || pylonsActivated < 2; + } + + @Override + public void die( Object cause ) { + + super.die( cause ); + + GameScene.bossSlain(); + Dungeon.level.unseal(); + + //60% chance of 2 shards, 30% chance of 3, 10% chance for 4. Average of 2.5 + int shards = Random.chances(new float[]{0, 0, 6, 3, 1}); + for (int i = 0; i < shards; i++){ + int ofs; + do { + ofs = PathFinder.NEIGHBOURS8[Random.Int(8)]; + } while (!Dungeon.level.passable[pos + ofs]); + Dungeon.level.drop( new MetalShard(), pos + ofs ).sprite.drop( pos ); + } + + Badges.validateBossSlain(); + + LloydsBeacon beacon = Dungeon.hero.belongings.getItem(LloydsBeacon.class); + if (beacon != null) { + beacon.upgrade(); + } + + yell( Messages.get(this, "defeated") ); + } + + @Override + protected boolean getCloser(int target) { + if (super.getCloser(target)){ + return true; + } else { + + int bestpos = pos; + for (int i : PathFinder.NEIGHBOURS8){ + if (Dungeon.level.openSpace[pos+i] && Actor.findChar(pos+i) == null && + Dungeon.level.distance(bestpos, target) > Dungeon.level.distance(pos+i, target)){ + bestpos = pos+i; + } + } + if (bestpos != pos){ + move( bestpos ); + return true; + } + + if (!supercharged || state != HUNTING){ + return false; + } + + for (int i : PathFinder.NEIGHBOURS8){ + if (Actor.findChar(pos+i) == null && + Dungeon.level.trueDistance(bestpos, target) > Dungeon.level.trueDistance(pos+i, target)){ + bestpos = pos+i; + } + } + if (bestpos != pos){ + Sample.INSTANCE.play( Assets.SND_ROCKS ); + + for (int i : PathFinder.NEIGHBOURS9){ + if (Dungeon.level.map[pos+i] == Terrain.WALL || Dungeon.level.map[pos+i] == Terrain.WALL_DECO){ + Level.set(pos+i, Terrain.EMPTY_DECO); + GameScene.updateMap(pos+i); + } + } + Dungeon.level.cleanWalls(); + Dungeon.observe(); + spend(2f); + + for (int i : PathFinder.NEIGHBOURS8){ + if (Actor.findChar(pos+i) == null && + Dungeon.level.trueDistance(bestpos, target) > Dungeon.level.trueDistance(pos+i, target)){ + bestpos = pos+i; + } + } + + if (bestpos != pos) { + move(bestpos); + } + Camera.main.shake( 5, 1f ); + + return true; + } + + return false; + } + } + + @Override + public String description() { + String desc = super.description(); + if (supercharged) { + desc += "\n\n" + Messages.get(this, "desc_supercharged"); + } + return desc; + } + + public static class FallingRocks extends Blob { + + { + alwaysVisible = true; + } + + @Override + protected void evolve() { + + boolean rocksFell = false; + + int cell; + for (int i = area.left; i < area.right; i++){ + for (int j = area.top; j < area.bottom; j++){ + cell = i + j* Dungeon.level.width(); + off[cell] = cur[cell] > 0 ? cur[cell] - 1 : 0; + + if (off[cell] > 0) { + volume += off[cell]; + } + + if (cur[cell] > 0 && off[cell] == 0){ + + CellEmitter.get( cell ).start( Speck.factory( Speck.ROCK ), 0.07f, 10 ); + + Char ch = Actor.findChar(cell); + if (ch != null && !(ch instanceof NewDM300)){ + Buff.prolong( ch, Paralysis.class, 3 ); + } + + rocksFell = true; + } + } + } + + if (rocksFell){ + Camera.main.shake( 3, 0.7f ); + Sample.INSTANCE.play(Assets.SND_ROCKS); + } + + } + + @Override + public void use(BlobEmitter emitter) { + super.use(emitter); + + emitter.bound = new RectF(0, -0.2f, 1, 0.4f); + emitter.pour(EarthParticle.FALLING, 0.1f); + } + + @Override + public String tileDesc() { + return Messages.get(this, "desc"); + } + } +} diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Pylon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Pylon.java new file mode 100644 index 000000000..c482b90ea --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Pylon.java @@ -0,0 +1,189 @@ +/* + * 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.blobs.Electricity; +import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Sleep; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Terror; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Vertigo; +import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.Lightning; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SparkParticle; +import com.shatteredpixel.shatteredpixeldungeon.levels.NewCavesBossLevel; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.PylonSprite; +import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.audio.Sample; +import com.watabou.utils.Bundle; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; + +public class Pylon extends Mob { + + { + spriteClass = PylonSprite.class; + + HP = HT = 50; + + maxLvl = -2; + + properties.add(Property.BOSS); + properties.add(Property.INORGANIC); + properties.add(Property.ELECTRIC); + properties.add(Property.IMMOVABLE); + + state = PASSIVE; + alignment = Alignment.NEUTRAL; + } + + private int targetNeighbor = Random.Int(8); + + @Override + protected boolean act() { + spend(TICK); + + if (alignment == Alignment.NEUTRAL){ + return true; + } + + int cell1 = pos + PathFinder.CIRCLE8[targetNeighbor]; + int cell2 = pos + PathFinder.CIRCLE8[(targetNeighbor+4)%8]; + + sprite.flash(); + if (Dungeon.level.heroFOV[pos] || Dungeon.level.heroFOV[cell1] || Dungeon.level.heroFOV[cell2]) { + sprite.parent.add(new Lightning(DungeonTilemap.raisedTileCenterToWorld(cell1), + DungeonTilemap.raisedTileCenterToWorld(cell2), null)); + CellEmitter.get(cell1).burst(SparkParticle.FACTORY, 3); + CellEmitter.get(cell2).burst(SparkParticle.FACTORY, 3); + Sample.INSTANCE.play( Assets.SND_LIGHTNING ); + } + + shockChar(Actor.findChar(cell1)); + shockChar(Actor.findChar(cell2)); + + targetNeighbor = (targetNeighbor+1)%8; + + return true; + } + + private void shockChar( Char ch ){ + if (ch != null && !(ch instanceof NewDM300)){ + ch.sprite.flash(); + ch.damage(Random.NormalIntRange(15, 25), Electricity.class); + + if (ch == Dungeon.hero && !ch.isAlive()){ + Dungeon.fail(NewDM300.class); + GLog.n( Messages.get(Electricity.class, "ondeath") ); + } + } + } + + public void activate(){ + alignment = Alignment.ENEMY; + ((PylonSprite) sprite).activate(); + } + + @Override + public CharSprite sprite() { + PylonSprite p = (PylonSprite) super.sprite(); + if (alignment != Alignment.NEUTRAL) p.activate(); + return p; + } + + @Override + public String description() { + if (alignment == Alignment.NEUTRAL){ + return Messages.get(this, "desc_inactive"); + } else { + return Messages.get(this, "desc_active"); + } + } + + @Override + public boolean interact() { + return true; + } + + @Override + public void add(Buff buff) { + //immune to all buffs/debuffs when inactive + if (alignment != Alignment.NEUTRAL) { + super.add(buff); + } + } + + @Override + public void damage(int dmg, Object src) { + //immune to damage when inactive + if (alignment == Alignment.NEUTRAL){ + return; + } + if (dmg >= 5){ + //takes 10/11/12/13/14/15 dmg at 10/12/15/19/24/31 incoming dmg + dmg = 9 + (int)(Math.sqrt(8*(dmg - 9) + 1) - 1)/2; + } + super.damage(dmg, src); + } + + @Override + public void die(Object cause) { + super.die(cause); + ((NewCavesBossLevel)Dungeon.level).eliminatePylon(); + } + + private static final String ALIGNMENT = "alignment"; + private static final String TARGET_NEIGHBOUR = "target_neighbour"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(ALIGNMENT, alignment); + bundle.put(TARGET_NEIGHBOUR, targetNeighbor); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + alignment = bundle.getEnum(ALIGNMENT, Alignment.class); + targetNeighbor = bundle.getInt(TARGET_NEIGHBOUR); + } + + { + immunities.add( Paralysis.class ); + immunities.add( Amok.class ); + immunities.add( Sleep.class ); + immunities.add( ToxicGas.class ); + immunities.add( Terror.class ); + immunities.add( Vertigo.class ); + } + +} diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/EarthParticle.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/EarthParticle.java index bd4a48140..f6db1e433 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/EarthParticle.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/EarthParticle.java @@ -66,8 +66,9 @@ public class EarthParticle extends PixelParticle { left = lifespan = 1f; size = 8; - acc.y = 15; - speed.y = 0; + acc.y = 30; + speed.y = -5; + angularSpeed = Random.Float(-90, 90); } @Override diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/SparkParticle.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/SparkParticle.java index c5bd5311b..668169336 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/SparkParticle.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/particles/SparkParticle.java @@ -63,6 +63,7 @@ public class SparkParticle extends PixelParticle { this.x = x; this.y = y; + size = 5; left = lifespan = Random.Float( 0.5f, 1.0f ); @@ -77,10 +78,14 @@ public class SparkParticle extends PixelParticle { acc.set( 0, 0 ); speed.set( 0, 0 ); } + + public void setMaxSize( float value ){ + size = value; + } @Override public void update() { super.update(); - size( Random.Float( 5 * left / lifespan ) ); + size( Random.Float( size * left / lifespan ) ); } } \ No newline at end of file diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfBlastWave.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfBlastWave.java index 85f2aee05..7495a88a4 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfBlastWave.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfBlastWave.java @@ -109,6 +109,10 @@ public class WandOfBlastWave extends DamageWand { } public static void throwChar(final Char ch, final Ballistica trajectory, int power){ + throwChar(ch, trajectory, power, true); + } + + public static void throwChar(final Char ch, final Ballistica trajectory, int power, boolean collideDmg){ if (ch.properties().contains(Char.Property.BOSS)) { power /= 2; } @@ -142,7 +146,7 @@ public class WandOfBlastWave extends DamageWand { if (newPos == ch.pos) return; final int finalDist = dist; - final boolean finalCollided = collided; + final boolean finalCollided = collided && collideDmg; final int initialpos = ch.pos; Actor.addDelayed(new Pushing(ch, ch.pos, newPos, new Callback() { diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java index cb57f4335..cd7d273b2 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java @@ -653,8 +653,10 @@ public abstract class Level implements Bundlable { set( pos, Terrain.EMBERS ); } - protected void cleanWalls() { - discoverable = new boolean[length()]; + public void cleanWalls() { + if (discoverable == null || discoverable.length != length) { + discoverable = new boolean[length()]; + } for (int i=0; i < length(); i++) { diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCavesBossLevel.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCavesBossLevel.java new file mode 100644 index 000000000..041f01847 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/NewCavesBossLevel.java @@ -0,0 +1,786 @@ +/* + * 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.levels; + +import com.shatteredpixel.shatteredpixeldungeon.Assets; +import com.shatteredpixel.shatteredpixeldungeon.Bones; +import com.shatteredpixel.shatteredpixeldungeon.Dungeon; +import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; +import com.shatteredpixel.shatteredpixeldungeon.actors.Char; +import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob; +import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Electricity; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.DM300; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Pylon; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.NewDM300; +import com.shatteredpixel.shatteredpixeldungeon.effects.BlobEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.BlastParticle; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SparkParticle; +import com.shatteredpixel.shatteredpixeldungeon.items.Heap; +import com.shatteredpixel.shatteredpixeldungeon.items.Item; +import com.shatteredpixel.shatteredpixeldungeon.levels.painters.CavesPainter; +import com.shatteredpixel.shatteredpixeldungeon.levels.painters.Painter; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; +import com.shatteredpixel.shatteredpixeldungeon.tiles.CustomTilemap; +import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.Camera; +import com.watabou.noosa.Group; +import com.watabou.noosa.Image; +import com.watabou.noosa.Tilemap; +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.Point; +import com.watabou.utils.Random; +import com.watabou.utils.Rect; + +import java.util.ArrayList; + +public class NewCavesBossLevel extends Level { + + { + color1 = 0x534f3e; + color2 = 0xb9d661; + } + + @Override + public String tilesTex() { + return Assets.TILES_CAVES; + } + + @Override + public String waterTex() { + return Assets.WATER_CAVES; + } + + private static int WIDTH = 33; + private static int HEIGHT = 42; + + public static Rect mainArena = new Rect(5, 14, 28, 37); + public static Rect gate = new Rect(14, 13, 19, 14); + public static int[] pylonPositions = new int[]{ 4 + 13*WIDTH, 28 + 13*WIDTH, 4 + 37*WIDTH, 28 + 37*WIDTH }; + + private ArenaVisuals customArenaVisuals; + + @Override + protected boolean build() { + + setSize(WIDTH, HEIGHT); + + //Painter.fill(this, 0, 0, width(), height(), Terrain.EMBERS); + + //setup exit area above main boss arena + Painter.fill(this, 0, 3, width(), 4, Terrain.CHASM); + Painter.fill(this, 6, 7, 21, 1, Terrain.CHASM); + Painter.fill(this, 10, 8, 13, 1, Terrain.CHASM); + Painter.fill(this, 12, 9, 9, 1, Terrain.CHASM); + Painter.fill(this, 13, 10, 7, 1, Terrain.CHASM); + Painter.fill(this, 14, 3, 5, 10, Terrain.EMPTY); + + //fill in special floor, statues, and exits + Painter.fill(this, 15, 2, 3, 3, Terrain.EMPTY_SP); + Painter.fill(this, 15, 5, 3, 1, Terrain.STATUE); + Painter.fill(this, 15, 7, 3, 1, Terrain.STATUE); + Painter.fill(this, 15, 9, 3, 1, Terrain.STATUE); + Painter.fill(this, 16, 5, 1, 6, Terrain.EMPTY_SP); + Painter.fill(this, 15, 0, 3, 3, Terrain.EXIT); + + exit = 16 + 2*width(); + + //These signs are visually overridden with custom tile visuals + Painter.fill(this, gate, Terrain.SIGN); + + //set up main boss arena + Painter.fillEllipse(this, mainArena, Terrain.EMPTY); + + boolean[] patch = Patch.generate( width, height-14, 0.20f, 2, true ); + for (int i= 14*width(); i < length(); i++) { + if (map[i] == Terrain.EMPTY) { + if (patch[i - 14*width()]){ + map[i] = Terrain.WATER; + } else if (Random.Int(6) == 0){ + map[i] = Terrain.INACTIVE_TRAP; + map[i] = Terrain.INACTIVE_TRAP; + } + } + } + + buildEntrance(); + buildCorners(); + + CustomTilemap customVisuals = new CityEntrance(); + customVisuals.setRect(0, 0, width(), 11); + customTiles.add(customVisuals); + + customVisuals = new EntranceOverhang(); + customVisuals.setRect(0, 0, width(), 11); + customWalls.add(customVisuals); + + customVisuals = customArenaVisuals = new ArenaVisuals(); + customVisuals.setRect(0, 12, width(), 27); + customTiles.add(customVisuals); + + new CavesPainter().paint(this, null); + + return true; + + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + + for (CustomTilemap c : customTiles){ + if (c instanceof ArenaVisuals){ + customArenaVisuals = (ArenaVisuals) c; + } + } + } + + @Override + protected void createMobs() { } + + @Override + public Actor respawner() { + return null; + } + + @Override + protected void createItems() { + Item item = Bones.get(); + if (item != null) { + int pos; + do { + pos = randomRespawnCell(null); + } while (pos == entrance); + drop( item, pos ).setHauntedIfCursed().type = Heap.Type.REMAINS; + } + } + + @Override + public int randomRespawnCell( Char ch ) { + int cell; + do { + cell = entrance + PathFinder.NEIGHBOURS8[Random.Int(8)]; + } while (!passable[cell] + || (Char.hasProp(ch, Char.Property.LARGE) && !openSpace[cell]) + || Actor.findChar(cell) != null); + return cell; + } + + @Override + public void occupyCell(Char ch) { + super.occupyCell(ch); + + //seal the level when the hero moves off the entrance, the level isn't already sealed, and the gate hasn't been destroyed + int gatePos = pointToCell(new Point(gate.left, gate.top)); + if (ch == Dungeon.hero && ch.pos != entrance && !locked && solid[gatePos]){ + + seal(); + + } + } + + @Override + public void seal() { + super.seal(); + + NewDM300 boss = new NewDM300(); + boss.state = boss.WANDERING; + do { + boss.pos = pointToCell(Random.element(mainArena.getPoints())); + } while (!openSpace[boss.pos] || map[boss.pos] == Terrain.EMPTY_SP || heroFOV[boss.pos]); + GameScene.add( boss ); + + set( entrance, Terrain.WALL ); + GameScene.updateMap( entrance ); + Dungeon.observe(); + + CellEmitter.get( entrance ).start( Speck.factory( Speck.ROCK ), 0.07f, 10 ); + Camera.main.shake( 3, 0.7f ); + Sample.INSTANCE.play( Assets.SND_ROCKS ); + + for (int i : pylonPositions) { + Pylon pylon = new Pylon(); + pylon.pos = i; + GameScene.add(pylon); + } + + } + + @Override + public void unseal() { + super.unseal(); + + blobs.get(PylonEnergy.class).fullyClear(); + + set( entrance, Terrain.ENTRANCE ); + int i = 14 + 13*width(); + for (int j = 0; j < 5; j++){ + set( i+j, Terrain.EMPTY ); + if (Dungeon.level.heroFOV[i+j]){ + CellEmitter.get(i+j).burst(BlastParticle.FACTORY, 10); + } + } + GameScene.updateMap(); + + customArenaVisuals.updateState(); + + Dungeon.observe(); + + } + + public void activatePylon(){ + ArrayList pylons = new ArrayList<>(); + for (Mob m : mobs){ + if (m instanceof Pylon && m.alignment == Char.Alignment.NEUTRAL){ + pylons.add((Pylon) m); + } + } + + if (pylons.size() == 1){ + pylons.get(0).activate(); + } else if (!pylons.isEmpty()) { + Pylon closest = null; + for (Pylon p : pylons){ + if (closest == null || trueDistance(p.pos, Dungeon.hero.pos) < trueDistance(closest.pos, Dungeon.hero.pos)){ + closest = p; + } + } + pylons.remove(closest); + Random.element(pylons).activate(); + } + + for( int i = (mainArena.top-1)*width; i 2) { + blobs.get(PylonEnergy.class).fullyClear(); + } + } + + @Override + public String tileName( int tile ) { + switch (tile) { + case Terrain.GRASS: + return Messages.get(CavesLevel.class, "grass_name"); + case Terrain.HIGH_GRASS: + return Messages.get(CavesLevel.class, "high_grass_name"); + case Terrain.WATER: + return Messages.get(CavesLevel.class, "water_name"); + case Terrain.STATUE: + //city statues are used + return Messages.get(CityLevel.class, "statue_name"); + default: + return super.tileName( tile ); + } + } + + @Override + public String tileDesc( int tile ) { + switch (tile) { + case Terrain.WATER: + return super.tileDesc( tile ) + "\n\n" + Messages.get(CavesBossLevel.class, "water_desc"); + case Terrain.ENTRANCE: + return Messages.get(CavesLevel.class, "entrance_desc"); + case Terrain.EXIT: + //city exit is used + return Messages.get(CityLevel.class, "exit_desc"); + case Terrain.HIGH_GRASS: + return Messages.get(CavesLevel.class, "high_grass_desc"); + case Terrain.WALL_DECO: + return Messages.get(CavesLevel.class, "wall_deco_desc"); + case Terrain.BOOKSHELF: + return Messages.get(CavesLevel.class, "bookshelf_desc"); + //city statues are used + case Terrain.STATUE: + return Messages.get(CityLevel.class, "statue_desc"); + default: + return super.tileDesc( tile ); + } + } + + @Override + public Group addVisuals() { + super.addVisuals(); + CavesLevel.addCavesVisuals(this, visuals); + return visuals; + } + + /** + * semi-randomized setup for entrance and corners + */ + + private static final short n = -1; //used when a tile shouldn't be changed + private static final short W = Terrain.WALL; + private static final short e = Terrain.EMPTY; + private static final short s = Terrain.EMPTY_SP; + + private static short[] entrance1 = { + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, n, n, + n, n, n, n, W, e, W, W, + n, n, n, W, W, e, W, W, + n, n, W, W, e, e, e, e, + n, n, e, e, e, W, W, e, + n, n, W, W, e, W, e, e, + n, n, W, W, e, e, e, e + }; + + private static short[] entrance2 = { + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, W, W, + n, n, n, n, n, n, e, e, + n, n, n, n, e, W, W, W, + n, n, n, e, e, e, e, e, + n, n, n, W, e, W, W, e, + n, W, e, W, e, W, e, e, + n, W, e, W, e, e, e, e + }; + + private static short[] entrance3 = { + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, n, n, + n, n, n, W, W, e, W, W, + n, n, n, W, W, e, W, W, + n, n, n, e, e, e, e, e, + n, n, n, W, W, e, W, e, + n, n, n, W, W, e, e, e + }; + + private static short[] entrance4 = { + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, n, e, + n, n, n, n, n, n, W, e, + n, n, n, n, n, W, W, e, + n, n, n, n, W, W, W, e, + n, n, n, W, W, W, W, e, + n, n, W, W, W, W, e, e, + n, e, e, e, e, e, e, e + }; + + private static short[][] entranceVariants = { + entrance1, + entrance2, + entrance3, + entrance4 + }; + + private void buildEntrance(){ + entrance = 16 + 25*width(); + + //entrance area + int NW = entrance - 7 - 7*width(); + int NE = entrance + 7 - 7*width(); + int SE = entrance + 7 + 7*width(); + int SW = entrance - 7 + 7*width(); + + short[] entranceTiles = Random.oneOf(entranceVariants); + for (int i = 0; i < entranceTiles.length; i++){ + if (i % 8 == 0 && i != 0){ + NW += (width() - 8); + NE += (width() + 8); + SE -= (width() - 8); + SW -= (width() + 8); + } + + if (entranceTiles[i] != n) map[NW] = map[NE] = map[SE] = map[SW] = entranceTiles[i]; + NW++; NE--; SW++; SE--; + } + + Painter.set(this, entrance, Terrain.ENTRANCE); + } + + private static short[] corner1 = { + W, W, W, W, W, W, W, W, W, W, + W, s, s, s, e, e, e, W, W, W, + W, s, s, s, W, W, e, e, W, W, + W, s, s, s, W, W, W, e, e, W, + W, e, W, W, W, W, W, W, e, n, + W, e, W, W, W, W, W, n, n, n, + W, e, e, W, W, W, n, n, n, n, + W, W, e, e, W, n, n, n, n, n, + W, W, W, e, e, n, n, n, n, n, + W, W, W, W, n, n, n, n, n, n, + }; + + private static short[] corner2 = { + W, W, W, W, W, W, W, W, W, W, + W, s, s, s, W, W, W, W, W, W, + W, s, s, s, e, e, e, e, e, e, + W, s, s, s, W, W, W, W, W, e, + W, W, e, W, W, W, W, W, W, e, + W, W, e, W, W, W, W, n, n, n, + W, W, e, W, W, W, n, n, n, n, + W, W, e, W, W, n, n, n, n, n, + W, W, e, W, W, n, n, n, n, n, + W, W, e, e, e, n, n, n, n, n, + }; + + private static short[] corner3 = { + W, W, W, W, W, W, W, W, W, W, + W, s, s, s, W, e, e, e, W, W, + W, s, s, s, e, e, W, e, W, W, + W, s, s, s, W, W, W, e, W, W, + W, W, e, W, W, W, W, e, W, n, + W, e, e, W, W, W, W, e, e, n, + W, e, W, W, W, W, n, n, n, n, + W, e, e, e, e, e, n, n, n, n, + W, W, W, W, W, e, n, n, n, n, + W, W, W, W, n, n, n, n, n, n, + }; + + private static short[] corner4 = { + W, W, W, W, W, W, W, W, W, W, + W, s, s, s, W, W, W, W, W, W, + W, s, s, s, e, e, e, W, W, W, + W, s, s, s, W, W, e, W, W, W, + W, W, e, W, W, W, e, W, W, n, + W, W, e, W, W, W, e, e, n, n, + W, W, e, e, e, e, e, n, n, n, + W, W, W, W, W, e, n, n, n, n, + W, W, W, W, W, n, n, n, n, n, + W, W, W, W, n, n, n, n, n, n, + }; + + private static short[][] cornerVariants = { + corner1, + corner2, + corner3, + corner4 + }; + + private void buildCorners(){ + int NW = 2 + 11*width(); + int NE = 30 + 11*width(); + int SE = 30 + 39*width(); + int SW = 2 + 39*width(); + + short[] cornerTiles = Random.oneOf(cornerVariants); + for(int i = 0; i < cornerTiles.length; i++){ + if (i % 10 == 0 && i != 0){ + NW += (width() - 10); + NE += (width() + 10); + SE -= (width() - 10); + SW -= (width() + 10); + } + + if (cornerTiles[i] != n) map[NW] = map[NE] = map[SE] = map[SW] = cornerTiles[i]; + NW++; NE--; SW++; SE--; + } + } + + /** + * Visual Effects + */ + + public static class CityEntrance extends CustomTilemap{ + + { + texture = Assets.CAVES_BOSS; + } + + private static short[] entryWay = new short[]{ + -1, 7, 7, 7, -1, + -1, 1, 2, 3, -1, + 8, 1, 2, 3, 12, + 16, 9, 10, 11, 20, + 16, 16, 18, 20, 20, + 16, 17, 18, 19, 20, + 16, 16, 18, 20, 20, + 16, 17, 18, 19, 20, + 16, 16, 18, 20, 20, + 16, 17, 18, 19, 20, + 24, 25, 26, 27, 28 + }; + + @Override + public Tilemap create() { + Tilemap v = super.create(); + int[] data = new int[tileW*tileH]; + int entryPos = 0; + for (int i = 0; i < data.length; i++){ + + //override the entryway + if (i % tileW == tileW/2 - 2){ + data[i++] = entryWay[entryPos++]; + data[i++] = entryWay[entryPos++]; + data[i++] = entryWay[entryPos++]; + data[i++] = entryWay[entryPos++]; + data[i] = entryWay[entryPos++]; + + //otherwise check if we are on row 2 or 3, in which case we need to override walls + } else { + if (i / tileW == 2) data[i] = 13; + else if (i / tileW == 3) data[i] = 21; + else data[i] = -1; + } + } + v.map( data, tileW ); + return v; + } + + } + + public static class EntranceOverhang extends CustomTilemap{ + + { + texture = Assets.CAVES_BOSS; + } + + private static short[] entryWay = new short[]{ + 0, 7, 7, 7, 4, + 0, 15, 15, 15, 4, + 8, 23, 23, 23, 12, + -1, -1, -1, -1, -1, + -1, 6, -1, 14, -1, + -1, -1, -1, -1, -1, + -1, 6, -1, 14, -1, + -1, -1, -1, -1, -1, + -1, 6, -1, 14, -1, + -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, + }; + + @Override + public Tilemap create() { + Tilemap v = super.create(); + int[] data = new int[tileW*tileH]; + int entryPos = 0; + for (int i = 0; i < data.length; i++){ + + //copy over this row of the entryway + if (i % tileW == tileW/2 - 2){ + data[i++] = entryWay[entryPos++]; + data[i++] = entryWay[entryPos++]; + data[i++] = entryWay[entryPos++]; + data[i++] = entryWay[entryPos++]; + data[i] = entryWay[entryPos++]; + } else { + data[i] = -1; + } + } + v.map( data, tileW ); + return v; + } + + } + + public static class ArenaVisuals extends CustomTilemap { + + { + texture = Assets.CAVES_BOSS; + } + + @Override + public Tilemap create() { + Tilemap v = super.create(); + updateState( ); + + return v; + } + + public void updateState( ){ + if (vis != null){ + int[] data = new int[tileW*tileH]; + int j = Dungeon.level.width() * tileY; + for (int i = 0; i < data.length; i++){ + + if (Dungeon.level.map[j] == Terrain.EMPTY_SP) { + for (int k : pylonPositions) { + if (k == j) { + if (Dungeon.level.locked + && !(Actor.findChar(k) instanceof Pylon)) { + data[i] = 38; + } else { + data[i] = -1; + } + } else if (Dungeon.level.adjacent(k, j)) { + int w = Dungeon.level.width; + data[i] = 54 + (j % w + 8 * (j / w)) - (k % w + 8 * (k / w)); + } + } + } else if (Dungeon.level.map[j] == Terrain.INACTIVE_TRAP){ + data[i] = 37; + } else if (gate.inside(Dungeon.level.cellToPoint(j))){ + int idx = Dungeon.level.solid[j] ? 40 : 32; + data[i++] = idx++; + data[i++] = idx++; + data[i++] = idx++; + data[i++] = idx++; + data[i] = idx; + j += 4; + } else { + data[i] = -1; + } + + j++; + } + vis.map(data, tileW); + } + } + + @Override + public String name(int tileX, int tileY) { + int i = tileX + tileW*(tileY + this.tileY); + if (Dungeon.level.map[i] == Terrain.INACTIVE_TRAP){ + return Messages.get(CavesBossLevel.class, "wires_name"); + } else if (gate.inside(Dungeon.level.cellToPoint(i))){ + return Messages.get(CavesBossLevel.class, "gate_name"); + } + + return super.name(tileX, tileY); + } + + @Override + public String desc(int tileX, int tileY) { + int i = tileX + tileW*(tileY + this.tileY); + if (Dungeon.level.map[i] == Terrain.INACTIVE_TRAP){ + return Messages.get(CavesBossLevel.class, "wires_desc"); + } else if (gate.inside(Dungeon.level.cellToPoint(i))){ + if (Dungeon.level.solid[i]){ + return Messages.get(CavesBossLevel.class, "gate_desc"); + } else { + return Messages.get(CavesBossLevel.class, "gate_desc_broken"); + } + } + return super.desc(tileX, tileY); + } + + @Override + public Image image(int tileX, int tileY) { + int i = tileX + tileW*(tileY + this.tileY); + for (int k : pylonPositions){ + if (Dungeon.level.distance(i, k) <= 1){ + return null; + } + } + + return super.image(tileX, tileY); + + } + } + + public static class PylonEnergy extends Blob { + + @Override + protected void evolve() { + int cell; + for (int i=area.top-1; i <= area.bottom; i++) { + for (int j = area.left-1; j <= area.right; j++) { + cell = j + i* Dungeon.level.width(); + if (Dungeon.level.insideMap(cell)) { + off[cell] = cur[cell]; + volume += off[cell]; + if (off[cell] > 0){ + + Char ch = Actor.findChar(cell); + if (ch != null && !(ch instanceof NewDM300)) { + Sample.INSTANCE.play( Assets.SND_LIGHTNING ); + ch.damage( Random.NormalIntRange(5, 15), Electricity.class); + ch.sprite.flash(); + + if (ch == Dungeon.hero && !ch.isAlive()) { + Dungeon.fail(NewDM300.class); + GLog.n( Messages.get(Electricity.class, "ondeath") ); + } + } + } + } + } + } + } + + @Override + public void fullyClear() { + super.fullyClear(); + energySourceSprite = null; + } + + private static CharSprite energySourceSprite = null; + + private static Emitter.Factory DIRECTED_SPARKS = new Emitter.Factory() { + @Override + public void emit(Emitter emitter, int index, float x, float y) { + if (energySourceSprite == null){ + for (Char c : Actor.chars()){ + if (c instanceof Pylon && c.alignment != Char.Alignment.NEUTRAL){ + energySourceSprite = c.sprite; + break; + } else if (c instanceof DM300){ + energySourceSprite = c.sprite; + } + } + } + + float dist = Math.abs(energySourceSprite.x - x) + Math.abs(energySourceSprite.y - y); + dist /= DungeonTilemap.SIZE; + + SparkParticle s = ((SparkParticle) emitter.recycle(SparkParticle.class)); + s.reset(x, y); + s.setMaxSize( 8 - dist/6 ); + } + + @Override + public boolean lightMode() { + return true; + } + }; + + @Override + public String tileDesc() { + return Messages.get(CavesBossLevel.class, "energy_desc"); + } + + @Override + public void use( BlobEmitter emitter ) { + super.use( emitter ); + energySourceSprite = null; + emitter.bound.set( 4/16f, 4/16f, 12/16f, 12/16f); + emitter.pour(DIRECTED_SPARKS, 0.2f); + } + + } +} diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Terrain.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Terrain.java index 8625af3a5..c53918e1b 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Terrain.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Terrain.java @@ -96,7 +96,7 @@ public class Terrain { flags[EMPTY_DECO] = flags[EMPTY]; flags[LOCKED_EXIT] = SOLID; flags[UNLOCKED_EXIT]= PASSABLE; - flags[SIGN] = PASSABLE | FLAMABLE; + flags[SIGN] = SOLID; //Currently these are unused except for visual tile overrides where we want terrain to be solid with no other properties flags[WELL] = AVOID; flags[STATUE] = SOLID; flags[STATUE_SP] = flags[STATUE]; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/DM300Sprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/DM300Sprite.java index d46bdbba7..cf2770976 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/DM300Sprite.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/DM300Sprite.java @@ -22,10 +22,22 @@ package com.shatteredpixel.shatteredpixeldungeon.sprites; import com.shatteredpixel.shatteredpixeldungeon.Assets; -import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; +import com.shatteredpixel.shatteredpixeldungeon.actors.Char; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.NewDM300; +import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.BlastParticle; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SparkParticle; +import com.watabou.noosa.Camera; import com.watabou.noosa.TextureFilm; +import com.watabou.noosa.audio.Sample; +import com.watabou.noosa.particles.Emitter; +import com.watabou.utils.Callback; public class DM300Sprite extends MobSprite { + + private Animation slam; + + private Emitter superchargeSparks; public DM300Sprite() { super(); @@ -42,23 +54,113 @@ public class DM300Sprite extends MobSprite { attack = new Animation( 15, false ); attack.frames( frames, 4, 5, 6 ); - + + slam = attack.clone(); + + zap = attack.clone(); + die = new Animation( 20, false ); die.frames( frames, 0, 7, 0, 7, 0, 7, 0, 7, 0, 7, 0, 7, 8 ); play( idle ); } + + public void zap( int cell ) { + + turnTo( ch.pos , cell ); + play( zap ); + + MagicMissile.boltFromChar( parent, + MagicMissile.TOXIC_VENT, + this, + cell, + new Callback() { + @Override + public void call() { + ((NewDM300)ch).onZapComplete(); + } + } ); + Sample.INSTANCE.play( Assets.SND_PUFF ); + } + + public void slam( int cell ){ + turnTo( ch.pos , cell ); + play( slam ); + Sample.INSTANCE.play( Assets.SND_ROCKS ); + Camera.main.shake( 3, 0.7f ); + } @Override public void onComplete( Animation anim ) { - + + if (anim == zap || anim == slam){ + idle(); + } + + if (anim == slam){ + ((NewDM300)ch).onSlamComplete(); + } + super.onComplete( anim ); if (anim == die) { - emitter().burst( Speck.factory( Speck.WOOL ), 15 ); + Sample.INSTANCE.play(Assets.SND_BLAST); + emitter().burst( BlastParticle.FACTORY, 25 ); } } - + + @Override + public void link(Char ch) { + super.link(ch); + + superchargeSparks = emitter(); + superchargeSparks.autoKill = false; + superchargeSparks.pour(SparkParticle.STATIC, 0.05f); + superchargeSparks.on = false; + + if (ch instanceof NewDM300 && ((NewDM300) ch).isSupercharged()){ + tint(1, 0, 0, 0.33f); + superchargeSparks.on = true; + } + } + + @Override + public void update() { + super.update(); + + if (ch instanceof NewDM300){ + superchargeSparks.on = ((NewDM300) ch).isSupercharged(); + } + + if (superchargeSparks != null){ + superchargeSparks.visible = visible; + } + } + + @Override + public void die() { + super.die(); + if (superchargeSparks != null){ + superchargeSparks.on = false; + } + } + + @Override + public void kill() { + super.kill(); + if (superchargeSparks != null){ + superchargeSparks.killAndErase(); + } + } + + @Override + public void resetColor() { + super.resetColor(); + if (ch instanceof NewDM300 && ((NewDM300) ch).isSupercharged()){ + tint(1, 0, 0, 0.33f); + } + } + @Override public int blood() { return 0xFFFFFF88; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PylonSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PylonSprite.java new file mode 100644 index 000000000..ae91c1dde --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PylonSprite.java @@ -0,0 +1,93 @@ +/* + * 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.shatteredpixel.shatteredpixeldungeon.actors.Char; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Pylon; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.BlastParticle; +import com.watabou.noosa.TextureFilm; +import com.watabou.noosa.audio.Sample; + +public class PylonSprite extends MobSprite { + + private Animation activeIdle; + + public PylonSprite() { + super(); + + perspectiveRaise = 5/16f; //1 pixel less + renderShadow = false; + + texture( Assets.PYLON ); + + TextureFilm frames = new TextureFilm( texture, 10, 20 ); + + idle = new Animation( 1, false ); + idle.frames( frames, 0 ); + + activeIdle = new Animation( 1, false ); + activeIdle.frames( frames, 1 ); + + run = idle.clone(); + + attack = idle.clone(); + + die = new Animation( 1, false ); + die.frames( frames, 2 ); + + play( idle ); + } + + @Override + public void link(Char ch) { + super.link(ch); + if (ch instanceof Pylon && ch.alignment == Char.Alignment.ENEMY){ + activate(); + } + if (parent != null) parent.bringToFront(this); + renderShadow = false; + } + + public void activate(){ + idle = activeIdle.clone(); + idle(); + } + + @Override + public void play(Animation anim) { + if (anim == die){ + turnTo(ch.pos, ch.pos+1); //always face right to merge with custom tiles + emitter().burst(BlastParticle.FACTORY, 20); + Sample.INSTANCE.play(Assets.SND_BLAST); + } + super.play(anim); + } + + @Override + public void onComplete(Animation anim) { + if (anim == attack){ + flash(); + } + super.onComplete(anim); + } +} 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 650d01611..33de95402 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 @@ -497,6 +497,23 @@ actors.mobs.dm300.def_verb=blocked actors.mobs.dm300.rankings_desc=Crushed by the DM-300 actors.mobs.dm300.desc=This machine was created by the Dwarves several centuries ago. Later, Dwarves started to replace machines with golems, elementals and even demons. Eventually it led their civilization to the decline. The DM-300 and similar machines were typically used for construction and mining, and in some cases, for city defense. +actors.mobs.newdm300.name=DM-300 +actors.mobs.newdm300.notice=UNAUTHORIZED PERSONNEL DETECTED! +actors.mobs.newdm300.shield=DM-300 pulls power from the exposed wires and shields itself! +actors.mobs.newdm300.vent=DM-300 fires a jet of toxic exhaust at you! +actors.mobs.newdm300.rocks=DM-300 slams the ground, loosening rocks from the ceiling! +actors.mobs.newdm300.charging=SUSTAINING DAMAGE! CHARGING FROM POWER GRID... +actors.mobs.newdm300.supercharged=SUPERCHARGE COMPLETE, OPERATING AT 200% POWER! +actors.mobs.newdm300.charge_lost=POWER GRID DAMAGED, REVERTING TO LOCAL POWER! +actors.mobs.newdm300.pylons_destroyed=ALERT, INSTABILITY DETECTED IN POWER GRID! +actors.mobs.newdm300.rankings_desc=Crushed by the DM-300 +actors.mobs.newdm300.immune=IMMUNE +actors.mobs.newdm300.def_verb=blocked +actors.mobs.newdm300.defeated=CRITICAL DAMAGE! ATTEMPTING SHUTDO- +actors.mobs.newdm300.desc=The DM-300 is the largest and most powerful 'defense machine' that the dwarves ever built. Such an awesome machine is difficult to manufacture, so the dwarves only ever made a few to guard the entrances to their their underground metropolis.\n\nIt is equipped with vents to jet its toxic exhaust fumes and a high power drill that it can use both to attack and disrupt the earth. DM-300 can also connect to an energy grid, further enhancing its power. +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 dril 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.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 @@ -589,6 +606,10 @@ actors.mobs.monk$focus.desc=This monk is perfectly honed in on their target, and actors.mobs.piranha.name=giant piranha actors.mobs.piranha.desc=These carnivorous fish are not natural inhabitants of underground pools. They were bred specifically to protect flooded treasure vaults. +actors.mobs.pylon.name=power pylon +actors.mobs.pylon.desc_inactive=A power pylon, meant to help regulate the electricity which powers the machinery in this area.\n\nThe pylon is currently inactive and immune to damage. +actors.mobs.pylon.desc_active=A power pylon, meant to help regulate the electricity which powers the machinery in this area. The pylon is currently surging with electrical energy, this must be what's supercharging DM-300!\n\nIn this state pylons are vulnerable, but will arc electricity to nearby cells in a clockwise pattern. Due to their heavy metal construction, pylons are resistant to large amounts of damage. + actors.mobs.rat.name=marsupial rat actors.mobs.rat.desc=Marsupial rats are aggressive but rather weak denizens of the sewers. They have a nasty bite, but are only life threatening in large numbers. diff --git a/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/levels/levels.properties b/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/levels/levels.properties index 204fbf6ac..28f4ffdd9 100644 --- a/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/levels/levels.properties +++ b/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/levels/levels.properties @@ -128,9 +128,17 @@ levels.traps.worndarttrap.desc=A small dart-blower must be hidden nearby, activa ###levels +levels.cavesbosslevel.wires_name=Exposed wiring +levels.cavesbosslevel.wires_desc=The ground is partially dug up here, showing some large exposed wires. They must be connecting the various electrical machines together.\n\nThe wires must have some current running through them. If DM-300 steps here it may be able to draw power from there. +levels.cavesbosslevel.energy_desc=The ground here is sparking with electricity, and is harmful to step on. The sparks seem to be stronger in some places than others... perhaps they lead to the power source? +levels.cavesbosslevel.gate_name=Metal gate +levels.cavesbosslevel.gate_desc=A large metal gate that blocks the path into the dwarven metropolis. The metal box in the center is producing a loud humming noice, it must be connected to the circuitry and machines nearby. Perhaps destroying DM-300 will open the gate? +levels.cavesbosslevel.gate_desc_broken=The gate must have been connected to DM-300 in some way, as it exploded when DM-300 was defeated. Now only broken pieces remain. +levels.cavesbosslevel.water_desc=With all the electricity around here water might not always be safe... + levels.caveslevel.grass_name=Fluorescent moss levels.caveslevel.high_grass_name=Fluorescent mushrooms -levels.caveslevel.water_name=Freezing cold water. +levels.caveslevel.water_name=Freezing cold water levels.caveslevel.entrance_desc=The ladder leads up to the upper depth. levels.caveslevel.exit_desc=The ladder leads down to the lower depth. levels.caveslevel.high_grass_desc=Huge mushrooms block the view. @@ -195,7 +203,7 @@ levels.level.statue_desc=Someone wanted to adorn this place, but failed, obvious levels.level.alchemy_desc=This pot is filled with magical water. Items can be mixed into the pot to create something new! levels.level.empty_well_desc=The well has run dry. -levels.prisonlevel.water_name=Dark cold water. +levels.prisonlevel.water_name=Dark cold water levels.prisonlevel.empty_deco_desc=There are old blood stains on the floor. levels.prisonlevel.bookshelf_desc=This is probably a vestige of a prison library. Might it burn?