From 367a364eb36c2fdc7457cfe71a27c0773dea74e3 Mon Sep 17 00:00:00 2001
From: Evan Debenham <Evan@ShatteredPixel.com>
Date: Thu, 20 May 2021 01:23:10 -0400
Subject: [PATCH] v0.9.3: externalize some ghost hero properties to new class

---
 .../actors/mobs/Mob.java                      |   7 +-
 .../actors/mobs/npcs/DirectableAlly.java      | 148 +++++++++++++++++
 .../items/artifacts/DriedRose.java            | 157 ++++--------------
 3 files changed, 181 insertions(+), 131 deletions(-)
 create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/DirectableAlly.java

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 71ff8bd1b..89ae0c8ce 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
@@ -41,6 +41,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.SoulMark;
 import com.shatteredpixel.shatteredpixeldungeon.actors.buffs.Terror;
 import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
 import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent;
+import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.DirectableAlly;
 import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter;
 import com.shatteredpixel.shatteredpixeldungeon.effects.Speck;
 import com.shatteredpixel.shatteredpixeldungeon.effects.Surprise;
@@ -1062,9 +1063,9 @@ public abstract class Mob extends Char {
 	public static void holdAllies( Level level, int holdFromPos ){
 		heldAllies.clear();
 		for (Mob mob : level.mobs.toArray( new Mob[0] )) {
-			//preserve the ghost no matter where they are
-			if (mob instanceof DriedRose.GhostHero) {
-				((DriedRose.GhostHero) mob).clearDefensingPos();
+			//preserve directable allies no matter where they are
+			if (mob instanceof DirectableAlly) {
+				((DirectableAlly) mob).clearDefensingPos();
 				level.mobs.remove( mob );
 				heldAllies.add(mob);
 				
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/DirectableAlly.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/DirectableAlly.java
new file mode 100644
index 000000000..5bc488699
--- /dev/null
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/actors/mobs/npcs/DirectableAlly.java
@@ -0,0 +1,148 @@
+package com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs;
+
+import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
+import com.shatteredpixel.shatteredpixeldungeon.actors.Actor;
+import com.shatteredpixel.shatteredpixeldungeon.actors.Char;
+import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
+import com.shatteredpixel.shatteredpixeldungeon.items.artifacts.DriedRose;
+import com.shatteredpixel.shatteredpixeldungeon.messages.Messages;
+import com.watabou.utils.Bundle;
+import com.watabou.utils.Random;
+
+public class DirectableAlly extends NPC {
+
+	{
+		alignment = Char.Alignment.ALLY;
+		intelligentAlly = true;
+		WANDERING = new Wandering();
+
+		//before other mobs
+		actPriority = MOB_PRIO + 1;
+
+	}
+
+	protected int defendingPos = -1;
+	protected boolean movingToDefendPos = false;
+
+	public void defendPos( int cell ){
+		aggro(null);
+		state = WANDERING;
+		defendingPos = cell;
+		movingToDefendPos = true;
+	}
+
+	public void clearDefensingPos(){
+		defendingPos = -1;
+		movingToDefendPos = false;
+	}
+
+	public void followHero(){
+		aggro(null);
+		state = WANDERING;
+		defendingPos = -1;
+		movingToDefendPos = false;
+	}
+
+	public void targetChar( Char ch ){
+		aggro(ch);
+		target = ch.pos;
+		movingToDefendPos = false;
+	}
+
+	public void directTocell( int cell ){
+		if (!Dungeon.level.heroFOV[cell]
+				|| Actor.findChar(cell) == null
+				|| (Actor.findChar(cell) != Dungeon.hero && Actor.findChar(cell).alignment != Char.Alignment.ENEMY)){
+			defendPos( cell );
+			return;
+		}
+
+		//TODO commenting this out for now, it should be pointless??
+		/*if (fieldOfView == null || fieldOfView.length != Dungeon.level.length()){
+			fieldOfView = new boolean[Dungeon.level.length()];
+		}
+		Dungeon.level.updateFieldOfView( this, fieldOfView );*/
+
+		if (Actor.findChar(cell) == Dungeon.hero){
+			followHero();
+
+		} else if (Actor.findChar(cell).alignment == Char.Alignment.ENEMY){
+			targetChar(Actor.findChar(cell));
+
+		}
+	}
+
+
+	@Override
+	protected Char chooseEnemy() {
+		Char enemy = super.chooseEnemy();
+
+		int targetPos = defendingPos != -1 ? defendingPos : Dungeon.hero.pos;
+
+		//will never attack something far from their target
+		if (enemy != null
+				&& Dungeon.level.mobs.contains(enemy)
+				&& (Dungeon.level.distance(enemy.pos, targetPos) <= 8)){
+			return enemy;
+		}
+
+		return null;
+	}
+
+	private static final String DEFEND_POS = "defend_pos";
+	private static final String MOVING_TO_DEFEND = "moving_to_defend";
+
+	@Override
+	public void storeInBundle(Bundle bundle) {
+		super.storeInBundle(bundle);
+		bundle.put(DEFEND_POS, defendingPos);
+		bundle.put(MOVING_TO_DEFEND, movingToDefendPos);
+	}
+
+	@Override
+	public void restoreFromBundle(Bundle bundle) {
+		super.restoreFromBundle(bundle);
+		if (bundle.contains(DEFEND_POS)) defendingPos = bundle.getInt(DEFEND_POS);
+		movingToDefendPos = bundle.getBoolean(MOVING_TO_DEFEND);
+	}
+
+	private class Wandering extends Mob.Wandering {
+
+		@Override
+		public boolean act( boolean enemyInFOV, boolean justAlerted ) {
+			if ( enemyInFOV && !movingToDefendPos ) {
+
+				enemySeen = true;
+
+				notice();
+				alerted = true;
+				state = HUNTING;
+				target = enemy.pos;
+
+			} else {
+
+				enemySeen = false;
+
+				int oldPos = pos;
+				target = defendingPos != -1 ? defendingPos : Dungeon.hero.pos;
+				//always move towards the hero when wandering
+				if (getCloser( target )) {
+					spend( 1 / speed() );
+					if (pos == defendingPos) movingToDefendPos = false;
+					return moveSprite( oldPos, pos );
+				} else {
+					//if it can't move closer to defending pos, then give up and defend current position
+					if (movingToDefendPos){
+						defendingPos = pos;
+						movingToDefendPos = false;
+					}
+					spend( TICK );
+				}
+
+			}
+			return true;
+		}
+
+	}
+
+}
diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/DriedRose.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/DriedRose.java
index 3af696dcd..e55c85b5d 100644
--- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/DriedRose.java
+++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/items/artifacts/DriedRose.java
@@ -35,6 +35,7 @@ import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Hero;
 import com.shatteredpixel.shatteredpixeldungeon.actors.hero.Talent;
 import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Mob;
 import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.Wraith;
+import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.DirectableAlly;
 import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.Ghost;
 import com.shatteredpixel.shatteredpixeldungeon.actors.mobs.npcs.NPC;
 import com.shatteredpixel.shatteredpixeldungeon.effects.CellEmitter;
@@ -434,37 +435,9 @@ public class DriedRose extends Artifact {
 			if (cell == null) return;
 			
 			Sample.INSTANCE.play( Assets.Sounds.GHOST );
-			
-			if (!Dungeon.level.heroFOV[cell]
-					|| Actor.findChar(cell) == null
-					|| (Actor.findChar(cell) != Dungeon.hero && Actor.findChar(cell).alignment != Char.Alignment.ENEMY)){
-				ghost.yell(Messages.get(ghost, "directed_position_" + Random.IntRange(1, 5)));
-				ghost.aggro(null);
-				ghost.state = ghost.WANDERING;
-				ghost.defendingPos = cell;
-				ghost.movingToDefendPos = true;
-				return;
-			}
-			
-			if (ghost.fieldOfView == null || ghost.fieldOfView.length != Dungeon.level.length()){
-				ghost.fieldOfView = new boolean[Dungeon.level.length()];
-			}
-			Dungeon.level.updateFieldOfView( ghost, ghost.fieldOfView );
-			
-			if (Actor.findChar(cell) == Dungeon.hero){
-				ghost.yell(Messages.get(ghost, "directed_follow_" + Random.IntRange(1, 5)));
-				ghost.aggro(null);
-				ghost.state = ghost.WANDERING;
-				ghost.defendingPos = -1;
-				ghost.movingToDefendPos = false;
-				
-			} else if (Actor.findChar(cell).alignment == Char.Alignment.ENEMY){
-				ghost.yell(Messages.get(ghost, "directed_attack_" + Random.IntRange(1, 5)));
-				ghost.aggro(Actor.findChar(cell));
-				ghost.setTarget(cell);
-				ghost.movingToDefendPos = false;
-				
-			}
+
+			ghost.directTocell(cell);
+
 		}
 		
 		@Override
@@ -510,22 +483,15 @@ public class DriedRose extends Artifact {
 
 	}
 
-	public static class GhostHero extends NPC {
+	public static class GhostHero extends DirectableAlly {
 
 		{
 			spriteClass = GhostSprite.class;
 
 			flying = true;
-
-			alignment = Alignment.ALLY;
-			intelligentAlly = true;
-			WANDERING = new Wandering();
 			
 			state = HUNTING;
 			
-			//before other mobs
-			actPriority = MOB_PRIO + 1;
-			
 			properties.add(Property.UNDEAD);
 		}
 		
@@ -541,7 +507,25 @@ public class DriedRose extends Artifact {
 			updateRose();
 			HP = HT;
 		}
-		
+
+		@Override
+		public void defendPos(int cell) {
+			yell(Messages.get(this, "directed_position_" + Random.IntRange(1, 5)));
+			super.defendPos(cell);
+		}
+
+		@Override
+		public void followHero() {
+			yell(Messages.get(this, "directed_follow_" + Random.IntRange(1, 5)));
+			super.followHero();
+		}
+
+		@Override
+		public void targetChar(Char ch) {
+			yell(Messages.get(this, "directed_attack_" + Random.IntRange(1, 5)));
+			super.targetChar(ch);
+		}
+
 		private void updateRose(){
 			if (rose == null) {
 				rose = Dungeon.hero.belongings.getItem(DriedRose.class);
@@ -552,14 +536,6 @@ public class DriedRose extends Artifact {
 			if (rose == null) return;
 			HT = 20 + 8*rose.level();
 		}
-		
-		private int defendingPos = -1;
-		private boolean movingToDefendPos = false;
-		
-		public void clearDefensingPos(){
-			defendingPos = -1;
-			movingToDefendPos = false;
-		}
 
 		@Override
 		protected boolean act() {
@@ -573,22 +549,6 @@ public class DriedRose extends Artifact {
 			}
 			return super.act();
 		}
-		
-		@Override
-		protected Char chooseEnemy() {
-			Char enemy = super.chooseEnemy();
-			
-			int targetPos = defendingPos != -1 ? defendingPos : Dungeon.hero.pos;
-			
-			//will never attack something far from their target
-			if (enemy != null
-					&& Dungeon.level.mobs.contains(enemy)
-					&& (Dungeon.level.distance(enemy.pos, targetPos) <= 8)){
-				return enemy;
-			}
-			
-			return null;
-		}
 
 		@Override
 		public int attackSkill(Char target) {
@@ -672,6 +632,11 @@ public class DriedRose extends Artifact {
 			if (rose != null && rose.armor != null){
 				speed = rose.armor.speedFactor(this, speed);
 			}
+
+			//moves 2 tiles at a time when returning to the hero
+			if (state == WANDERING && defendingPos == -1){
+				speed *= 2;
+			}
 			
 			return speed;
 		}
@@ -709,10 +674,6 @@ public class DriedRose extends Artifact {
 			}
 			return block;
 		}
-		
-		private void setTarget(int cell) {
-			target = cell;
-		}
 
 		@Override
 		public boolean isImmune(Class effect) {
@@ -832,23 +793,6 @@ public class DriedRose extends Artifact {
 			Sample.INSTANCE.play( Assets.Sounds.GHOST );
 		}
 		
-		private static final String DEFEND_POS = "defend_pos";
-		private static final String MOVING_TO_DEFEND = "moving_to_defend";
-		
-		@Override
-		public void storeInBundle(Bundle bundle) {
-			super.storeInBundle(bundle);
-			bundle.put(DEFEND_POS, defendingPos);
-			bundle.put(MOVING_TO_DEFEND, movingToDefendPos);
-		}
-		
-		@Override
-		public void restoreFromBundle(Bundle bundle) {
-			super.restoreFromBundle(bundle);
-			if (bundle.contains(DEFEND_POS)) defendingPos = bundle.getInt(DEFEND_POS);
-			movingToDefendPos = bundle.getBoolean(MOVING_TO_DEFEND);
-		}
-		
 		{
 			immunities.add( ToxicGas.class );
 			immunities.add( CorrosiveGas.class );
@@ -857,49 +801,6 @@ public class DriedRose extends Artifact {
 			immunities.add( ScrollOfPsionicBlast.class );
 			immunities.add( Corruption.class );
 		}
-		
-		private class Wandering extends Mob.Wandering {
-			
-			@Override
-			public boolean act( boolean enemyInFOV, boolean justAlerted ) {
-				if ( enemyInFOV && !movingToDefendPos ) {
-					
-					enemySeen = true;
-					
-					notice();
-					alerted = true;
-					state = HUNTING;
-					target = enemy.pos;
-					
-				} else {
-					
-					enemySeen = false;
-					
-					int oldPos = pos;
-					target = defendingPos != -1 ? defendingPos : Dungeon.hero.pos;
-					//always move towards the hero when wandering
-					if (getCloser( target )) {
-						//moves 2 tiles at a time when returning to the hero
-						if (defendingPos == -1 && !Dungeon.level.adjacent(target, pos)){
-							getCloser( target );
-						}
-						spend( 1 / speed() );
-						if (pos == defendingPos) movingToDefendPos = false;
-						return moveSprite( oldPos, pos );
-					} else {
-						//if ghost can't move closer to defending pos, then give up an defend current position
-						if (movingToDefendPos){
-							defendingPos = pos;
-							movingToDefendPos = false;
-						}
-						spend( TICK );
-					}
-					
-				}
-				return true;
-			}
-			
-		}
 
 	}