From 36edb9c87db5064ccb54757b582c867347570db5 Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Fri, 3 Mar 2023 13:12:19 -0500 Subject: [PATCH] v2.0.0: base implementation for Feint ability --- .../assets/messages/actors/actors.properties | 13 +- .../actors/hero/Talent.java | 2 +- .../actors/hero/abilities/duelist/Feint.java | 244 ++++++++++++++++++ .../actors/mobs/Mob.java | 14 + .../sprites/MirrorSprite.java | 10 + .../sprites/PrismaticSprite.java | 39 +-- .../shatteredpixeldungeon/ui/HeroIcon.java | 2 +- 7 files changed, 285 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/duelist/Feint.java diff --git a/core/src/main/assets/messages/actors/actors.properties b/core/src/main/assets/messages/actors/actors.properties index 35b4a4ca2..0a90f7300 100644 --- a/core/src/main/assets/messages/actors/actors.properties +++ b/core/src/main/assets/messages/actors/actors.properties @@ -489,6 +489,12 @@ actors.hero.abilities.duelist.elementalstrike.name=elemental strike actors.hero.abilities.duelist.elementalstrike.short_desc=The Duelist performs an _elemental strike_, spreading an effect in a conical AOE based on her weapon enchantment. actors.hero.abilities.duelist.elementalstrike.desc=The Duelist strikes an enemy or location, performing a regular attack that's guaranteed to hit and spreading a magical effect that travels up to 3 tiles in a 65 degree cone. This magical effect varies based on the enchantment on the Duelist's primary weapon. actors.hero.abilities.duelist.elementalstrike.generic_desc=An elemental strike with no enchantment will release a small burst of magic, dealing 5-10 damage to all enemies in range. +actors.hero.abilities.duelist.feint.name=feint +actors.hero.abilities.duelist.feint.prompt=Choose a location to dash to +actors.hero.abilities.duelist.feint.too_far=That location is not adjacent to you +actors.hero.abilities.duelist.feint.bad_location=You can't move to that location +actors.hero.abilities.duelist.feint.short_desc=The Duelist _feints_, faking an attack while dashing to an adjacent tile. Enemies will attack her afterimage, leaving them open to a counterattack. +actors.hero.abilities.duelist.feint.desc=The Duelist fakes an attack while dashing to an adjacent tile, leaving behind a momentary afterimage of herself. Enemies that were attacking the Duelist will attack her afterimage instead.\n\nEnemies that attack the afterimage become confused, which cancels their next action and leaves them open to a surprise attack. actors.hero.abilities.ratmogrify.name=ratmogrify actors.hero.abilities.ratmogrify.cant_transform=You can't ratmogrify that! @@ -904,7 +910,12 @@ actors.hero.talent.striking_force.desc=_+1:_ The power of elemental strike's eff actors.hero.talent.directed_power.title=directed power actors.hero.talent.directed_power.desc=_+1:_ The direct attack from elemental strike gains _+30% enchantment power_ for each enemy in range of elemental strike's effect, including the attack target.\n\n_+2:_ The direct attack from elemental strike gains _+60% enchantment power_ for each enemy in range of elemental strike's effect, including the attack target.\n\n_+3:_ The direct attack from elemental strike gains _+90% enchantment power_ for each enemy in range of elemental strike's effect, including the attack target.\n\n_+4:_ The direct attack from elemental strike gains _+120% enchantment power_ for each enemy in range of elemental strike's effect, including the attack target. -#third armor ability +actors.hero.talent.feigned_retreat.title=feigned retreat +actors.hero.talent.feigned_retreat.desc=_+1:_ If an enemy attacks the Duelist's afterimage, she gains _2 turns_ of haste.\n\n_+2:_ If an enemy attacks the Duelist's afterimage, she gains _4 turns_ of haste.\n\n_+3:_ If an enemy attacks the Duelist's afterimage, she gains _6 turns_ of haste.\n\n_+4:_ If an enemy attacks the Duelist's afterimage, she gains _8 turns_ of haste. +actors.hero.talent.expose_weakness.title=expose weakness +actors.hero.talent.expose_weakness.desc=_+1:_ Enemies that attack the Duelist's afterimage become vulnerable for _1 turn_.\n\n_+2:_ Enemies that attack the Duelist's afterimage become vulnerable for _2 turns_.\n\n_+3:_ Enemies that attack the Duelist's afterimage become vulnerable for _3 turns_.\n\n_+1:_ Enemies that attack the Duelist's afterimage become vulnerable for _4 turns_. +actors.hero.talent.lasting_image.title=TODO +actors.hero.talent.lasting_image.desc=specific mechanics TBD #universal actors.hero.talent.heroic_energy.title=heroic energy 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 cbaa3b7f7..99ea72cd3 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 @@ -165,7 +165,7 @@ public enum Talent { //Elemental Strike T4 ELEMENTAL_REACH(148, 4), STRIKING_FORCE(149, 4), DIRECTED_POWER(150, 4), //Duelist A3 T4 - DUELIST_A3_1(151, 4), DUELIST_A3_2(152, 4), DUELIST_A3_3(153, 4), + FEIGNED_RETREAT(151, 4), EXPOSE_WEAKNESS(152, 4), LASTING_IMAGE(153, 4), //universal T4 HEROIC_ENERGY(26, 4), //See icon() and title() for special logic for this one diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/duelist/Feint.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/duelist/Feint.java new file mode 100644 index 000000000..2d1ed0dc9 --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/hero/abilities/duelist/Feint.java @@ -0,0 +1,244 @@ +/* + * Pixel Dungeon + * Copyright (C) 2012-2015 Oleg Dolya + * + * Shattered Pixel Dungeon + * Copyright (C) 2014-2023 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.abilities.duelist; + +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.BlobImmunity; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff; +import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.FlavourBuff; +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.items.Item; +import com.shatteredpixel.shatteredpixeldungeon.items.armor.ClassArmor; +import com.shatteredpixel.shatteredpixeldungeon.levels.Terrain; +import com.shatteredpixel.shatteredpixeldungeon.levels.features.Door; +import com.shatteredpixel.shatteredpixeldungeon.messages.Messages; +import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene; +import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite; +import com.shatteredpixel.shatteredpixeldungeon.sprites.MirrorSprite; +import com.shatteredpixel.shatteredpixeldungeon.ui.HeroIcon; +import com.shatteredpixel.shatteredpixeldungeon.ui.TargetHealthIndicator; +import com.shatteredpixel.shatteredpixeldungeon.utils.GLog; +import com.watabou.noosa.audio.Sample; +import com.watabou.noosa.tweeners.AlphaTweener; +import com.watabou.noosa.tweeners.Delayer; +import com.watabou.utils.Callback; + +public class Feint extends ArmorAbility { + + { + baseChargeUse = 0; + } + + @Override + public int icon() { + return HeroIcon.FEINT; + } + + @Override + public String targetingPrompt() { + return Messages.get(this, "prompt"); + } + + @Override + public int targetedPos(Char user, int dst) { + return dst; + } + + @Override + protected void activate(ClassArmor armor, Hero hero, Integer target) { + if (target == null){ + return; + } + + if (!Dungeon.level.adjacent(hero.pos, target)){ + GLog.w(Messages.get(this, "too_far")); + return; + } + + if (!Dungeon.level.passable[target] || Actor.findChar(target) != null){ + GLog.w(Messages.get(this, "bad_location")); + return; + } + + hero.busy(); + Sample.INSTANCE.play(Assets.Sounds.MISS); + hero.sprite.jump(hero.pos, target, 0, 0.1f, new Callback() { + @Override + public void call() { + if (Dungeon.level.map[hero.pos] == Terrain.OPEN_DOOR) { + Door.leave( hero.pos ); + } + hero.pos = target; + Dungeon.level.occupyCell(hero); + hero.spendAndNext(1f); + } + }); + + AfterImage image = new AfterImage(); + image.pos = hero.pos; + GameScene.add(image, 1); + + int imageAttackPos; + Char enemyTarget = TargetHealthIndicator.instance.target(); + if (enemyTarget != null && enemyTarget.alignment == Char.Alignment.ENEMY){ + imageAttackPos = enemyTarget.pos; + } else { + imageAttackPos = image.pos + (image.pos - target); + } + //do a purely visual attack + hero.sprite.parent.add(new Delayer(0f){ + @Override + protected void onComplete() { + image.sprite.attack(imageAttackPos, new Callback() { + @Override + public void call() { + //do nothing, attack is purely visual + } + }); + } + }); + + for (Mob m : Dungeon.level.mobs.toArray( new Mob[0] )){ + if (m.focusingHero() || + (m.alignment == Char.Alignment.ENEMY && m.state != m.PASSIVE && Dungeon.level.distance(m.pos, image.pos) <= 2)){ + m.aggro(image); + } + } + + armor.charge -= chargeUse(hero); + Item.updateQuickslot(); + } + + @Override + public Talent[] talents() { + return new Talent[]{Talent.FEIGNED_RETREAT, Talent.EXPOSE_WEAKNESS, Talent.LASTING_IMAGE, Talent.HEROIC_ENERGY}; + } + + public static class AfterImage extends Mob { + + { + spriteClass = AfterImageSprite.class; + defenseSkill = 0; + + properties.add(Property.IMMOVABLE); + + alignment = Alignment.ALLY; + state = PASSIVE; + + HP = HT = 1; + + //fades just before the hero's next action + actPriority = Actor.HERO_PRIO+1; + } + + @Override + public boolean canInteract(Char c) { + return false; + } + + @Override + protected boolean act() { + destroy(); + sprite.die(); + return true; + } + + @Override + public void damage( int dmg, Object src ) { + + } + + @Override + public int defenseSkill(Char enemy) { + if (enemy instanceof Mob){ + ((Mob) enemy).clearEnemy(); + } + Buff.affect(enemy, FeintConfusion.class, 1); + if (enemy.sprite != null) enemy.sprite.showLost(); + return 0; + } + + @Override + public int defenseProc(Char enemy, int damage) { + if (enemy instanceof Mob){ + ((Mob) enemy).clearEnemy(); + } + Buff.affect(enemy, FeintConfusion.class, 1f); + if (enemy.sprite != null) enemy.sprite.showLost(); + return super.defenseProc(enemy, damage); + } + + @Override + public void add( Buff buff ) { + + } + + { + immunities.addAll(new BlobImmunity().immunities()); + } + + @Override + public CharSprite sprite() { + CharSprite s = super.sprite(); + ((AfterImageSprite)s).updateArmor(); + return s; + } + + public static class FeintConfusion extends FlavourBuff { + + } + + public static class AfterImageSprite extends MirrorSprite { + @Override + public void updateArmor() { + updateArmor(6); //we can assume heroic armor + } + + @Override + public void resetColor() { + super.resetColor(); + alpha(0.6f); + } + + @Override + public void die() { + //don't interrupt current animation to start fading + //this ensures the face attack animation plays + if (parent != null) { + parent.add( new AlphaTweener( this, 0, 3f ) { + @Override + protected void onComplete() { + AfterImageSprite.this.killAndErase(); + } + } ); + } + } + } + + } +} 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 ef21c8c1c..d28bbacc7 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 @@ -50,6 +50,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero; 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.duelist.Feint; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.DirectableAlly; import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter; import com.shatteredpixel.shatteredpixeldungeon.effects.Speck; @@ -225,6 +226,13 @@ public abstract class Mob extends Char { boolean enemyInFOV = enemy != null && enemy.isAlive() && fieldOfView[enemy.pos] && enemy.invisible <= 0; + //prevents action, but still updates enemy seen status + if (buff(Feint.AfterImage.FeintConfusion.class) != null){ + enemySeen = enemyInFOV; + spend( TICK ); + return true; + } + return state.act( enemyInFOV, justAlerted ); } @@ -690,6 +698,12 @@ public abstract class Mob extends Char { state = HUNTING; } } + + public void clearEnemy(){ + enemy = null; + enemySeen = false; + if (state == HUNTING) state = WANDERING; + } public boolean isTargeting( Char ch){ return enemy == ch; diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MirrorSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MirrorSprite.java index 77651a007..79caf5cd0 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MirrorSprite.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/MirrorSprite.java @@ -25,6 +25,7 @@ import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.actors.Char; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.MirrorImage; import com.watabou.noosa.TextureFilm; +import com.watabou.utils.PointF; public class MirrorSprite extends MobSprite { @@ -42,6 +43,15 @@ public class MirrorSprite extends MobSprite { @Override public void link( Char ch ) { super.link( ch ); + updateArmor(); + } + + @Override + public void bloodBurstA(PointF from, int damage) { + //do nothing + } + + public void updateArmor(){ updateArmor( ((MirrorImage)ch).armTier ); } diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PrismaticSprite.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PrismaticSprite.java index 32f233447..f5da7f983 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PrismaticSprite.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/sprites/PrismaticSprite.java @@ -21,49 +21,16 @@ package com.shatteredpixel.shatteredpixeldungeon.sprites; -import com.shatteredpixel.shatteredpixeldungeon.Dungeon; -import com.shatteredpixel.shatteredpixeldungeon.actors.Char; import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.PrismaticImage; import com.watabou.noosa.Game; -import com.watabou.noosa.TextureFilm; -public class PrismaticSprite extends MobSprite { - - private static final int FRAME_WIDTH = 12; - private static final int FRAME_HEIGHT = 15; - - public PrismaticSprite() { - super(); - - texture( Dungeon.hero.heroClass.spritesheet() ); - updateArmor( 0 ); - idle(); - } - +public class PrismaticSprite extends MirrorSprite { + @Override - public void link( Char ch ) { - super.link( ch ); + public void updateArmor() { updateArmor( ((PrismaticImage)ch).armTier ); } - public void updateArmor( int tier ) { - TextureFilm film = new TextureFilm( HeroSprite.tiers(), tier, FRAME_WIDTH, FRAME_HEIGHT ); - - 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(); - } - @Override public void update() { super.update(); 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 84d829cfc..bb89f5c58 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/HeroIcon.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/HeroIcon.java @@ -63,7 +63,7 @@ public class HeroIcon extends Image { public static final int SPIRIT_HAWK = 27; public static final int CHALLENGE = 28; public static final int ELEMENTAL_STRIKE= 29; - public static final int DUELIST_3 = 30; + public static final int FEINT = 30; public static final int RATMOGRIFY = 31; public HeroIcon(HeroSubClass subCls){