v2.5.0: added four new rare cursed wand effects

This commit is contained in:
Evan Debenham
2024-08-15 11:46:57 -04:00
parent 360df46f8e
commit fe66e03e74
11 changed files with 328 additions and 19 deletions

View File

@@ -58,9 +58,6 @@ actors.buffs.adrenalinesurge.desc=A surge of great might, but sadly not permanen
actors.buffs.amok.name=amok
actors.buffs.amok.desc=Amok causes a state of great rage and confusion in its target.\n\nWhen a creature is amoked, they will attack whatever is near them, whether they be friend or foe.\n\nTurns of amok remaining: %s.
actors.buffs.ankhinvulnerability.name=invulnerable
actors.buffs.ankhinvulnerability.desc=Your blessed ankh has expended its energy, granting you some health and a brief period of invulnerability!\n\nTurns remaining: %s.
actors.buffs.arcanearmor.name=arcane armor
actors.buffs.arcanearmor.desc=A thin shield is surrounding you, blocking some of the damage from magical attacks.\n\nYour magical armor is currently boosted by: 0-%d.\n\nTurns until arcane armor weakens: %s.
@@ -253,6 +250,9 @@ actors.buffs.hunger.desc=\n\nHunger slowly increases as you spend time in the du
actors.buffs.invisibility.name=invisible
actors.buffs.invisibility.desc=You are completely blended into the surrounding terrain, making you impossible to see.\n\nWhile you are invisible enemies are unable to attack or follow you. Physical attacks and magical effects (such as scrolls and wands) will immediately cancel invisibility.\n\nTurns of invisibility remaining: %s.
actors.buffs.invulnerability.name=invulnerable
actors.buffs.invulnerability.desc=You are suffuse with great protective power, granting you a brief period of invulnerability!\n\nTurns remaining: %s.
actors.buffs.levitation.name=levitating
actors.buffs.levitation.desc=A magical force is levitating you over the ground, making you feel weightless.\n\nWhile levitating you ignore all ground-based effects. Traps won't trigger, water won't put out fire, plants won't be trampled, roots will miss you, and you will hover right over pits. Be careful, as all these things can come into effect the second the levitation ends!\n\nTurns of levitation remaining: %s.

View File

@@ -1392,6 +1392,8 @@ items.trinkets.trinket$placeholder.name=trinket
###wands
items.wands.cursedwand.ondeath=You were killed by your own %s.
items.wands.cursedwand.nothing=Nothing happens.
items.wands.cursedwand.mass_invuln=Brilliant light erupts from your wand!
items.wands.cursedwand.petrify=You suddenly freeze in place!
items.wands.cursedwand.grass=Grass erupts around you!
items.wands.cursedwand.fire=You smell burning...
items.wands.cursedwand.transmogrify_wand=Your wand transmogrifies into a different item!

View File

