Merge branch 'pathfinder' into v070

This commit is contained in:
Oskar Wiksten
2013-06-27 20:42:58 +02:00
15 changed files with 311 additions and 33 deletions

View File

@@ -504,7 +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>
<!-- =========================================== -->
<!-- Added in v0.6.12 -->
@@ -562,6 +562,7 @@
<string name="heroinfo_reequip_cost">Re-equip cost (AP):</string>
<string name="heroinfo_useitem_cost">Use item cost (AP):</string>
<string name="combat_result_monstermoved">%1$s moves.</string>
<string name="combat_log_title">Combat log</string>
<string name="conversation_reward_quest_finished">[Quest completed: \"%1$s\"]</string>
<string name="conversation_reward_quest_updated">[Quest updated: \"%1$s\"]</string>

View File

@@ -323,6 +323,12 @@ public final class MainActivity extends Activity implements PlayerMovementListen
}
}
@Override
public void onMonsterMovedDuringCombat(Monster m) {
String monsterName = m.getName();
message(getString(R.string.combat_result_monstermoved, monsterName));
}
@Override
public void onPlayerKilledMonster(Monster target) { }

View File

@@ -15,6 +15,7 @@ import com.gpl.rpg.AndorsTrail.model.AttackResult;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
import com.gpl.rpg.AndorsTrail.model.actor.Actor;
import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.actor.MonsterType;
import com.gpl.rpg.AndorsTrail.model.actor.Player;
import com.gpl.rpg.AndorsTrail.model.item.ItemTraits_OnUse;
import com.gpl.rpg.AndorsTrail.model.item.Loot;
@@ -296,34 +297,83 @@ 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.getAttackCost())) return false;
if (!m.rectPosition.isAdjacentTo(playerPosition)) return false;
return true;
}
private static boolean shouldMoveMonsterInCombat(Monster m, MonsterSpawnArea a, Coord playerPosition) {
final int movementAggressionType = m.getMovementAggressionType();
if (movementAggressionType == MonsterType.AGGRESSIONTYPE_NONE) return false;
if (!m.hasAPs(m.getMoveCost())) return false;
if (m.position.isAdjacentTo(playerPosition)) return false;
if (movementAggressionType == MonsterType.AGGRESSIONTYPE_PROTECT_SPAWN) {
if (a.area.contains(playerPosition)) return true;
} else if (movementAggressionType == MonsterType.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(world.model.player.position);
if (nextMonsterAction == ACTION_NONE) {
endMonsterTurn();
} else if (nextMonsterAction == ACTION_ATTACK) {
attackWithCurrentMonster();
} else if (nextMonsterAction == ACTION_MOVE) {
moveCurrentMonster();
}
}
private void moveCurrentMonster() {
controllers.actorStatsController.useAPs(currentActiveMonster, currentActiveMonster.getMoveCost());
if (!controllers.monsterMovementController.findPathFor(currentActiveMonster, world.model.player.position)) {
// Couldn't find a path to move on.
handleNextMonsterAction();
return;
}
controllers.monsterMovementController.moveMonsterToNextPosition(currentActiveMonster, world.model.currentMap);
combatActionListeners.onMonsterMovedDuringCombat(currentActiveMonster);
waitForNextMonsterAction();
}
private void attackWithCurrentMonster() {
controllers.actorStatsController.useAPs(currentActiveMonster, currentActiveMonster.getAttackCost());
combatTurnListeners.onMonsterIsAttacking(currentActiveMonster);
combatTurnListeners.onMonsterIsAttacking(currentActiveMonster);
AttackResult attack = monsterAttacks(currentActiveMonster);
this.lastAttackResult = attack;
@@ -337,7 +387,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 = 33;
public static final int MAX_MAP_HEIGHT = 33;
public static final int MONSTER_MOVEMENT_TURN_DURATION_MS = 1200;
public static final int ATTACK_ANIMATION_FPS = 10;

View File

@@ -3,16 +3,18 @@ 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;
import com.gpl.rpg.AndorsTrail.model.map.MapObject;
import com.gpl.rpg.AndorsTrail.model.actor.MonsterType;
import com.gpl.rpg.AndorsTrail.model.map.MonsterSpawnArea;
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 +81,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, world.model.player.position);
if (!monsterCanMoveTo(map, tileMap, m.nextPosition)) {
cancelCurrentMonsterMovement(m);
@@ -97,14 +95,26 @@ public final class MonsterMovementController {
monsterMovementListeners.onMonsterSteppedOnPlayer(m);
controllers.combatController.monsterSteppedOnPlayer(m);
} else {
CoordRect previousPosition = new CoordRect(new Coord(m.position), m.rectPosition.size);
m.position.set(m.nextPosition.topLeft);
monsterMovementListeners.onMonsterMoved(map, m, previousPosition);
moveMonsterToNextPosition(m, map);
}
}
}
private static void cancelCurrentMonsterMovement(final Monster m) {
private void determineMonsterNextPosition(Monster m, MonsterSpawnArea area, Coord playerPosition) {
if (m.getMovementAggressionType() == MonsterType.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 static void cancelCurrentMonsterMovement(final Monster m) {
m.movementDestination = null;
m.nextActionTime += getMillisecondsPerMove(m) * Constants.rollValue(Constants.monsterWaitTurns);
}
@@ -118,4 +128,20 @@ 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 monsterCanMoveTo(world.model.currentMap, world.model.currentTileMap, r);
}
public void moveMonsterToNextPosition(Monster m, PredefinedMap map) {
CoordRect previousPosition = new CoordRect(new Coord(m.position), m.rectPosition.size);
m.position.set(m.nextPosition.topLeft);
monsterMovementListeners.onMonsterMoved(map, m, previousPosition);
}
}

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

@@ -8,6 +8,7 @@ public interface CombatActionListener {
void onPlayerAttackSuccess(Monster target, AttackResult attackResult);
void onMonsterAttackMissed(Monster attacker, AttackResult attackResult);
void onMonsterAttackSuccess(Monster attacker, AttackResult attackResult);
void onMonsterMovedDuringCombat(Monster m);
void onPlayerKilledMonster(Monster target);
void onPlayerStartedFleeing();
void onPlayerFailedFleeing();

View File

@@ -21,8 +21,12 @@ public final class CombatActionListeners extends ListOfListeners<CombatActionLis
private final Function2<CombatActionListener, Monster, AttackResult> onMonsterAttackSuccess = new Function2<CombatActionListener, Monster, AttackResult>() {
@Override public void call(CombatActionListener listener, Monster attacker, AttackResult attackResult) { listener.onMonsterAttackSuccess(attacker, attackResult); }
};
private final Function1<CombatActionListener, Monster> onPlayerKilledMonster = new Function1<CombatActionListener, Monster>() {
private final Function1<CombatActionListener, Monster> onMonsterMovedDuringCombat = new Function1<CombatActionListener, Monster>() {
@Override public void call(CombatActionListener listener, Monster m) { listener.onMonsterMovedDuringCombat(m); }
};
private final Function1<CombatActionListener, Monster> onPlayerKilledMonster = new Function1<CombatActionListener, Monster>() {
@Override public void call(CombatActionListener listener, Monster target) { listener.onPlayerKilledMonster(target); }
};
@@ -58,6 +62,11 @@ public final class CombatActionListeners extends ListOfListeners<CombatActionLis
callAllListeners(this.onMonsterAttackSuccess, attacker, attackResult);
}
@Override
public void onMonsterMovedDuringCombat(Monster m) {
callAllListeners(this.onMonsterMovedDuringCombat, m);
}
@Override
public void onPlayerKilledMonster(Monster target) {
callAllListeners(this.onPlayerKilledMonster, target);

View File

@@ -16,20 +16,20 @@ import com.gpl.rpg.AndorsTrail.util.CoordRect;
import com.gpl.rpg.AndorsTrail.util.Range;
public final class Monster extends Actor {
public Coord movementDestination = null;
public long nextActionTime = 0;
public final CoordRect nextPosition;
private boolean forceAggressive = false;
private ItemContainer shopItems = null;
private final MonsterType monsterType;
public Monster(MonsterType monsterType) {
super(monsterType.tileSize, false, monsterType.isImmuneToCriticalHits());
this.monsterType = monsterType;
this.iconID = monsterType.iconID;
this.iconID = monsterType.iconID;
this.nextPosition = new CoordRect(new Coord(), monsterType.tileSize);
resetStatsToBaseTraits();
this.ap.setMax();
@@ -58,7 +58,8 @@ public final class Monster extends Actor {
public String getMonsterTypeID() { return monsterType.id; }
public String getFaction() { return monsterType.faction; }
public int getMonsterClass() { return monsterType.monsterClass; }
public int getMovementAggressionType() { return monsterType.aggressionType; }
public void createLoot(Loot container, Player player) {
int exp = this.getExp();
exp += exp * player.getSkillLevel(SkillCollection.SKILL_MORE_EXP) * SkillCollection.PER_SKILLPOINT_INCREASE_MORE_EXP_PERCENT / 100;

View File

@@ -25,6 +25,8 @@ public final class MonsterType {
public final boolean isUnique; // Unique monsters are not respawned.
public final String faction;
public final int monsterClass;
public final int aggressionType;
public final Size tileSize;
public final int iconID;
public final int maxAP;
@@ -49,6 +51,7 @@ public final class MonsterType {
boolean isUnique,
String faction,
int monsterClass,
int aggressionType,
Size tileSize,
int iconID,
int maxAP,
@@ -71,6 +74,7 @@ public final class MonsterType {
this.faction = faction;
this.isUnique = isUnique;
this.monsterClass = monsterClass;
this.aggressionType = aggressionType;
this.tileSize = tileSize;
this.iconID = iconID;
this.maxAP = maxAP;
@@ -85,11 +89,34 @@ public final class MonsterType {
this.damageResistance = damageResistance;
this.onHitEffects = onHitEffects;
}
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 static int getSuggestedAggressionType(int monsterClass) {
switch (monsterClass) {
case MONSTERCLASS_CONSTRUCT:
case MONSTERCLASS_GIANT:
case MONSTERCLASS_GHOST:
return AGGRESSIONTYPE_NONE;
case MONSTERCLASS_DEMON:
case MONSTERCLASS_ANIMAL:
case MONSTERCLASS_REPTILE:
case MONSTERCLASS_INSECT:
return AGGRESSIONTYPE_PROTECT_SPAWN;
case MONSTERCLASS_UNDEAD:
case MONSTERCLASS_HUMANOID:
return AGGRESSIONTYPE_HELP_OTHERS;
default:
return AGGRESSIONTYPE_NONE;
}
}
public boolean isImmuneToCriticalHits() {
if (monsterClass == MONSTERCLASS_GHOST) return true;
else if (monsterClass == MONSTERCLASS_UNDEAD) return true;
else if (monsterClass == MONSTERCLASS_DEMON) return true;
if (monsterClass == MONSTERCLASS_UNDEAD) return true;
if (monsterClass == MONSTERCLASS_DEMON) return true;
return false;
}

View File

@@ -55,6 +55,7 @@ public final class MonsterTypeParser extends JsonCollectionParserFor<MonsterType
, o.optInt(JsonFieldNames.Monster.unique, 0) > 0
, o.optString(JsonFieldNames.Monster.faction, null)
, o.optInt(JsonFieldNames.Monster.monsterClass, MonsterType.MONSTERCLASS_HUMANOID)
, o.optInt(JsonFieldNames.Monster.movementAggressionType, MonsterType.AGGRESSIONTYPE_NONE)
, ResourceParserUtils.parseSize(o.optString(JsonFieldNames.Monster.size, null), size1x1) //TODO: This could be loaded from the tileset size instead.
, ResourceParserUtils.parseImageID(tileLoader, o.getString(JsonFieldNames.Monster.iconID))
, maxAP

View File

@@ -109,6 +109,7 @@ public final class JsonFieldNames {
public static final String spawnGroup = "spawnGroup";
public static final String size = "size";
public static final String monsterClass = "monsterClass";
public static final String movementAggressionType = "movementAggressionType";
public static final String unique = "unique";
public static final String faction = "faction";
public static final String maxHP = "maxHP";

View File

@@ -455,8 +455,13 @@ public final class MainView extends SurfaceView
@Override
public void onMonsterMoved(PredefinedMap map, Monster m, CoordRect previousPosition) {
if (map != currentMap) return;
if (!mapViewArea.intersects(m.rectPosition)) return;
redrawNextTick = true;
if (!mapViewArea.intersects(m.rectPosition) && !mapViewArea.intersects(previousPosition)) return;
if (model.uiSelections.isInCombat) {
redrawArea(previousPosition, REDRAW_AREA_MONSTER_MOVED);
redrawArea(m.rectPosition, REDRAW_AREA_MONSTER_MOVED);
} else {
redrawNextTick = true;
}
}
@Override

View File

@@ -34,6 +34,7 @@ var ATEditor = (function(ATEditor, _) {
,moveCost: 10
,unique: 0
,monsterClass: 0
,movementAggressionType: 0
,attackDamage: {}
,hitEffect: { increaseCurrentHP: {}, increaseCurrentAP: {}, conditionsSource: [], conditionsTarget: [] }
}

View File

@@ -65,6 +65,14 @@
<label for="faction" class="hint hint--top" data-hint="Used for making whole groups of monsters aggressive from dialogue. Dialogue replies may affect the player's faction ratings, and having rating less than 0 for a faction will make all monsters in a faction automatically attack.">Faction:</label>
<input type="text" size="30" id="faction" ng-model="obj.faction" class="field at-input-id"/>
</div>
<div class="fieldWithLabel" ng-ds-fade="obj.showAdvanced">
<label for="movementAggressionType">Movement in combat:</label>
<select class="field" id="movementAggressionType" ng-model="obj.movementAggressionType">
<option value="0">Does not move in combat</option>
<option value="1">Help others in same spawn area</option>
<option value="2">Move towards player if inside spawn area</option>
</select>
</div>
<div class="fieldWithLabel">
<label class="checkbox"><input type="checkbox" id="hasConversation" ng-model="obj.hasConversation" />Has conversation</label>
</div>