First try at a Pathfinder for moving monsters towards the player based on different aggression types.

This commit is contained in:
Oskar Wiksten
2012-08-06 18:14:46 +02:00
parent 93c8a098bd
commit d84d6e6c12
6 changed files with 233 additions and 18 deletions

View File

@@ -504,6 +504,7 @@
<string name="skill_longdescription_concussion">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.</string>
<string name="about_button4">About</string>
<string name="combat_result_monstermoved">%1$s moves.</string>
<!-- =========================================== -->
<!-- Added in v0.6.12 -->

View File

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

View File

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

View File

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

View File

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

View File

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