v2.2.0: lots more crystal enemy mechanics implementation

This commit is contained in:
Evan Debenham
2023-09-22 15:53:48 -04:00
parent 3eec9c8d6e
commit 069f2fdbc2
5 changed files with 266 additions and 12 deletions

View File

@@ -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

View File

@@ -1222,10 +1222,12 @@ public class Hero extends Char {
GameScene.updateMap( action.dst+i );
}
spendAndNext(TICK);
ready();
}
});
} else {
spendAndNext(TICK);
ready();
}
Dungeon.observe();

View File

@@ -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);
}
}

View File

@@ -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 );

View File

@@ -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() );