diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index 6a480a520..1a0a5e4f0 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -695,7 +695,9 @@ actors.hero.spells.sunray.desc=The Cleric fires a ray of blinding light at a tar actors.hero.spells.walloflight.name=wall of light actors.hero.spells.walloflight.short_desc=Creates a wall that blocks enemies. -actors.hero.spells.walloflight.desc=The Paladin creates a wall of solid light directly in front of themselves that's 1 tile thick, %1$d tiles wide, and lasts for 20 turns.\n\nThis wall blocks movement and ranged attacks, but can be seen through. Enemies that are caught in the wall when its created will be pushed back if possible, otherwise they will be caught inside it and be able to exit through either side.\n\nThe wall can be cast in any of the four cardinal or four diagonal directions. Only one wall can exist at a time. +actors.hero.spells.walloflight.desc=The Paladin creates a wall made out of panels of solid light directly in front of themselves which is 1 tile thick, %1$d tiles wide, and lasts for 20 turns.\n\nThis wall acts just like a regular once, except it can be seen through. Enemies that are caught in the wall when its created will be momentarily stunned and pushed back if possible. Anything stuck in the wall will be able to move out of it.\n\nThe wall can be cast in any of the four cardinal or four diagonal directions. If a wall is already active, the spell can be re-used for free to instantly clear the wall. +actors.hero.spells.walloflight.early_end=You dispel the wall of light. +actors.hero.spells.walloflight$lightwall.desc=Shimmering panels are light are blocking passage here. ##main hero actors.hero.hero.name=you diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/WallOfLight.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/WallOfLight.java index b0eeb9d86..e2cf77865 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/WallOfLight.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/WallOfLight.java @@ -21,12 +21,28 @@ package com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells; +import com.shatteredpixel.shatteredpixeldungeon.Assets; import com.shatteredpixel.shatteredpixeldungeon.Dungeon; +import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; +import com.shatteredpixel.shatteredpixeldungeon.actors.Char; +import com.shatteredpixel.shatteredpixeldungeon.actors.blobs.Blob; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; +import com.shatteredpixel.shatteredpixeldungeon.effects.BlobEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile; import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.HolyTome; +import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfBlastWave; +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.ui.HeroIcon; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.audio.Sample; +import com.watabou.utils.PathFinder; public class WallOfLight extends TargetedClericSpell { @@ -49,6 +65,10 @@ public class WallOfLight extends TargetedClericSpell { @Override public float chargeUse(Hero hero) { + if (Dungeon.level.blobs.get(LightWall.class) != null + && Dungeon.level.blobs.get(LightWall.class).volume > 0){ + return 0f; + } return 3f; } @@ -57,8 +77,225 @@ public class WallOfLight extends TargetedClericSpell { return super.canCast(hero) && hero.hasTalent(Talent.WALL_OF_LIGHT); } + @Override + public void onCast(HolyTome tome, Hero hero) { + if (Dungeon.level.blobs.get(LightWall.class) != null + && Dungeon.level.blobs.get(LightWall.class).volume > 0){ + Dungeon.level.blobs.get(LightWall.class).fullyClear(); + GLog.i(Messages.get(this, "early_end")); + return; + } + super.onCast(tome, hero); + } + @Override protected void onTargetSelected(HolyTome tome, Hero hero, Integer target) { + if (target == null){ + return; + } + + if (target == hero.pos){ + GLog.w(Messages.get(this, "invalid_target")); + return; + } + + int closest = hero.pos; + int closestIdx = -1; + + for (int i = 0; i < PathFinder.CIRCLE8.length; i++){ + int ofs = PathFinder.CIRCLE8[i]; + if (Dungeon.level.trueDistance(target, hero.pos+ofs) < Dungeon.level.trueDistance(target, closest)){ + closest = hero.pos+ofs; + closestIdx = i; + } + } + + int leftDirX = 0; + int leftDirY = 0; + + int rightDirX = 0; + int rightDirY = 0; + + int steps = Dungeon.hero.pointsInTalent(Talent.WALL_OF_LIGHT); + + switch (closestIdx){ + case 0: //top left + leftDirX = -1; + leftDirY = 1; + rightDirX = 1; + rightDirY = -1; + break; + case 1: //top + leftDirX = -1; + rightDirX = 1; + leftDirY = rightDirY = 0; + break; + case 2: //top right (left and right DIR are purposefully inverted) + leftDirX = 1; + leftDirY = 1; + rightDirX = -1; + rightDirY = -1; + break; + case 3: //right + leftDirY = -1; + rightDirY = 1; + leftDirX = rightDirX = 0; + break; + case 4: //bottom right (left and right DIR are purposefully inverted) + leftDirX = 1; + leftDirY = -1; + rightDirX = -1; + rightDirY = 1; + break; + case 5: //bottom + leftDirX = 1; + rightDirX = -1; + leftDirY = rightDirY = 0; + break; + case 6: //bottom left + leftDirX = -1; + leftDirY = -1; + rightDirX = 1; + rightDirY = 1; + break; + case 7: //left + leftDirY = -1; + rightDirY = 1; + leftDirX = rightDirX = 0; + break; + } + + if (Dungeon.level.blobs.get(LightWall.class) != null){ + Dungeon.level.blobs.get(LightWall.class).fullyClear(); + } + + boolean placedWall = false; + + int knockBackDir = PathFinder.CIRCLE8[closestIdx]; + + //if all 3 tiles infront of Paladin are blocked, assume cast was in error and cancel + if (Dungeon.level.solid[closest] + && Dungeon.level.solid[hero.pos + PathFinder.CIRCLE8[(closestIdx+1)%8]] + && Dungeon.level.solid[hero.pos + PathFinder.CIRCLE8[(closestIdx+7)%8]]){ + GLog.w(Messages.get(this, "invalid_target")); + return; + } + + //process early so that cost is calculated before walls are added + onSpellCast(tome, hero); + + placeWall(closest, knockBackDir); + + int leftPos = closest; + int rightPos = closest; + + //iterate to the left and right, placing walls as we go + for (int i = 0; i < steps; i++) { + if (leftDirY != 0) { + leftPos += leftDirY * Dungeon.level.width(); + placeWall(leftPos, knockBackDir); + } + if (leftDirX != 0) { + leftPos += leftDirX; + placeWall(leftPos, knockBackDir); + } + + if (rightDirX != 0) { + rightPos += rightDirX; + placeWall(rightPos, knockBackDir); + } + if (rightDirY != 0) { + rightPos += rightDirY * Dungeon.level.width(); + placeWall(rightPos, knockBackDir); + } + } + + Sample.INSTANCE.play(Assets.Sounds.CHARGEUP); + + hero.sprite.zap(closest); + Dungeon.hero.spendAndNext(1f); + } + + private void placeWall( int pos, int knockbackDIR){ + if (!Dungeon.level.solid[pos]) { + GameScene.add(Blob.seed(pos, 20, LightWall.class)); + + Char ch = Actor.findChar(pos); + if (ch != null && ch.alignment == Char.Alignment.ENEMY){ + WandOfBlastWave.throwChar(ch, new Ballistica(pos, pos+knockbackDIR, Ballistica.PROJECTILE), 1, false, false, WallOfLight.INSTANCE); + Buff.affect(ch, Paralysis.class, ch.cooldown()); + } + } + } + + public static class LightWall extends Blob { + + @Override + protected void evolve() { + + int cell; + + Level l = Dungeon.level; + for (int i = area.left; i < area.right; i++){ + for (int j = area.top; j < area.bottom; j++){ + cell = i + j*l.width(); + off[cell] = cur[cell] > 0 ? cur[cell] - 1 : 0; + + volume += off[cell]; + + l.solid[cell] = off[cell] > 0 || (Terrain.flags[l.map[cell]] & Terrain.SOLID) != 0; + l.passable[cell] = off[cell] == 0 && (Terrain.flags[l.map[cell]] & Terrain.PASSABLE) != 0; + l.avoid[cell] = off[cell] == 0 && (Terrain.flags[l.map[cell]] & Terrain.AVOID) != 0; + } + } + } + + @Override + public void seed(Level level, int cell, int amount) { + super.seed(level, cell, amount); + level.solid[cell] = cur[cell] > 0 || (Terrain.flags[level.map[cell]] & Terrain.SOLID) != 0; + level.passable[cell] = cur[cell] == 0 && (Terrain.flags[level.map[cell]] & Terrain.PASSABLE) != 0; + level.avoid[cell] = cur[cell] == 0 && (Terrain.flags[level.map[cell]] & Terrain.AVOID) != 0; + } + + @Override + public void clear(int cell) { + super.clear(cell); + if (cur == null) return; + Level l = Dungeon.level; + l.solid[cell] = cur[cell] > 0 || (Terrain.flags[l.map[cell]] & Terrain.SOLID) != 0; + l.passable[cell] = cur[cell] == 0 && (Terrain.flags[l.map[cell]] & Terrain.PASSABLE) != 0; + l.avoid[cell] = cur[cell] == 0 && (Terrain.flags[l.map[cell]] & Terrain.AVOID) != 0; + } + + @Override + public void fullyClear() { + super.fullyClear(); + Dungeon.level.buildFlagMaps(); + } + + @Override + public void onBuildFlagMaps(Level l) { + if (volume > 0){ + for (int i=0; i < l.length(); i++) { + l.solid[i] = l.solid[i] || cur[i] > 0; + l.passable[i] = l.passable[i] && cur[i] == 0; + l.avoid[i] = l.avoid[i] && cur[i] == 0; + } + } + } + + @Override + public void use(BlobEmitter emitter) { + super.use( emitter ); + emitter.pour( MagicMissile.WhiteParticle.WALL, 0.02f ); + } + + @Override + public String tileDesc() { + return Messages.get(this, "desc"); + } } + } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/MagicMissile.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/MagicMissile.java index 0dcb151cc..3faa772fe 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/MagicMissile.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/MagicMissile.java @@ -509,6 +509,17 @@ public class MagicMissile extends Emitter { return true; } }; + + public static final Emitter.Factory WALL = new Factory() { + @Override + public void emit( Emitter emitter, int index, float x, float y ) { + ((WhiteParticle)emitter.recycle( WhiteParticle.class )).resetWall( x, y ); + } + @Override + public boolean lightMode() { + return true; + } + }; public WhiteParticle() { super(); @@ -532,6 +543,16 @@ public class MagicMissile extends Emitter { reset(x, y); hardlight(r, g, b); } + + public void resetWall( float x, float y){ + reset(x, y); + + left = lifespan = 2f; + + this.x = Math.round(x/4)*4; + this.y = Math.round(y/4)*4 - 6; + this.x += Math.round(this.y % 16)/4f - 2; + } @Override public void update() {