diff --git a/core/src/main/assets/interfaces/hero_icons.png b/core/src/main/assets/interfaces/hero_icons.png index cb185a633..1c3577925 100644 Binary files a/core/src/main/assets/interfaces/hero_icons.png and b/core/src/main/assets/interfaces/hero_icons.png differ diff --git a/core/src/main/assets/interfaces/talent_icons.png b/core/src/main/assets/interfaces/talent_icons.png index f3e7ab828..78d9b3d66 100644 Binary files a/core/src/main/assets/interfaces/talent_icons.png and b/core/src/main/assets/interfaces/talent_icons.png differ diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index f9cd577fa..3041f0ceb 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -554,8 +554,15 @@ actors.hero.spells.cleanse.desc=The Cleric instantly clears all harmful effects actors.hero.spells.clericspell.prompt=Choose a target actors.hero.spells.clericspell.no_target=There is no target there. +actors.hero.spells.clericspell.invalid_target=You can't target that location. actors.hero.spells.clericspell.charge_cost=Charge cost: %d +actors.hero.spells.hallowedground.name=hallowed ground +actors.hero.spells.hallowedground.prompt=choose a location +actors.hero.spells.hallowedground.short_desc=Heals allies, slows enemies, and spreads grass in an AOE. +actors.hero.spells.hallowedground.desc=The Priest focuses their divine magic into the ground nearby, creating a %1$dx%1$d area of hallowed terrain for 20 turns.\n\nWhen it is cast, this spell heals all allies for 10 HP (healthy allies and the Priest get shielding), briefly roots enemies, and spreads short grass.\n\nThe hallowed terrain heals allies for 1 HP per turn (healthy allies and the Priest get shielding), cripples enemies, and randomly causes tall grass to grow.\n\nHallowed terrain is destroyed by fire, and will produce furrowed grass if passive regen effects are disabled or the Priest has not gained exp in a while. +actors.hero.spells.hallowedground$hallowedterrain.desc=The ground has been hallowed here. Hallowed ground slowly enemies, heals allies, and causes grass to spread. + actors.hero.spells.holyintuition.name=holy intuition actors.hero.spells.holyintuition.prompt=choose an item actors.hero.spells.holyintuition.cursed=You sense malevolent magic lurking within this item. @@ -1082,8 +1089,8 @@ actors.hero.talent.light_reading.desc=_+1:_ The Cleric can use their holy tome w actors.hero.talent.holy_lance.title=Holy Lance actors.hero.talent.holy_lance.desc=_+1:_ The Priest can cast _Holy Lance,_ a devastating spell that deals _30-55 damage_ at the cost of 4 charges.\n\n_+2:_ The Priest can cast _Holy Lance,_ a devastating spell that deals _45-83 damage_ at the cost of 4 charges.\n\n_+3:_ The Priest can cast _Holy Lance,_ a devastating spell that deals _60-110 damage_ at the cost of 4 charges.\n\nHoly Lance always deals maximum damage to demonic and undead foes. Holy Lance has a 50 turn cooldown before it can be cast again. -actors.hero.talent.priestt3b.title=Unknown -actors.hero.talent.priestt3b.desc=This talent hasn't been implemented yet, it currently does nothing. +actors.hero.talent.hallowed_ground.title=Hallowed Ground +actors.hero.talent.hallowed_ground.desc=_+1:_ The Priest can cast _Hallowed Ground,_ a spell that hallows terrain in a _3x3 area_ for 20 turns, at the cost of 2 charges.\n\n_+2:_ The Priest can cast _Hallowed Ground,_ a spell that hallows terrain in a _5x5 area_ for 20 turns, at the cost of 2 charges.\n\n_+3:_ The Priest can cast _Hallowed Ground,_ a spell that hallows terrain in a _7x7 area_ for 20 turns, at the cost of 2 charges.\n\nWhen it is cast, Hallowed Ground heals allies for 10 HP, briefly roots enemies, and spreads short grass. Afterward, it slowly heals allies, cripples enemies, and causes tall grass to randomly grow. Hallowed ground grants shielding to the Priest instead of healing and is destroyed by fire. actors.hero.talent.priestt3c.title=Unknown actors.hero.talent.priestt3c.desc=This talent hasn't been implemented yet, it currently does nothing. 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 eab21699f..73616c0bf 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 @@ -70,6 +70,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.duelist.Ch import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.duelist.ElementalStrike; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.huntress.NaturesPower; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.warrior.Endure; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.HallowedGround; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mimic; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Monk; @@ -1907,6 +1908,12 @@ public class Hero extends Char { buff(ElementalStrike.ElementalStrikeFurrowCounter.class).detach(); } } + if (buff(HallowedGround.HallowedFurrowTracker.class) != null){ + buff(ElementalStrike.ElementalStrikeFurrowCounter.class).countDown(percent*5f); + if (buff(ElementalStrike.ElementalStrikeFurrowCounter.class).count() <= 0){ + buff(ElementalStrike.ElementalStrikeFurrowCounter.class).detach(); + } + } } boolean levelUp = false; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Talent.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Talent.java index 2f78e811f..fcaea8a65 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Talent.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/Talent.java @@ -181,7 +181,7 @@ public enum Talent { //Cleric T3 CLEANSE(169, 3), LIGHT_READING(170, 3), //Priest T3 - HOLY_LANCE(171, 3), PRIESTT3B(172, 3), PRIESTT3C(173, 3), + HOLY_LANCE(171, 3), HALLOWED_GROUND(172, 3), PRIESTT3C(173, 3), //Paladin T3 PALADINT3A(174, 3), PALADINT3B(175, 3), PALADINT3C(176, 3), //Cleric A1 T4 @@ -1019,7 +1019,7 @@ public enum Talent { Collections.addAll(tierTalents, UNENCUMBERED_SPIRIT, MONASTIC_VIGOR, COMBINED_ENERGY); break; case PRIEST: - Collections.addAll(tierTalents, HOLY_LANCE, PRIESTT3B, PRIESTT3C); + Collections.addAll(tierTalents, HOLY_LANCE, HALLOWED_GROUND, PRIESTT3C); break; case PALADIN: Collections.addAll(tierTalents, PALADINT3A, PALADINT3B, PALADINT3C); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/BlessSpell.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/BlessSpell.java index 889df3ffa..8324c8949 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/BlessSpell.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/BlessSpell.java @@ -76,12 +76,15 @@ public class BlessSpell extends TargetedClericSpell { int totalHeal = 5 + 10*hero.pointsInTalent(Talent.BLESS); if (ch.HT - ch.HP < totalHeal){ int barrier = totalHeal - (ch.HT - ch.HP); + barrier = Math.max(barrier, 0); if (ch.HP != ch.HT) { ch.HP = ch.HT; ch.sprite.showStatusWithIcon(CharSprite.POSITIVE, Integer.toString(totalHeal - barrier), FloatingText.HEALING); } - Buff.affect(ch, Barrier.class).setShield(barrier); - ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, Integer.toString(barrier), FloatingText.SHIELDING ); + if (barrier > 0) { + Buff.affect(ch, Barrier.class).setShield(barrier); + ch.sprite.showStatusWithIcon(CharSprite.POSITIVE, Integer.toString(barrier), FloatingText.SHIELDING); + } } else { ch.HP = ch.HP + totalHeal; ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, Integer.toString(totalHeal), FloatingText.HEALING ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/ClericSpell.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/ClericSpell.java index 72ee77918..5612e8178 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/ClericSpell.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/ClericSpell.java @@ -124,6 +124,10 @@ public abstract class ClericSpell { spells.add(HolyLance.INSTANCE); } + if (cleric.hasTalent(Talent.HALLOWED_GROUND)){ + spells.add(HallowedGround.INSTANCE); + } + } return spells; @@ -143,6 +147,7 @@ public abstract class ClericSpell { spells.add(Cleanse.INSTANCE); spells.add(Radiance.INSTANCE); spells.add(HolyLance.INSTANCE); + spells.add(HallowedGround.INSTANCE); return spells; } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/HallowedGround.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/HallowedGround.java new file mode 100644 index 000000000..4395f0219 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/HallowedGround.java @@ -0,0 +1,212 @@ +/* + * 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 + */ + +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.blobs.Fire; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Barrier; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.CounterBuff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Cripple; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Regeneration; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Roots; +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.CellEmitter; +import com.shatteredpixel.shatteredpixeldungeon.effects.FloatingText; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.LeafParticle; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.ShaftParticle; +import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.HolyTome; +import com.shatteredpixel.shatteredpixeldungeon.levels.Level; +import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; +import com.shatteredpixel.shatteredpixeldungeon.ui.HeroIcon; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.audio.Sample; +import com.watabou.utils.BArray; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; + +public class HallowedGround extends TargetedClericSpell { + + public static final HallowedGround INSTANCE = new HallowedGround(); + + @Override + public int icon() { + return HeroIcon.HALLOWED_GROUND; + } + + @Override + public float chargeUse(Hero hero) { + return 2; + } + + @Override + protected void onTargetSelected(HolyTome tome, Hero hero, Integer target) { + + if (target == null){ + return; + } + + if (Dungeon.level.solid[target] || !Dungeon.level.heroFOV[target]){ + GLog.w(Messages.get(this, "invalid_target")); + return; + } + + PathFinder.buildDistanceMap(target, BArray.not(Dungeon.level.solid, null), hero.pointsInTalent(Talent.HALLOWED_GROUND)); + for (int i = 0; i < Dungeon.level.length(); i++){ + if (PathFinder.distance[i] != Integer.MAX_VALUE){ + int c = Dungeon.level.map[i]; + if (c == Terrain.EMPTY || c == Terrain.EMBERS || c == Terrain.EMPTY_DECO) { + Level.set( i, Terrain.GRASS); + GameScene.updateMap( i ); + CellEmitter.get(i).burst(LeafParticle.LEVEL_SPECIFIC, 2); + } + GameScene.add(Blob.seed(i, 20, HallowedTerrain.class)); + CellEmitter.get(i).burst(ShaftParticle.FACTORY, 2); + + Char ch = Actor.findChar(i); + if (ch != null){ + if (ch.alignment == Char.Alignment.ALLY){ + + if (ch == Dungeon.hero || ch.HP == ch.HT){ + Buff.affect(ch, Barrier.class).incShield(10); + ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, "10", FloatingText.SHIELDING ); + } else { + int barrier = 10 - (ch.HT - ch.HP); + barrier = Math.max(barrier, 0); + ch.HP += 10 - barrier; + ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, Integer.toString(10-barrier), FloatingText.HEALING ); + if (barrier > 0){ + Buff.affect(ch, Barrier.class).incShield(barrier); + ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, Integer.toString(barrier), FloatingText.SHIELDING ); + } + } + } else if (!ch.flying) { + Buff.affect(ch, Roots.class, 1f); + } + } + } + } + //5 casts per hero level before furrowing + Buff.affect(hero, HallowedFurrowTracker.class).countUp(1); + + Sample.INSTANCE.play(Assets.Sounds.MELD); + hero.sprite.zap(target); + hero.spendAndNext( 1f ); + + onSpellCast(tome, hero); + + } + + public String desc(){ + int area = 1 + 2*Dungeon.hero.pointsInTalent(Talent.HALLOWED_GROUND); + return Messages.get(this, "desc", area) + "\n\n" + Messages.get(this, "charge_cost", (int)chargeUse(Dungeon.hero)); + } + + public static class HallowedTerrain extends Blob { + + @Override + protected void evolve() { + + int cell; + + Fire fire = (Fire)Dungeon.level.blobs.get( Fire.class ); + + // on avg, hallowed ground produces 9/17/25 tiles of grass, 100/67/50% of total tiles + int chance = 10 + 10*Dungeon.hero.pointsInTalent(Talent.HALLOWED_GROUND); + + for (int i = area.left-1; i <= area.right; i++) { + for (int j = area.top-1; j <= area.bottom; j++) { + cell = i + j*Dungeon.level.width(); + if (cur[cell] > 0) { + + //fire destroys hallowed terrain + if (fire != null && fire.volume > 0 && fire.cur[cell] > 0){ + off[cell] = cur[cell] = 0; + continue; + } + + int c = Dungeon.level.map[cell]; + if (c == Terrain.GRASS) { + if (Random.Int(chance) == 0) { + if (!Regeneration.regenOn() + || (Dungeon.hero.buff(HallowedFurrowTracker.class) != null && Dungeon.hero.buff(HallowedFurrowTracker.class).count() > 5)){ + Level.set(cell, Terrain.FURROWED_GRASS); + } else { + Level.set(cell, Terrain.HIGH_GRASS); + } + GameScene.updateMap(cell); + CellEmitter.get(cell).burst(LeafParticle.LEVEL_SPECIFIC, 5); + } + } else if (c == Terrain.EMPTY || c == Terrain.EMBERS || c == Terrain.EMPTY_DECO) { + Level.set(cell, Terrain.GRASS); + GameScene.updateMap(cell); + CellEmitter.get(cell).burst(LeafParticle.LEVEL_SPECIFIC, 2); + } + + Char ch = Actor.findChar(cell); + if (ch != null){ + if (ch.alignment == Char.Alignment.ALLY){ + if (ch == Dungeon.hero || ch.HP == ch.HT){ + Buff.affect(ch, Barrier.class).incShield(1); + ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, "1", FloatingText.SHIELDING ); + } else { + ch.HP++; + ch.sprite.showStatusWithIcon( CharSprite.POSITIVE, "1", FloatingText.HEALING ); + } + } else if (!ch.flying && ch.buff(Roots.class) == null){ + Buff.prolong(ch, Cripple.class, 1f); + } + } + + off[cell] = cur[cell] - 1; + volume += off[cell]; + } else { + off[cell] = 0; + } + } + } + } + + @Override + public void use(BlobEmitter emitter) { + super.use( emitter ); + emitter.pour( ShaftParticle.FACTORY, 1f ); + } + + @Override + public String tileDesc() { + return Messages.get(this, "desc"); + } + } + + public static class HallowedFurrowTracker extends CounterBuff{{revivePersists = true;}} + +} diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/HeroIcon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/HeroIcon.java index 55fc14534..7c8c10b2e 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/HeroIcon.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/HeroIcon.java @@ -85,6 +85,7 @@ public class HeroIcon extends Image { public static final int CLEANSE = 49; public static final int RADIANCE = 50; public static final int HOLY_LANCE = 51; + public static final int HALLOWED_GROUND = 52; //all cleric spells have a separate icon with no background for the action indicator public static final int SPELL_ACTION_OFFSET = 32;