From fbcf0da0d6145053ea7de686f6702684e4f0ca15 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Mon, 17 Feb 2025 17:28:46 -0500 Subject: [PATCH] v3.0.0: initial base impl of stasis, missing big chunks of functionality --- .../assets/messages/actors/actors.properties | 8 +- .../hero/abilities/cleric/PowerOfMany.java | 5 + .../actors/hero/spells/ClericSpell.java | 4 + .../actors/hero/spells/Stasis.java | 172 ++++++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/Stasis.java diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index b9a26ff33..fe09a44ec 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -750,6 +750,12 @@ actors.hero.spells.smite.name=smite actors.hero.spells.smite.short_desc=Guarantees a hit with bonus damage and enchant power. actors.hero.spells.smite.desc=The Paladin infuses a melee strike with righteous destructive power.\n\nIn addition to dealing normal melee damage, Smite makes non-encumbered attacks guaranteed to hit, has +300%% enchantment power, and deals %1$d - %2$d bonus magic damage.\n\nSmite's bonus magic damage scales with the Paladin's level, and it will always deal maximum bonus magic damage against undead or demonic foes. +actors.hero.spells.stasis.name=stasis +actors.hero.spells.stasis.short_desc=Removes ally from dungeon and preserves them. +actors.hero.spells.stasis.desc=The Cleric focuses their connection with their ally toward themselves, causing the ally to be temporarily held in a sort of energy stasis within the Cleric. The ally will be preserved for up to %d turns, with all positive effects on them being unaffected by their time in stasis.\n\nOnce the spell ends naturally, or once the Cleric re-casts it, the ally will appear next to the Cleric. +actors.hero.spells.stasis$stasisbuff.name=ally in stasis +actors.hero.spells.stasis$stasisbuff.desc=The Cleric currently has an ally stored in stasis. All positive effects on this ally are preserved, including Power of Many, and they will reappear next to the Cleric when the effect ends. The effect can also be ended early by re-casting Stasis.\n\nAlly in stasis: %1$s.\nTurns Remaining: %2$s. + actors.hero.spells.sunray.name=sunray actors.hero.spells.sunray.short_desc=Deals ranged magic damage and blinds a target once. actors.hero.spells.sunray.desc=The Cleric fires a ray of blinding light at a target, dealing %1$d-%2$d damage and blinding them for %3$d turns. Sunray always deals maximum damage to undead and demonic targets.\n\nAfter being struck with this spell an enemy's vision will adjust, preventing them from being blinded by it again. However, if they are struck again while blinded by this spell, then the light is overwhelming and paralyses them instead. @@ -1255,7 +1261,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 cleric spells of tier 3 or lower to apply to both if either is being affected.\n\n_+1:_ Life Link lasts for _6 turns_ and increases Power of Many's bonus to _-35% damage taken._\n\n_+2:_ Life Link lasts for _8 turns_ and increases Power of Many's bonus to _-40% damage taken._\n\n_+3:_ Life Link lasts for _10 turns_ and increases Power of Many's bonus to _-45% damage taken._\n\n_+4:_ Life Link lasts for _12 turns_ and increases Power of Many's bonus to _-50% damage taken._ 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. 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. +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 instantly and 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/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 5181b77d1..2467c5cfa 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 @@ -37,6 +37,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.ArmorAbility; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.BeamingRay; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.LifeLinkSpell; +import com.shatteredpixel.shatteredpixeldungeon.actors.hero.spells.Stasis; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.DirectableAlly; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; @@ -113,6 +114,10 @@ public class PowerOfMany extends ArmorAbility { allyExists = true; } + if (hero.buff(Stasis.StasisBuff.class) != null){ + allyExists = true; + } + if (ally instanceof LightAlly){ if (target == null){ return; 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 cbbad1029..bf2a1bbf6 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 @@ -197,6 +197,9 @@ public abstract class ClericSpell { if (cleric.hasTalent(Talent.LIFE_LINK)){ spells.add(LifeLinkSpell.INSTANCE); } + if (cleric.hasTalent(Talent.STASIS)){ + spells.add(Stasis.INSTANCE); + } } @@ -231,6 +234,7 @@ public abstract class ClericSpell { spells.add(SpiritForm.INSTANCE); spells.add(BeamingRay.INSTANCE); spells.add(LifeLinkSpell.INSTANCE); + spells.add(Stasis.INSTANCE); return spells; } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/Stasis.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/Stasis.java new file mode 100644 index 000000000..c0bf67455 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/spells/Stasis.java @@ -0,0 +1,172 @@ +/* + * Pixel Dungeon + * Copyright (C) 2012-2015 Oleg Dolya + * + * Shattered Pixel Dungeon + * Copyright (C) 2014-2025 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.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.FlavourBuff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LifeLink; +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.mobs.Mob; +import com.shatteredpixel.shatteredpixeldungeon.effects.MagicMissile; +import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.HolyTome; +import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator; +import com.shatteredpixel.shatteredpixeldungeon.ui.HeroIcon; +import com.watabou.noosa.audio.Sample; +import com.watabou.utils.Bundle; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; + +import java.util.ArrayList; + +public class Stasis extends ClericSpell { + + public static Stasis INSTANCE = new Stasis(); + + @Override + public int icon() { + return HeroIcon.STASIS; + } + + @Override + public String desc() { + return Messages.get(this, "desc", 20 + 20*Dungeon.hero.pointsInTalent(Talent.STASIS)) + "\n\n" + Messages.get(this, "charge_cost", (int)chargeUse(Dungeon.hero)); + } + + @Override + public boolean canCast(Hero hero) { + return super.canCast(hero) + && hero.hasTalent(Talent.STASIS) + && (PowerOfMany.getPoweredAlly() != null || hero.buff(StasisBuff.class) != null); + } + + @Override + public float chargeUse(Hero hero) { + if (hero.buff(StasisBuff.class) != null){ + return 0; + } + return super.chargeUse(hero); + } + + @Override + public void onCast(HolyTome tome, Hero hero) { + + onSpellCast(tome, hero); + + if (hero.buff(StasisBuff.class) != null){ + hero.sprite.operate(hero.pos); + hero.buff(StasisBuff.class).act(); + return; + } + + Char ally = PowerOfMany.getPoweredAlly(); + + hero.sprite.zap(ally.pos); + MagicMissile.boltFromChar(hero.sprite.parent, MagicMissile.LIGHT_MISSILE, ally.sprite, hero.pos, null); + + //TODO preserve positive effects on ally, don't care about negative ones? + Actor.remove(ally); + Buff.prolong(hero, StasisBuff.class, 20 + 20*hero.pointsInTalent(Talent.STASIS)).stasisAlly = (Mob)ally; + Sample.INSTANCE.play(Assets.Sounds.TELEPORT); + + if (hero.buff(LifeLink.class) != null && hero.buff(LifeLink.class).object == ally.id()){ + hero.buff(LifeLink.class).detach(); + } + + //TODO will need to make affordances for lots of edge cases like earth golem, wards limits, ghost hero, PoE ally, others? + + //TODO need code in beamign ray and life link to work here, also life link cleric spells? + + hero.spendAndNext(Actor.TICK); + + } + + public static class StasisBuff extends FlavourBuff { + + { + type = buffType.POSITIVE; + } + + @Override + public int icon() { + return BuffIndicator.MANY_POWER; + } + + @Override + public float iconFadePercent() { + int duration = 20 + 20*Dungeon.hero.pointsInTalent(Talent.STASIS); + return Math.max(0, (duration - visualcooldown()) / duration); + } + + @Override + public String desc() { + return Messages.get(this, "desc", Messages.titleCase(stasisAlly.name()), dispTurns()); + } + + @Override + public boolean act() { + ArrayList spawnPoints = new ArrayList<>(); + for (int i = 0; i < PathFinder.NEIGHBOURS8.length; i++) { + int p = target.pos + PathFinder.NEIGHBOURS8[i]; + if (Actor.findChar(p) == null && (Dungeon.level.passable[p] || Dungeon.level.avoid[p])) { + spawnPoints.add(p); + } + } + if (spawnPoints.isEmpty()){ + spawnPoints.add(target.pos + PathFinder.NEIGHBOURS8[Random.Int(8)]); + } + stasisAlly.pos = Random.element(spawnPoints); + stasisAlly.clearTime(); + GameScene.add(stasisAlly); + + ScrollOfTeleportation.appear(stasisAlly, stasisAlly.pos); + Sample.INSTANCE.play(Assets.Sounds.TELEPORT); + + return super.act(); + } + + Mob stasisAlly; + + private static final String ALLY = "ally"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(ALLY, stasisAlly); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + stasisAlly = (Mob)bundle.get(ALLY); + } + } + +}