diff --git a/core/src/main/assets/sprites/items.png b/core/src/main/assets/sprites/items.png index 7a041ffad..40de034d6 100644 Binary files a/core/src/main/assets/sprites/items.png and b/core/src/main/assets/sprites/items.png differ diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DM300.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DM300.java index 96a9fb8e4..e7ab71c0a 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DM300.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/DM300.java @@ -55,6 +55,7 @@ import com.shatteredpixel.shatteredpixeldungeon.items.quest.MetalShard; import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfBlastWave; import com.shatteredpixel.shatteredpixeldungeon.levels.CavesBossLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.Level; +import com.shatteredpixel.shatteredpixeldungeon.levels.MiningLevel; import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; import com.shatteredpixel.shatteredpixeldungeon.mechanics.Ballistica; import com.shatteredpixel.shatteredpixeldungeon.mechanics.ConeAOE; @@ -676,6 +677,7 @@ public class DM300 extends Mob { resistances.add(Slow.class); } + //TODO we probably want to exernalize this as it's now being used by multiple characters public static class FallingRockBuff extends FlavourBuff { private int[] rockPositions; @@ -697,11 +699,20 @@ public class DM300 extends Mob { Char ch = Actor.findChar(i); if (ch != null && !(ch instanceof DM300)){ - Buff.prolong( ch, Paralysis.class, Dungeon.isChallenged(Challenges.STRONGER_BOSSES) ? 5 : 3 ); - if (ch == Dungeon.hero){ - Statistics.bossScores[2] -= 100; + if (Dungeon.level instanceof MiningLevel){ + Buff.prolong(ch, Paralysis.class, ch instanceof GnollGuard ? 10 : 3); + } else { + Buff.prolong(ch, Paralysis.class, Dungeon.isChallenged(Challenges.STRONGER_BOSSES) ? 5 : 3); + if (ch == Dungeon.hero) { + Statistics.bossScores[2] -= 100; + } } } + + if (ch == null && Dungeon.level instanceof MiningLevel && Random.Int(3) == 0){ + Level.set( i, Terrain.MINE_BOULDER ); + GameScene.updateMap(i); + } } PixelScene.shake( 3, 0.7f ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollSapper.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollSapper.java index 245a57b3c..3ad1aa6d9 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollSapper.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollSapper.java @@ -21,16 +21,40 @@ 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.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis; +import com.shatteredpixel.shatteredpixeldungeon.effects.Splash; +import com.shatteredpixel.shatteredpixeldungeon.effects.TargetedCell; +import com.shatteredpixel.shatteredpixeldungeon.items.Item; +import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfBlastWave; +import com.shatteredpixel.shatteredpixeldungeon.levels.Level; +import com.shatteredpixel.shatteredpixeldungeon.levels.MiningLevel; +import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; +import com.shatteredpixel.shatteredpixeldungeon.mechanics.Ballistica; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.scenes.PixelScene; import com.shatteredpixel.shatteredpixeldungeon.sprites.GnollSapperSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; +import com.shatteredpixel.shatteredpixeldungeon.sprites.MissileSprite; +import com.watabou.noosa.audio.Sample; import com.watabou.utils.Bundle; +import com.watabou.utils.Callback; +import com.watabou.utils.ColorMath; +import com.watabou.utils.GameMath; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; import java.util.ArrayList; public class GnollSapper extends Mob { { + //TODO act after gnoll guards? makes it easier to kite them spriteClass = GnollSapperSprite.class; HP = HT = 35; @@ -41,13 +65,17 @@ public class GnollSapper extends Mob { properties.add(Property.MINIBOSS); - SLEEPING = new Sleeping(); + HUNTING = new Hunting(); + WANDERING = new Wandering(); state = SLEEPING; - //TODO wandering and hunting too. Partly for abilities, but also for other logic } + public int spawnPos; private ArrayList guardIDs = new ArrayList<>(); + private int throwingRockFromPos = -1; + private int throwingRockToPos = -1; + public void linkGuard(GnollGuard g){ guardIDs.add(g.id()); g.linkSapper(this); @@ -63,6 +91,7 @@ public class GnollSapper extends Mob { } } + private static final String SPAWN_POS = "spawn_pos"; private static final String GUARD_IDS = "guard_ids"; @Override @@ -73,6 +102,7 @@ public class GnollSapper extends Mob { toStore[i] = guardIDs.get(i); } bundle.put(GUARD_IDS, toStore); + bundle.put(SPAWN_POS, spawnPos); } @Override @@ -82,21 +112,196 @@ public class GnollSapper extends Mob { for (int g : bundle.getIntArray(GUARD_IDS)){ guardIDs.add(g); } + spawnPos = bundle.getInt(SPAWN_POS); } - public class Sleeping extends Mob.Sleeping { + @Override + protected boolean act() { + if (throwingRockFromPos != -1){ + Level.set(throwingRockFromPos, Terrain.EMPTY); + GameScene.updateMap(throwingRockFromPos); + sprite.attack(throwingRockToPos, new Callback() { + @Override + public void call() { + //do nothing + } + }); + Ballistica rockPath = new Ballistica(throwingRockFromPos, throwingRockToPos, Ballistica.MAGIC_BOLT); + + Sample.INSTANCE.play(Assets.Sounds.MISS); + ((MissileSprite)sprite.parent.recycle( MissileSprite.class )). + reset( throwingRockFromPos, rockPath.collisionPos, new Boulder(), new Callback() { + @Override + public void call() { + //TODO can probably have better particles + Splash.at(rockPath.collisionPos, ColorMath.random( 0x444444, 0x777766 ), 15); + Sample.INSTANCE.play(Assets.Sounds.ROCKS); + + Char ch = Actor.findChar(rockPath.collisionPos); + if (ch == Dungeon.hero){ + PixelScene.shake( 3, 0.7f ); + } else { + PixelScene.shake(0.5f, 0.5f); + } + + if (ch != null){ + ch.damage(Random.NormalIntRange(5, 10), this); + + if (ch.isAlive()){ + Buff.prolong( ch, Paralysis.class, ch instanceof GnollGuard ? 10 : 3 ); + } else if (!ch.isAlive() && ch == Dungeon.hero) { + Badges.validateDeathFromEnemyMagic(); + Dungeon.fail( GnollSapper.this ); + //TODO GLog.n( Messages.get(this, "bolt_kill") ); + } + + if (rockPath.path.size() > rockPath.dist+1) { + Ballistica trajectory = new Ballistica(ch.pos, rockPath.path.get(rockPath.dist + 1), Ballistica.MAGIC_BOLT); + WandOfBlastWave.throwChar(ch, trajectory, 1, false, false, GnollSapper.this); + } + } + + next(); + } + } ); + + throwingRockFromPos = -1; + throwingRockToPos = -1; + + spend(TICK); + return false; + } else { + return super.act(); + } + + } + + public class Hunting extends Mob.Hunting { @Override public boolean act(boolean enemyInFOV, boolean justAlerted) { - if (guardIDs.size() < 2){ - for (Char ch : Actor.chars()){ - if (fieldOfView[ch.pos] && ch instanceof GnollGuard && !guardIDs.contains(ch.id())){ - linkGuard((GnollGuard) ch); - break; + if (!enemyInFOV) { + return super.act(enemyInFOV, justAlerted); + } else { + + //TODO we need a cooldown mechanic + if (Random.Int(4) == 0){ + if (Random.Int(3) != 0 && prepRockAttack(enemy)) { + Dungeon.hero.interrupt(); + spend(GameMath.gate(TICK, (int)Math.ceil(enemy.cooldown()), 3*TICK)); + return true; + } else if (prepRockFallAttack(enemy)) { + spend(GameMath.gate(TICK, (int)Math.ceil(enemy.cooldown()), 3*TICK)); + return true; } + + } + + spend(TICK); + return true; + } + } + } + + private boolean prepRockAttack( Char target ){ + ArrayList candidateRocks = new ArrayList<>(); + + for (int i = 0; i < Dungeon.level.length(); i++){ + if (fieldOfView[i] && Dungeon.level.map[i] == Terrain.MINE_BOULDER){ + if (new Ballistica(i, target.pos, Ballistica.PROJECTILE).collisionPos == target.pos){ + candidateRocks.add(i); } } - return super.act(enemyInFOV, justAlerted); + } + + if (candidateRocks.isEmpty()){ + return false; + } else { + + //TODO always random, or pick based on some criteria? maybe based on distance? + throwingRockFromPos = Random.element(candidateRocks); + throwingRockToPos = enemy.pos; + + Ballistica warnPath = new Ballistica(throwingRockFromPos, throwingRockToPos, Ballistica.STOP_SOLID); + for (int i : warnPath.subPath(0, warnPath.dist)){ + sprite.parent.add(new TargetedCell(i, 0xFF0000)); + } + + } + + return true; + } + + //similar overall logic as DM-300's rock fall attack, but 5x5 and can't hit barricades + // TODO externalize? we're going to use this for geomancer too + private boolean prepRockFallAttack( Char target ){ + + Dungeon.hero.interrupt(); + final int rockCenter = target.pos; + + int safeCell; + do { + safeCell = rockCenter + PathFinder.NEIGHBOURS8[Random.Int(8)]; + } while (safeCell == pos + || (Dungeon.level.solid[safeCell] && Random.Int(2) == 0) + || (Dungeon.level.traps.containsKey(safeCell) && Random.Int(2) == 0)); + + ArrayList rockCells = new ArrayList<>(); + + int start = rockCenter - Dungeon.level.width() * 2 - 2; + int pos; + for (int y = 0; y < 5; y++) { + pos = start + Dungeon.level.width() * y; + for (int x = 0; x < 5; x++) { + if (!Dungeon.level.insideMap(pos)) { + pos++; + continue; + } + if (Dungeon.level instanceof MiningLevel){ + boolean barricade = false; + for (int j : PathFinder.NEIGHBOURS9){ + if (Dungeon.level.map[pos+j] == Terrain.BARRICADE){ + barricade = true; + } + } + if (barricade){ + pos++; + continue; + } + } + //add rock cell to pos, if it is not solid, and isn't the safecell + if (!Dungeon.level.solid[pos] && pos != safeCell && Random.Int(Dungeon.level.distance(rockCenter, pos)) == 0) { + //don't want to overly punish players with slow move or attack speed + rockCells.add(pos); + } + pos++; + } + } + for (int i : rockCells){ + sprite.parent.add(new TargetedCell(i, 0xFF0000)); + } + Buff.append(this, DM300.FallingRockBuff.class, GameMath.gate(TICK, (int)Math.ceil(target.cooldown()), 3*TICK)).setRockPositions(rockCells); + + sprite.attack(target.pos, new Callback() { + @Override + public void call() { + //do nothing + } + }); + + return true; + } + + public class Boulder extends Item { + { + image = ItemSpriteSheet.GEO_BOULDER; + } + } + + public class Wandering extends Mob.Wandering { + @Override + protected int randomDestination() { + return spawnPos; } } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/ItemSpriteSheet.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/ItemSpriteSheet.java index 70b4aae8a..375214df7 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/ItemSpriteSheet.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/ItemSpriteSheet.java @@ -90,6 +90,7 @@ public class ItemSpriteSheet { public static final int TENGU_BOMB = UNCOLLECTIBLE+8; public static final int TENGU_SHOCKER = UNCOLLECTIBLE+9; + public static final int GEO_BOULDER = UNCOLLECTIBLE+10; static{ assignItemRect(GOLD, 15, 13); assignItemRect(ENERGY, 16, 16); @@ -101,6 +102,7 @@ public class ItemSpriteSheet { assignItemRect(TENGU_BOMB, 10, 10); assignItemRect(TENGU_SHOCKER, 10, 10); + assignItemRect(GEO_BOULDER, 16, 14); } private static final int CONTAINERS = xy(1, 3); //16 slots diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MissileSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MissileSprite.java index 0f9b6b0ef..4761bb501 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MissileSprite.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MissileSprite.java @@ -22,6 +22,7 @@ package com.shatteredpixel.shatteredpixeldungeon.sprites; import com.shatteredpixel.shatteredpixeldungeon.Dungeon; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.GnollSapper; import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.Crossbow; @@ -102,13 +103,14 @@ public class MissileSprite extends ItemSprite implements Tweener.Listener { ANGULAR_SPEEDS.put(ScorpioSprite.ScorpioShot.class, 0); //720 is default + + ANGULAR_SPEEDS.put(GnollSapper.Boulder.class, 90); ANGULAR_SPEEDS.put(HeavyBoomerang.class,1440); ANGULAR_SPEEDS.put(Bolas.class, 1440); - ANGULAR_SPEEDS.put(Shuriken.class, 2160); - - ANGULAR_SPEEDS.put(TenguSprite.TenguShuriken.class, 2160); + ANGULAR_SPEEDS.put(Shuriken.class, 2160); + ANGULAR_SPEEDS.put(TenguSprite.TenguShuriken.class, 2160); } //TODO it might be nice to have a source and destination angle, to improve thrown weapon visuals @@ -149,6 +151,12 @@ public class MissileSprite extends ItemSprite implements Tweener.Listener { flipHorizontal = true; updateFrame(); } + + if (item instanceof GnollSapper.Boulder){ + angle = 0; + flipHorizontal = false; + updateFrame(); + } float speed = SPEED; if (item instanceof Dart