diff --git a/core/src/main/assets/interfaces/buffs.png b/core/src/main/assets/interfaces/buffs.png index 59b3120ca..f1e5d34c3 100644 Binary files a/core/src/main/assets/interfaces/buffs.png and b/core/src/main/assets/interfaces/buffs.png differ diff --git a/core/src/main/assets/interfaces/large_buffs.png b/core/src/main/assets/interfaces/large_buffs.png index 19fe65a52..244aa5769 100644 Binary files a/core/src/main/assets/interfaces/large_buffs.png and b/core/src/main/assets/interfaces/large_buffs.png differ diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index 9cedfd0f1..952c48e84 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -370,6 +370,7 @@ actors.buffs.preparation.assassinated=assassinated actors.buffs.prismaticguard.name=prismatic Guard actors.buffs.prismaticguard.desc=You are being guarded by a prismatic image which is currently inactive. When enemies are present the prismatic image will spring to action and protect you!\n\nWhile inactive, the prismatic image will steadily recover from any damage it has taken.\n\nCurrent HP: %d/%d. +actors.buffs.prismaticguard.desc_many=This prismatic image is also benefitting from Power of Many. Turns Remaining: %d. actors.buffs.recharging.name=recharging actors.buffs.recharging.desc=Energy is coursing through you, improving the rate that your wands and staffs charge.\n\nEach turn this buff will increase current charge by one quarter, in addition to regular recharge.\n\nTurns of recharging remaining: %s. @@ -577,8 +578,20 @@ actors.hero.abilities.cleric.trinity$wnditemconfirm.body=Assign to body form actors.hero.abilities.cleric.trinity$wnditemconfirm.mind=Assign to mind form actors.hero.abilities.cleric.trinity$wnditemconfirm.spirit=Assign to spirit form actors.hero.abilities.cleric.powerofmany.name=power of many +actors.hero.abilities.cleric.powerofmany.prompt_default=Choose an ally or location +actors.hero.abilities.cleric.powerofmany.prompt_ally=Direct your light ally +actors.hero.abilities.cleric.powerofmany.ally_exists=You already have an empowered ally. +actors.hero.abilities.cleric.powerofmany.no_vision=You can't target a location you can't see. +actors.hero.abilities.cleric.powerofmany.only_allies=You can only empower allies. actors.hero.abilities.cleric.powerofmany.short_desc=The Cleric channels the _Power of Many_, empowering an existing ally or creating a new one. -actors.hero.abilities.cleric.powerofmany.desc=The Cleric channels the _Power of Many_, which either empowers an existing ally or creates a new empowered one for 100 turns.\n\nWhile empowered by Power of Many, any ally deals +25% damage, takes -25% damage, and shares their vision with the Cleric. They also gain a burst of 25 shielding when the ability is used.\n\nThe Cleric also gains three new spells that can only be cast with an empowered ally. Power of Many won't end while one of these spells is active. +actors.hero.abilities.cleric.powerofmany.desc=The Cleric channels the _Power of Many_, which either empowers an existing ally or creates a new empowered one for 100 turns. Targeting an empty space creates a new ally.\n\nWhile empowered by Power of Many, any ally deals +25% damage, takes -25% damage, and shares their vision with the Cleric. They also gain 25 shielding when the ability is used.\n\nThe Cleric also gains three new spells that can only be cast with an empowered ally. Power of Many won't end while one of these spells is active. +actors.hero.abilities.cleric.powerofmany$powerbuff.name=power of many +actors.hero.abilities.cleric.powerofmany$powerbuff.desc=This ally has been empowered by Power of Many, granting them +25%% damage dealt and -25%% damage taken. They will also share their vision range with the Cleric, and can benefit from power of many spells.\n\nPower of Many will persist when allies such as prismatic images hide away, but its duration will still count down.\n\nTurns Remaining: %s. +actors.hero.abilities.cleric.powerofmany$lightally.name=light ally +actors.hero.abilities.cleric.powerofmany$lightally.direct_defend=Your ally moves to that position. +actors.hero.abilities.cleric.powerofmany$lightally.direct_follow=Your ally moves to follow you. +actors.hero.abilities.cleric.powerofmany$lightally.direct_attack=Your ally moves to attack! +actors.hero.abilities.cleric.powerofmany$lightally.desc=A copy of one of the Cleric's adventuring companions, made out of solid light. While it has the shape of another hero, it does not have any of their unique abilities. This ally will persist for as long as Power of Many is applied to them.\n\nThis ally can be directed at no cost by re-using the Power of Many armor ability. actors.hero.abilities.ratmogrify.name=ratmogrify @@ -1228,7 +1241,7 @@ actors.hero.talent.beaming_ray.desc=The Cleric can cast _Beaming Ray_ from an em actors.hero.talent.life_link.title=life link actors.hero.talent.life_link.desc=The Cleric can cast _Life Link_ between themselves and an empowered ally at the cost of 2 charges. This spell causes damage to be shared between the Cleric and their ally, and causes beneficial spells to apply to both if either is being targeted.\n\n_+1:_ Life Link lasts for _6 turns_ and grants the ally an additional _-10% damage taken._\n\n_+2:_ Life Link lasts for _8 turns_ and grants the ally an additional _-15% damage taken._\n\n_+3:_ Life Link lasts for _10 turns_ and grants the ally an additional _-20% damage taken._\n\n_+4:_ Life Link lasts for _12 turns_ and grants the ally an additional _-25% damage taken._\n\nSpells that can be shared are: shield of light, divine sense, bless, hallowed ground, mnemonic prayer, lay on hands, aura of protection. actors.hero.talent.stasis.title=Stasis -actors.hero.talent.stasis.desc=The Cleric can cast _Stasis_ on an empowered ally at the cost of 1 charge. Stasis temporarily removes the ally from the dungeon and preserves the remaining time on all buffs, including Power of Many itself. The ally will reappear next to you when the effect ends. Stasis can be re-cast at no cost to end the effect early.\n\n_+1:_ Stasis lasts for a max of _40 turns._\n\n_+2:_ Stasis lasts for a max of _60 turns._\n\n_+3:_ Stasis lasts for a max of _80 turns._\n\n_+4:_ Stasis lasts for a max of _100 turns._\n\nThe Cleric can also cast Beaming Ray when an ally in in stasis to resummon them, or cast Life Link to pre-emptively apply the effect. An ally in stasis still benefits from life link if relevant. +actors.hero.talent.stasis.desc=The Cleric can cast _Stasis_ on an empowered ally at the cost of 1 charge. Stasis temporarily removes the ally from the dungeon and preserves the remaining time on all buffs, including Power of Many. The ally will reappear next to you when the effect ends. Stasis can be re-cast at no cost to end the effect early.\n\n_+1:_ Stasis lasts for a max of _40 turns._\n\n_+2:_ Stasis lasts for a max of _60 turns._\n\n_+3:_ Stasis lasts for a max of _80 turns._\n\n_+4:_ Stasis lasts for a max of _100 turns._\n\nThe Cleric can also cast Beaming Ray when an ally is in stasis to resummon them, or cast Life Link to pre-emptively apply its effect. An ally in stasis still benefits from life link if relevant. #universal diff --git a/core/src/main/assets/messages/items/items.properties b/core/src/main/assets/messages/items/items.properties index bdbd27209..4bfa0cab4 100644 --- a/core/src/main/assets/messages/items/items.properties +++ b/core/src/main/assets/messages/items/items.properties @@ -1540,6 +1540,7 @@ items.wands.wandoflivingearth.bmage_desc=When _the Battlemage_ strikes an enemy items.wands.wandoflivingearth.eleblast_desc=An elemental blast with a staff of living earth deals 50% damage, and heals an active earthen guardian for each enemy hit. items.wands.wandoflivingearth$rockarmor.name=rock armor items.wands.wandoflivingearth$rockarmor.desc=Magical rocks are surrounding your body, when you are attacked they will attempt to block for you, and will reduce the damage you take by 50%%. Each damage blocked scrapes away some of the rock however.\n\nRemaining Armor: %1$d.\n\nIf enough rock is built around you, the next zap from your wand of living earth will cause the rocks to form up into a guardian which will fight with you.\n\nArmor needed for Guardian: %2$d. +items.wands.wandoflivingearth$rockarmor.desc_many=This earthen guardian is also benefitting from Power of Many. Turns Remaining: %d. items.wands.wandoflivingearth$earthguardian.name=earthen guardian items.wands.wandoflivingearth$earthguardian.desc=The rocks from your wand of living earth have formed into a protective earthen guardian! This rocky protector will attack nearby enemies, which will force them to attack the guardian instead of you. When all nearby threats are gone, the guardian will re-form around you, and will return when you next use your wand. items.wands.wandoflivingearth$earthguardian.wand_info=The guardian's defensive power is tied to the level of your wand. It currently blocks _%1$d-%2$d damage._ diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java index 876d08534..985568113 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Dungeon.java @@ -34,6 +34,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.RevealedArea; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Terror; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.huntress.SpiritHawk; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.DivineSense; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mimic; @@ -1002,7 +1003,8 @@ public class Dungeon { for (Char ch : Actor.chars()){ if (ch instanceof WandOfWarding.Ward || ch instanceof WandOfRegrowth.Lotus - || ch instanceof SpiritHawk.HawkAlly){ + || ch instanceof SpiritHawk.HawkAlly + || ch.buff(PowerOfMany.PowerBuff.class) != null){ x = ch.pos % level.width(); y = ch.pos / level.width(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java index 36c831c59..34dc23dfb 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java @@ -77,6 +77,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Weakness; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.duelist.Challenge; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.rogue.DeathMark; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.warrior.Endure; @@ -425,6 +426,10 @@ public abstract class Char extends Actor { dmg *= 1.5f; } + if (buff( PowerOfMany.PowerBuff.class) != null){ + dmg *= 1.25f; + } + for (ChampionEnemy buff : buffs(ChampionEnemy.class)){ dmg *= buff.meleeDamageFactor(); } @@ -451,6 +456,10 @@ public abstract class Char extends Actor { dmg *= 0.925f - 0.075f*Dungeon.hero.pointsInTalent(Talent.AURA_OF_PROTECTION); } + if (enemy.buff(PowerOfMany.PowerBuff.class) != null){ + dmg *= 0.75f; + } + if (enemy.buff(MonkEnergy.MonkAbility.Meditate.MeditateResistance.class) != null){ dmg *= 0.2f; } @@ -777,12 +786,18 @@ public abstract class Char extends Actor { } } + //temporarily assign to a float to avoid rounding a bunch + float damage = dmg; + //if dmg is from a character we already reduced it in defenseProc if (!(src instanceof Char)) { if (Dungeon.hero.alignment == alignment && Dungeon.level.distance(pos, Dungeon.hero.pos) <= 2 && Dungeon.hero.buff(AuraOfProtection.AuraBuff.class) != null) { - dmg *= 0.925f - 0.075f*Dungeon.hero.pointsInTalent(Talent.AURA_OF_PROTECTION); + damage *= 0.925f - 0.075f*Dungeon.hero.pointsInTalent(Talent.AURA_OF_PROTECTION); + } + if (buff(PowerOfMany.PowerBuff.class) != null){ + damage *= 0.75f; } } @@ -805,10 +820,10 @@ public abstract class Char extends Actor { Buff.detach(this, MagicalSleep.class); } if (this.buff(Doom.class) != null && !isImmune(Doom.class)){ - dmg *= 1.67f; + damage *= 1.67f; } if (alignment != Alignment.ALLY && this.buff(DeathMark.DeathMarkTracker.class) != null){ - dmg *= 1.25f; + damage *= 1.25f; } if (buff(Sickle.HarvestBleedTracker.class) != null){ @@ -827,15 +842,19 @@ public abstract class Char extends Actor { } } - for (ChampionEnemy buff : buffs(ChampionEnemy.class)){ - dmg = (int) Math.ceil(dmg * buff.damageTakenFactor()); - } - Class srcClass = src.getClass(); if (isImmune( srcClass )) { - dmg = 0; + damage = 0; } else { - dmg = Math.round( dmg * resist( srcClass )); + damage *= resist( srcClass ); + } + + dmg = Math.round(damage); + + //we ceil these specifically to favor the player vs. champ dmg reduction + // most important vs. giant champions in the earlygame + for (ChampionEnemy buff : buffs(ChampionEnemy.class)){ + dmg = (int) Math.ceil(dmg * buff.damageTakenFactor()); } //TODO improve this when I have proper damage source logic diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/PrismaticGuard.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/PrismaticGuard.java index 6a7ed4608..56f451b27 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/PrismaticGuard.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/PrismaticGuard.java @@ -24,6 +24,7 @@ package com.shatteredpixel.shatteredpixeldungeon.actors.buffs; import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.actors.Actor; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.PrismaticImage; import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; @@ -41,6 +42,8 @@ public class PrismaticGuard extends Buff { } private float HP; + + private float powerOfManyTurns = 0; @Override public boolean act() { @@ -72,6 +75,9 @@ public class PrismaticGuard extends Buff { if (bestPos != -1) { PrismaticImage pris = new PrismaticImage(); pris.duplicate(hero, (int)Math.floor(HP) ); + if (powerOfManyTurns > 0){ + Buff.affect(pris, PowerOfMany.PowerBuff.class, powerOfManyTurns); + } pris.state = pris.HUNTING; GameScene.add(pris, 1); ScrollOfTeleportation.appear(pris, bestPos); @@ -86,16 +92,32 @@ public class PrismaticGuard extends Buff { spend(TICK); } - LockedFloor lock = target.buff(LockedFloor.class); if (HP < maxHP() && Regeneration.regenOn()){ HP += 0.1f; } + if (powerOfManyTurns > 0){ + powerOfManyTurns--; + if (powerOfManyTurns <= 0){ + powerOfManyTurns = 0; + BuffIndicator.refreshHero(); + } + } return true; } public void set( int HP ){ this.HP = HP; + powerOfManyTurns = 0; + } + + public void set( PrismaticImage img){ + this.HP = img.HP; + if (img.buff(PowerOfMany.PowerBuff.class) != null){ + powerOfManyTurns = img.buff(PowerOfMany.PowerBuff.class).cooldown()+1; + } else { + powerOfManyTurns = 0; + } } public int maxHP(){ @@ -105,6 +127,10 @@ public class PrismaticGuard extends Buff { public static int maxHP( Hero hero ){ return 10 + (int)Math.floor(hero.lvl * 2.5f); //half of hero's HP } + + public boolean isEmpowered(){ + return powerOfManyTurns > 0; + } @Override public int icon() { @@ -113,7 +139,11 @@ public class PrismaticGuard extends Buff { @Override public void tintIcon(Image icon) { - icon.hardlight(1f, 1f, 2f); + if (isEmpowered()){ + icon.hardlight(3f, 3f, 2f); + } else { + icon.hardlight(1f, 1f, 2f); + } } @Override @@ -128,20 +158,27 @@ public class PrismaticGuard extends Buff { @Override public String desc() { - return Messages.get(this, "desc", (int)HP, maxHP()); + String desc = Messages.get(this, "desc", (int)HP, maxHP()); + if (isEmpowered()){ + desc += "\n\n" + Messages.get(this, "desc_many", (int)powerOfManyTurns); + } + return desc; } private static final String HEALTH = "hp"; + private static final String POWER_TURNS = "power_turns"; @Override public void storeInBundle(Bundle bundle) { super.storeInBundle(bundle); bundle.put(HEALTH, HP); + bundle.put(POWER_TURNS, powerOfManyTurns); } @Override public void restoreFromBundle(Bundle bundle) { super.restoreFromBundle(bundle); HP = bundle.getFloat(HEALTH); + powerOfManyTurns = bundle.getFloat(POWER_TURNS); } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/cleric/PowerOfMany.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/cleric/PowerOfMany.java index 514ee91f5..ed86bd2b4 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/cleric/PowerOfMany.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/cleric/PowerOfMany.java @@ -21,17 +21,140 @@ package com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric; +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.buffs.AllyBuff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Barrier; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.FlavourBuff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Invisibility; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.PrismaticGuard; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroClass; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.ArmorAbility; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; +import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.DirectableAlly; +import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.ShaftParticle; import com.shatteredpixel.shatteredpixeldungeon.items.armor.ClassArmor; +import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; +import com.shatteredpixel.shatteredpixeldungeon.items.wands.WandOfLivingEarth; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.HeroSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.MobSprite; +import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator; import com.shatteredpixel.shatteredpixeldungeon.ui.HeroIcon; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.TextureFilm; +import com.watabou.noosa.audio.Sample; +import com.watabou.utils.Bundle; +import com.watabou.utils.Random; public class PowerOfMany extends ArmorAbility { + @Override + public float chargeUse(Hero hero) { + if (getPoweredAlly() instanceof LightAlly){ + return 0; + } + return super.chargeUse(hero); + } + + @Override + public String targetingPrompt() { + Char ally = getPoweredAlly(); + + boolean allyExists = ally != null; + + if (Dungeon.hero.buff(PrismaticGuard.class) != null + && Dungeon.hero.buff(PrismaticGuard.class).isEmpowered()){ + allyExists = true; + } + + if (Dungeon.hero.buff(WandOfLivingEarth.RockArmor.class) != null + && Dungeon.hero.buff(WandOfLivingEarth.RockArmor.class).isEmpowered()){ + allyExists = true; + } + + if (ally instanceof LightAlly){ + return Messages.get(this, "prompt_ally"); + } else if (!allyExists){ + return Messages.get(this, "prompt_default"); + } else { + return null; + } + } + @Override protected void activate(ClassArmor armor, Hero hero, Integer target) { + Char ally = getPoweredAlly(); + + boolean allyExists = ally != null; + + if (hero.buff(PrismaticGuard.class) != null + && hero.buff(PrismaticGuard.class).isEmpowered()){ + allyExists = true; + } + + if (hero.buff(WandOfLivingEarth.RockArmor.class) != null + && hero.buff(WandOfLivingEarth.RockArmor.class).isEmpowered()){ + allyExists = true; + } + + if (ally instanceof LightAlly){ + if (target == null){ + return; + } else { + ((LightAlly) ally).directTocell(target); + } + } else if (allyExists) { + GLog.w( Messages.get(this, "ally_exists")); + } else { + if (target == null){ + return; + } + + if (!Dungeon.level.heroFOV[target]){ + GLog.w(Messages.get(this, "no_vision")); + return; + } + + //pre-calculate as cost becomes 0 if light ally starts to exist + float chargeUse = chargeUse(hero); + + Char ch = Actor.findChar(target); + if (ch != null){ + if (ch.alignment != Char.Alignment.ALLY || ch == Dungeon.hero){ + GLog.w(Messages.get(this, "only_allies")); + return; + } + } else { + ch = new LightAlly(hero.lvl); + ch.pos = target; + GameScene.add((Mob) ch); + ScrollOfTeleportation.appear(ch, ch.pos); + } + + Buff.affect(ch, PowerBuff.class, 100f); + Buff.affect(ch, Barrier.class).setShield(25); + + armor.charge -= chargeUse; + armor.updateQuickslot(); + + hero.sprite.zap(target); + Sample.INSTANCE.play(Assets.Sounds.CHARGEUP); + + Invisibility.dispel(); + hero.spendAndNext(Actor.TICK); + + } + } @Override @@ -44,4 +167,216 @@ public class PowerOfMany extends ArmorAbility { return new Talent[]{Talent.BEAMING_RAY, Talent.LIFE_LINK, Talent.STASIS, Talent.HEROIC_ENERGY}; } + private static Char getPoweredAlly(){ + for (Char ch : Actor.chars()){ + if (ch.buff(PowerBuff.class) != null){ + return ch; + } + } + return null; + } + + public static class PowerBuff extends FlavourBuff { + + public static float DURATION = 100f; + + { + type = buffType.POSITIVE; + announced = true; + } + + @Override + public int icon() { + return BuffIndicator.MANY_POWER; + } + + @Override + public float iconFadePercent() { + return Math.max(0, (DURATION - visualcooldown()) / DURATION); + } + + @Override + public void fx(boolean on) { + if (on) target.sprite.add(CharSprite.State.GLOWING); + else target.sprite.remove(CharSprite.State.GLOWING); + } + + @Override + public void detach() { + super.detach(); + if (target instanceof LightAlly){ + target.die(this); + } + Dungeon.observe(); + GameScene.updateFog(); + } + } + + public static class LightAlly extends DirectableAlly { + + { + spriteClass = LightAllySprite.class; + + HP = HT = 80; + + immunities.add(AllyBuff.class); + + properties.add(Property.INORGANIC); + } + + HeroClass cls; + + public LightAlly(){ + super(); + cls = HeroClass.values()[Random.Int(5)]; + } + + public LightAlly(int heroLevel ){ + this(); + defenseSkill = heroLevel + 5; //equal to base hero defense skill + } + + @Override + protected boolean act() { + int oldPos = pos; + boolean result = super.act(); + //partially simulates how the hero switches to idle animation + if ((pos == target || oldPos == pos) && sprite.looping()){ + sprite.idle(); + } + return result; + } + + @Override + public void defendPos(int cell) { + GLog.i(Messages.get(this, "direct_defend")); + super.defendPos(cell); + } + + @Override + public void followHero() { + GLog.i(Messages.get(this, "direct_follow")); + super.followHero(); + } + + @Override + public void targetChar(Char ch) { + GLog.i(Messages.get(this, "direct_attack")); + super.targetChar(ch); + } + + @Override + public int attackSkill(Char target) { + return defenseSkill+5; //equal to base hero attack skill + } + + @Override + public int damageRoll() { + return Random.NormalIntRange(5, 30); //+0 greatsword + } + + @Override + public int drRoll() { + return super.drRoll() + Random.NormalIntRange(1, 5); //+0 plate + } + + @Override + public float speed() { + float speed = super.speed(); + + //moves 2 tiles at a time when returning to the hero + if (state == WANDERING + && defendingPos == -1 + && Dungeon.level.distance(pos, Dungeon.hero.pos) > 1){ + speed *= 2; + } + + return speed; + } + + @Override + public CharSprite sprite() { + CharSprite sprite = super.sprite(); + ((LightAllySprite)sprite).setup(cls); + return sprite; + } + + private static final String HERO_CLS = "hero_cls"; + private static final String DEF_SKILL = "def_skill"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(HERO_CLS, cls); + bundle.put(DEF_SKILL, defenseSkill); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + cls = bundle.getEnum(HERO_CLS, HeroClass.class); + defenseSkill = bundle.getInt(DEF_SKILL); + } + } + + public static class LightAllySprite extends MobSprite { + + public LightAllySprite() { + super(); + + setup(HeroClass.values()[Random.Int(5)]); + } + + public void setup(HeroClass cls){ + texture(cls.spritesheet()); + + TextureFilm film = new TextureFilm( HeroSprite.tiers(), 6, 12, 15 ); + + idle = new Animation( 1, true ); + idle.frames( film, 0, 0, 0, 1, 0, 0, 1, 1 ); + + run = new Animation( 20, true ); + run.frames( film, 2, 3, 4, 5, 6, 7 ); + + die = new Animation( 20, false ); + die.frames( film, 0 ); + + attack = new Animation( 15, false ); + attack.frames( film, 13, 14, 15, 0 ); + + play(idle, true); + resetColor(); + } + + @Override + public void link(Char ch) { + super.link(ch); + if (ch instanceof LightAlly){ + setup(((LightAlly) ch).cls); + } + } + + @Override + public void resetColor() { + super.resetColor(); + alpha(0.8f); + tint(1.33f, 1.33f, 0.8f, 0.6f); + rm = gm = bm = 0; + } + + @Override + public void die() { + super.die(); + emitter().start( ShaftParticle.FACTORY, 0.3f, 4 ); + emitter().start( Speck.factory( Speck.LIGHT ), 0.2f, 3 ); + } + + @Override + public void draw() { + if (alpha() >= 0.8f) alpha(0.8f); + rm = gm = bm = 0; //always flat and transparent + super.draw(); + } + } + } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java index c020a099a..cc106cc24 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/Mob.java @@ -51,6 +51,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroClass; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.ArmorAbility; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.duelist.Feint; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.rogue.ShadowClone; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.ClericSpell; @@ -249,7 +250,15 @@ public abstract class Mob extends Char { return true; } - return state.act( enemyInFOV, justAlerted ); + boolean result = state.act( enemyInFOV, justAlerted ); + + //for updating hero FOV + if (buff(PowerOfMany.PowerBuff.class) != null){ + Dungeon.level.updateFieldOfView( this, fieldOfView ); + GameScene.updateFog(pos, viewDistance+(int)Math.ceil(speed())); + } + + return result; } //FIXME this is sort of a band-aid correction for allies needing more intelligent behaviour diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/PrismaticImage.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/PrismaticImage.java index c71679248..e31360040 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/PrismaticImage.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/PrismaticImage.java @@ -247,7 +247,7 @@ public class PrismaticImage extends NPC { @Override public boolean act(boolean enemyInFOV, boolean justAlerted) { if (!enemyInFOV){ - Buff.affect(hero, PrismaticGuard.class).set( HP ); + Buff.affect(hero, PrismaticGuard.class).set( PrismaticImage.this ); destroy(); CellEmitter.get(pos).start( Speck.factory(Speck.LIGHT), 0.2f, 3 ); sprite.die(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfLivingEarth.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfLivingEarth.java index c19088d30..4c52ad61c 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfLivingEarth.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/wands/WandOfLivingEarth.java @@ -30,6 +30,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.AllyBuff; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok; import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mimic; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.NPC; @@ -110,6 +111,10 @@ public class WandOfLivingEarth extends DamageWand { guardian = new EarthGuardian(); guardian.setInfo(curUser, buffedLvl(), buff.armor); + if (buff.powerOfManyTurns > 0){ + Buff.affect(guardian, PowerOfMany.PowerBuff.class, buff.powerOfManyTurns); + } + //if the collision pos is occupied (likely will be), then spawn the guardian in the //adjacent cell which is closes to the user of the wand. if (ch != null){ @@ -255,7 +260,22 @@ public class WandOfLivingEarth extends DamageWand { private int wandLevel; private int armor; - private void addArmor( int wandLevel, int toAdd ){ + private float powerOfManyTurns = 0; + + @Override + public boolean act() { + if (powerOfManyTurns > 0){ + powerOfManyTurns--; + if (powerOfManyTurns <= 0){ + powerOfManyTurns = 0; + BuffIndicator.refreshHero(); + } + } + spend(TICK); + return true; + } + + private void addArmor(int wandLevel, int toAdd ){ this.wandLevel = Math.max(this.wandLevel, wandLevel); armor += toAdd; armor = Math.min(armor, 2*armorToGuardian()); @@ -276,6 +296,10 @@ public class WandOfLivingEarth extends DamageWand { } } + public boolean isEmpowered(){ + return powerOfManyTurns > 0; + } + @Override public int icon() { return BuffIndicator.ARMOR; @@ -283,7 +307,11 @@ public class WandOfLivingEarth extends DamageWand { @Override public void tintIcon(Image icon) { - icon.brightness(0.6f); + if (isEmpowered()){ + icon.hardlight(1.8f, 1.8f, 0.6f); + } else { + icon.brightness(0.6f); + } } @Override @@ -298,17 +326,24 @@ public class WandOfLivingEarth extends DamageWand { @Override public String desc() { - return Messages.get( this, "desc", armor, armorToGuardian()); + String desc = Messages.get( this, "desc", armor, armorToGuardian()); + if (isEmpowered()){ + desc += "\n\n" + Messages.get(this, "desc_many", (int)powerOfManyTurns); + } + return desc; } private static final String WAND_LEVEL = "wand_level"; private static final String ARMOR = "armor"; + private static final String POWER_TURNS = "power_turns"; + @Override public void storeInBundle(Bundle bundle) { super.storeInBundle(bundle); bundle.put(WAND_LEVEL, wandLevel); bundle.put(ARMOR, armor); + bundle.put(POWER_TURNS, powerOfManyTurns); } @Override @@ -316,6 +351,7 @@ public class WandOfLivingEarth extends DamageWand { super.restoreFromBundle(bundle); wandLevel = bundle.getInt(WAND_LEVEL); armor = bundle.getInt(ARMOR); + powerOfManyTurns = bundle.getFloat(POWER_TURNS); } } @@ -423,6 +459,9 @@ public class WandOfLivingEarth extends DamageWand { public boolean act(boolean enemyInFOV, boolean justAlerted) { if (!enemyInFOV){ Buff.affect(Dungeon.hero, RockArmor.class).addArmor(wandLevel, HP); + if (buff(PowerOfMany.PowerBuff.class) != null){ + Buff.affect(Dungeon.hero, RockArmor.class).powerOfManyTurns = buff(PowerOfMany.PowerBuff.class).cooldown()+1; + } Dungeon.hero.sprite.centerEmitter().burst(MagicMissile.EarthParticle.ATTRACT, 8 + wandLevel/2); destroy(); sprite.die(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/journal/Bestiary.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/journal/Bestiary.java index 9436854aa..19661f05f 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/journal/Bestiary.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/journal/Bestiary.java @@ -22,6 +22,7 @@ package com.shatteredpixel.shatteredpixeldungeon.journal; import com.shatteredpixel.shatteredpixeldungeon.Badges; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.huntress.SpiritHawk; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.rogue.ShadowClone; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.rogue.SmokeBomb; @@ -235,7 +236,7 @@ public enum Bestiary { ALLY.addEntities(MirrorImage.class, PrismaticImage.class, DriedRose.GhostHero.class, WandOfWarding.Ward.class, WandOfWarding.Ward.WardSentry.class, WandOfLivingEarth.EarthGuardian.class, - ShadowClone.ShadowAlly.class, SmokeBomb.NinjaLog.class, SpiritHawk.HawkAlly.class); + ShadowClone.ShadowAlly.class, SmokeBomb.NinjaLog.class, SpiritHawk.HawkAlly.class, PowerOfMany.LightAlly.class); TRAP.addEntities(WornDartTrap.class, PoisonDartTrap.class, DisintegrationTrap.class, GatewayTrap.class, ChillingTrap.class, BurningTrap.class, ShockingTrap.class, AlarmTrap.class, GrippingTrap.class, TeleportationTrap.class, OozeTrap.class, diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java index ce3d84b4b..e910b3f31 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/levels/Level.java @@ -48,6 +48,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Shadows; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.cleric.PowerOfMany; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.huntress.SpiritHawk; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.DivineSense; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.GnollGeomancer; @@ -1415,7 +1416,8 @@ public abstract class Level implements Bundlable { for (Mob m : mobs){ if (m instanceof WandOfWarding.Ward || m instanceof WandOfRegrowth.Lotus - || m instanceof SpiritHawk.HawkAlly){ + || m instanceof SpiritHawk.HawkAlly + || m.buff(PowerOfMany.PowerBuff.class) != null){ if (m.fieldOfView == null || m.fieldOfView.length != length()){ m.fieldOfView = new boolean[length()]; Dungeon.level.updateFieldOfView( m, m.fieldOfView ); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/BuffIndicator.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/BuffIndicator.java index 646975644..27f3839ed 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/BuffIndicator.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/BuffIndicator.java @@ -132,6 +132,7 @@ public class BuffIndicator extends Component { public static final int PROT_AURA = 80; public static final int ILLUMINATED = 81; public static final int TRINITY_FORM= 82; + public static final int MANY_POWER = 83; public static final int SIZE_SMALL = 7; public static final int SIZE_LARGE = 16;