v0.7.5: fully implemented Tengu abilities and balanced the boss fight

This commit is contained in:
Evan Debenham 2019-09-25 20:08:56 -04:00
parent b9282c42c1
commit c2accf8ae6
5 changed files with 279 additions and 27 deletions

View File

@ -29,7 +29,9 @@ 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.blobs.Fire;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Blindness;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Burning;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
@ -56,6 +58,7 @@ import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSprite;
import com.shatteredpixel.shatteredpixeldungeon.sprites.ItemSpriteSheet;
import com.shatteredpixel.shatteredpixeldungeon.sprites.MissileSprite;
import com.shatteredpixel.shatteredpixeldungeon.sprites.TenguSprite;
import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap;
import com.shatteredpixel.shatteredpixeldungeon.ui.BossHealthBar;
@ -63,13 +66,13 @@ import com.shatteredpixel.shatteredpixeldungeon.utils.BArray;
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.PathFinder;
import com.watabou.utils.PointF;
import com.watabou.utils.Random;
import java.util.HashSet;
//TODO currently has attack/defence stats for testing, need to add those
public class NewTengu extends Mob {
{
@ -77,7 +80,7 @@ public class NewTengu extends Mob {
HP = HT = 160;
EXP = 20;
defenseSkill = 0;
defenseSkill = 15;
HUNTING = new Hunting();
@ -95,17 +98,21 @@ public class NewTengu extends Mob {
@Override
public int damageRoll() {
return Random.NormalIntRange( 0, 0 );
return Random.NormalIntRange( 6, 12 );
}
@Override
public int attackSkill( Char target ) {
return 20;
if (target.invisible > 0){
return 12;
} else {
return 20;
}
}
@Override
public int drRoll() {
return Random.NormalIntRange(0, 0);
return Random.NormalIntRange(0, 5);
}
@Override
@ -238,20 +245,24 @@ public class NewTengu extends Mob {
//otherwise, jump in a larger possible area, as the room is bigger
} else {
do {
newPos = Random.Int(level.length());
} while (
level.solid[newPos] ||
level.distance(newPos, enemy.pos) < 5 ||
level.distance(newPos, enemy.pos) > 7 ||
level.distance(newPos, enemy.pos) < 6 ||
level.distance(newPos, enemy.pos) > 8 ||
level.distance(newPos, pos) < 6 ||
Actor.findChar(newPos) != null);
Actor.findChar(newPos) != null ||
Dungeon.level.heaps.get(newPos) != null);
if (level.heroFOV[pos]) CellEmitter.get( pos ).burst( Speck.factory( Speck.WOOL ), 6 );
sprite.move( pos, newPos );
move( newPos );
if (arenaJumps < 4) arenaJumps++;
if (level.heroFOV[newPos]) CellEmitter.get( newPos ).burst( Speck.factory( Speck.WOOL ), 6 );
Sample.INSTANCE.play( Assets.SND_PUFF );
@ -284,7 +295,7 @@ public class NewTengu extends Mob {
BossHealthBar.assignBoss(this);
if (HP <= HT/2) BossHealthBar.bleed(true);
if (HP == HT) {
yell(Messages.get(this, "notice_mine", Dungeon.hero.givenName()));
yell(Messages.get(this, "notice_gotcha", Dungeon.hero.givenName()));
for (Char ch : Actor.chars()){
if (ch instanceof DriedRose.GhostHero){
GLog.n("\n");
@ -292,26 +303,65 @@ public class NewTengu extends Mob {
}
}
} else {
yell(Messages.get(this, "notice_face", Dungeon.hero.givenName()));
yell(Messages.get(this, "notice_have", Dungeon.hero.givenName()));
}
}
}
{
immunities.add( Blindness.class );
}
private static final String LAST_ABILITY = "last_ability";
private static final String ABILITIES_USED = "abilities_used";
private static final String ARENA_JUMPS = "arena_jumps";
private static final String ABILITY_COOLDOWN = "ability_cooldown";
@Override
public void storeInBundle(Bundle bundle) {
super.storeInBundle(bundle);
bundle.put( LAST_ABILITY, lastAbility );
bundle.put( ABILITIES_USED, abilitiesUsed );
bundle.put( ARENA_JUMPS, arenaJumps );
bundle.put( ABILITY_COOLDOWN, abilityCooldown );
}
@Override
public void restoreFromBundle(Bundle bundle) {
super.restoreFromBundle(bundle);
lastAbility = bundle.getInt( LAST_ABILITY );
abilitiesUsed = bundle.getInt( ABILITIES_USED );
arenaJumps = bundle.getInt( ARENA_JUMPS );
abilityCooldown = bundle.getInt( ABILITY_COOLDOWN );
BossHealthBar.assignBoss(this);
if (HP <= HT/2) BossHealthBar.bleed(true);
}
//don't bother bundling this, as its purely cosmetic
private boolean yelledCoward = false;
//tengu is always hunting
private class Hunting extends Mob.Hunting{
@Override
public boolean act(boolean enemyInFOV, boolean justAlerted) {
if (!enemyInFOV && fieldOfView[enemy.pos]){
if (!yelledCoward) {
yell(Messages.get(NewTengu.class, "coward"));
yelledCoward = true;
}
enemyInFOV = true;
}
enemySeen = enemyInFOV;
if (enemyInFOV && !isCharmedBy( enemy ) && canAttack( enemy )) {
if (canUseAbility()){
return useAbility();
}
return doAttack( enemy );
} else {
@ -336,10 +386,137 @@ public class NewTengu extends Mob {
//***** Tengu abilities. These are expressed in game logic as buffs, blobs, and items *****
//*****************************************************************************************
//so that mobs can also use this
private static Char throwingChar;
private int lastAbility = -1;
private int abilitiesUsed = 0;
private int arenaJumps = 0;
//starts at 2, so one turn and then first ability
private int abilityCooldown = 2;
private static final int BOMB_ABILITY = 0;
private static final int FIRE_ABILITY = 1;
private static final int SHOCKER_ABILITY = 2;
//expects to be called once per turn;
public boolean canUseAbility(){
if (HP > HT/2) return false;
//1 base ability use, plus 2 uses per jump
int targetAbilityUses = 1 + 2*arenaJumps;
//and ane extra 2 use for jumps 3 and 4
targetAbilityUses += Math.max(0, arenaJumps-2);
if (abilitiesUsed >= targetAbilityUses){
return false;
} else {
abilityCooldown--;
if (targetAbilityUses - abilitiesUsed >= 5){
//Very behind in ability uses, use one right away!
abilityCooldown = 0;
} else if (targetAbilityUses - abilitiesUsed >= 3){
//moderately behind in uses, use one every other action.
if (abilityCooldown == -1 || abilityCooldown > 1) abilityCooldown = 1;
} else {
//standard delay before ability use, 1-4 turns
if (abilityCooldown == -1) abilityCooldown = Random.IntRange(1, 4);
}
if (abilityCooldown == 0){
return true;
} else {
return false;
}
}
}
public boolean useAbility(){
boolean abilityUsed = false;
int abilityToUse = -1;
while (!abilityUsed){
if (abilitiesUsed == 0){
abilityToUse = BOMB_ABILITY;
} else if (abilitiesUsed == 1){
abilityToUse = SHOCKER_ABILITY;
} else {
abilityToUse = Random.Int(3);
}
//If we roll the same ability as last time, 9/10 chance to reroll
if (abilityToUse != lastAbility || Random.Int(10) == 0){
switch (abilityToUse){
case BOMB_ABILITY : default:
abilityUsed = throwBomb(NewTengu.this, enemy);
break;
case FIRE_ABILITY:
abilityUsed = throwFire(NewTengu.this, enemy);
break;
case SHOCKER_ABILITY:
abilityUsed = throwShocker(NewTengu.this, enemy);
break;
}
}
}
lastAbility = abilityToUse;
abilitiesUsed++;
spend(2 * TICK);
return lastAbility == FIRE_ABILITY;
}
//******************
//***Bomb Ability***
//******************
//returns true if bomb was thrown
public static boolean throwBomb(final Char thrower, final Char target){
int targetCell = -1;
//Targets closest cell which is adjacent to target, and at least 3 tiles away
for (int i : PathFinder.NEIGHBOURS8){
int cell = target.pos + i;
if (Dungeon.level.distance(cell, thrower.pos) >= 3){
if (targetCell == -1 ||
Dungeon.level.trueDistance(cell, thrower.pos) < Dungeon.level.trueDistance(targetCell, thrower.pos)){
targetCell = cell;
}
}
}
if (targetCell == -1){
return false;
}
final int finalTargetCell = targetCell;
throwingChar = thrower;
final BombAbility.BombItem item = new BombAbility.BombItem();
thrower.sprite.zap(finalTargetCell);
((MissileSprite) thrower.sprite.parent.recycle(MissileSprite.class)).
reset(thrower.sprite,
finalTargetCell,
item,
new Callback() {
@Override
public void call() {
item.onThrow(finalTargetCell);
thrower.next();
}
});
return true;
}
public static class BombAbility extends Buff {
public int bombPos;
@ -419,7 +596,7 @@ public class NewTengu extends Mob {
if (cur[cell] > 0 && off[cell] == 0){
Char ch = Actor.findChar(cell);
if (ch != null){
if (ch != null && !(ch instanceof NewTengu)){
int dmg = Random.NormalIntRange(5 + Dungeon.depth, 10 + Dungeon.depth*2);
dmg -= ch.drRoll();
@ -473,11 +650,15 @@ public class NewTengu extends Mob {
return false;
}
//TODO change for when tengu throws this
@Override
protected void onThrow(int cell) {
super.onThrow(cell);
Buff.append(curUser, BombAbility.class).bombPos = cell;
if (throwingChar != null){
Buff.append(throwingChar, BombAbility.class).bombPos = cell;
throwingChar = null;
} else {
Buff.append(curUser, BombAbility.class).bombPos = cell;
}
}
@Override
@ -491,6 +672,23 @@ public class NewTengu extends Mob {
//***Fire Ability***
//******************
public static boolean throwFire(final Char thrower, final Char target){
Ballistica aim = new Ballistica(thrower.pos, target.pos, Ballistica.WONT_STOP);
for (int i = 0; i < PathFinder.CIRCLE8.length; i++){
if (aim.sourcePos+PathFinder.CIRCLE8[i] == aim.path.get(1)){
thrower.sprite.zap(target.pos);
Buff.append(thrower, NewTengu.FireAbility.class).direction = i;
thrower.sprite.emitter().start(Speck.factory(Speck.STEAM), .03f, 10);
return true;
}
}
return false;
}
public static class FireAbility extends Buff {
public int direction;
@ -593,7 +791,11 @@ public class NewTengu extends Mob {
}
if (cur[cell] > 0 && off[cell] == 0){
Fire.burn( cell );
Char ch = Actor.findChar( cell );
if (ch != null && !ch.isImmune(Fire.class) && !(ch instanceof NewTengu)) {
Buff.affect( ch, Burning.class ).reignite( ch );
}
if (Dungeon.level.flamable[cell]){
Dungeon.level.destroy( cell );
@ -637,6 +839,50 @@ public class NewTengu extends Mob {
//***Shocker Ability***
//*********************
//returns true if shocker was thrown
public static boolean throwShocker(final Char thrower, final Char target){
int targetCell = -1;
//Targets closest cell which is adjacent to target, and not adjacent to thrower or another shocker
for (int i : PathFinder.NEIGHBOURS8){
int cell = target.pos + i;
if (Dungeon.level.distance(cell, thrower.pos) >= 2){
boolean validTarget = true;
for (ShockerAbility s : thrower.buffs(ShockerAbility.class)){
if (Dungeon.level.distance(cell, s.shockerPos) < 2){
validTarget = false;
break;
}
}
if (validTarget && Dungeon.level.trueDistance(cell, thrower.pos) < Dungeon.level.trueDistance(targetCell, thrower.pos)){
targetCell = cell;
}
}
}
if (targetCell == -1){
return false;
}
final int finalTargetCell = targetCell;
throwingChar = thrower;
final ShockerAbility.ShockerItem item = new ShockerAbility.ShockerItem();
thrower.sprite.zap(finalTargetCell);
((MissileSprite) thrower.sprite.parent.recycle(MissileSprite.class)).
reset(thrower.sprite,
finalTargetCell,
item,
new Callback() {
@Override
public void call() {
item.onThrow(finalTargetCell);
thrower.next();
}
});
return true;
}
public static class ShockerAbility extends Buff {
public int shockerPos;
@ -725,7 +971,7 @@ public class NewTengu extends Mob {
if (cur[cell] > 0 && off[cell] == 0){
Char ch = Actor.findChar(cell);
if (ch != null){
if (ch != null && !(ch instanceof NewTengu)){
ch.damage(2 + Dungeon.depth, Electricity.class);
if (ch == Dungeon.hero && !ch.isAlive()) {
@ -766,11 +1012,15 @@ public class NewTengu extends Mob {
return false;
}
//TODO change for when tengu throws this
@Override
protected void onThrow(int cell) {
super.onThrow(cell);
Buff.append(curUser, ShockerAbility.class).shockerPos = cell;
if (throwingChar != null){
Buff.append(throwingChar, ShockerAbility.class).shockerPos = cell;
throwingChar = null;
} else {
Buff.append(curUser, ShockerAbility.class).shockerPos = cell;
}
}
@Override

View File

@ -487,7 +487,10 @@ public class NewPrisonBossLevel extends Level {
clearEntities(tenguCell);
for (Item item : storedItems) {
drop(item, randomTenguCellPos());
if (!(item instanceof NewTengu.BombAbility.BombItem)
&& !(item instanceof NewTengu.ShockerAbility.ShockerItem)) {
drop(item, randomTenguCellPos());
}
}
GameScene.flash(0xFFFFFF);
@ -535,7 +538,7 @@ public class NewPrisonBossLevel extends Level {
FadingTraps f = new FadingTraps();
f.setCoveringArea(mazeCells[i]);
GameScene.add(f, false);
f.startFade(2.5f);
f.startFade( 3f );
Sample.INSTANCE.play(Assets.SND_TELEPORT);
int roomCenter = (mazeCells[i].left + mazeCells[i].right)/2 +
@ -629,7 +632,7 @@ public class NewPrisonBossLevel extends Level {
FadingTraps t = new FadingTraps();
t.setCoveringArea(tenguCell);
GameScene.add(t, false);
t.startFade( 2.5f );
t.startFade( 3f );
}
@Override

View File

@ -33,7 +33,7 @@ public class TenguDartTrap extends PoisonDartTrap {
@Override
protected int poisonAmount() {
return 9; //21 damage total
return 8; //17 damage total
}
@Override

View File

@ -30,8 +30,6 @@ import com.watabou.utils.Callback;
public class TenguSprite extends MobSprite {
private Animation cast;
public TenguSprite() {
super();
@ -48,7 +46,7 @@ public class TenguSprite extends MobSprite {
attack = new Animation( 15, false );
attack.frames( frames, 6, 7, 7, 0 );
cast = attack.clone();
zap = attack.clone();
die = new Animation( 8, false );
die.frames( frames, 8, 9, 10, 10, 10, 10, 10, 10 );
@ -90,7 +88,7 @@ public class TenguSprite extends MobSprite {
}
} );
play( cast );
play( zap );
turnTo( ch.pos , cell );
} else {

View File

@ -585,9 +585,10 @@ actors.mobs.swarm.def_verb=evaded
actors.mobs.swarm.desc=The deadly swarm of flies buzzes angrily. Every non-magical attack will split it into two smaller but equally dangerous swarms.
actors.mobs.newtengu.name=Tengu
actors.mobs.newtengu.notice_mine=You're mine, %s!
actors.mobs.newtengu.notice_face=Face me, %s!
actors.mobs.newtengu.notice_gotcha=Gotcha, %s!
actors.mobs.newtengu.interesting=Let's make this interesting...
actors.mobs.newtengu.notice_have=I have you now, %s!
actors.mobs.newtengu.coward=I can still hear you, Coward!
actors.mobs.newtengu.defeated=Free at last...
actors.mobs.newtengu.rankings_desc=Assassinated by the Tengu
actors.mobs.newtengu.desc=A famous and enigmatic assassin, named for the mask grafted to his face.\n\nTengu is held down with large clasps on his wrists and knees, though he seems to have gotten rid of his chains long ago.\n\nHe will try to use traps, deceptive magic, and precise attacks to eliminate the only thing stopping his escape: you.