From c69ba4408a4d190978b64c1b27e7202ad27f6b41 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 28 May 2021 19:37:37 -0400 Subject: [PATCH] v0.9.3: implemented the shadow clone ability --- .../assets/messages/actors/actors.properties | 22 +- .../shatteredpixeldungeon/actors/Char.java | 12 +- .../actors/hero/Talent.java | 4 +- .../actors/hero/abilities/ArmorAbility.java | 4 + .../hero/abilities/huntress/SpiritHawk.java | 22 +- .../hero/abilities/rogue/ShadowClone.java | 313 +++++++++++++++++- .../items/armor/ClassArmor.java | 2 +- 7 files changed, 351 insertions(+), 28 deletions(-) diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index eb38e857d..bf4d6927c 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -385,8 +385,12 @@ actors.hero.abilities.rogue.deathmark.desc=The Rogue places a mark on a chosen e actors.hero.abilities.rogue.deathmark$deathmarktracker.name=Marked for Death actors.hero.abilities.rogue.deathmark$deathmarktracker.desc=This enemy has been marked, causing them to take 25%% bonus damage, but also rendering them unable to die until the mark ends.\n\nTurns remaining: %s. actors.hero.abilities.rogue.shadowclone.name=shadow clone -actors.hero.abilities.rogue.shadowclone.short_desc=The Rogue summons a _Shadow Clone_, which is frail, but can be directed and deals damage based on his weapon. -actors.hero.abilities.rogue.shadowclone.desc=TODO +actors.hero.abilities.rogue.shadowclone.short_desc=The Rogue summons a _Shadow Clone_, which can be directed to aid him in combat. +actors.hero.abilities.rogue.shadowclone.desc=The Rogue summons a shadowy mimic of himself, which can be directed to aid him in combat. Directing the shadow clone does not cost any charge.\n\nThe clone has 100 HP, no armor, and deals 10-20 damage. All of these traits can be improved with talents, such that the clone benefits from weapons and armor the hero has. +actors.hero.abilities.rogue.shadowclone$shadowally.name=shadow rogue +actors.hero.abilities.rogue.shadowclone$shadowally.direct_follow=Your clone moves to follow you. +actors.hero.abilities.rogue.shadowclone$shadowally.direct_attack=Your clone moves to attack! +actors.hero.abilities.rogue.shadowclone$shadowally.desc=A copy of the Rogue, made from shadowy darkness. It stands stock still, with empty eyes and tiny wisps of darkness rising from it like steam.\n\nThe clone is not a perfect copy of the Rogue, but is still a decent fighter, and can benefit from the Rogue's equipment with the right talents. actors.hero.abilities.huntress.spectralblades.name=spectral blades actors.hero.abilities.huntress.spectralblades.short_desc=The Huntress throws _Spectral Blades_ at a target, inflicting damage depending on her currently equipped melee weapon. @@ -403,7 +407,7 @@ actors.hero.abilities.huntress.spirithawk.desc=The Huntress summons a spirit haw actors.hero.abilities.huntress.spirithawk$hawkally.name=spirit hawk actors.hero.abilities.huntress.spirithawk$hawkally.direct_follow=Your hawk moves to follow you. actors.hero.abilities.huntress.spirithawk$hawkally.direct_attack=Your hawk moves to attack! -actors.hero.abilities.huntress.spirithawk$hawkally.desc=A magical hawk, summoned by the Huntress.\n\nWhile it isn't much of a fighter its speed and vision make it excellent for scouting and distracting enemies.\n\nTurns remaining: %d. +actors.hero.abilities.huntress.spirithawk$hawkally.desc=A magical hawk, summoned by the Huntress. It glows a bright ethereal blue, its head constantly shifts around as it surveys the area.\n\nWhile it isn't much of a fighter its speed and vision make it excellent for scouting and distracting enemies.\n\nTurns remaining: %d. actors.hero.abilities.huntress.spirithawk$hawkally.desc_dodges=Guaranteed dodges remaining: %d. actors.hero.abilities.ratmogrify.name=ratmogrify @@ -658,12 +662,12 @@ actors.hero.talent.deathly_durability.desc=_+1:_ Enemies killed by death mark gi actors.hero.talent.double_mark.title=double mark actors.hero.talent.double_mark.desc=_+1:_ Marking a second target at the same time as the first one has a _33% reduced_ charge cost.\n\n_+2:_ Marking a second target at the same time as the first one has a _55% reduced_ charge cost.\n\n_+3:_ Marking a second target at the same time as the first one has a _70% reduced_ charge cost.\n\n_+4:_ Marking a second target at the same time as the first one has a _80% reduced_ charge cost. -actors.hero.talent.rogue_3_1.title=TODO NAME -actors.hero.talent.rogue_3_1.desc=TODO DESC -actors.hero.talent.rogue_3_2.title=TODO NAME -actors.hero.talent.rogue_3_2.desc=TODO DESC -actors.hero.talent.rogue_3_3.title=TODO NAME -actors.hero.talent.rogue_3_3.desc=TODO DESC +actors.hero.talent.shadow_blade.title=shadow blade +actors.hero.talent.shadow_blade.desc=_+1:_ The shadow clone gains _6%_ of the hero's damage per turn, and has a _25% chance_ to use the enchantment on the hero's weapon.\n\n_+2:_ The shadow clone gains _13%_ of the hero's damage per turn, and has a _50% chance_ to use the enchantment on the hero's weapon.\n\n_+3:_ The shadow clone gains _18%_ of the hero's damage per turn, and has a _75% chance_ to use the enchantment on the hero's weapon.\n\n_+4:_ The shadow clone gains _25%_ of the hero's damage per turn, and has a _100% chance_ to use the enchantment on the hero's weapon +actors.hero.talent.cloned_armor.title=cloned armor +actors.hero.talent.cloned_armor.desc=_+1:_ The shadow clone gains _13%_ of the hero's armor value, and has a _25% chance_ to use the glyph on the hero's armor.\n\n_+2:_ The shadow clone gains _25%_ of the hero's armor value, and has a _50% chance_ to use the glyph on the hero's armor.\n\n_+3:_ The shadow clone gains _38%_ of the hero's armor value, and has a _75% chance_ to use the glyph on the hero's armor.\n\n_+4:_ The shadow clone gains _50%_ of the hero's armor value, and has a _100% chance_ to use the glyph on the hero's armor. +actors.hero.talent.perfect_copy.title=perfect copy +actors.hero.talent.perfect_copy.desc=_+1:_ The shadow clone gains _10%_ of the hero's max HP, and can instantly swap places with the hero up to _1 tile_ away.\n\n_+2:_ The shadow clone gains _20%_ of the hero's max HP, and can instantly swap places with the hero up to _2 tiles_ away.\n\n_+3:_ The shadow clone gains _30%_ of the hero's max HP, and can instantly swap places with the hero up to _3 tiles_ away.\n\n_+4:_ The shadow clone gains _40%_ of the hero's max HP, and can instantly swap places with the hero up to _4 tiles_ away. #huntress actors.hero.talent.natures_bounty.title=nature's bounty 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 518bf5ccd..3547119c6 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java @@ -176,11 +176,6 @@ public abstract class Char extends Actor { //swaps places by default public boolean interact(Char c){ - //can't spawn places if one char has restricted movement - if (rooted || c.rooted || buff(Vertigo.class) != null || c.buff(Vertigo.class) != null){ - return true; - } - //don't allow char to swap onto hazard unless they're flying //you can swap onto a hazard though, as you're not the one instigating the swap if (!Dungeon.level.passable[pos] && !c.flying){ @@ -207,7 +202,12 @@ public abstract class Char extends Actor { GameScene.updateFog(); return true; } - + + //can't swap places if one char has restricted movement + if (rooted || c.rooted || buff(Vertigo.class) != null || c.buff(Vertigo.class) != null){ + return true; + } + moveSprite( pos, Dungeon.hero.pos ); move( Dungeon.hero.pos ); 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 b8909e7c2..4b9a65e66 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 @@ -119,8 +119,8 @@ public enum Talent { HASTY_RETREAT(81, 4), BODY_REPLACEMENT(82, 4), SHADOW_STEP(83, 4), //Death Mark T4 FEAR_THE_REAPER(84, 4), DEATHLY_DURABILITY(85, 4), DOUBLE_MARK(86, 4), - //??? T4 - ROGUE_3_1(87, 4), ROGUE_3_2(88, 4), ROGUE_3_3(89, 4), + //Shadow Clone T4 + SHADOW_BLADE(87, 4), CLONED_ARMOR(88, 4), PERFECT_COPY(89, 4), //Huntress T1 NATURES_BOUNTY(96), SURVIVALISTS_INTUITION(97), FOLLOWUP_STRIKE(98), NATURES_AID(99), diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/ArmorAbility.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/ArmorAbility.java index 060b68021..4c9452ff8 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/ArmorAbility.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/ArmorAbility.java @@ -57,6 +57,10 @@ public abstract class ArmorAbility implements Bundlable { return null; } + public boolean useTargeting(){ + return targetingPrompt() != null; + } + public float chargeUse( Hero hero ){ float chargeUse = baseChargeUse; if (hero.hasTalent(Talent.HEROIC_ENERGY)){ diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/huntress/SpiritHawk.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/huntress/SpiritHawk.java index 99c047ecd..e985115b0 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/huntress/SpiritHawk.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/huntress/SpiritHawk.java @@ -36,6 +36,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.ArmorAbility; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.DirectableAlly; import com.shatteredpixel.shatteredpixeldungeon.items.armor.ClassArmor; +import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; import com.shatteredpixel.shatteredpixeldungeon.sprites.BatSprite; @@ -58,6 +59,11 @@ public class SpiritHawk extends ArmorAbility { } } + @Override + public boolean useTargeting(){ + return false; + } + { baseChargeUse = 35f; } @@ -91,16 +97,16 @@ public class SpiritHawk extends ArmorAbility { } if (!spawnPoints.isEmpty()){ - ally = new HawkAlly(); - - ally.pos = Random.element(spawnPoints); - - GameScene.add(ally); - Dungeon.level.occupyCell(ally); - Dungeon.observe(); - armor.charge -= chargeUse(hero); armor.updateQuickslot(); + + ally = new HawkAlly(); + ally.pos = Random.element(spawnPoints); + GameScene.add(ally); + + ScrollOfTeleportation.appear(ally, ally.pos); + Dungeon.observe(); + Invisibility.dispel(); hero.spendAndNext(Actor.TICK); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/rogue/ShadowClone.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/rogue/ShadowClone.java index a93e8ae84..8c5633e3d 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/rogue/ShadowClone.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/rogue/ShadowClone.java @@ -21,20 +21,329 @@ package com.shatteredpixel.shatteredpixeldungeon.actors.hero.abilities.rogue; +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.Invisibility; import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; 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.actors.mobs.npcs.MirrorImage; +import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; +import com.shatteredpixel.shatteredpixeldungeon.effects.particles.SmokeParticle; import com.shatteredpixel.shatteredpixeldungeon.items.armor.ClassArmor; +import com.shatteredpixel.shatteredpixeldungeon.items.scrolls.ScrollOfTeleportation; +import com.shatteredpixel.shatteredpixeldungeon.levels.CityLevel; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.HeroSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.MobSprite; +import com.shatteredpixel.shatteredpixeldungeon.utils.BArray; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.TextureFilm; +import com.watabou.noosa.audio.Sample; +import com.watabou.noosa.tweeners.AlphaTweener; +import com.watabou.noosa.tweeners.Tweener; +import com.watabou.utils.Bundle; +import com.watabou.utils.PathFinder; +import com.watabou.utils.Random; + +import java.util.ArrayList; public class ShadowClone extends ArmorAbility { + @Override + public String targetingPrompt() { + if (getShadowAlly() == null) { + return super.targetingPrompt(); + } else { + return Messages.get(this, "prompt"); + } + } + + @Override + public boolean useTargeting(){ + return false; + } + + { + baseChargeUse = 50f; + } + + @Override + public float chargeUse(Hero hero) { + if (getShadowAlly() == null) { + return super.chargeUse(hero); + } else { + return 0; + } + } + @Override protected void activate(ClassArmor armor, Hero hero, Integer target) { - //TODO + ShadowAlly ally = getShadowAlly(); + + if (ally != null){ + if (target == null){ + return; + } else { + ally.directTocell(target); + } + } else { + ArrayList spawnPoints = new ArrayList<>(); + for (int i = 0; i < PathFinder.NEIGHBOURS8.length; i++) { + int p = hero.pos + PathFinder.NEIGHBOURS8[i]; + if (Actor.findChar(p) == null && (Dungeon.level.passable[p] || Dungeon.level.avoid[p])) { + spawnPoints.add(p); + } + } + + if (!spawnPoints.isEmpty()){ + armor.charge -= chargeUse(hero); + armor.updateQuickslot(); + + ally = new ShadowAlly(hero.lvl); + ally.pos = Random.element(spawnPoints); + GameScene.add(ally); + + ShadowAlly.appear(ally, ally.pos); + + Invisibility.dispel(); + hero.spendAndNext(Actor.TICK); + + } else { + GLog.w(Messages.get(this, "no_space")); + } + } + } @Override public Talent[] talents() { - return new Talent[]{Talent.ROGUE_3_1, Talent.ROGUE_3_2, Talent.ROGUE_3_3, Talent.HEROIC_ENERGY}; + return new Talent[]{Talent.SHADOW_BLADE, Talent.CLONED_ARMOR, Talent.PERFECT_COPY, Talent.HEROIC_ENERGY}; + } + + private static ShadowAlly getShadowAlly(){ + for (Char ch : Actor.chars()){ + if (ch instanceof ShadowAlly){ + return (ShadowAlly) ch; + } + } + return null; + } + + public static class ShadowAlly extends DirectableAlly { + + { + spriteClass = ShadowSprite.class; + + HP = HT = 100; + } + + public ShadowAlly(){ + super(); + } + + public ShadowAlly( int heroLevel ){ + super(); + int hpBonus = 20 + 5*heroLevel; + hpBonus = Math.round(0.1f * Dungeon.hero.pointsInTalent(Talent.PERFECT_COPY) * hpBonus); + if (hpBonus > 0){ + HT += hpBonus; + HP += hpBonus; + } + 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 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() { + int damage = Random.NormalIntRange(10, 20); + int heroDamage = Dungeon.hero.damageRoll(); + heroDamage /= Dungeon.hero.attackDelay(); //normalize hero damage based on atk speed + heroDamage = Math.round(0.0625f * Dungeon.hero.pointsInTalent(Talent.SHADOW_BLADE) * heroDamage); + if (heroDamage > 0){ + damage += heroDamage; + } + return damage; + } + + @Override + public int attackProc( Char enemy, int damage ) { + damage = super.attackProc( enemy, damage ); + if (Random.Int(4) < Dungeon.hero.pointsInTalent(Talent.SHADOW_BLADE) + && Dungeon.hero.belongings.weapon != null){ + return Dungeon.hero.belongings.weapon.proc( enemy, this, damage ); + } else { + return damage; + } + } + + @Override + public int drRoll() { + int dr = super.drRoll(); + int heroRoll = Dungeon.hero.drRoll(); + heroRoll = Math.round(0.125f * Dungeon.hero.pointsInTalent(Talent.CLONED_ARMOR) * heroRoll); + if (heroRoll > 0){ + dr += heroRoll; + } + return dr; + } + + @Override + public int defenseProc(Char enemy, int damage) { + damage = super.defenseProc(enemy, damage); + if (Random.Int(4) < Dungeon.hero.pointsInTalent(Talent.CLONED_ARMOR) + && Dungeon.hero.belongings.armor != null){ + return Dungeon.hero.belongings.armor.proc( enemy, this, damage ); + } else { + return damage; + } + } + + @Override + public boolean canInteract(Char c) { + if (super.canInteract(c)){ + return true; + } else if (Dungeon.level.distance(pos, c.pos) <= Dungeon.hero.pointsInTalent(Talent.PERFECT_COPY)) { + return true; + } else { + return false; + } + } + + @Override + public boolean interact(Char c) { + if (!Dungeon.hero.hasTalent(Talent.PERFECT_COPY)){ + return super.interact(c); + } + + //some checks from super.interact + if (!Dungeon.level.passable[pos] && !c.flying){ + return true; + } + + if (properties().contains(Property.LARGE) && !Dungeon.level.openSpace[c.pos] + || c.properties().contains(Property.LARGE) && !Dungeon.level.openSpace[pos]){ + return true; + } + + int curPos = pos; + + //warp instantly with the clone + PathFinder.buildDistanceMap(c.pos, BArray.or(Dungeon.level.passable, Dungeon.level.avoid, null)); + if (PathFinder.distance[pos] == Integer.MAX_VALUE){ + return true; + } + appear(this, Dungeon.hero.pos); + appear(Dungeon.hero, curPos); + Dungeon.observe(); + GameScene.updateFog(); + return true; + } + + private static void appear( Char ch, int pos ) { + + ch.sprite.interruptMotion(); + + if (Dungeon.level.heroFOV[pos] || Dungeon.level.heroFOV[ch.pos]){ + Sample.INSTANCE.play(Assets.Sounds.PUFF); + } + + ch.move( pos ); + if (ch.pos == pos) ch.sprite.place( pos ); + + if (Dungeon.level.heroFOV[pos] || ch == Dungeon.hero ) { + ch.sprite.emitter().burst(SmokeParticle.FACTORY, 10); + } + } + + private static final String DEF_SKILL = "def_skill"; + + @Override + public void storeInBundle(Bundle bundle) { + super.storeInBundle(bundle); + bundle.put(DEF_SKILL, defenseSkill); + } + + @Override + public void restoreFromBundle(Bundle bundle) { + super.restoreFromBundle(bundle); + defenseSkill = bundle.getInt(DEF_SKILL); + } + } + + public static class ShadowSprite extends MobSprite { + + public ShadowSprite() { + super(); + + texture( Dungeon.hero.heroClass.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 ); + + idle(); + resetColor(); + } + + @Override + public void onComplete(Tweener tweener) { + super.onComplete(tweener); + } + + @Override + public void link(Char ch) { + super.link(ch); + renderShadow = false; + } + + @Override + public void resetColor() { + super.resetColor(); + alpha(0.6f); + brightness(0.0f); + } } } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/armor/ClassArmor.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/armor/ClassArmor.java index 962da4d62..5d3933246 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/armor/ClassArmor.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/armor/ClassArmor.java @@ -170,7 +170,7 @@ abstract public class ClassArmor extends Armor { } else if (charge < hero.armorAbility.chargeUse(hero)) { GLog.w( Messages.get(this, "low_charge") ); } else { - usesTargeting = hero.armorAbility.targetingPrompt() != null; + usesTargeting = hero.armorAbility.useTargeting(); hero.armorAbility.use(this, hero); }