From 069f2fdbc2ade2b270d2af25c16753108c87f8ab Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 22 Sep 2023 15:53:48 -0400 Subject: [PATCH] v2.2.0: lots more crystal enemy mechanics implementation --- .../assets/messages/actors/actors.properties | 1 + .../actors/hero/Hero.java | 2 + .../actors/mobs/CrystalGuardian.java | 16 +- .../actors/mobs/CrystalSpire.java | 257 +++++++++++++++++- .../actors/mobs/CrystalWisp.java | 2 +- 5 files changed, 266 insertions(+), 12 deletions(-) diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index 187186c91..fe9d208cd 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -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 diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java index d756fb059..87e1bcd30 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Hero.java @@ -1222,10 +1222,12 @@ public class Hero extends Char { GameScene.updateMap( action.dst+i ); } spendAndNext(TICK); + ready(); } }); } else { spendAndNext(TICK); + ready(); } Dungeon.observe(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalGuardian.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalGuardian.java index ce245516a..f94887a92 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalGuardian.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalGuardian.java @@ -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); } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalSpire.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalSpire.java index 0b1191ea2..580cf03d0 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalSpire.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalSpire.java @@ -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> 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 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 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 spreadDiamondAOE(ArrayList currentCells){ + ArrayList 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 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 spreadAOE(ArrayList currentCells){ + ArrayList 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 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 ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalWisp.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalWisp.java index 13de3753d..5d55c93db 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalWisp.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/CrystalWisp.java @@ -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() );