@@ -52,6 +52,9 @@ public class ShatteredPixelDungeon extends Game {
com.watabou.utils.Bundle.addAlias(
com.shatteredpixel.shatteredpixeldungeon.actors.mobs.MobSpawner.class,
"com.shatteredpixel.shatteredpixeldungeon.levels.Level$Respawner" );
com.watabou.utils.Bundle.addAlias(
com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invulnerability.class,
"com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AnkhInvulnerability" );
//pre-v2.4.0
com.watabou.utils.Bundle.addAlias(

View File

@@ -30,6 +30,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Adrenaline;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AllyBuff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invulnerability;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ArcaneArmor;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AscensionChallenge;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Barkskin;
@@ -1146,7 +1147,7 @@ public abstract class Char extends Actor {
//similar to isImmune, but only factors in damage.
//Is used in AI decision-making
public boolean isInvulnerable( Class effect ){
return buff(Challenge.SpectatorFreeze.class) != null;
return buff(Challenge.SpectatorFreeze.class) != null || buff(Invulnerability.class) != null;
}
protected HashSet<Property> properties = new HashSet<>();

View File

@@ -99,7 +99,9 @@ public class Burning extends Buff implements Hero.Doom {
int damage = Random.NormalIntRange( 1, 3 + Dungeon.scalingDepth()/4 );
Buff.detach( target, Chill.class);
if (target instanceof Hero && target.buff(TimekeepersHourglass.timeStasis.class) == null) {
if (target instanceof Hero
&& target.buff(TimekeepersHourglass.timeStasis.class) == null
&& target.buff(TimeStasis.class) == null) {
Hero hero = (Hero)target;

View File

@@ -24,16 +24,18 @@ package com.shatteredpixel.shatteredpixeldungeon.actors.buffs;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator;
public class AnkhInvulnerability extends FlavourBuff {
public class Invulnerability extends FlavourBuff {
{
type = Buff.buffType.POSITIVE;
announced = true;
}
public static final float DURATION = 3f;
@Override
public void fx(boolean on) {
if (target.buff(ChampionEnemy.class) != null) return;
if (on) target.sprite.aura( 0xFFFF00 );
else target.sprite.clearAura();
}

View File

@@ -102,7 +102,7 @@ public class SuperNovaTracker extends Buff {
Sample.INSTANCE.playDelayed(Assets.Sounds.BLAST, 0.5f);
PixelScene.shake( 5, 2f );
for (int i = 0; i < Dungeon.level.length(); i++){
if (fieldOfView[i]){
if (fieldOfView[i] && !Dungeon.level.solid[i]){
new Bomb.ConjuredBomb().explode(i); //yes, a bomb at every cell
//this means that something in the blast effectively takes:
//5.33x bomb dmg when fully inside

View File

@@ -0,0 +1,80 @@
/*
* Pixel Dungeon
* Copyright (C) 2012-2015 Oleg Dolya
*
* Shattered Pixel Dungeon
* Copyright (C) 2014-2024 Evan Debenham
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package com.shatteredpixel.shatteredpixeldungeon.actors.buffs;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
//this is largely a copy-paste from timekeeper's hourglass with the artifact-specific code removed
public class TimeStasis extends FlavourBuff {
{
type = Buff.buffType.POSITIVE;
actPriority = BUFF_PRIO-3; //acts after all other buffs, so they are prevented
}
@Override
public boolean attachTo(Char target) {
if (super.attachTo(target)) {
target.invisible++;
target.paralysed++;
target.next();
if (Dungeon.hero != null) {
Dungeon.observe();
}
return true;
} else {
return false;
}
}
@Override
protected void spend(float time) {
super.spend(time);
//don't punish the player for going into stasis frequently
Hunger hunger = Buff.affect(target, Hunger.class);
if (hunger != null && !hunger.isStarving()) {
hunger.affectHunger(cooldown(), true);
}
}
@Override
public void detach() {
if (target.invisible > 0) target.invisible--;
if (target.paralysed > 0) target.paralysed--;
super.detach();
Dungeon.observe();
}
@Override
public void fx(boolean on) {
if (on) target.sprite.add( CharSprite.State.PARALYSED );
else if (target.invisible == 0) target.sprite.remove( CharSprite.State.PARALYSED );
}
}

View File

@@ -34,7 +34,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.SacrificialFire;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AdrenalineSurge;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AnkhInvulnerability;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invulnerability;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.ArtifactRecharge;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AscensionChallenge;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Awareness;
@@ -62,6 +62,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.PhysicalEmpower;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Recharging;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Regeneration;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.SnipersMark;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.TimeStasis;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Vertigo;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.ArmorAbility;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.duelist.Challenge;
@@ -1439,8 +1440,10 @@ public class Hero extends Char {
@Override
public void damage( int dmg, Object src ) {
if (buff(TimekeepersHourglass.timeStasis.class) != null)
if (buff(TimekeepersHourglass.timeStasis.class) != null
|| buff(TimeStasis.class) != null) {
return;
}
//regular damage interrupt, triggers on any damage except specific mild DOT effects
// unless the player recently hit 'continue moving', in which case this is ignored
@@ -1920,7 +1923,8 @@ public class Hero extends Char {
@Override
public boolean add( Buff buff ) {
if (buff(TimekeepersHourglass.timeStasis.class) != null) {
if (buff(TimekeepersHourglass.timeStasis.class) != null
|| buff(TimeStasis.class) != null) {
return false;
}
@@ -1984,7 +1988,7 @@ public class Hero extends Char {
this.HP = HT / 4;
PotionOfHealing.cure(this);
Buff.prolong(this, AnkhInvulnerability.class, AnkhInvulnerability.DURATION);
Buff.prolong(this, Invulnerability.class, Invulnerability.DURATION);
SpellSprite.show(this, SpellSprite.ANKH);
GameScene.flash(0x80FFFF40);
@@ -2253,11 +2257,6 @@ public class Hero extends Char {
return super.isImmune(effect);
}
@Override
public boolean isInvulnerable(Class effect) {
return super.isInvulnerable(effect) || buff(AnkhInvulnerability.class) != null;
}
public boolean search( boolean intentional ) {
if (!isAlive()) return false;

View File

@@ -35,14 +35,19 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Fire;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ParalyticGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Regrowth;
import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.ToxicGas;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invulnerability;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Bless;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Burning;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Frost;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.HeroDisguise;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Hex;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Ooze;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Poison;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Recharging;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.SuperNovaTracker;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.TimeStasis;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.GoldenMimic;
import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mimic;
@@ -55,7 +60,11 @@ import com.shatteredpixel.shatteredpixeldungeon.effects.Lightning;
import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile;
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
import com.shatteredpixel.shatteredpixeldungeon.effects.SpellSprite;
import com.shatteredpixel.shatteredpixeldungeon.effects.Splash;
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.FlameParticle;
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.PoisonParticle;
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.ShadowParticle;
import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SparkParticle;
import com.shatteredpixel.shatteredpixeldungeon.items.Generator;
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.items.bombs.Bomb;
@@ -70,6 +79,8 @@ import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain;
import com.shatteredpixel.shatteredpixeldungeon.levels.traps.CursingTrap;
import com.shatteredpixel.shatteredpixeldungeon.levels.traps.SummoningTrap;
import com.shatteredpixel.shatteredpixeldungeon.mechanics.Ballistica;
import com.shatteredpixel.shatteredpixeldungeon.mechanics.ConeAOE;
import com.shatteredpixel.shatteredpixeldungeon.mechanics.ShadowCaster;
import com.shatteredpixel.shatteredpixeldungeon.messages.Languages;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.plants.Plant;
@@ -85,6 +96,7 @@ import com.watabou.noosa.Game;
import com.watabou.noosa.audio.Sample;
import com.watabou.utils.Callback;
import com.watabou.utils.PathFinder;
import com.watabou.utils.Point;
import com.watabou.utils.Random;
import java.io.IOException;
@@ -459,6 +471,10 @@ public class CursedWand {
RARE_EFFECTS.add(new CurseEquipment());
RARE_EFFECTS.add(new InterFloorTeleport());
RARE_EFFECTS.add(new SummonMonsters());
RARE_EFFECTS.add(new FireBall());
RARE_EFFECTS.add(new ConeOfColors());
RARE_EFFECTS.add(new MassInvuln());
RARE_EFFECTS.add(new Petrify());
}
public static CursedEffect randomRareEffect(){
@@ -576,6 +592,204 @@ public class CursedWand {
}
}
public static class FireBall extends CursedEffect {
@Override
public boolean effect(Item origin, Char user, Ballistica bolt, boolean positiveOnly) {
Point c = Dungeon.level.cellToPoint(bolt.collisionPos);
boolean[] fieldOfView = new boolean[Dungeon.level.length()];
ShadowCaster.castShadow(c.x, c.y, Dungeon.level.width(), fieldOfView, Dungeon.level.solid, 3);
for (int i = 0; i < Dungeon.level.length(); i++){
if (fieldOfView[i] && !Dungeon.level.solid[i]){
//does not directly harm allies
if (positiveOnly && Actor.findChar(i) != null && Actor.findChar(i).alignment == Char.Alignment.ALLY){
continue;
}
CellEmitter.get(i).burst(FlameParticle.FACTORY, 10);
if (Actor.findChar(i) != null){
Char ch = Actor.findChar(i);
Burning burning = Buff.affect(ch, Burning.class);
burning.reignite(ch);
int dmg = Random.NormalIntRange(5 + Dungeon.scalingDepth(), 10 + Dungeon.scalingDepth()*2);
ch.damage(dmg, burning);
}
if (Dungeon.level.flamable[i]){
GameScene.add(Blob.seed(i, 4, Fire.class));
}
}
}
WandOfBlastWave.BlastWave.blast(bolt.collisionPos, 6);
Sample.INSTANCE.play(Assets.Sounds.BLAST);
Sample.INSTANCE.play(Assets.Sounds.BURNING);
return false;
}
}
public static class ConeOfColors extends CursedEffect {
private ConeAOE cone = null;
@Override
public void FX(Item origin, Char user, Ballistica bolt, Callback callback) {
//need to re-do bolt as it should go through chars
bolt = new Ballistica(bolt.sourcePos, bolt.collisionPos, Ballistica.STOP_SOLID);
cone = new ConeAOE( bolt,
8,
90,
Ballistica.STOP_SOLID);
Ballistica longestRay = null;
for (Ballistica ray : cone.outerRays){
if (longestRay == null || ray.dist > longestRay.dist){
longestRay = ray;
}
((MagicMissile)user.sprite.parent.recycle( MagicMissile.class )).reset(
MagicMissile.RAINBOW_CONE,
user.sprite,
ray.path.get(ray.dist),
null
);
}
//final zap at half distance of the longest ray, for timing of the actual effect
MagicMissile.boltFromChar( user.sprite.parent,
MagicMissile.RAINBOW_CONE,
user.sprite,
longestRay.path.get(longestRay.dist/2),
callback );
Sample.INSTANCE.play( Assets.Sounds.ZAP );
}
@Override
public boolean effect(Item origin, Char user, Ballistica bolt, boolean positiveOnly) {
ArrayList<Char> affectedChars = new ArrayList<>();
if (cone == null && Actor.findChar(bolt.collisionPos) != null){
//cone may be null in cases like chaos elemental melee
affectedChars.add(Actor.findChar(bolt.collisionPos));
} else {
for (Integer cell : cone.cells){
if (cell == user.pos) {
continue;
}
if (Actor.findChar(cell) != null){
affectedChars.add(Actor.findChar(cell));
}
}
}
for (Char ch : affectedChars){
//positive only does not harm allies
if (positiveOnly && ch.alignment == Char.Alignment.ALLY){
continue;
} else {
int dmg = Random.NormalIntRange(5 + Dungeon.scalingDepth(), 10 + Dungeon.scalingDepth()*2);
switch (Random.Int(5)){
case 0: default:
Burning burning = Buff.affect(ch, Burning.class);
burning.reignite(ch);
ch.damage(dmg, burning);
ch.sprite.emitter().burst(FlameParticle.FACTORY, 20);
break;
case 1:
ch.damage(dmg, new Frost());
if (ch.isAlive()) Buff.affect(ch, Frost.class, Frost.DURATION);
Splash.at( ch.sprite.center(), 0xFFB2D6FF, 20 );
break;
case 2:
Poison poison = Buff.affect(ch, Poison.class);
poison.set(3 + Dungeon.scalingDepth() / 2);
ch.damage(dmg, poison);
ch.sprite.emitter().burst(PoisonParticle.SPLASH, 20);
break;
case 3:
Ooze ooze = Buff.affect(ch, Ooze.class);
ooze.set(Ooze.DURATION);
ch.damage(dmg, ooze);
Splash.at( ch.sprite.center(), 0x000000, 20 );
break;
case 4:
ch.damage(dmg, new Electricity());
if (ch.isAlive()) Buff.affect(ch, Paralysis.class, Paralysis.DURATION);
ch.sprite.emitter().burst(SparkParticle.FACTORY, 20);
break;
}
if (ch == Dungeon.hero && !ch.isAlive()){
if (user == Dungeon.hero && origin != null) {
Badges.validateDeathFromFriendlyMagic();
Dungeon.fail( origin );
GLog.n( Messages.get( CursedWand.class, "ondeath", origin.name() ) );
} else {
Badges.validateDeathFromEnemyMagic();
Dungeon.fail( user );
}
}
}
}
return true;
}
}
public static class MassInvuln extends CursedEffect {
@Override
public void FX(Item origin, Char user, Ballistica bolt, Callback callback) {
callback.call(); //no vfx
}
@Override
public boolean effect(Item origin, Char user, Ballistica bolt, boolean positiveOnly) {
for (Char ch : Actor.chars()){
Buff.affect(ch, Invulnerability.class, 10f);
Buff.affect(ch, Bless.class, Bless.DURATION);
}
new Flare(5, 48).color(0xFFFF00, true).show(user.sprite, 3f);
GameScene.flash(0x80FFFF40);
Sample.INSTANCE.play(Assets.Sounds.TELEPORT);
GLog.p(Messages.get(CursedWand.class, "mass_invuln"));
return true;
}
}
public static class Petrify extends CursedEffect {
@Override
public boolean valid(Item origin, Char user, Ballistica bolt, boolean positiveOnly) {
return user == Dungeon.hero;
}
@Override
public void FX(Item origin, Char user, Ballistica bolt, Callback callback) {
callback.call(); //no vfx
}
@Override
public boolean effect(Item origin, Char user, Ballistica bolt, boolean positiveOnly) {
Buff.affect(user, TimeStasis.class, 100f);
Sample.INSTANCE.play(Assets.Sounds.TELEPORT);
user.sprite.emitter().burst(Speck.factory(Speck.STEAM), 10);
GLog.w(Messages.get(CursedWand.class, "petrify"));
return true;
}
}
//*************************
//*** Very Rare Effects ***
//*************************

View File

@@ -248,19 +248,21 @@ public class WandOfBlastWave extends DamageWand {
private static final float TIME_TO_FADE = 0.2f;
private float time;
private float size;
public BlastWave(){
super(Effects.get(Effects.Type.RIPPLE));
origin.set(width / 2, height / 2);
}
public void reset(int pos) {
public void reset(int pos, float size) {
revive();
x = (pos % Dungeon.level.width()) * DungeonTilemap.SIZE + (DungeonTilemap.SIZE - width) / 2;
y = (pos / Dungeon.level.width()) * DungeonTilemap.SIZE + (DungeonTilemap.SIZE - height) / 2;
time = TIME_TO_FADE;
this.size = size;
}
@Override
@@ -272,15 +274,19 @@ public class WandOfBlastWave extends DamageWand {
} else {
float p = time / TIME_TO_FADE;
alpha(p);
scale.y = scale.x = (1-p)*3;
scale.y = scale.x = (1-p)*size;
}
}
public static void blast(int pos) {
blast(pos, 3);
}
public static void blast(int pos, float radius) {
Group parent = Dungeon.hero.sprite.parent;
BlastWave b = (BlastWave) parent.recycle(BlastWave.class);
parent.bringToFront(b);
b.reset(pos);
b.reset(pos, radius);
}
}