v0.9.3: implemented the shadow clone ability

This commit is contained in:
Evan Debenham 2021-05-28 19:37:37 -04:00
parent 56dfe58117
commit c69ba4408a
7 changed files with 351 additions and 28 deletions

View File

@ -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

View File

@ -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 );

View File

@ -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),

View File

@ -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)){

View File

@ -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);

View File

@ -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<Integer> 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);
}
}
}

View File

@ -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);
}