diff --git a/core/src/main/assets/effects.png b/core/src/main/assets/effects.png
index d28872aa2..872c23073 100644
Binary files a/core/src/main/assets/effects.png and b/core/src/main/assets/effects.png differ
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 04c8ea02d..8589f89fc 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/Char.java
@@ -35,6 +35,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Frost;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Hunger;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.MagicalSleep;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Paralysis;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Preparation;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Slow;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Speed;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Vertigo;
@@ -133,7 +134,13 @@ public abstract class Char extends Actor {
int dr = this instanceof Hero && ((Hero)this).rangedWeapon != null && ((Hero)this).subClass ==
HeroSubClass.SNIPER ? 0 : enemy.drRoll();
- int dmg = damageRoll();
+ int dmg;
+ Preparation prep = buff(Preparation.class);
+ if (prep != null){
+ dmg = prep.damageRoll(this, enemy);
+ } else {
+ dmg = damageRoll();
+ }
int effectiveDamage = Math.max( dmg - dr, 0 );
effectiveDamage = attackProc( enemy, effectiveDamage );
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Invisibility.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Invisibility.java
index f2123b4d9..def02a8c2 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Invisibility.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Invisibility.java
@@ -23,6 +23,8 @@ package com.shatteredpixel.shatteredpixeldungeon.actors.buffs;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
+import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
+import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.CloakOfShadows;
import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.TimekeepersHourglass;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
@@ -41,6 +43,9 @@ public class Invisibility extends FlavourBuff {
public boolean attachTo( Char target ) {
if (super.attachTo( target )) {
target.invisible++;
+ if (target instanceof Hero && ((Hero) target).subClass == HeroSubClass.ASSASSIN){
+ Buff.affect(target, Preparation.class);
+ }
return true;
} else {
return false;
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Preparation.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Preparation.java
new file mode 100644
index 000000000..dee7c3d8d
--- /dev/null
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/buffs/Preparation.java
@@ -0,0 +1,294 @@
+/*
+ * Pixel Dungeon
+ * Copyright (C) 2012-2015 Oleg Dolya
+ *
+ * Shattered Pixel Dungeon
+ * Copyright (C) 2014-2017 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.buffs;
+
+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.mobs.Rat;
+import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter;
+import com.shatteredpixel.shatteredpixeldungeon.effects.Effects;
+import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
+import com.shatteredpixel.shatteredpixeldungeon.levels.Level;
+import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
+import com.shatteredpixel.shatteredpixeldungeon.scenes.CellSelector;
+import com.shatteredpixel.shatteredpixeldungeon.scenes.GameScene;
+import com.shatteredpixel.shatteredpixeldungeon.ui.ActionIndicator;
+import com.shatteredpixel.shatteredpixeldungeon.ui.BuffIndicator;
+import com.shatteredpixel.shatteredpixeldungeon.utils.BArray;
+import com.shatteredpixel.shatteredpixeldungeon.utils.GLog;
+import com.watabou.noosa.Image;
+import com.watabou.noosa.audio.Sample;
+import com.watabou.utils.Bundle;
+import com.watabou.utils.PathFinder;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class Preparation extends Buff implements ActionIndicator.Action {
+
+ {
+ //always acts after other buffs, so invisibility effects can process first
+ actPriority = 4;
+ }
+
+ public enum AttackLevel{
+ LVL_1( 1, 0.1f, 0.0f, 1, 0),
+ LVL_2( 3, 0.2f, 0.0f, 1, 1),
+ LVL_3( 6, 0.3f, 0.0f, 2, 3),
+ LVL_4( 11, 0.4f, 0.4f, 2, 5),
+ LVL_5( 16, 0.6f, 0.6f, 1, 7);
+
+ final int turnsReq;
+ final float baseDmgBonus, missingHPBonus;
+ final int damageRolls, blinkDistance;
+
+ AttackLevel( int turns, float base, float missing, int rolls, int dist){
+ turnsReq = turns;
+ baseDmgBonus = base; missingHPBonus = missing;
+ damageRolls =rolls; blinkDistance = dist;
+ }
+
+ public boolean canInstakill(Char defender){
+ return this == LVL_5
+ && !defender.properties().contains(Char.Property.MINIBOSS)
+ && !defender.properties().contains(Char.Property.MINIBOSS);
+ }
+
+ public int damageRoll( Char attacker, Char defender){
+ int dmg = attacker.damageRoll();
+ for( int i = 1; i < damageRolls; i++){
+ int newDmg = attacker.damageRoll();
+ if (newDmg > dmg) dmg = newDmg;
+ }
+ float defenderHPPercent = defender.HP / (float)defender.HT;
+ return Math.round(dmg * (1f + baseDmgBonus + (missingHPBonus * defenderHPPercent)));
+ }
+
+ public static AttackLevel getLvl(int turnsInvis){
+ List values = Arrays.asList(values());
+ Collections.reverse(values);
+ for ( AttackLevel lvl : values ){
+ if (turnsInvis >= lvl.turnsReq){
+ return lvl;
+ }
+ }
+ return LVL_1;
+ }
+ }
+
+ private int turnsInvis = 0;
+
+ @Override
+ public boolean act() {
+ if (target.invisible > 0){
+ turnsInvis++;
+ if (AttackLevel.getLvl(turnsInvis).blinkDistance > 0 && target == Dungeon.hero){
+ ActionIndicator.setAction(this);
+ }
+ BuffIndicator.refreshHero();
+ spend(TICK);
+ } else {
+ detach();
+ }
+ return true;
+ }
+
+ @Override
+ public void detach() {
+ super.detach();
+ ActionIndicator.clearAction(this);
+ }
+
+ public int damageRoll(Char attacker, Char defender ){
+ AttackLevel lvl = AttackLevel.getLvl(turnsInvis);
+ if (lvl.canInstakill(defender)){
+ int dmg = lvl.damageRoll(attacker, defender);
+ defender.damage( Math.max(defender.HT, dmg), attacker );
+ //even though the defender is dead, other effects should still proc (enchants, etc.)
+ return Math.max( defender.HT, dmg);
+ } else {
+ return lvl.damageRoll(attacker, defender);
+ }
+ }
+
+ @Override
+ public int icon() {
+ return BuffIndicator.PREPARATION;
+ }
+
+ @Override
+ public void tintIcon(Image icon) {
+ switch (AttackLevel.getLvl(turnsInvis)){
+ case LVL_1:
+ icon.hardlight(1f, 1f, 1f);
+ break;
+ case LVL_2:
+ icon.hardlight(0f, 1f, 0f);
+ break;
+ case LVL_3:
+ icon.hardlight(1f, 1f, 0f);
+ break;
+ case LVL_4:
+ icon.hardlight(1f, 0.6f, 0f);
+ break;
+ case LVL_5:
+ icon.hardlight(1f, 0f, 0f);
+ break;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return Messages.get(this, "name");
+ }
+
+ @Override
+ public String desc() {
+ String desc = Messages.get(this, "desc");
+
+ AttackLevel lvl = AttackLevel.getLvl(turnsInvis);
+
+ if (lvl.canInstakill(new Rat())){
+ desc += "\n\n" + Messages.get(this, "desc_dmg_instakill",
+ (int)(lvl.baseDmgBonus*100),
+ (int)(lvl.baseDmgBonus*100 + lvl.missingHPBonus*100));
+ } else if (lvl.missingHPBonus > 0){
+ desc += "\n\n" + Messages.get(this, "desc_dmg_scale",
+ (int)(lvl.baseDmgBonus*100),
+ (int)(lvl.baseDmgBonus*100 + lvl.missingHPBonus*100));
+ } else {
+ desc += "\n\n" + Messages.get(this, "desc_dmg", (int)(lvl.baseDmgBonus*100));
+ }
+
+ if (lvl.damageRolls > 1){
+ desc += " " + Messages.get(this, "desc_dmg_likely");
+ }
+
+ if (lvl.blinkDistance > 0){
+ desc += "\n\n" + Messages.get(this, "desc_blink", lvl.blinkDistance);
+ }
+
+ desc += "\n\n" + Messages.get(this, "desc_invis_time", turnsInvis);
+
+ if (lvl.ordinal() != AttackLevel.values().length-1){
+ AttackLevel next = AttackLevel.values()[lvl.ordinal()+1];
+ desc += "\n" + Messages.get(this, "desc_invis_next", next.turnsReq);
+ }
+
+ return desc;
+ }
+
+ private static final String TURNS = "turnsInvis";
+
+ @Override
+ public void restoreFromBundle(Bundle bundle) {
+ super.restoreFromBundle(bundle);
+ turnsInvis = bundle.getInt(TURNS);
+ if (AttackLevel.getLvl(turnsInvis).blinkDistance > 0){
+ ActionIndicator.setAction(this);
+ }
+ }
+
+ @Override
+ public void storeInBundle(Bundle bundle) {
+ super.storeInBundle(bundle);
+ bundle.put(TURNS, turnsInvis);
+ }
+
+ @Override
+ public Image getIcon() {
+ Image actionIco = Effects.get(Effects.Type.WOUND);
+ tintIcon(actionIco);
+ return actionIco;
+ }
+
+ @Override
+ public void doAction() {
+ GameScene.selectCell(attack);
+ }
+
+ private CellSelector.Listener attack = new CellSelector.Listener() {
+
+ @Override
+ public void onSelect(Integer cell) {
+ if (cell == null) return;
+ final Char enemy = Actor.findChar( cell );
+ if (enemy == null || Dungeon.hero.isCharmedBy(enemy)){
+ GLog.w(Messages.get(Preparation.class, "no_target"));
+ } else {
+
+ //just attack them then!
+ if (Dungeon.hero.canAttack(enemy)){
+ if (Dungeon.hero.handle( cell )) {
+ Dungeon.hero.next();
+ }
+ }
+
+ AttackLevel lvl = AttackLevel.getLvl(turnsInvis);
+
+ boolean[] passable = new boolean[Dungeon.level.length()];
+ PathFinder.buildDistanceMap(Dungeon.hero.pos, BArray.or(Level.passable, Level.avoid, passable), lvl.blinkDistance+1);
+ if (PathFinder.distance[cell] == Integer.MAX_VALUE){
+ GLog.w(Messages.get(Preparation.class, "out_of_reach"));
+ return;
+ }
+
+ //we can move through enemies when determining blink distance,
+ // but not when actually jumping to a location
+ for (Char ch : Actor.chars()){
+ if (ch != Dungeon.hero) passable[ch.pos] = false;
+ }
+
+ PathFinder.Path path = PathFinder.find(Dungeon.hero.pos, cell, passable);
+ int attackPos = path.get(path.size()-2);
+
+ if (Dungeon.level.distance(attackPos, Dungeon.hero.pos) > lvl.blinkDistance){
+ GLog.w(Messages.get(Preparation.class, "out_of_reach"));
+ return;
+ }
+
+ Dungeon.hero.pos = attackPos;
+ Dungeon.level.press(Dungeon.hero.pos, Dungeon.hero);
+ //prevents the hero from being interrupted by seeing new enemies
+ Dungeon.level.updateFieldOfView(Dungeon.hero, Level.fieldOfView);
+ Dungeon.hero.checkVisibleMobs();
+
+ Dungeon.hero.sprite.place( Dungeon.hero.pos );
+ Dungeon.hero.sprite.turnTo( Dungeon.hero.pos, cell);
+ CellEmitter.get( Dungeon.hero.pos ).burst( Speck.factory( Speck.WOOL ), 6 );
+ Sample.INSTANCE.play( Assets.SND_PUFF );
+
+ if (Dungeon.hero.handle( cell )) {
+ Dungeon.hero.next();
+ }
+ }
+ }
+
+ @Override
+ public String prompt() {
+ return Messages.get(Preparation.class, "prompt", AttackLevel.getLvl(turnsInvis).blinkDistance);
+ }
+ };
+}
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 36f6a8d9c..8f67db27e 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
@@ -970,7 +970,7 @@ public class Hero extends Char {
super.damage( dmg, src );
}
- private void checkVisibleMobs() {
+ public void checkVisibleMobs() {
ArrayList visible = new ArrayList<>();
boolean newMob = false;
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 1b02ddae8..fab898175 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
@@ -32,12 +32,12 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Amok;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Corruption;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Hunger;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Preparation;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Sleep;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.SoulMark;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Terror;
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.effects.Flare;
import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
import com.shatteredpixel.shatteredpixeldungeon.effects.Surprise;
@@ -475,8 +475,7 @@ public abstract class Mob extends Char {
@Override
public int defenseProc( Char enemy, int damage ) {
if (!enemySeen && enemy == Dungeon.hero && Dungeon.hero.canSurpriseAttack()) {
- if (((Hero)enemy).subClass == HeroSubClass.ASSASSIN) {
- damage *= 1.25f;
+ if (enemy.buff(Preparation.class) != null) {
Wound.hit(this);
} else {
Surprise.hit(this);
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Wound.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Wound.java
index 85810bad6..8c7099c65 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Wound.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/effects/Wound.java
@@ -22,8 +22,8 @@
package com.shatteredpixel.shatteredpixeldungeon.effects;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
-import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
+import com.shatteredpixel.shatteredpixeldungeon.tiles.DungeonTilemap;
import com.watabou.noosa.Game;
import com.watabou.noosa.Group;
import com.watabou.noosa.Image;
@@ -37,6 +37,7 @@ public class Wound extends Image {
public Wound() {
super( Effects.get( Effects.Type.WOUND ) );
+ hardlight(1f, 0f, 0f);
origin.set( width / 2, height / 2 );
}
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/CloakOfShadows.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/CloakOfShadows.java
index 08cfc2f82..5c638336f 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/CloakOfShadows.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/CloakOfShadows.java
@@ -24,8 +24,11 @@ package com.shatteredpixel.shatteredpixeldungeon.items.artifacts;
import com.shatteredpixel.shatteredpixeldungeon.Assets;
import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Buff;
import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.LockedFloor;
+import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Preparation;
import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
+import com.shatteredpixel.shatteredpixeldungeon.actors.hero.HeroSubClass;
import com.shatteredpixel.shatteredpixeldungeon.items.Item;
import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
import com.shatteredpixel.shatteredpixeldungeon.sprites.CharSprite;
@@ -208,6 +211,9 @@ public class CloakOfShadows extends Artifact {
public boolean attachTo( Char target ) {
if (super.attachTo( target )) {
target.invisible++;
+ if (target instanceof Hero && ((Hero) target).subClass == HeroSubClass.ASSASSIN){
+ Buff.affect(target, Preparation.class);
+ }
return true;
} else {
return false;
diff --git a/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties b/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties
index 15fce39df..31a9d15f4 100644
--- a/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties
+++ b/core/src/main/resources/com/shatteredpixel/shatteredpixeldungeon/messages/actors/actors.properties
@@ -172,6 +172,19 @@ actors.buffs.poison.ondeath=You died from poison...
actors.buffs.poison.rankings_desc=Succumbed to Poison
actors.buffs.poison.desc=Poison works its way through the body, slowly impairing its internal functioning.\n\nPoison deals damage each turn proportional to how long until it expires.\n\nTurns of poison remaining: %s.
+actors.buffs.preparation.name=Preparation
+actors.buffs.preparation.desc=The Assassin is waiting patiently, preparing to strike from the shadows.
+actors.buffs.preparation.desc_dmg=His next attack will do _%d%% bonus damage._
+actors.buffs.preparation.desc_dmg_scale=His next attack will do _%d%%-%d%% bonus damage,_ depending on how injured the target is.
+actors.buffs.preparation.desc_dmg_instakill=His next attack will _instantly kill_ any none-boss enemy!\n\nOtherwise it will do _%d%%-%d%% bonus damage,_ depending on how injured the target is.
+actors.buffs.preparation.desc_dmg_likely=The attack will also be more likely to deal a larger amount of damage.
+actors.buffs.preparation.desc_blink=He is able to blink towards an enemy before striking them, with a max distance of _%d._
+actors.buffs.preparation.desc_invis_time=The Assassin has been invisible for _%d turns._
+actors.buffs.preparation.desc_invis_next=His attack will become stronger at _%d turns._
+actors.buffs.preparation.prompt=Select a target to attack!\nMax blink distance: %d
+actors.buffs.preparation.nothing_there=There's nothing to attack there.
+actors.buffs.preparation.out_of_reach=That target is out of reach.
+
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.
@@ -262,7 +275,7 @@ actors.hero.herosubclass.warlock_desc=When using wands on an enemy, the _Warlock
actors.hero.herosubclass.battlemage=battlemage
actors.hero.herosubclass.battlemage_desc=When fighting with his staff, the _Battlemage_ conjures bonus effects depending on the wand his staff is imbued with. His staff will also gain charge through combat.
actors.hero.herosubclass.assassin=assassin
-actors.hero.herosubclass.assassin_desc=When performing a surprise attack, the _Assassin_ inflicts additional damage to his target.
+actors.hero.herosubclass.assassin_desc=While invisible the _Assassin_ prepares a deadly strike on his next attack. The longer spent invisible, the more powerful the attack will be.
actors.hero.herosubclass.freerunner=freerunner
actors.hero.herosubclass.freerunner_desc=The _Freerunner_ builds momentum as he runs. Momentum increases his movement speed and evasion, but it quickly fades when he isn't moving.
actors.hero.herosubclass.sniper=sniper