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;