From a7440afe2fe7c7acfbe41194ba861995152fa5f6 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 24 Nov 2023 13:32:01 -0500 Subject: [PATCH] v2.3.0: lots of coding and ability implementation for geomancer/sappers --- .../assets/messages/actors/actors.properties | 2 +- .../actors/mobs/GnollGeomancer.java | 302 +++++++++++++++++- .../actors/mobs/GnollSapper.java | 199 +----------- .../sprites/MissileSprite.java | 6 +- 4 files changed, 312 insertions(+), 197 deletions(-) diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index c11ae4df9..a8c5c05d2 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -1211,6 +1211,7 @@ actors.mobs.gnoll.desc=Gnolls are hyena-like humanoids. They dwell in sewers and actors.mobs.gnollgeomancer.name=gnoll geomancer actors.mobs.gnollgeomancer.warning=The geomancer begins to stir as you start picking at its rock armor. Be ready for a fight if you continue! actors.mobs.gnollgeomancer.alert=The geomancer awakens! The entire cave seems to shift around as it leaps away while laughing madly! +actors.mobs.gnollgeomancer.rock_kill=The flying boulder killed you... actors.mobs.gnollgeomancer.desc=TODO, fight info actors.mobs.gnollgeomancer.desc_sleeping=This impressively tall gnoll shaman is surrounded by a layer of rock, and looks almost like a statue. Looking closely you can see the rock magically move in time with the senior gnoll's breathing. It can't be harmed while encased in rock like this, and it appears to be enjoying a literal dirt nap.\n\nYou can probably break through the layers of rock with your pickaxe, but _be sure you're ready for a fight when you do so._ The geomancer must be the source of the various earth-moving magic around here, and the organizer of all the gnoll activity. _Defeating the gnolls scattered around here before fighting it might be a good idea._ @@ -1219,7 +1220,6 @@ actors.mobs.gnollguard.desc=A large and tough looking gnoll wielding a spear and actors.mobs.gnollguard.desc_armor=_A nearby gnoll sapper is holding a device that is granting this guard earthen armor, heavily reducing the damage it takes._ actors.mobs.gnollsapper.name=gnoll sapper -actors.mobs.gnollsapper.rock_kill=The flying boulder killed you... actors.mobs.gnollsapper.desc=A small and weak but intelligent gnoll with pale fur and a satchel full of various gadgets. They are likely here to assist with prospecting and mining dark gold, and have a nearby guard for protection. They are capable of fighting, but their attacks are pitifully weak, no stronger than a gnoll scout.\n\nInstead of fighting directly, sappers prefer to use the various gadgets they have to make the rocky environment attack for them. They don't know earth-moving magic themselves though, and so are pretty likely to use their gadgets recklessly. _There should be lots of ways to use their attacks against them, or their guard._ actors.mobs.gnolltrickster.name=gnoll trickster diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollGeomancer.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollGeomancer.java index 36e4d1139..96e862154 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollGeomancer.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/GnollGeomancer.java @@ -22,27 +22,39 @@ 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.Invisibility; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ShieldBuff; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.Blacksmith; import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; import com.shatteredpixel.shatteredpixeldungeon.effects.Pushing; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; +import com.shatteredpixel.shatteredpixeldungeon.effects.Splash; +import com.shatteredpixel.shatteredpixeldungeon.effects.TargetedCell; +import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.quest.DarkGold; import com.shatteredpixel.shatteredpixeldungeon.items.quest.Pickaxe; +import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfBlastWave; +import com.shatteredpixel.shatteredpixeldungeon.levels.Level; 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.scenes.PixelScene; import com.shatteredpixel.shatteredpixeldungeon.sprites.GnollGeomancerSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet; +import com.shatteredpixel.shatteredpixeldungeon.sprites.MissileSprite; import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; 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; @@ -52,20 +64,46 @@ public class GnollGeomancer extends Mob { { //TODO - HP = HT = 100; + HP = HT = 150; spriteClass = GnollGeomancerSprite.class; EXP = 20; - //acts after other mobs? + //acts after other mobs, just like sappers actPriority = MOB_PRIO-1; SLEEPING = new Sleeping(); + HUNTING = new Hunting(); state = SLEEPING; + //can see the hero from a distance + viewDistance = 12; + properties.add(Property.BOSS); } + private int abilityCooldown = Random.NormalIntRange(3, 5); + + //TODO do we want to allow for multple rock throws at once here? + private int throwingRockFromPos = -1; + private int throwingRockToPos = -1; + + @Override + protected boolean act() { + if (throwingRockFromPos != -1){ + GnollGeomancer.doRockThrowAttack(this, throwingRockFromPos, throwingRockToPos); + + throwingRockFromPos = -1; + throwingRockToPos = -1; + + spend(TICK); + return false; + } else { + return super.act(); + } + + } + @Override public boolean isInvulnerable(Class effect) { return super.isInvulnerable(effect) || (buff(RockArmor.class) != null && effect != Pickaxe.class); @@ -109,7 +147,7 @@ public class GnollGeomancer extends Mob { boolean wasSleeping = state == SLEEPING; - //ensure we don't do enough damage to break the barrier at the start + //ensure we don't do enough damage to break the armor at the start if (wasSleeping) dmg = Math.min(dmg, 15); dmg = Math.min(dmg, buff(RockArmor.class).shielding()); @@ -120,7 +158,7 @@ public class GnollGeomancer extends Mob { hits++; if (hits == 1){ - GLog.n( Messages.get(GnollGeomancer.this, "warning")); + GLog.w( Messages.get(GnollGeomancer.this, "warning")); } if (hits == 3){ GLog.n( Messages.get(GnollGeomancer.this, "alert")); wasSleeping = false; @@ -133,6 +171,9 @@ public class GnollGeomancer extends Mob { target = Random.Int(Dungeon.level.length()); } while (!Dungeon.level.insideMap(target) || Dungeon.level.distance(pos, target) != 10); carveRock(target); + + state = HUNTING; + enemy = Dungeon.hero; } if (wasSleeping) { @@ -160,21 +201,21 @@ public class GnollGeomancer extends Mob { int beforeHitHP = HP; super.damage(dmg, src); - dmg = beforeHitHP - HP; //geomancer cannot be hit through multiple brackets at a time if ((beforeHitHP/hpBracket - HP/hpBracket) >= 2){ HP = hpBracket * ((beforeHitHP/hpBracket)-1) + 1; } - if (beforeHitHP / hpBracket != HP / hpBracket) { + //taking damage from full HP does not trigger a jump + if (beforeHitHP != HT && beforeHitHP / hpBracket != HP / hpBracket) { //this is a start, but need a lot more fight logic int target; do { target = Random.Int(Dungeon.level.length()); } while (!Dungeon.level.insideMap(target) || Dungeon.level.distance(pos, target) != 10); carveRock(target); - Buff.affect(this, RockArmor.class).setShield(50); + Buff.affect(this, RockArmor.class).setShield(30); } } @@ -257,5 +298,252 @@ public class GnollGeomancer extends Mob { } } + private class Hunting extends Mob.Hunting { + + @Override + public boolean act(boolean enemyInFOV, boolean justAlerted) { + if (!enemyInFOV){ + spend(TICK); + return true; + } else { + enemySeen = true; + //sprite.showStatus(CharSprite.DEFAULT, "seen"); + + //TODO cooldown + if (abilityCooldown-- <= 0){ + //do we care? + boolean targetNextToBarricade = false; + for (int i : PathFinder.NEIGHBOURS8){ + if (Dungeon.level.map[enemy.pos+i] == Terrain.BARRICADE + || Dungeon.level.map[enemy.pos+i] == Terrain.ENTRANCE){ + targetNextToBarricade = true; + break; + } + } + + // 50/50 to either throw a rock or do rockfall + // unless target is next to a barricade, then always try to throw + // unless nothing to throw, then always rockfall + Ballistica aim = GnollGeomancer.prepRockThrowAttack(enemy, GnollGeomancer.this); + if (aim != null && (targetNextToBarricade || Random.Int(2) == 0)) { + + throwingRockFromPos = aim.sourcePos; + throwingRockToPos = aim.collisionPos; + + Ballistica warnPath = new Ballistica(aim.sourcePos, aim.collisionPos, Ballistica.STOP_SOLID); + for (int i : warnPath.subPath(0, warnPath.dist)){ + sprite.parent.add(new TargetedCell(i, 0xFF0000)); + } + + Dungeon.hero.interrupt(); + abilityCooldown = Random.NormalIntRange(3, 5); + spend(GameMath.gate(TICK, (int)Math.ceil(enemy.cooldown()), 3*TICK)); + return true; + } else if (GnollGeomancer.prepRockFallAttack(enemy, GnollGeomancer.this, 3, true)) { + Dungeon.hero.interrupt(); + spend(GameMath.gate(TICK, (int)Math.ceil(enemy.cooldown()), 3*TICK)); + abilityCooldown = Random.NormalIntRange(3, 5); + return true; + } + } + + spend(TICK); + return true; + } + } + + } + + //*** These methods are public static as their logic is also accessed by gnoll sappers *** + + public static Ballistica prepRockThrowAttack( Char target, Char source ){ + ArrayList candidateRocks = new ArrayList<>(); + + for (int i = 0; i < Dungeon.level.length(); i++){ + if (source.fieldOfView[i] && Dungeon.level.map[i] == Terrain.MINE_BOULDER){ + if (new Ballistica(i, target.pos, Ballistica.PROJECTILE).collisionPos == target.pos){ + candidateRocks.add(i); + } + } + } + + if (candidateRocks.isEmpty()){ + return null; + } else { + + //throw closest rock to enemy + int throwingFromPos = candidateRocks.get(0); + for (int i : candidateRocks){ + if (Dungeon.level.trueDistance(i, target.pos) < Dungeon.level.trueDistance(throwingFromPos, target.pos)){ + throwingFromPos = i; + } + } + int throwingToPos = target.pos; + + return new Ballistica(throwingFromPos, throwingToPos, Ballistica.PROJECTILE); + + } + } + + public static void doRockThrowAttack( Char source, int from, int to ){ + + Level.set(from, Terrain.EMPTY); + GameScene.updateMap(from); + source.sprite.attack(from, new Callback() { + @Override + public void call() { + //do nothing + } + }); + + Ballistica rockPath = new Ballistica(from, to, Ballistica.MAGIC_BOLT); + + Sample.INSTANCE.play(Assets.Sounds.MISS); + ((MissileSprite)source.sprite.parent.recycle( MissileSprite.class )). + reset( from, rockPath.collisionPos, new GnollGeomancer.Boulder(), new Callback() { + @Override + public void call() { + 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 instanceof GnollGeomancer)){ + 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( source.getClass() ); + GLog.n( Messages.get( GnollGeomancer.class, "rock_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, source); + } + } else if (ch == null) { + Dungeon.level.pressCell(rockPath.collisionPos); + } + + source.next(); + } + } ); + } + + public static class Boulder extends Item { + { + image = ItemSpriteSheet.GEO_BOULDER; + } + } + + //similar overall logic as DM-300's rock fall attack, but with more parameters + public static boolean prepRockFallAttack( Char target, Char source, int range, boolean avoidBarricades ){ + final int rockCenter = target.pos; + + int safeCell; + do { + safeCell = rockCenter + PathFinder.NEIGHBOURS8[Random.Int(8)]; + } while (safeCell == source.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() * range - range; + int pos; + for (int y = 0; y < 1+2*range; y++) { + pos = start + Dungeon.level.width() * y; + for (int x = 0; x < 1+2*range; x++) { + if (!Dungeon.level.insideMap(pos)) { + pos++; + continue; + } + if (avoidBarricades){ + boolean barricade = false; + for (int j : PathFinder.NEIGHBOURS9){ + if (Dungeon.level.map[pos+j] == Terrain.BARRICADE + || Dungeon.level.map[pos+j] == Terrain.ENTRANCE){ + barricade = true; + } + } + if (barricade){ + pos++; + continue; + } + } + //add rock cell to pos, if it is not solid, isn't the safecell, and isn't where geomancer is standing + if (!Dungeon.level.solid[pos] + && pos != safeCell + && !(Actor.findChar(pos) instanceof GnollGeomancer) + && Random.Int(Dungeon.level.distance(rockCenter, pos)) == 0) { + rockCells.add(pos); + } + pos++; + } + } + for (int i : rockCells){ + source.sprite.parent.add(new TargetedCell(i, 0xFF0000)); + } + //don't want to overly punish players with slow move or attack speed + Buff.append(source, GnollRockFall.class, GameMath.gate(TICK, (int)Math.ceil(target.cooldown()), 3*TICK)).setRockPositions(rockCells); + + source.sprite.attack(target.pos, new Callback() { + @Override + public void call() { + //do nothing + } + }); + + return true; + } + + public static class GnollRockFall extends DelayedRockFall{ + + @Override + public void affectChar(Char ch) { + Buff.prolong(ch, Paralysis.class, ch instanceof GnollGuard ? 10 : 3); + } + + @Override + public void affectCell(int cell) { + if (Random.Int(3) == 0) { + Level.set(cell, Terrain.MINE_BOULDER); + GameScene.updateMap(cell); + } + } + + } + public static class RockArmor extends ShieldBuff { } + + public static final String HITS = "hits"; + + private static final String ABILITY_COOLDOWN = "ability_cooldown"; + private static final String ROCK_FROM_POS = "rock_from_pos"; + private static final String ROCK_TO_POS = "rock_to_pos"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(HITS, hits); + bundle.put(ABILITY_COOLDOWN, abilityCooldown); + bundle.put(ROCK_FROM_POS, throwingRockFromPos); + bundle.put(ROCK_TO_POS, throwingRockToPos); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + hits = bundle.getInt(HITS); + abilityCooldown = bundle.getInt(ABILITY_COOLDOWN); + throwingRockFromPos = bundle.getInt(ROCK_FROM_POS); + throwingRockToPos = bundle.getInt(ROCK_TO_POS); + } } 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 6a5c9250b..506f0bd6a 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,38 +21,18 @@ 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.messages.Messages; -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.shatteredpixel.shatteredpixeldungeon.utils.GLog; -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 { { @@ -119,54 +99,7 @@ public class GnollSapper extends Mob { @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() { - 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 ); - GLog.n( Messages.get(this, "rock_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); - } - } else { - Dungeon.level.pressCell(rockPath.collisionPos); - } - - next(); - } - } ); + GnollGeomancer.doRockThrowAttack(this, throwingRockFromPos, throwingRockToPos); throwingRockFromPos = -1; throwingRockToPos = -1; @@ -210,12 +143,22 @@ public class GnollSapper extends Mob { // 50/50 to either throw a rock or do rockfall // unless target is next to a barricade, then always try to throw // unless nothing to throw, then always rockfall - if ((targetNextToBarricade || Random.Int(2) == 0) && prepRockAttack(enemy)) { + Ballistica aim = GnollGeomancer.prepRockThrowAttack(enemy, GnollSapper.this); + if (aim != null && (targetNextToBarricade || Random.Int(2) == 0)) { + + throwingRockFromPos = aim.sourcePos; + throwingRockToPos = aim.collisionPos; + + Ballistica warnPath = new Ballistica(aim.sourcePos, aim.collisionPos, Ballistica.STOP_SOLID); + for (int i : warnPath.subPath(0, warnPath.dist)){ + sprite.parent.add(new TargetedCell(i, 0xFF0000)); + } + Dungeon.hero.interrupt(); abilityCooldown = Random.NormalIntRange(4, 6); spend(GameMath.gate(TICK, (int)Math.ceil(enemy.cooldown()), 3*TICK)); return true; - } else if (prepRockFallAttack(enemy)) { + } else if (GnollGeomancer.prepRockFallAttack(enemy, GnollSapper.this, 2, true)) { Dungeon.hero.interrupt(); spend(GameMath.gate(TICK, (int)Math.ceil(enemy.cooldown()), 3*TICK)); abilityCooldown = Random.NormalIntRange(4, 6); @@ -234,122 +177,6 @@ public class GnollSapper extends Mob { } } - 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); - } - } - } - - if (candidateRocks.isEmpty()){ - return false; - } else { - - //throw closest rock to enemy - throwingRockFromPos = candidateRocks.get(0); - for (int i : candidateRocks){ - if (Dungeon.level.trueDistance(i, target.pos) < Dungeon.level.trueDistance(throwingRockFromPos, target.pos)){ - throwingRockFromPos = i; - } - } - 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 - private boolean prepRockFallAttack( Char target ){ - - 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 - || Dungeon.level.map[pos+j] == Terrain.ENTRANCE){ - 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) { - rockCells.add(pos); - } - pos++; - } - } - for (int i : rockCells){ - sprite.parent.add(new TargetedCell(i, 0xFF0000)); - } - //don't want to overly punish players with slow move or attack speed - Buff.append(this, SapperRockFall.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 static class SapperRockFall extends DelayedRockFall { - - @Override - public void affectChar(Char ch) { - Buff.prolong(ch, Paralysis.class, ch instanceof GnollGuard ? 10 : 3); - } - - @Override - public void affectCell(int cell) { - if (Random.Int(3) == 0) { - Level.set(cell, Terrain.MINE_BOULDER); - GameScene.updateMap(cell); - } - } - - } - - public class Boulder extends Item { - { - image = ItemSpriteSheet.GEO_BOULDER; - } - } - public class Wandering extends Mob.Wandering { @Override protected int randomDestination() { 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 4761bb501..d924e2321 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MissileSprite.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MissileSprite.java @@ -22,7 +22,7 @@ package com.shatteredpixel.shatteredpixeldungeon.sprites; import com.shatteredpixel.shatteredpixeldungeon.Dungeon; -import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.GnollSapper; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.GnollGeomancer; import com.shatteredpixel.shatteredpixeldungeon.items.Item; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.SpiritBow; import com.shatteredpixel.shatteredpixeldungeon.items.weapon.melee.Crossbow; @@ -104,7 +104,7 @@ public class MissileSprite extends ItemSprite implements Tweener.Listener { //720 is default - ANGULAR_SPEEDS.put(GnollSapper.Boulder.class, 90); + ANGULAR_SPEEDS.put(GnollGeomancer.Boulder.class, 90); ANGULAR_SPEEDS.put(HeavyBoomerang.class,1440); ANGULAR_SPEEDS.put(Bolas.class, 1440); @@ -152,7 +152,7 @@ public class MissileSprite extends ItemSprite implements Tweener.Listener { updateFrame(); } - if (item instanceof GnollSapper.Boulder){ + if (item instanceof GnollGeomancer.Boulder){ angle = 0; flipHorizontal = false; updateFrame();