/******************************************************************************* * Copyright (c) 2015, 2016 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.logic.movable.strategies.soldiers; import jsettlers.algorithms.path.Path; import jsettlers.common.buildings.OccupierPlace; import jsettlers.common.movable.EDirection; import jsettlers.common.movable.EMovableType; import jsettlers.common.movable.ESoldierClass; import jsettlers.common.position.ShortPoint2D; import jsettlers.logic.buildings.military.IBuildingOccupyableMovable; import jsettlers.logic.buildings.military.IOccupyableBuilding; import jsettlers.logic.constants.MatchConstants; import jsettlers.logic.movable.EGoInDirectionMode; import jsettlers.logic.movable.Movable; import jsettlers.logic.movable.MovableStrategy; import jsettlers.logic.movable.interfaces.AbstractMovableGrid; import jsettlers.logic.movable.interfaces.IAttackable; public abstract class SoldierStrategy extends MovableStrategy implements IBuildingOccupyableMovable { private static final long serialVersionUID = 5246120883607071865L; /** * Internal state of the {@link SoldierStrategy} class. * * @author Andreas Eberle */ private static enum ESoldierState { AGGRESSIVE, SEARCH_FOR_ENEMIES, HITTING, INIT_GOTO_TOWER, GOING_TO_TOWER, } private final EMovableType movableType; private ESoldierState state = ESoldierState.SEARCH_FOR_ENEMIES; private IOccupyableBuilding building; private IAttackable enemy; private ShortPoint2D oldPathTarget; private boolean inSaveGotoMode = false; private boolean isInTower; private ShortPoint2D inTowerAttackPosition; private boolean defending; public SoldierStrategy(Movable movable, EMovableType movableType) { super(movable); this.movableType = movableType; } @Override protected void action() { switch (state) { case AGGRESSIVE: break; case HITTING: if (!isEnemyAttackable(enemy, isInTower)) { changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); } else { hitEnemy(enemy); // after the animation, execute the actual hit. if (state != ESoldierState.HITTING) { break; // the soldier could have entered an attacked tower } if (enemy.getHealth() <= 0) { enemy = null; changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); break; // don't directly walk on the enemy's position, because there may be others to walk in first } } changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); case SEARCH_FOR_ENEMIES: final short minSearchDistance = getMinSearchDistance(); IAttackable oldEnemy = enemy; enemy = super.getGrid().getEnemyInSearchArea(getAttackPosition(), movable, minSearchDistance, getMaxSearchDistance(isInTower), !defending); // check if we have a new enemy. If so, go in unsafe mode again. if (oldEnemy != null && oldEnemy != enemy) { inSaveGotoMode = false; } // no enemy found, go back in normal mode if (enemy == null) { if (minSearchDistance > 0) { IAttackable toCloseEnemy = super.getGrid().getEnemyInSearchArea( getAttackPosition(), movable, (short) 0, minSearchDistance, !defending); if (toCloseEnemy != null) { if (!isInTower) { // we are in danger because an enemy entered our range where we can't attack => run away EDirection escapeDirection = EDirection.getApproxDirection(toCloseEnemy.getPos(), movable.getPos()); super.goInDirection(escapeDirection, EGoInDirectionMode.GO_IF_ALLOWED_AND_FREE); movable.moveTo(null); // reset moveToRequest, so the soldier doesn't go there after fleeing. } // else { // we are in the tower, so wait and check again next time. break; } } if (defending) { building.towerDefended(this); defending = false; } changeStateTo(ESoldierState.AGGRESSIVE); } else if (isEnemyAttackable(enemy, isInTower)) { // if enemy is close enough, attack it super.lookInDirection(EDirection.getApproxDirection(movable.getPos(), enemy.getPos())); startAttackAnimation(enemy); changeStateTo(ESoldierState.HITTING); } else if (!isInTower) { changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); goToEnemy(enemy); } else { changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); } break; case INIT_GOTO_TOWER: changeStateTo(ESoldierState.GOING_TO_TOWER); // change state before requesting path because of checkPathStepPreconditions() if (!movable.getPos().equals(building.getDoor()) && !super.goToPos(building.getDoor())) { notifyTowerThatRequestFailed(); } break; case GOING_TO_TOWER: if (building.isNotDestroyed() && building.getPlayer() == movable.getPlayer()) { OccupierPlace place = building.addSoldier(this); super.setVisible(false); super.setPosition(place.getPosition().calculatePoint(building.getPos())); super.enableNothingToDoAction(false); if (isBowman()) { this.inTowerAttackPosition = building.getTowerBowmanSearchPosition(place); changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); } else { changeStateTo(ESoldierState.AGGRESSIVE); } isInTower = true; } else { changeStateTo(ESoldierState.AGGRESSIVE); // do a check of the surrounding to find possible enemies. building = null; } break; } } private void notifyTowerThatRequestFailed() { if (building.getPlayer() == movable.getPlayer()) { // only notify, if the tower still belongs to this player building.requestFailed(this); building = null; state = ESoldierState.AGGRESSIVE; } } protected abstract short getMaxSearchDistance(boolean isInTower); protected abstract short getMinSearchDistance(); protected ShortPoint2D getAttackPosition() { return isInTower && isBowman() ? inTowerAttackPosition : movable.getPos(); } private boolean isBowman() { return getSoldierClass() == ESoldierClass.BOWMAN; } private void goToEnemy(IAttackable enemy) { if (inSaveGotoMode) { goToSavely(enemy); } else { ShortPoint2D pos = movable.getPos(); EDirection dir = EDirection.getApproxDirection(pos, enemy.getPos()); if (super.goInDirection(dir, EGoInDirectionMode.GO_IF_ALLOWED_AND_FREE)) { return; } else { inSaveGotoMode = true; goToSavely(enemy); } } } private void goToSavely(IAttackable enemy) { super.goToPos(enemy.getPos()); } private void changeStateTo(ESoldierState state) { this.state = state; switch (state) { case AGGRESSIVE: if (oldPathTarget != null) { super.goToPos(oldPathTarget); oldPathTarget = null; } break; default: break; } } protected abstract void hitEnemy(IAttackable enemy); protected abstract void startAttackAnimation(IAttackable enemy); protected abstract boolean isEnemyAttackable(IAttackable enemy, boolean isInTower); public IBuildingOccupyableMovable setOccupyableBuilding(IOccupyableBuilding building) { if (state != ESoldierState.GOING_TO_TOWER && state != ESoldierState.INIT_GOTO_TOWER) { this.building = building; changeStateTo(ESoldierState.INIT_GOTO_TOWER); super.abortPath(); this.oldPathTarget = null; // this prevents that the soldiers go to this position after he leaves the tower. return this; } else { return null; } } @Override public EMovableType getMovableType() { return movableType; } @Override public Movable getMovable() { return movable; } @Override public void leaveOccupyableBuilding(ShortPoint2D newPosition) { if (isInTower) { super.setPosition(newPosition); super.enableNothingToDoAction(true); super.setVisible(true); super.movable.setSelected(false); isInTower = false; building = null; defending = false; changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); } else if (state == ESoldierState.INIT_GOTO_TOWER || state == ESoldierState.GOING_TO_TOWER) { super.abortPath(); building = null; changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); } } @Override public void setSelected(boolean selected) { movable.setSelected(selected); } @Override public void informAboutAttackable(IAttackable other) { if (state == ESoldierState.AGGRESSIVE && (!isInTower || getSoldierClass() == ESoldierClass.BOWMAN)) { changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); // this searches for the enemy on the next timer click } } @Override public void setDefendingAt(ShortPoint2D pos) { super.setPosition(pos); changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); defending = true; } @Override protected boolean checkPathStepPreconditions(ShortPoint2D pathTarget, int step) { if (state == ESoldierState.INIT_GOTO_TOWER) { return false; // abort previous path when we just got a tower set } boolean result = !((state == ESoldierState.SEARCH_FOR_ENEMIES || state == ESoldierState.HITTING) && step >= 2); if (!result && oldPathTarget == null) { oldPathTarget = pathTarget; } if (state == ESoldierState.GOING_TO_TOWER && (building == null || !building.isNotDestroyed() || building.getPlayer() != movable.getPlayer())) { result = false; } if (enemy != null && state == ESoldierState.SEARCH_FOR_ENEMIES && isEnemyAttackable(enemy, false)) { result = false; } return result; } @Override protected void moveToPathSet(ShortPoint2D oldPosition, ShortPoint2D oldTargetPos, ShortPoint2D targetPos) { if (targetPos != null && this.oldPathTarget != null) { oldPathTarget = null; // reset the path target to be able to get the new one when we hijack the path inSaveGotoMode = false; } changeStateTo(ESoldierState.SEARCH_FOR_ENEMIES); } @Override protected boolean canBeControlledByPlayer() { return state != ESoldierState.INIT_GOTO_TOWER && state != ESoldierState.GOING_TO_TOWER && !isInTower; } @Override protected Path findWayAroundObstacle(ShortPoint2D position, Path path) { if (state == ESoldierState.SEARCH_FOR_ENEMIES) { EDirection direction = EDirection.getDirection(position, path.getNextPos()); EDirection rightDir = direction.getNeighbor(-1); ShortPoint2D rightPos = rightDir.getNextHexPoint(position); EDirection leftDir = direction.getNeighbor(1); ShortPoint2D leftPos = leftDir.getNextHexPoint(position); ShortPoint2D freePosition = getRandomFreePosition(rightPos, leftPos); if (freePosition != null) { return new Path(freePosition); } else { EDirection twoRightDir = direction.getNeighbor(-2); ShortPoint2D twoRightPos = twoRightDir.getNextHexPoint(position); EDirection twoLeftDir = direction.getNeighbor(2); ShortPoint2D twoLeftPos = twoLeftDir.getNextHexPoint(position); freePosition = getRandomFreePosition(twoRightPos, twoLeftPos); if (freePosition != null) { return new Path(freePosition); } else { return path; } } } else { return super.findWayAroundObstacle(position, path); } } private ShortPoint2D getRandomFreePosition(ShortPoint2D pos1, ShortPoint2D pos2) { boolean pos1Free = getGrid().isFreePosition(pos1); boolean pos2Free = getGrid().isFreePosition(pos2); if (pos1Free && pos2Free) { return MatchConstants.random().nextBoolean() ? pos1 : pos2; } else if (pos1Free) { return pos1; } else if (pos2Free) { return pos2; } else { return null; } } @Override protected void strategyKilledEvent(ShortPoint2D pathTarget) { if (building != null) { if (isInTower) { building.removeSoldier(this); } else { notifyTowerThatRequestFailed(); } } } @Override protected void pathAborted(ShortPoint2D pathTarget) { switch (state) { case INIT_GOTO_TOWER: case GOING_TO_TOWER: notifyTowerThatRequestFailed(); break; default: state = ESoldierState.AGGRESSIVE; break; } } protected float getCombatStrength() { return movable.getPlayer().getCombatStrengthInformation().getCombatStrength(isOnOwnGround()); } }