v2.2.0: lots more crystal enemy mechanics implementation
This commit is contained in:
@@ -1125,6 +1125,7 @@ actors.mobs.crystalspire.alert=The vibration grows and rumbles through the whole
|
||||
actors.mobs.crystalspire.desc=This gigantic spire of ultra hard crystal is probably the source of all the strange crystalline creatures in this cave. Your regular weapons are ineffective against it, so you'll need to use your pickaxe if you want to destroy it.\n\nBreaking such a big crystal is likely to take a while though, and you probably won't be able to do it undisturbed. The spire itself may even have some means of defending itself as well. _Make sure you're prepared before you start breaking it._
|
||||
|
||||
actors.mobs.crystalwisp.name=crystal wisp
|
||||
actors.mobs.crystalwisp.beam_kill=The beam of light killed you...
|
||||
actors.mobs.crystalwisp.desc=A small angry floating chunk of hardened crystal that's producing a bright glow. While not especially strong, crystal wisps will shoot a damaging beam of light at you if they have a clear shot.\n\nWisps are small enough to easily move through the various crystal outcroppings, but they aren't able to shoot through them.
|
||||
|
||||
actors.mobs.demonspawner.name=demon spawner
|
||||
|
||||
@@ -1222,10 +1222,12 @@ public class Hero extends Char {
|
||||
GameScene.updateMap( action.dst+i );
|
||||
}
|
||||
spendAndNext(TICK);
|
||||
ready();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
spendAndNext(TICK);
|
||||
ready();
|
||||
}
|
||||
|
||||
Dungeon.observe();
|
||||
|
||||
+12
-4
@@ -113,11 +113,17 @@ public class CrystalGuardian extends Mob{
|
||||
|
||||
@Override
|
||||
public float speed() {
|
||||
if (Dungeon.level.openSpace[pos]) {
|
||||
return super.speed();
|
||||
} else {
|
||||
return super.speed()/4f;
|
||||
//crystal guardians move at 1/4 speed when enclosed, but only if enclosed by walls
|
||||
//TODO maybe we want crystals to count?
|
||||
if (!Dungeon.level.openSpace[pos]) {
|
||||
for (int i = 0; i < PathFinder.CIRCLE4.length / 2; i++) {
|
||||
if (Dungeon.level.solid[pos + PathFinder.CIRCLE4[i]] && Dungeon.level.solid[pos + PathFinder.CIRCLE4[i + 2]]
|
||||
&& Dungeon.level.map[pos + PathFinder.CIRCLE4[i]] != Terrain.MINE_CRYSTAL && Dungeon.level.map[pos + PathFinder.CIRCLE4[i + 2]] != Terrain.MINE_CRYSTAL) {
|
||||
return super.speed() / 4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.speed();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,6 +136,8 @@ public class CrystalGuardian extends Mob{
|
||||
Splash.at(pos, 0xFFFFFF, 5);
|
||||
Sample.INSTANCE.play( Assets.Sounds.SHATTER );
|
||||
}
|
||||
//breaking a crystal costs an extra turn
|
||||
spend(TICK);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+250
-7
@@ -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 );
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ public class CrystalWisp extends Mob{
|
||||
if (!enemy.isAlive() && enemy == Dungeon.hero) {
|
||||
Badges.validateDeathFromEnemyMagic();
|
||||
Dungeon.fail( this );
|
||||
GLog.n( Messages.get(this, "bolt_kill") );
|
||||
GLog.n( Messages.get(this, "beam_kill") );
|
||||
}
|
||||
} else {
|
||||
enemy.sprite.showStatus( CharSprite.NEUTRAL, enemy.defenseVerb() );
|
||||
|
||||
Reference in New Issue
Block a user