mirror of
https://github.com/OMGeeky/andors-trail.git
synced 2026-02-23 15:38:29 +01:00
First try at a Pathfinder for moving monsters towards the player based on different aggression types.
This commit is contained in:
@@ -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 -->
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user