diff --git a/AndorsTrail/res/values/strings.xml b/AndorsTrail/res/values/strings.xml
index 08ac9aaa2..a571b2bbf 100644
--- a/AndorsTrail/res/values/strings.xml
+++ b/AndorsTrail/res/values/strings.xml
@@ -504,6 +504,7 @@
When making an attack on a target whose block chance (BC) is at least %1$d lower than your attack chance (AC), there is a %2$d %% chance that the hit will cause a concussion on the target. A concussion will severely lower the target\'s offensive combat abilities, making the target less able to land successful attacks.
About
+ %1$s moves.
diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/CombatController.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/CombatController.java
index c9431f6a3..e96b988e3 100644
--- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/CombatController.java
+++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/CombatController.java
@@ -296,33 +296,80 @@ public final class CombatController implements VisualEffectCompletedCallback {
handleNextMonsterAction();
}
- private Monster determineNextMonster(Monster previousMonster) {
- if (previousMonster != null) {
- if (previousMonster.hasAPs(previousMonster.getAttackCost())) return previousMonster;
+ private static final int ACTION_NONE = 0;
+ private static final int ACTION_ATTACK = 1;
+ private static final int ACTION_MOVE = 2;
+ private int determineNextMonsterAction(Coord playerPosition) {
+ if (currentActiveMonster != null) {
+ if (shouldAttackWithMonsterInCombat(currentActiveMonster, playerPosition)) return ACTION_ATTACK;
}
for (MonsterSpawnArea a : world.model.currentMap.spawnAreas) {
for (Monster m : a.monsters) {
if (!m.isAgressive()) continue;
- if (m.isAdjacentTo(world.model.player)) {
- if (m.hasAPs(m.getAttackCost())) return m;
+ if (shouldAttackWithMonsterInCombat(m, playerPosition)) {
+ currentActiveMonster = m;
+ return ACTION_ATTACK;
+ } else if (shouldMoveMonsterInCombat(m, a, playerPosition)) {
+ currentActiveMonster = m;
+ return ACTION_MOVE;
}
}
}
- return null;
+ return ACTION_NONE;
+ }
+
+ private static boolean shouldAttackWithMonsterInCombat(Monster m, Coord playerPosition) {
+ if (!m.hasAPs(m.combatTraits.attackCost)) return false;
+ if (!m.rectPosition.isAdjacentTo(playerPosition)) return false;
+ return true;
+ }
+ private static boolean shouldMoveMonsterInCombat(Monster m, MonsterSpawnArea a, Coord playerPosition) {
+ if (m.aggressionType == Monster.AGGRESSIONTYPE_NONE) return false;
+
+ if (!m.hasAPs(m.actorTraits.moveCost)) return false;
+ if (m.position.isAdjacentTo(playerPosition)) return false;
+
+ if (m.aggressionType == Monster.AGGRESSIONTYPE_PROTECT_SPAWN) {
+ if (a.area.contains(playerPosition)) return true;
+ } else if (m.aggressionType == Monster.AGGRESSIONTYPE_HELP_OTHERS) {
+ for (Monster o : a.monsters) {
+ if (o == m) continue;
+ if (o.rectPosition.isAdjacentTo(playerPosition)) return true;
+ }
+ }
+ return false;
}
private void handleNextMonsterAction() {
if (!world.model.uiSelections.isMainActivityVisible) return;
- currentActiveMonster = determineNextMonster(currentActiveMonster);
- if (currentActiveMonster == null) {
+ int nextMonsterAction = determineNextMonsterAction(model.player.position);
+ if (nextMonsterAction == ACTION_NONE) {
endMonsterTurn();
- return;
+ } else if (nextMonsterAction == ACTION_ATTACK) {
+ attackWithCurrentMonster();
+ } else if (nextMonsterAction == ACTION_MOVE) {
+ moveCurrentMonster();
}
+ }
+
+ private void moveCurrentMonster() {
+ currentActiveMonster.useAPs(currentActiveMonster.actorTraits.moveCost);
+ if (context.monsterMovementController.findPathFor(currentActiveMonster, model.player.position)) {
+ currentActiveMonster.position.set(currentActiveMonster.nextPosition.topLeft);
+ String monsterName = currentActiveMonster.actorTraits.name;
+ Resources r = context.mainActivity.getResources();
+ message(r.getString(R.string.combat_result_monstermoved, monsterName));
+ context.mainActivity.redrawAll(MainView.REDRAW_ALL_MONSTER_MOVED);
+ }
+ monsterTurnHandler.sendEmptyMessageDelayed(0, context.preferences.attackspeed_milliseconds);
+ }
+
+ private void attackWithCurrentMonster() {
controllers.actorStatsController.useAPs(currentActiveMonster, currentActiveMonster.getAttackCost());
-
+
combatTurnListeners.onMonsterIsAttacking(currentActiveMonster);
AttackResult attack = monsterAttacks(currentActiveMonster);
this.lastAttackResult = attack;
@@ -337,7 +384,7 @@ public final class CombatController implements VisualEffectCompletedCallback {
waitForNextMonsterAction();
}
}
-
+
private static final int CALLBACK_MONSTERATTACK = 0;
private static final int CALLBACK_PLAYERATTACK = 1;
diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/Constants.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/Constants.java
index 9b299b3fe..cc2c3678a 100644
--- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/Constants.java
+++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/Constants.java
@@ -20,6 +20,8 @@ public final class Constants {
public static final float EXP_FACTOR_SCALING = 0.7f;
public static final int FLEE_FAIL_CHANCE_PERCENT = 20;
public static final long MINIMUM_INPUT_INTERVAL = AndorsTrailApplication.DEVELOPMENT_DEBUGBUTTONS ? 50 : 200;
+ public static final int MAX_MAP_WIDTH = 30;
+ public static final int MAX_MAP_HEIGHT = 30;
public static final int MONSTER_MOVEMENT_TURN_DURATION_MS = 1200;
public static final int ATTACK_ANIMATION_FPS = 10;
diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/MonsterMovementController.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/MonsterMovementController.java
index 6a01e1fcf..c9c0382e3 100644
--- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/MonsterMovementController.java
+++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/MonsterMovementController.java
@@ -3,6 +3,7 @@ package com.gpl.rpg.AndorsTrail.controller;
import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.listeners.MonsterMovementListeners;
+import com.gpl.rpg.AndorsTrail.controller.PathFinder.EvaluateWalkable;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.map.LayeredTileMap;
@@ -12,7 +13,7 @@ import com.gpl.rpg.AndorsTrail.model.map.PredefinedMap;
import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.CoordRect;
-public final class MonsterMovementController {
+public final class MonsterMovementController implements EvaluateWalkable {
private final ControllerContext controllers;
private final WorldContext world;
public final MonsterMovementListeners monsterMovementListeners = new MonsterMovementListeners();
@@ -79,11 +80,7 @@ public final class MonsterMovementController {
// Monster has been moving and arrived at the destination.
cancelCurrentMonsterMovement(m);
} else {
- // Monster is moving.
- m.nextPosition.topLeft.set(
- m.position.x + sgn(m.movementDestination.x - m.position.x)
- ,m.position.y + sgn(m.movementDestination.y - m.position.y)
- );
+ determineMonsterNextPosition(m, area, model.player.position);
if (!monsterCanMoveTo(map, tileMap, m.nextPosition)) {
cancelCurrentMonsterMovement(m);
@@ -104,7 +101,21 @@ public final class MonsterMovementController {
}
}
- private static void cancelCurrentMonsterMovement(final Monster m) {
+ private void determineMonsterNextPosition(Monster m, MonsterSpawnArea area, Coord playerPosition) {
+ if (m.aggressionType == Monster.AGGRESSIONTYPE_PROTECT_SPAWN) {
+ if (area.area.contains(playerPosition)) {
+ if (findPathFor(m, playerPosition)) return;
+ }
+ }
+
+ // Monster is moving in a straight line.
+ m.nextPosition.topLeft.set(
+ m.position.x + sgn(m.movementDestination.x - m.position.x)
+ ,m.position.y + sgn(m.movementDestination.y - m.position.y)
+ );
+ }
+
+ private void cancelCurrentMonsterMovement(final Monster m) {
m.movementDestination = null;
m.nextActionTime += getMillisecondsPerMove(m) * Constants.rollValue(Constants.monsterWaitTurns);
}
@@ -118,4 +129,14 @@ public final class MonsterMovementController {
if (i >= 1) return 1;
return 0;
}
+
+ private final PathFinder pathfinder = new PathFinder(Constants.MAX_MAP_WIDTH, Constants.MAX_MAP_HEIGHT, this);
+ public boolean findPathFor(Monster m, Coord to) {
+ return pathfinder.findPathBetween(m.rectPosition, to, m.nextPosition);
+ }
+
+ @Override
+ public boolean isWalkable(CoordRect r) {
+ return model.currentMap.monsterCanMoveTo(r);
+ }
}
diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/PathFinder.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/PathFinder.java
new file mode 100644
index 000000000..31efbca3a
--- /dev/null
+++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/controller/PathFinder.java
@@ -0,0 +1,139 @@
+package com.gpl.rpg.AndorsTrail.controller;
+
+import java.util.Arrays;
+
+import com.gpl.rpg.AndorsTrail.util.Coord;
+import com.gpl.rpg.AndorsTrail.util.CoordRect;
+
+public class PathFinder {
+ private final int maxWidth;
+ private final int maxHeight;
+ private final boolean visited[];
+ private final ListOfCoords visitQueue;
+ private final EvaluateWalkable map;
+ public int iterations = 0;
+
+ public PathFinder(int maxWidth, int maxHeight, EvaluateWalkable map) {
+ this.maxWidth = maxWidth;
+ this.maxHeight = maxHeight;
+ this.map = map;
+ this.visited = new boolean[maxWidth*maxHeight];
+ this.visitQueue = new ListOfCoords(maxWidth*maxHeight);
+ }
+
+ public interface EvaluateWalkable {
+ public boolean isWalkable(CoordRect r);
+ }
+
+ public boolean findPathBetween(final CoordRect from, final Coord to, CoordRect nextStep) {
+ iterations = 0;
+ if (from.equals(to)) return false;
+
+ Coord measureDistanceTo = from.topLeft;
+ Coord p = nextStep.topLeft;
+ Arrays.fill(visited, false);
+ visitQueue.reset();
+
+ visitQueue.push(to.x, to.y, 0);
+ visited[(to.y * maxWidth) + to.x] = true;
+
+ while (!visitQueue.isEmpty()) {
+ visitQueue.popFirst(p);
+ ++iterations;
+
+ if (iterations > 100) return false;
+
+ if (from.isAdjacentTo(p)) return true;
+
+ p.x -= 1; visit(nextStep, measureDistanceTo);
+ p.x += 2; visit(nextStep, measureDistanceTo);
+ p.x -= 1; p.y -= 1; visit(nextStep, measureDistanceTo);
+ p.y += 2; visit(nextStep, measureDistanceTo);
+ p.x -= 1; visit(nextStep, measureDistanceTo);
+ p.x += 2; visit(nextStep, measureDistanceTo);
+ p.y -= 2; visit(nextStep, measureDistanceTo);
+ p.x -= 2; visit(nextStep, measureDistanceTo);
+ }
+ return false;
+ }
+
+ private void visit(CoordRect r, Coord measureDistanceTo) {
+ final int x = r.topLeft.x;
+ final int y = r.topLeft.y;
+
+ if (x < 0) return;
+ if (y < 0) return;
+ if (x >= maxWidth) return;
+ if (y >= maxHeight) return;
+
+ final int i = (y * maxWidth) + x;
+ if (visited[i]) return;
+ visited[i] = true;
+ if (!map.isWalkable(r)) return;
+
+ int dx = (measureDistanceTo.x - x);
+ int dy = (measureDistanceTo.y - y);
+ visitQueue.push(x, y, dx * dx + dy * dy);
+ }
+
+ private static final class ListOfCoords {
+ private final int coords[];
+ private final int weights[];
+ private final int maxIndex;
+ private int lastIndex; // Index of the last coord that was inserted
+ private int frontIndex; // Index to the first coord that is not discarded
+ private static final int DISCARDED = -1;
+
+ public ListOfCoords(int maxSize) {
+ this.maxIndex = maxSize-1;
+ this.coords = new int[maxSize];
+ this.weights = new int[maxSize];
+ }
+ public void reset() {
+ lastIndex = -1;
+ frontIndex = 0;
+ }
+ private static int coordsToInt(int x, int y) {
+ return ((y << 8) & 0xff00) | (x & 0xff);
+ }
+ private static void intToCoords(int c, Coord dest) {
+ dest.x = c & 0xff;
+ dest.y = (c >> 8) & 0xff;
+ }
+
+ public void push(int x, int y, int weight) {
+ if (lastIndex == maxIndex) return;
+ ++lastIndex;
+ coords[lastIndex] = coordsToInt(x, y);
+ weights[lastIndex] = weight;
+ }
+
+ public int popFirst(Coord dest) {
+ int i = frontIndex;
+ int lowestWeightIndex = i;
+ int lowestWeight = weights[i];
+ ++i;
+ for(;i <= lastIndex; ++i) {
+ if (weights[i] == DISCARDED) continue;
+ if (weights[i] < lowestWeight) {
+ lowestWeightIndex = i;
+ lowestWeight = weights[i];
+ }
+ }
+ intToCoords(coords[lowestWeightIndex], dest);
+ weights[lowestWeightIndex] = DISCARDED;
+
+ // Increase frontIndex to the first index that is not discarded.
+ while (frontIndex <= lastIndex) {
+ if (weights[frontIndex] == DISCARDED) ++frontIndex;
+ else break;
+ }
+
+ return lowestWeight;
+ }
+
+ public boolean isEmpty() {
+ return frontIndex > lastIndex;
+ }
+ }
+}
diff --git a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/actor/Monster.java b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/actor/Monster.java
index 83dfdcc37..b7359c1af 100644
--- a/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/actor/Monster.java
+++ b/AndorsTrail/src/com/gpl/rpg/AndorsTrail/model/actor/Monster.java
@@ -23,6 +23,11 @@ public final class Monster extends Actor {
private boolean forceAggressive = false;
private ItemContainer shopItems = null;
+
+ public final int aggressionType = AGGRESSIONTYPE_PROTECT_SPAWN;
+ public static final int AGGRESSIONTYPE_NONE = 0;
+ public static final int AGGRESSIONTYPE_HELP_OTHERS = 1; // Will move to help if the player attacks some other monster in the same spawn.
+ public static final int AGGRESSIONTYPE_PROTECT_SPAWN = 2; // Will move to attack if the player stands inside the spawn.
private final MonsterType monsterType;