|
|
|
|
@@ -26,14 +26,24 @@ import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Blindness;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Dread;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invisibility;
|
|
|
|
|
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.actors.mobs.npcs.Blacksmith;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.effects.Pushing;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.effects.Splash;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.effects.TargetedCell;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.items.quest.Pickaxe;
|
|
|
|
|
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.CrystalSpireSprite;
|
|
|
|
|
import com.shatteredpixel.shatteredpixeldungeon.ui.BossHealthBar;
|
|
|
|
|
@@ -41,12 +51,17 @@ 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.GameMath;
|
|
|
|
|
import com.watabou.utils.PathFinder;
|
|
|
|
|
import com.watabou.utils.Random;
|
|
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
|
|
|
|
|
public class CrystalSpire extends Mob {
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
HP = HT = 200;
|
|
|
|
|
//this translates to roughly 33/27/23/20/18/16 pickaxe hits at +0/1/2/3/4/5
|
|
|
|
|
HP = HT = 300;
|
|
|
|
|
spriteClass = CrystalSpireSprite.class;
|
|
|
|
|
|
|
|
|
|
state = PASSIVE;
|
|
|
|
|
@@ -58,13 +73,192 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
properties.add(Property.INORGANIC);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//TODO this fight needs some mechanics and balance tuning now
|
|
|
|
|
|
|
|
|
|
private float abilityCooldown;
|
|
|
|
|
private static final int ABILITY_CD = 15;
|
|
|
|
|
|
|
|
|
|
private ArrayList<ArrayList<Integer>> targetedCells = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected boolean act() {
|
|
|
|
|
alerted = false;
|
|
|
|
|
return super.act();
|
|
|
|
|
//char logic
|
|
|
|
|
if (fieldOfView == null || fieldOfView.length != Dungeon.level.length()){
|
|
|
|
|
fieldOfView = new boolean[Dungeon.level.length()];
|
|
|
|
|
}
|
|
|
|
|
Dungeon.level.updateFieldOfView( this, fieldOfView );
|
|
|
|
|
|
|
|
|
|
throwItems();
|
|
|
|
|
|
|
|
|
|
sprite.hideAlert();
|
|
|
|
|
sprite.hideLost();
|
|
|
|
|
|
|
|
|
|
//mob logic
|
|
|
|
|
enemy = Dungeon.hero;
|
|
|
|
|
|
|
|
|
|
//crystal can still track an invisible hero
|
|
|
|
|
enemySeen = enemy.isAlive() && fieldOfView[enemy.pos];
|
|
|
|
|
//end of char/mob logic
|
|
|
|
|
|
|
|
|
|
if (hits < 3 || !enemySeen){
|
|
|
|
|
spend(TICK);
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
if (!targetedCells.isEmpty()){
|
|
|
|
|
|
|
|
|
|
ArrayList<Integer> cellsToAttack = targetedCells.remove(0);
|
|
|
|
|
|
|
|
|
|
for (int i : cellsToAttack){
|
|
|
|
|
|
|
|
|
|
//TODO would be nice to find a way to crystal these cells
|
|
|
|
|
if(i == pos+1 || i == pos-1 || i == pos-Dungeon.level.width() || i == pos-2*Dungeon.level.width()){
|
|
|
|
|
continue; //don't spawn crystals in these locations
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Char ch = Actor.findChar(i);
|
|
|
|
|
if (ch instanceof CrystalGuardian || ch instanceof CrystalSpire){
|
|
|
|
|
continue; //don't spawn crystals on these chars
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Level.set(i, Terrain.MINE_CRYSTAL);
|
|
|
|
|
GameScene.updateMap(i);
|
|
|
|
|
|
|
|
|
|
Splash.at(i, 0xFFFFFF, 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i : cellsToAttack){
|
|
|
|
|
Char ch = Actor.findChar(i);
|
|
|
|
|
|
|
|
|
|
if (ch != null && !(ch instanceof CrystalWisp || ch instanceof CrystalGuardian || ch instanceof CrystalSpire)){
|
|
|
|
|
ch.damage(10, CrystalSpire.this);
|
|
|
|
|
|
|
|
|
|
int movePos = i;
|
|
|
|
|
for (int j : PathFinder.NEIGHBOURS8){
|
|
|
|
|
if (!Dungeon.level.solid[i+j] && Actor.findChar(i+j) == null &&
|
|
|
|
|
Dungeon.level.trueDistance(i+j, pos) > Dungeon.level.trueDistance(movePos, pos)){
|
|
|
|
|
movePos = i+j;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (movePos != i){
|
|
|
|
|
Actor.add(new Pushing(ch, i, movePos));
|
|
|
|
|
ch.pos = movePos;
|
|
|
|
|
Dungeon.level.occupyCell(ch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//PixelScene.shake( 3, 0.7f );
|
|
|
|
|
Sample.INSTANCE.play( Assets.Sounds.SHATTER );
|
|
|
|
|
|
|
|
|
|
if (!targetedCells.isEmpty()){
|
|
|
|
|
for (int i : targetedCells.get(0)){
|
|
|
|
|
sprite.parent.add(new TargetedCell(i, 0xFF0000));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (abilityCooldown <= 0){
|
|
|
|
|
|
|
|
|
|
if (Random.Int(2) == 0) {
|
|
|
|
|
diamondAOEAttack();
|
|
|
|
|
} else {
|
|
|
|
|
lineAttack();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i : targetedCells.get(0)){
|
|
|
|
|
sprite.parent.add(new TargetedCell(i, 0xFF0000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
abilityCooldown += ABILITY_CD;
|
|
|
|
|
|
|
|
|
|
spend(GameMath.gate(TICK, Dungeon.hero.cooldown(), 3*TICK));
|
|
|
|
|
} else {
|
|
|
|
|
abilityCooldown -= 1;
|
|
|
|
|
spend(TICK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//TODO just whaling on this thing is boring, it has to do something in retaliation other than aggroing guardians
|
|
|
|
|
private void diamondAOEAttack(){
|
|
|
|
|
targetedCells.clear();
|
|
|
|
|
|
|
|
|
|
ArrayList<Integer> aoeCells = new ArrayList<>();
|
|
|
|
|
aoeCells.add(pos);
|
|
|
|
|
aoeCells.addAll(spreadDiamondAOE(aoeCells));
|
|
|
|
|
targetedCells.add(new ArrayList<>(aoeCells));
|
|
|
|
|
|
|
|
|
|
if (HP < 2*HT/3f){
|
|
|
|
|
aoeCells.addAll(spreadDiamondAOE(aoeCells));
|
|
|
|
|
targetedCells.add(new ArrayList<>(aoeCells));
|
|
|
|
|
if (HP < HT/3f) {
|
|
|
|
|
aoeCells.addAll(spreadDiamondAOE(aoeCells));
|
|
|
|
|
targetedCells.add(aoeCells);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ArrayList<Integer> spreadDiamondAOE(ArrayList<Integer> currentCells){
|
|
|
|
|
ArrayList<Integer> spreadCells = new ArrayList<>();
|
|
|
|
|
for (int i : currentCells){
|
|
|
|
|
for (int j : PathFinder.NEIGHBOURS4){
|
|
|
|
|
if (!Dungeon.level.solid[i+j] && !spreadCells.contains(i+j) && !currentCells.contains(i+j)){
|
|
|
|
|
spreadCells.add(i+j);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (int i : spreadCells.toArray(new Integer[0])){
|
|
|
|
|
for (int j : PathFinder.NEIGHBOURS4){
|
|
|
|
|
if ((!Dungeon.level.solid[i+j] || Dungeon.level.map[i+j] == Terrain.MINE_CRYSTAL)
|
|
|
|
|
&& !spreadCells.contains(i+j) && !currentCells.contains(i+j)){
|
|
|
|
|
spreadCells.add(i+j);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return spreadCells;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void lineAttack(){
|
|
|
|
|
targetedCells.clear();
|
|
|
|
|
|
|
|
|
|
ArrayList<Integer> lineCells = new ArrayList<>();
|
|
|
|
|
Ballistica aim = new Ballistica(pos, Dungeon.hero.pos, Ballistica.WONT_STOP);
|
|
|
|
|
for (int i : aim.subPath(1, 7)){
|
|
|
|
|
if (!Dungeon.level.solid[i] || Dungeon.level.map[i] == Terrain.MINE_CRYSTAL){
|
|
|
|
|
lineCells.add(i);
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetedCells.add(new ArrayList<>(lineCells));
|
|
|
|
|
if (HP < 2*HT/3f){
|
|
|
|
|
lineCells.addAll(spreadAOE(lineCells));
|
|
|
|
|
targetedCells.add(new ArrayList<>(lineCells));
|
|
|
|
|
if (HP < HT/3f) {;
|
|
|
|
|
lineCells.addAll(spreadAOE(lineCells));
|
|
|
|
|
targetedCells.add(lineCells);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private ArrayList<Integer> spreadAOE(ArrayList<Integer> currentCells){
|
|
|
|
|
ArrayList<Integer> spreadCells = new ArrayList<>();
|
|
|
|
|
for (int i : currentCells){
|
|
|
|
|
for (int j : PathFinder.NEIGHBOURS8){
|
|
|
|
|
if ((!Dungeon.level.solid[i+j] || Dungeon.level.map[i+j] == Terrain.MINE_CRYSTAL)
|
|
|
|
|
&& !spreadCells.contains(i+j) && !currentCells.contains(i+j)){
|
|
|
|
|
spreadCells.add(i+j);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return spreadCells;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void beckon(int cell) {
|
|
|
|
|
@@ -84,6 +278,11 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
super.damage(dmg, src);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean add( Buff buff ) {
|
|
|
|
|
return false; //immune to all buffs and debuffs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int hits = 0;
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
@@ -99,10 +298,12 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
Dungeon.hero.sprite.attack(pos, new Callback() {
|
|
|
|
|
@Override
|
|
|
|
|
public void call() {
|
|
|
|
|
//does its own special damage calculation that's only influenced by pickaxe level
|
|
|
|
|
int dmg = Random.NormalIntRange(3+p.buffedLvl(), 15+3*p.buffedLvl());
|
|
|
|
|
//does its own special damage calculation that's only influenced by pickaxe level and augment
|
|
|
|
|
//we pretend the spire is the owner here so that properties like hero str or or other equipment do not factor in
|
|
|
|
|
int dmg = p.damageRoll(CrystalSpire.this);
|
|
|
|
|
|
|
|
|
|
damage(dmg, p);
|
|
|
|
|
abilityCooldown -= dmg/10f;
|
|
|
|
|
sprite.bloodBurstA(Dungeon.hero.sprite.center(), dmg);
|
|
|
|
|
sprite.flash();
|
|
|
|
|
|
|
|
|
|
@@ -114,6 +315,21 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
Sample.INSTANCE.playDelayed(Assets.Sounds.ROCKS, 0.1f);
|
|
|
|
|
PixelScene.shake( 3, 0.7f );
|
|
|
|
|
Blacksmith.Quest.beatBoss();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < Dungeon.level.length(); i++){
|
|
|
|
|
if (fieldOfView[i] && Dungeon.level.map[i] == Terrain.MINE_CRYSTAL){
|
|
|
|
|
Level.set(i, Terrain.EMPTY);
|
|
|
|
|
GameScene.updateMap(i);
|
|
|
|
|
Splash.at(i, 0xFFFFFF, 5);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (Char ch : Actor.chars()){
|
|
|
|
|
if (ch instanceof CrystalGuardian){
|
|
|
|
|
ch.damage(ch.HT, this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hits++;
|
|
|
|
|
@@ -143,7 +359,8 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Dungeon.hero.spendAndNext(Actor.TICK);
|
|
|
|
|
Invisibility.dispel(Dungeon.hero);
|
|
|
|
|
Dungeon.hero.spendAndNext(p.delayFactor(CrystalSpire.this));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
@@ -175,11 +392,23 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
public static final String SPRITE = "sprite";
|
|
|
|
|
public static final String HITS = "hits";
|
|
|
|
|
|
|
|
|
|
public static final String ABILITY_COOLDOWN = "ability_cooldown";
|
|
|
|
|
public static final String TARGETED_CELLS = "targeted_cells";
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void storeInBundle(Bundle bundle) {
|
|
|
|
|
super.storeInBundle(bundle);
|
|
|
|
|
bundle.put(SPRITE, spriteClass);
|
|
|
|
|
bundle.put(HITS, hits);
|
|
|
|
|
|
|
|
|
|
bundle.put(ABILITY_COOLDOWN, abilityCooldown);
|
|
|
|
|
for (int i = 0; i < targetedCells.size(); i++){
|
|
|
|
|
int[] bundleArr = new int[targetedCells.get(i).size()];
|
|
|
|
|
for (int j = 0; j < targetedCells.get(i).size(); j++){
|
|
|
|
|
bundleArr[j] = targetedCells.get(i).get(j);
|
|
|
|
|
}
|
|
|
|
|
bundle.put(TARGETED_CELLS+i, bundleArr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
@@ -187,9 +416,23 @@ public class CrystalSpire extends Mob {
|
|
|
|
|
super.restoreFromBundle(bundle);
|
|
|
|
|
spriteClass = bundle.getClass(SPRITE);
|
|
|
|
|
hits = bundle.getInt(HITS);
|
|
|
|
|
|
|
|
|
|
abilityCooldown = bundle.getFloat(ABILITY_COOLDOWN);
|
|
|
|
|
targetedCells.clear();
|
|
|
|
|
int i = 0;
|
|
|
|
|
while (bundle.contains(TARGETED_CELLS+i)){
|
|
|
|
|
ArrayList<Integer> targets = new ArrayList<>();
|
|
|
|
|
for (int j : bundle.getIntArray(TARGETED_CELLS+i)){
|
|
|
|
|
targets.add(j);
|
|
|
|
|
}
|
|
|
|
|
targetedCells.add(targets);
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
immunities.add( Blindness.class );
|
|
|
|
|
|
|
|
|
|
immunities.add( Paralysis.class );
|
|
|
|
|
immunities.add( Amok.class );
|
|
|
|
|
immunities.add( Sleep.class );
|
|
|
|
|
|