From 07d9b5a8c2baf5ec836ccdf04688706ad19b3d3b Mon Sep 17 00:00:00 2001 From: Pedro Ruschel Date: Wed, 1 Jun 2022 18:13:31 +0200 Subject: [PATCH] Daan's assignment. Committing on his behalf --- .../handlers/BaseDamageMoveHandler.java | 79 +++++++ .../pirates/handlers/BaseMoveHandler.java | 62 ++++++ .../pirates/handlers/MoveHandler.java | 15 ++ .../pirates/handlers/MoveHandlerFactory.java | 28 +++ .../pirates/handlers/PhysicalMoveHandler.java | 27 +++ .../pirates/handlers/SpecialMoveHandler.java | 27 +++ .../pirates/handlers/StatusMoveHandler.java | 82 +++++++ .../pirates/helper/TurnActionHelper.java | 13 ++ .../pirates/model/battle/TurnAction.java | 2 + .../pirates/service/BattleService.java | 17 +- .../pirates/service/CalculationService.java | 12 +- .../pirates/service/GetMoveService.java | 36 ++++ .../service/TurnInformationService.java | 201 +++--------------- .../handlers/MoveHandlerFactoryTest.java | 34 +++ .../handlers/StatusMoveHandlerTest.java | 41 ++++ .../service/CalculationServiceTest.java | 12 +- .../service/TurnInformationServiceTest.java | 70 ++++++ .../nl/rabobank/pirates/smoke/TestConfig.java | 2 +- .../src/app/simulate/simulate.component.css | 1 + .../src/app/simulate/simulate.component.html | 5 +- .../src/app/simulate/simulate.component.ts | 48 ++++- 21 files changed, 621 insertions(+), 193 deletions(-) create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/BaseDamageMoveHandler.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/BaseMoveHandler.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandler.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandlerFactory.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/PhysicalMoveHandler.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/SpecialMoveHandler.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/handlers/StatusMoveHandler.java create mode 100644 backend/src/main/java/nl/rabobank/pirates/helper/TurnActionHelper.java create mode 100644 backend/src/test/java/nl/rabobank/pirates/handlers/MoveHandlerFactoryTest.java create mode 100644 backend/src/test/java/nl/rabobank/pirates/handlers/StatusMoveHandlerTest.java create mode 100644 backend/src/test/java/nl/rabobank/pirates/service/TurnInformationServiceTest.java diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/BaseDamageMoveHandler.java b/backend/src/main/java/nl/rabobank/pirates/handlers/BaseDamageMoveHandler.java new file mode 100644 index 0000000..aa95ae1 --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/BaseDamageMoveHandler.java @@ -0,0 +1,79 @@ +package nl.rabobank.pirates.handlers; + +import static nl.rabobank.pirates.model.battle.TurnActionFactory.makeFaintAnimation; + +import java.util.ArrayList; +import java.util.List; + +import nl.rabobank.pirates.helper.TurnActionHelper; +import nl.rabobank.pirates.model.battle.TurnAction; +import nl.rabobank.pirates.model.battle.TurnActionFactory; +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.move.Move; + +public abstract class BaseDamageMoveHandler extends BaseMoveHandler { + + public List handleMove(Move pokemonMove, Pokemon attackingPokemon, Pokemon defendingPokemon, + boolean isOwnPokemonAttacking) { + + final List actions = new ArrayList<>(); + + actions.add(TurnActionFactory.makePokemonUsedMove(attackingPokemon.getName().toUpperCase(), + pokemonMove.getName(), TurnActionHelper.getSubject(isOwnPokemonAttacking))); + + if (!willMoveHit(pokemonMove, attackingPokemon, defendingPokemon)) { + actions.add(TurnActionFactory.makeTextOnly("The attack missed!")); + return actions; + } + + // TODO calculate critical + // TODO calculate weakness/strenghts + + actions.addAll(processNumberOfHits(pokemonMove, defendingPokemon, isOwnPokemonAttacking, + determineDamage(pokemonMove, attackingPokemon, defendingPokemon))); + + actions.add(processFainting(defendingPokemon, isOwnPokemonAttacking)); + + actions.add(processStatusEffect(defendingPokemon, pokemonMove, isOwnPokemonAttacking)); + + return actions; + } + + protected abstract int determineDamage(Move pokemonMove, Pokemon attackingPokemon, Pokemon defendingPokemon); + + private List processNumberOfHits(final Move pokemonMove, final Pokemon defendingPokemon, + final boolean isOwnPokemonAttacking, final int damage) { + final List actions = new ArrayList<>(); + + int numberHits = calculationService.calculateNumberOfHitTimes(pokemonMove.getHitTimes()); + + for (int x = 0; x < numberHits; x++) { + + defendingPokemon.dealDamage(damage); + + actions.add(TurnActionFactory.makeDamageOnlyAnimation(damage, + TurnActionHelper.getSubject(isOwnPokemonAttacking))); + } + + if (numberHits > 1) { + actions.add(TurnActionFactory.makeTextHitXTimes(numberHits)); + } + + return actions; + } + + public TurnAction processFainting(final Pokemon defendingPokemon, boolean isOwnPokemonAttacking) { + + if (checkIfDefendingPokemonFainted(defendingPokemon)) { + return makeFaintAnimation(defendingPokemon.getName().toUpperCase(), + TurnActionHelper.getSubject(isOwnPokemonAttacking)); + } + + return null; + } + + public boolean checkIfDefendingPokemonFainted(final Pokemon defendingPokemon) { + return defendingPokemon.getCurrentHp() <= 0; + } + +} diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/BaseMoveHandler.java b/backend/src/main/java/nl/rabobank/pirates/handlers/BaseMoveHandler.java new file mode 100644 index 0000000..bcb4f6e --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/BaseMoveHandler.java @@ -0,0 +1,62 @@ +package nl.rabobank.pirates.handlers; + +import static nl.rabobank.pirates.model.move.StatusEffect.Condition.CONFUSED; +import static nl.rabobank.pirates.model.move.StatusEffect.Condition.SLEEP; + +import org.springframework.beans.factory.annotation.Autowired; + +import nl.rabobank.pirates.helper.TurnActionHelper; +import nl.rabobank.pirates.model.battle.TurnAction; +import nl.rabobank.pirates.model.battle.TurnActionFactory; +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.common.Stat; +import nl.rabobank.pirates.model.move.Move; +import nl.rabobank.pirates.service.CalculationService; + +/** + * Class containing methods for shared functionality across MoveHandlers. + * + * @author Daan + * + */ +public class BaseMoveHandler { + + @Autowired + protected CalculationService calculationService; + + protected boolean willMoveHit(Move move, Pokemon attackingPokemon, Pokemon defendingPokemon) { + int moveAccuracy = move.getAccuracy(); + int pokemonAccuracy = attackingPokemon.getStatAmount(Stat.ACCURACY); + + return calculationService.calculateAccuracyAndRollIfMoveHits(pokemonAccuracy, moveAccuracy); + } + + protected TurnAction processStatusEffect(final Pokemon defendingPokemon, final Move pokemonMove, + boolean isOwnPokemonAttacking) { + + if (pokemonMove.getStatusEffect() == null) { + return null; + } + + boolean isSuccessful = calculationService.isRollSuccessful(pokemonMove.getStatusEffect().getChance()); + if (!isSuccessful) { + return null; + } + + boolean canApplyStatusEffect = defendingPokemon.addStatusEffect(pokemonMove.getStatusEffect().getCondition()); + + if (!canApplyStatusEffect) { + return TurnActionFactory.makeTextOnly("The attack missed!"); + } + if (SLEEP.equals(pokemonMove.getStatusEffect().getCondition())) { + defendingPokemon.putPokemonToSleep(calculationService.randomSleepOrConfusedTurns()); + } + if (CONFUSED.equals(pokemonMove.getStatusEffect().getCondition())) { + defendingPokemon.confusePokemon(calculationService.randomSleepOrConfusedTurns()); + } + + return TurnActionFactory.makeStatusEffect(defendingPokemon.getName(), + pokemonMove.getStatusEffect().getCondition(), TurnActionHelper.getSubject(isOwnPokemonAttacking)); + } + +} diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandler.java b/backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandler.java new file mode 100644 index 0000000..e71afee --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandler.java @@ -0,0 +1,15 @@ +package nl.rabobank.pirates.handlers; + +import java.util.List; + +import nl.rabobank.pirates.model.battle.TurnAction; +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.move.DamageClass; +import nl.rabobank.pirates.model.move.Move; + +public interface MoveHandler { + + public List handleMove(final Move pokemonMove, final Pokemon attackingPokemon, final Pokemon defendingPokemon, boolean isOwnPokemonAttacking); + + public DamageClass getClassType(); +} diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandlerFactory.java b/backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandlerFactory.java new file mode 100644 index 0000000..b0d1f35 --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/MoveHandlerFactory.java @@ -0,0 +1,28 @@ +package nl.rabobank.pirates.handlers; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import nl.rabobank.pirates.model.move.DamageClass; + +@Component +public class MoveHandlerFactory { + + private static Map handlerMap; + + @Autowired + public MoveHandlerFactory(List handlers) { + handlerMap = handlers.stream().collect(Collectors.toUnmodifiableMap(MoveHandler::getClassType, Function.identity())); + } + + public static MoveHandler getHandler(DamageClass damageClass) { + return Optional.ofNullable(handlerMap.get(damageClass)).orElseThrow(IllegalArgumentException::new); + } + +} diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/PhysicalMoveHandler.java b/backend/src/main/java/nl/rabobank/pirates/handlers/PhysicalMoveHandler.java new file mode 100644 index 0000000..90d6c11 --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/PhysicalMoveHandler.java @@ -0,0 +1,27 @@ +package nl.rabobank.pirates.handlers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.move.DamageClass; +import nl.rabobank.pirates.model.move.Move; +import nl.rabobank.pirates.service.CalculationService; + +@Component +public class PhysicalMoveHandler extends BaseDamageMoveHandler implements MoveHandler { + + @Autowired + private CalculationService calculationService; + + @Override + public DamageClass getClassType() { + return DamageClass.PHYSICAL; + } + + @Override + public int determineDamage(Move pokemonMove, Pokemon attackingPokemon, Pokemon defendingPokemon) { + return calculationService.calculatePhysicalDamage(attackingPokemon, defendingPokemon, pokemonMove); + } + +} diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/SpecialMoveHandler.java b/backend/src/main/java/nl/rabobank/pirates/handlers/SpecialMoveHandler.java new file mode 100644 index 0000000..e435a7e --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/SpecialMoveHandler.java @@ -0,0 +1,27 @@ +package nl.rabobank.pirates.handlers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.move.DamageClass; +import nl.rabobank.pirates.model.move.Move; +import nl.rabobank.pirates.service.CalculationService; + +@Component +public class SpecialMoveHandler extends BaseDamageMoveHandler implements MoveHandler { + + @Autowired + private CalculationService calculationService; + + @Override + public DamageClass getClassType() { + return DamageClass.SPECIAL; + } + + @Override + public int determineDamage(Move pokemonMove, Pokemon attackingPokemon, Pokemon defendingPokemon) { + return calculationService.calculateSpecialDamage(attackingPokemon, defendingPokemon, pokemonMove); + } + +} diff --git a/backend/src/main/java/nl/rabobank/pirates/handlers/StatusMoveHandler.java b/backend/src/main/java/nl/rabobank/pirates/handlers/StatusMoveHandler.java new file mode 100644 index 0000000..13fc7b5 --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/handlers/StatusMoveHandler.java @@ -0,0 +1,82 @@ +package nl.rabobank.pirates.handlers; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import nl.rabobank.pirates.helper.TurnActionHelper; +import nl.rabobank.pirates.model.battle.TurnAction; +import nl.rabobank.pirates.model.battle.TurnActionFactory; +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.common.StatChange; +import nl.rabobank.pirates.model.common.StatMultiplier; +import nl.rabobank.pirates.model.move.DamageClass; +import nl.rabobank.pirates.model.move.Move; + +@Component +public class StatusMoveHandler extends BaseMoveHandler implements MoveHandler { + + @Override + public List handleMove(Move pokemonMove, Pokemon attackingPokemon, + Pokemon defendingPokemon, boolean isOwnPokemonAttacking) { + + final List actions = new ArrayList<>(); + + actions.add(TurnActionFactory.makePokemonUsedMove(attackingPokemon.getName().toUpperCase(), pokemonMove.getName(), TurnActionHelper.getSubject(isOwnPokemonAttacking))); + + if (!willMoveHit(pokemonMove, attackingPokemon, defendingPokemon)) { + actions.add(TurnActionFactory.makeTextOnly("The attack missed!")); + return actions; + } + actions.addAll(processMoveStatChanges(attackingPokemon, defendingPokemon, pokemonMove)); + + actions.add(processStatusEffect(defendingPokemon, pokemonMove, isOwnPokemonAttacking)); + + return actions; + } + + private List processMoveStatChanges(final Pokemon attackingPokemon, final Pokemon defendingPokemon, + final Move pokemonMove) { + final List actions = new ArrayList<>(); + + for (StatChange statChange : pokemonMove.getStatChanges()) { + + if (statChange.getChangeAmount() > 0) { + + boolean wasModified = attackingPokemon.addStatMultiplier(StatMultiplier.builder() + .stat(statChange.getStat()).stageModification(statChange.getChangeAmount()).build()); + + if (wasModified) { + actions.add(TurnActionFactory.makeTextStatRose(attackingPokemon.getName(), + statChange.getStat().getLabel())); + + } else { + actions.add(TurnActionFactory.makeTextStatWontRaiseHigher(attackingPokemon.getName(), + statChange.getStat().getLabel())); + + } + + } else { + boolean wasModified = defendingPokemon.addStatMultiplier(StatMultiplier.builder() + .stat(statChange.getStat()).stageModification(statChange.getChangeAmount()).build()); + if (wasModified) { + + actions.add(TurnActionFactory.makeTextStatFell(defendingPokemon.getName(), + statChange.getStat().getLabel())); + } else { + actions.add(TurnActionFactory.makeTextStatWontFallLower(defendingPokemon.getName(), + statChange.getStat().getLabel())); + } + } + + } + + return actions; + } + + @Override + public DamageClass getClassType() { + return DamageClass.STATUS; + } +} diff --git a/backend/src/main/java/nl/rabobank/pirates/helper/TurnActionHelper.java b/backend/src/main/java/nl/rabobank/pirates/helper/TurnActionHelper.java new file mode 100644 index 0000000..221a1e3 --- /dev/null +++ b/backend/src/main/java/nl/rabobank/pirates/helper/TurnActionHelper.java @@ -0,0 +1,13 @@ +package nl.rabobank.pirates.helper; + +import nl.rabobank.pirates.model.battle.TurnAction; + +public class TurnActionHelper { + + // TODO move to TurnAction class? + public static TurnAction.Subject getSubject(final boolean isOwnPokemonAttacking) { + TurnAction.Subject subjectAttackingPokemon = isOwnPokemonAttacking ? TurnAction.Subject.OWN + : TurnAction.Subject.ENEMY; + return subjectAttackingPokemon; + } +} diff --git a/backend/src/main/java/nl/rabobank/pirates/model/battle/TurnAction.java b/backend/src/main/java/nl/rabobank/pirates/model/battle/TurnAction.java index b84727b..6ee039b 100644 --- a/backend/src/main/java/nl/rabobank/pirates/model/battle/TurnAction.java +++ b/backend/src/main/java/nl/rabobank/pirates/model/battle/TurnAction.java @@ -3,10 +3,12 @@ import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.ToString; import nl.rabobank.pirates.model.move.StatusEffect; @Builder(toBuilder = true, access = AccessLevel.PACKAGE) @Getter +@ToString public class TurnAction { private TurnActionType type; private StatusEffect.Condition statusEffectCondition; diff --git a/backend/src/main/java/nl/rabobank/pirates/service/BattleService.java b/backend/src/main/java/nl/rabobank/pirates/service/BattleService.java index d17361b..d3b95c6 100644 --- a/backend/src/main/java/nl/rabobank/pirates/service/BattleService.java +++ b/backend/src/main/java/nl/rabobank/pirates/service/BattleService.java @@ -13,6 +13,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import static nl.rabobank.pirates.model.move.StatusEffect.Condition.BURN; import static nl.rabobank.pirates.model.move.StatusEffect.Condition.POISON; @@ -32,12 +34,16 @@ public class BattleService { @Getter private Pokemon currentEnemyPokemon; + + private AtomicInteger counter = new AtomicInteger(0); public TurnInformation executeTurn() { if (currentOwnPokemon == null || currentEnemyPokemon == null) return null; List actions = new ArrayList<>(); + System.out.println("Calls to battleService: " + counter.incrementAndGet()); + int ownPokemonMoveIndex = rollService.getRandomValue(0, currentOwnPokemon.getMoves().size()); int enemyPokemonMoveIndex = rollService.getRandomValue(0, currentEnemyPokemon.getMoves().size()); Move ownPokemonMove = currentOwnPokemon.getMoves().get(ownPokemonMoveIndex); @@ -74,7 +80,16 @@ public TurnInformation executeTurn() { actions.add(TurnActionFactory.makeWhatWillPokemonDo(currentOwnPokemon.getName())); } - + + //TODO: once adding of actions has been properly refactored (see TurnInformationService) this can be removed + actions.removeIf(Objects::isNull); + + //Below is used for debugging purposes and would not be part of PR + System.out.println("Number of actions: " + actions.size()); + for (TurnAction turnAction : actions) { + System.out.println(turnAction); + } + return TurnInformation.builder().actions(actions).build(); } diff --git a/backend/src/main/java/nl/rabobank/pirates/service/CalculationService.java b/backend/src/main/java/nl/rabobank/pirates/service/CalculationService.java index f081c0f..2c0f400 100644 --- a/backend/src/main/java/nl/rabobank/pirates/service/CalculationService.java +++ b/backend/src/main/java/nl/rabobank/pirates/service/CalculationService.java @@ -28,7 +28,8 @@ public int calculateSpecialDamage(final Pokemon attackingPokemon, final Pokemon attackingPokemon.getLevel(), move.getPower(), attackingPokemon.getStatAmount(Stat.SPECIAL_ATTACK), - defendingPokemon.getStatAmount(Stat.SPECIAL_DEFENSE) + defendingPokemon.getStatAmount(Stat.SPECIAL_DEFENSE), + attackingPokemon.getType().equals(move.getType()) ); } @@ -37,12 +38,17 @@ public int calculatePhysicalDamage(final Pokemon attackingPokemon, final Pokemon attackingPokemon.getLevel(), move.getPower(), attackingPokemon.getStatAmount(Stat.ATTACK), - defendingPokemon.getStatAmount(Stat.DEFENSE) + defendingPokemon.getStatAmount(Stat.DEFENSE), + attackingPokemon.getType().equals(move.getType()) ); } - int calculateDamage(int level, int movePower, int attack, int defense) { + int calculateDamage(int level, int movePower, int attack, int defense, boolean applySTAB) { if (movePower == 0) return 0; + + //STAB (an abbreviated form of Same-type attack bonus) amplifies a move's power when a Pokémon's type matches the move's type. This boost in power is increased by 50% + //Source: https://pokemon.fandom.com/wiki/STAB + if (applySTAB) movePower *= 1.5; return Math.round( ((((level * 2)/5 + 2) * movePower * attack/defense)/50) + 2); diff --git a/backend/src/main/java/nl/rabobank/pirates/service/GetMoveService.java b/backend/src/main/java/nl/rabobank/pirates/service/GetMoveService.java index eb42183..0f55736 100644 --- a/backend/src/main/java/nl/rabobank/pirates/service/GetMoveService.java +++ b/backend/src/main/java/nl/rabobank/pirates/service/GetMoveService.java @@ -34,6 +34,7 @@ public class GetMoveService { private final RollService rollService; + //DDiemer: this is useful, did not know about this class :) private final AtomicInteger counter = new AtomicInteger(0); private final Map moveStorage = new ConcurrentHashMap<>(); @@ -44,6 +45,11 @@ public class GetMoveService { // Round robin from lowest level to highest // store locally move names that are known to do damage from highest to lowest and save their names + + //DDiemer: please refactor so that there is only one getFourRandomMoves method (same method with params reversed to prevent duplicate method is bad practice) + //Make sure the method only does what it describes (determine and return 4 or less random moves for the given pokemon/level) and delegate the conversion part to another method + //I would suggest creating mapper classes to convert a DTO into an entity which would clean up the code in general. There are libraries that can do this + //e.g. 'modelmapper' or you could use reflection to copy the properties, but do so inside new mapper classes. public List getFourRandomMoves(PokemonDto pokemonDto, int level) { return convertToMoves(getFourRandomMoves(level, pokemonDto)); } @@ -53,6 +59,8 @@ public MoveDto getMoveByName(final String moveName) { return moveStorage.get(moveName); } final MoveDto moveDto = pokemonApiRestClient.getMoveByName(moveName); + + //DDiemer: use proper logging framework System.out.println("Current cash misses" + counter.incrementAndGet()); moveStorage.put(moveName, moveDto); @@ -68,8 +76,22 @@ private List getFourRandomMoves(int level, PokemonDto pokemonDto) { allPossibleMoves.add(thinMoveWrapperDto.getMove()); } } + + /* DDiemer: it could be that no moves are in the list of allPossibleMoves at this point and the program is frozen. Its a functional decision what should happen in this case: + * Either do no move, display action text like 'pokemon has no moves!' and let the other pokemon do it next move, but this could turn into a standoff when neither pokemon has moves. + * Alternatively display error message to the user or throw an exception to let the dev / tester know more moves need to implemented (my preference in this case because + * this situation is not part of the game play) + */ + + /* DDiemer: the below part is not technically doing anything random so it should not be part of this method. I suggest to split this method into 2 parts: + * 1) a method capable of determining the list of supported moves (List getSupportedMoves(PokemonDto pokemonDto, int level)) + * 2) a method capable of random selecting X moves from a list (List getRandomMoves(List allPossibleMoves, int numberOfMoves)) which will + * take into account the case IF allPossibleMoves.size() <= numberOfMoves the randomization is skipped and allPossibleMoves is directly returned + * Finally use the aforementioned to-be-created mapper class to convert the MoveDto items in the list to Move entities + */ List moves = new ArrayList<>(); // IF we have 4 or less + //DDiemer: 4 is a 'magic number', please move to a class level constant (MAX_NUMBER_OF_MOVES) and replace all instances (although the method name in this case clarifies it a bit) if (allPossibleMoves.size() <= 4) { for (ThinMoveDto thinMoveDto : allPossibleMoves) { moves.add(getMoveByName(thinMoveDto.getName())); @@ -89,11 +111,16 @@ private List getFourRandomMoves(int level, PokemonDto pokemonDto) { private boolean isMoveAllowed(int level, ThinMoveWrapperDto thinMoveWrapperDto) { boolean isMoveAllowed = false; + //DDiemer: not sure why MoveServiceConstants is an interface if (MoveServiceConstants.PROHIBITED_MOVES.contains(thinMoveWrapperDto.getMove().getName())) { return false; } for (VersionGroupDetailsDto versionGroupDetailsDto : thinMoveWrapperDto.getVersionGroupDetails()) { + /* DDiemer: not sure if this is supported with the API (I only briefly checked the documentation) but + * please check if it is possible to provide the versionGroup as a query parameter. No need to retrieve + * data that will never be used and it keeps the API calls smaller + */ if (RED_BLUE_VERSION_GROUP.equals(versionGroupDetailsDto.getVersionGroup().getName())) { if (isMoveLearnedWithoutLevel(versionGroupDetailsDto) && versionGroupDetailsDto.getLevelLearnedAt() <= level) { @@ -120,6 +147,9 @@ private List convertToMoves(List moveDtoList) { return moves; } + /* DDiemer: as suggested do this in mapper classes. Looking at the additional logic required to create the Move entity + * a custom-written mapper class is more suitable than using a model mapper API or reflection + */ private Move convertToMove(MoveDto moveDto) { Move move = Move.builder() .accuracy(moveDto.getAccuracy()) @@ -158,6 +188,7 @@ private Move convertToMove(MoveDto moveDto) { } move = move.toBuilder() + //DDiemer: watch out for NPEs, not sure if you can always trust the API to return EffectEntries for all moves .hitTimes(convertHitTimes(moveDto.getEffectEntries().get(0))) .statusEffect(convertStatusEffect(moveDto)) .build(); @@ -168,9 +199,14 @@ private Move convertToMove(MoveDto moveDto) { /* Refactor for statusEffect to contain an effect percentage. */ + /* DDiemer: The below two methods should not be part of this class but moved to StatusEffectMapper / HitTimesMapper or a factory. + * Additionally both methods are perfect for unit testing, please write those + */ private StatusEffect convertStatusEffect(MoveDto moveDto) { EffectDto effectDto = moveDto.getEffectEntries().get(0); int effectChance = moveDto.getEffectChance() != null ? moveDto.getEffectChance() : 100 ; + + //DDiemer: although there is no performance win or functional consequence it is slightly better practice to use if/else if/else if (effectDto.getEffect().toLowerCase().contains("puts the target to sleep")) { return StatusEffect.builder().chance(effectChance).condition(SLEEP).build(); } diff --git a/backend/src/main/java/nl/rabobank/pirates/service/TurnInformationService.java b/backend/src/main/java/nl/rabobank/pirates/service/TurnInformationService.java index b83b952..d6255ce 100644 --- a/backend/src/main/java/nl/rabobank/pirates/service/TurnInformationService.java +++ b/backend/src/main/java/nl/rabobank/pirates/service/TurnInformationService.java @@ -1,19 +1,23 @@ package nl.rabobank.pirates.service; +import static nl.rabobank.pirates.model.battle.TurnActionFactory.makeFaintAnimation; +import static nl.rabobank.pirates.model.move.StatusEffect.Condition.CONFUSED; +import static nl.rabobank.pirates.model.move.StatusEffect.Condition.PARALYZED; +import static nl.rabobank.pirates.model.move.StatusEffect.Condition.SLEEP; + +import java.util.List; + +import org.springframework.stereotype.Component; + import lombok.RequiredArgsConstructor; +import nl.rabobank.pirates.handlers.MoveHandler; +import nl.rabobank.pirates.handlers.MoveHandlerFactory; +import nl.rabobank.pirates.helper.TurnActionHelper; import nl.rabobank.pirates.model.battle.TurnAction; import nl.rabobank.pirates.model.battle.TurnActionFactory; import nl.rabobank.pirates.model.common.Pokemon; import nl.rabobank.pirates.model.common.Stat; -import nl.rabobank.pirates.model.common.StatChange; -import nl.rabobank.pirates.model.common.StatMultiplier; import nl.rabobank.pirates.model.move.Move; -import org.springframework.stereotype.Component; - -import java.util.List; - -import static nl.rabobank.pirates.model.battle.TurnActionFactory.makeFaintAnimation; -import static nl.rabobank.pirates.model.move.StatusEffect.Condition.*; @Component @RequiredArgsConstructor @@ -21,141 +25,33 @@ public class TurnInformationService { private final CalculationService calculationService; + /* + * TODO: more to refactor. + * 1) Refactor the below 3 checks into the handlers (add isMoveAllowed method to interface that checks whether the specific move + * is allowed for the attacking pokemon based on its current status effect). Separate any checks from the adding of actions to prevent null actions being added. + * 2) Also refactor each method in such a way that only one action is determined/added so that it is better unit testable (did this for only part of the methods) + */ public void processMoveAndAddToActions(final List actions, Move pokemonMove, Pokemon attackingPokemon, Pokemon defendingPokemon, boolean isOwnPokemonAttacking) { // Check if pokemon awakes/freeze // check confusion + if (!checkIfPokemonIsParalyzedAndCanAttackAndAddAction(attackingPokemon, actions)) { return; } if (!checkIfPokemonIsAsleepAndDecrementCounterAndAddAction(attackingPokemon, actions, isOwnPokemonAttacking)) { return; } - if (!checkIfPokemonIsConfusedAndDecrementCounterAndAddAction(attackingPokemon, actions, isOwnPokemonAttacking)) { - return; - } - switch (pokemonMove.getDamageClass()) { - case SPECIAL -> { - int damage = calculationService.calculateSpecialDamage(attackingPokemon, defendingPokemon, pokemonMove); - processDamageClassAndAddIntoActions(actions, pokemonMove, damage, attackingPokemon, defendingPokemon, isOwnPokemonAttacking); - } - case PHYSICAL -> { - int damage = calculationService.calculatePhysicalDamage(attackingPokemon, defendingPokemon, pokemonMove); - processDamageClassAndAddIntoActions(actions, pokemonMove, damage, attackingPokemon, defendingPokemon, isOwnPokemonAttacking); - } - case STATUS -> processStatusChangeClassAndAddIntoActions(actions, pokemonMove, attackingPokemon, defendingPokemon, isOwnPokemonAttacking); - } - } - - private void processDamageClassAndAddIntoActions( - final List actions, final Move pokemonMove, final int damage, - final Pokemon attackingPokemon, final Pokemon defendingPokemon, boolean isOwnPokemonAttacking) { - - addToActionsPokemonUsedMove(attackingPokemon, pokemonMove, actions, isOwnPokemonAttacking); - - if (!willMoveHit(pokemonMove, attackingPokemon, defendingPokemon)) { - actions.add(TurnActionFactory.makeTextOnly("The attack missed!")); + if (!checkIfPokemonIsConfusedAndDecrementCounterAndAddAction(attackingPokemon, actions, isOwnPokemonAttacking, pokemonMove)) { return; } - - // TODO calculate critical - // TODO calculate weakness/strenghts - - checkNumberOfHitsAndProcessNumberOfHitsAndAddAction(pokemonMove, defendingPokemon, isOwnPokemonAttacking, damage, actions); - - checkIfDefendingPokemonFaintedAndAddFaintingToActions(defendingPokemon, isOwnPokemonAttacking, actions); - - processMoveStatusEffectAndAddToAction(defendingPokemon, pokemonMove, actions, isOwnPokemonAttacking); - } - - private void processStatusChangeClassAndAddIntoActions(final List actions, final Move pokemonMove, - final Pokemon attackingPokemon, final Pokemon defendingPokemon, boolean isOwnPokemonAttacking) { - addToActionsPokemonUsedMove(attackingPokemon, pokemonMove, actions, isOwnPokemonAttacking); - - if (!willMoveHit(pokemonMove, attackingPokemon, defendingPokemon)) { - actions.add(TurnActionFactory.makeTextOnly("The attack missed!")); - return; - } - processMoveStatChangesAndAddToAction(attackingPokemon, defendingPokemon, pokemonMove, actions); - - processMoveStatusEffectAndAddToAction(defendingPokemon, pokemonMove, actions, isOwnPokemonAttacking); - } - - private void processMoveStatChangesAndAddToAction(final Pokemon attackingPokemon, final Pokemon defendingPokemon, final Move pokemonMove, final List actions) { - for (StatChange statChange : pokemonMove.getStatChanges()) { - - if (statChange.getChangeAmount() > 0) { - - boolean wasModified = attackingPokemon - .addStatMultiplier(StatMultiplier.builder().stat(statChange.getStat()) - .stageModification(statChange.getChangeAmount()).build()); - - if (wasModified) { - actions.add(TurnActionFactory.makeTextStatRose(attackingPokemon.getName(), statChange.getStat().getLabel())); - - } else { - actions.add(TurnActionFactory.makeTextStatWontRaiseHigher(attackingPokemon.getName(), statChange.getStat().getLabel())); - - } - - } else { - boolean wasModified = defendingPokemon - .addStatMultiplier(StatMultiplier.builder().stat(statChange.getStat()) - .stageModification(statChange.getChangeAmount()).build()); - if (wasModified) { - - actions.add(TurnActionFactory.makeTextStatFell(defendingPokemon.getName(), statChange.getStat().getLabel())); - } else { - actions.add(TurnActionFactory.makeTextStatWontFallLower(defendingPokemon.getName(), statChange.getStat().getLabel())); - } - } - - } - } - - private void processMoveStatusEffectAndAddToAction(final Pokemon defendingPokemon, final Move pokemonMove, final List actions, boolean isOwnPokemonAttacking) { - if (pokemonMove.getStatusEffect() == null) { - return; - } - - boolean isSuccessful = calculationService.isRollSuccessful(pokemonMove.getStatusEffect().getChance()); - if (!isSuccessful) { - return; - } - - boolean canApplyStatusEffect = defendingPokemon.addStatusEffect(pokemonMove.getStatusEffect().getCondition()); - - if (!canApplyStatusEffect) { - actions.add(TurnActionFactory.makeTextOnly("The attack missed!")); - return; - } - if (SLEEP.equals(pokemonMove.getStatusEffect().getCondition())) { - defendingPokemon.putPokemonToSleep(calculationService.randomSleepOrConfusedTurns()); - } - if (CONFUSED.equals(pokemonMove.getStatusEffect().getCondition())) { - defendingPokemon.confusePokemon(calculationService.randomSleepOrConfusedTurns()); - } - TurnAction.Subject subjectDefendingPokemon = isOwnPokemonAttacking ? TurnAction.Subject.ENEMY : TurnAction.Subject.OWN; - - actions.add(TurnActionFactory.makeStatusEffect(defendingPokemon.getName(), pokemonMove.getStatusEffect().getCondition(), subjectDefendingPokemon)); + MoveHandler moveHandler = MoveHandlerFactory.getHandler(pokemonMove.getDamageClass()); + + System.out.println("MoveHandler is of type " + moveHandler.getClass()); + + actions.addAll(moveHandler.handleMove(pokemonMove, attackingPokemon, defendingPokemon, isOwnPokemonAttacking)); } - private void checkNumberOfHitsAndProcessNumberOfHitsAndAddAction(final Move pokemonMove, final Pokemon defendingPokemon, final boolean isOwnPokemonAttacking, final int damage, final List actions) { - int numberHits = calculationService.calculateNumberOfHitTimes(pokemonMove.getHitTimes()); - - for (int x = 0; x < numberHits; x++) { - - defendingPokemon.dealDamage(damage); - - TurnAction.Subject subjectDefendingPokemon = isOwnPokemonAttacking ? TurnAction.Subject.ENEMY : TurnAction.Subject.OWN; - - actions.add(TurnActionFactory.makeDamageOnlyAnimation(damage, subjectDefendingPokemon)); - } - - if (numberHits > 1) { - actions.add(TurnActionFactory.makeTextHitXTimes(numberHits)); - } - } private boolean checkIfPokemonIsParalyzedAndCanAttackAndAddAction(final Pokemon attackingPokemon, final List actions) { if (attackingPokemon.isPokemonAfflictedBy(PARALYZED)) { @@ -170,7 +66,7 @@ private boolean checkIfPokemonIsParalyzedAndCanAttackAndAddAction(final Pokemon return true; } - private boolean checkIfPokemonIsConfusedAndDecrementCounterAndAddAction(final Pokemon attackingPokemon, final List actions, boolean isOwnPokemonAttacking) { + private boolean checkIfPokemonIsConfusedAndDecrementCounterAndAddAction(final Pokemon attackingPokemon, final List actions, boolean isOwnPokemonAttacking, Move pokemonMove) { if (attackingPokemon.isPokemonAfflictedBy(CONFUSED)) { if (attackingPokemon.decrementConfusionCounterAndReturn() > 0) { @@ -179,16 +75,12 @@ private boolean checkIfPokemonIsConfusedAndDecrementCounterAndAddAction(final Po if (cantAttack) { int confusionDamage = calculationService.calculateDamage(attackingPokemon.getLevel(), 40, attackingPokemon.getStatAmount(Stat.ATTACK), - attackingPokemon.getStatAmount(Stat.DEFENSE)); + attackingPokemon.getStatAmount(Stat.DEFENSE), + attackingPokemon.getType().equals(pokemonMove.getType())); attackingPokemon.dealDamage(confusionDamage); - TurnAction.Subject subjectAttackingPokemon = - isOwnPokemonAttacking ? - TurnAction.Subject.OWN: - TurnAction.Subject.ENEMY; - - actions.add(TurnActionFactory.makePokemonHurtItselfInConfusion(confusionDamage, subjectAttackingPokemon)); + actions.add(TurnActionFactory.makePokemonHurtItselfInConfusion(confusionDamage, TurnActionHelper.getSubject(isOwnPokemonAttacking))); checkIfDefendingPokemonFaintedAndAddFaintingToActions(attackingPokemon, isOwnPokemonAttacking, actions); @@ -201,8 +93,6 @@ private boolean checkIfPokemonIsConfusedAndDecrementCounterAndAddAction(final Po return true; } - - private boolean checkIfPokemonIsAsleepAndDecrementCounterAndAddAction(final Pokemon attackingPokemon, final List actions, boolean isOwnPokemonAttacking) { if (attackingPokemon.isPokemonAfflictedBy(SLEEP)) { if (attackingPokemon.decrementSleepCounterAndReturn() > 0) { @@ -210,12 +100,7 @@ private boolean checkIfPokemonIsAsleepAndDecrementCounterAndAddAction(final Poke return false; } - TurnAction.Subject subjectAttackingPokemon = - isOwnPokemonAttacking ? - TurnAction.Subject.OWN: - TurnAction.Subject.ENEMY; - - actions.add(TurnActionFactory.makePokemonWokeUp(attackingPokemon.getName(), subjectAttackingPokemon)); + actions.add(TurnActionFactory.makePokemonWokeUp(attackingPokemon.getName(), TurnActionHelper.getSubject(isOwnPokemonAttacking))); } return true; } @@ -223,31 +108,7 @@ private boolean checkIfPokemonIsAsleepAndDecrementCounterAndAddAction(final Poke public void checkIfDefendingPokemonFaintedAndAddFaintingToActions(final Pokemon defendingPokemon, boolean isOwnPokemonAttacking, final List actions) { if (defendingPokemon.getCurrentHp() <= 0) { - - TurnAction.Subject subjectDefendingPokemon = - isOwnPokemonAttacking ? - TurnAction.Subject.ENEMY: - TurnAction.Subject.OWN; - - actions.add(makeFaintAnimation(defendingPokemon.getName().toUpperCase(), subjectDefendingPokemon)); + actions.add(makeFaintAnimation(defendingPokemon.getName().toUpperCase(), TurnActionHelper.getSubject(isOwnPokemonAttacking))); } } - - private boolean willMoveHit(Move move, Pokemon attackingPokemon, Pokemon defendingPokemon) { - if (move.getAccuracy() == 0) return true; - int moveAccuracy = move.getAccuracy(); - int pokemonAccuracy = attackingPokemon.getStatAmount(Stat.ACCURACY); - - return calculationService.calculateAccuracyAndRollIfMoveHits(pokemonAccuracy, moveAccuracy); - } - - private void addToActionsPokemonUsedMove(final Pokemon attackingPokemon, final Move pokemonMove, final List actions, final boolean isOwnPokemonAttacking) { - TurnAction.Subject subjectAttackingPokemon = - isOwnPokemonAttacking ? - TurnAction.Subject.OWN: - TurnAction.Subject.ENEMY; - - actions.add( - TurnActionFactory.makePokemonUsedMove(attackingPokemon.getName().toUpperCase(), pokemonMove.getName(), subjectAttackingPokemon)); - } } diff --git a/backend/src/test/java/nl/rabobank/pirates/handlers/MoveHandlerFactoryTest.java b/backend/src/test/java/nl/rabobank/pirates/handlers/MoveHandlerFactoryTest.java new file mode 100644 index 0000000..bca88b0 --- /dev/null +++ b/backend/src/test/java/nl/rabobank/pirates/handlers/MoveHandlerFactoryTest.java @@ -0,0 +1,34 @@ +package nl.rabobank.pirates.handlers; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import nl.rabobank.pirates.model.move.DamageClass; + +public class MoveHandlerFactoryTest { + + @SuppressWarnings("static-access") + @Test + public void handler_factory_returns_correct_handler_class() { + + List movehandlers = new ArrayList<>(); + movehandlers.add(new PhysicalMoveHandler()); + movehandlers.add(new StatusMoveHandler()); + movehandlers.add(new SpecialMoveHandler()); + + MoveHandlerFactory moveHandlerFactory = new MoveHandlerFactory(movehandlers); + + MoveHandler physicalMoveHandler = moveHandlerFactory.getHandler(DamageClass.PHYSICAL); + assertEquals(physicalMoveHandler.getClass(), PhysicalMoveHandler.class); + + MoveHandler statusMoveHandler = moveHandlerFactory.getHandler(DamageClass.STATUS); + assertEquals(statusMoveHandler.getClass(), StatusMoveHandler.class); + + MoveHandler specialMoveHandler = moveHandlerFactory.getHandler(DamageClass.SPECIAL); + assertEquals(specialMoveHandler.getClass(), SpecialMoveHandler.class); + } +} diff --git a/backend/src/test/java/nl/rabobank/pirates/handlers/StatusMoveHandlerTest.java b/backend/src/test/java/nl/rabobank/pirates/handlers/StatusMoveHandlerTest.java new file mode 100644 index 0000000..09adee3 --- /dev/null +++ b/backend/src/test/java/nl/rabobank/pirates/handlers/StatusMoveHandlerTest.java @@ -0,0 +1,41 @@ +package nl.rabobank.pirates.handlers; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import nl.rabobank.pirates.model.battle.TurnAction; +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.move.DamageClass; +import nl.rabobank.pirates.model.move.Move; +import nl.rabobank.pirates.service.BattleService; +import nl.rabobank.pirates.service.CalculationService; +import nl.rabobank.pirates.smoke.TestConfig; + +@SpringBootTest(classes = TestConfig.class) +public class StatusMoveHandlerTest { + + @Autowired + private BattleService battleService; + + @Autowired + private StatusMoveHandler statusMoveHandler; + + @Test + public void verify_number_of_actions() { + Pokemon currentEnemyPokemon = battleService.selectEnemyPokemonByName("charmander", 5); + Pokemon currentOwnPokemon = battleService.selectOwnPokemonByName("bulbasaur", 5); + + Move ownPokemonStatusMove = currentOwnPokemon.getMoves().get(1); // Status.growl + + List actions = statusMoveHandler.handleMove(ownPokemonStatusMove, currentOwnPokemon, currentEnemyPokemon, true); + + assertEquals(3, actions.size()); + } +} diff --git a/backend/src/test/java/nl/rabobank/pirates/service/CalculationServiceTest.java b/backend/src/test/java/nl/rabobank/pirates/service/CalculationServiceTest.java index 719b2f2..cd2f8fa 100644 --- a/backend/src/test/java/nl/rabobank/pirates/service/CalculationServiceTest.java +++ b/backend/src/test/java/nl/rabobank/pirates/service/CalculationServiceTest.java @@ -1,6 +1,7 @@ package nl.rabobank.pirates.service; -import org.junit.Ignore; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.runner.RunWith; @@ -8,11 +9,13 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import nl.rabobank.pirates.smoke.TestConfig; @ExtendWith(MockitoExtension.class) @RunWith(MockitoJUnitRunner.class) +@SpringBootTest(classes = TestConfig.class) class CalculationServiceTest { @Mock @@ -21,18 +24,17 @@ class CalculationServiceTest { @InjectMocks private CalculationService sut; - @Ignore("Needs to be implemented") @Test public void when_pokemon_is_same_type_as_move_include_stab() { - int damage = sut.calculateDamage(15, 40, 23, 15); + int damage = sut.calculateDamage(15, 40, 23, 15, true); assertThat(damage).isBetween(16, 17); } @Test public void pokemon_is_not_same_type_as_move_dont_include_stab() { - int damage = sut.calculateDamage(15, 40, 20, 17); + int damage = sut.calculateDamage(15, 40, 20, 17, false); assertThat(damage).isEqualTo(9); } diff --git a/backend/src/test/java/nl/rabobank/pirates/service/TurnInformationServiceTest.java b/backend/src/test/java/nl/rabobank/pirates/service/TurnInformationServiceTest.java new file mode 100644 index 0000000..e82542b --- /dev/null +++ b/backend/src/test/java/nl/rabobank/pirates/service/TurnInformationServiceTest.java @@ -0,0 +1,70 @@ +package nl.rabobank.pirates.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import nl.rabobank.pirates.model.battle.TurnAction; +import nl.rabobank.pirates.model.common.Pokemon; +import nl.rabobank.pirates.model.move.Move; +import nl.rabobank.pirates.smoke.TestConfig; + +@SpringBootTest(classes = TestConfig.class) +class TurnInformationServiceTest { + @Autowired + private BattleService battleService; + + @SpyBean + private TurnInformationService turnInformationService; + + @Test + public void verify_number_of_actions() { + final List actions = new ArrayList<>(); + + Pokemon currentEnemyPokemon = battleService.selectEnemyPokemonByName("charmander", 5); + Pokemon currentOwnPokemon = battleService.selectOwnPokemonByName("bulbasaur", 5); + + Move ownPokemonFirstMove = currentOwnPokemon.getMoves().get(0); // Physical.tackle + Move enemyPokemonFirstMove = currentEnemyPokemon.getMoves().get(0); // Physical.scratch + Move ownPokemonSecondMove = currentOwnPokemon.getMoves().get(1); // Status.growl + Move enemyPokemonSecondMove = currentEnemyPokemon.getMoves().get(1); // Status.growl + + //TODO: split up in separate tests + turnInformationService.processMoveAndAddToActions(actions, ownPokemonFirstMove, currentOwnPokemon, + currentEnemyPokemon, true); + + assertThat(actions.size()).isEqualTo(4); + + // start anew with actions + actions.clear(); + + turnInformationService.processMoveAndAddToActions(actions, enemyPokemonFirstMove, currentEnemyPokemon, + currentOwnPokemon, false); + + + assertThat(actions.size()).isEqualTo(4); + + // start anew with actions + actions.clear(); + + turnInformationService.processMoveAndAddToActions(actions, ownPokemonSecondMove, currentOwnPokemon, + currentEnemyPokemon, true); + + assertThat(actions.size()).isEqualTo(3); + + // start anew with actions + actions.clear(); + + turnInformationService.processMoveAndAddToActions(actions, enemyPokemonSecondMove, currentEnemyPokemon, + currentOwnPokemon, false); + + assertThat(actions.size()).isEqualTo(3); + + } +} \ No newline at end of file diff --git a/backend/src/test/java/nl/rabobank/pirates/smoke/TestConfig.java b/backend/src/test/java/nl/rabobank/pirates/smoke/TestConfig.java index a9dbc16..bf5b2f7 100644 --- a/backend/src/test/java/nl/rabobank/pirates/smoke/TestConfig.java +++ b/backend/src/test/java/nl/rabobank/pirates/smoke/TestConfig.java @@ -4,6 +4,6 @@ import org.springframework.context.annotation.Configuration; @Configuration -@ComponentScan(basePackages = {"nl.rabobank.pirates.service", "nl.rabobank.pirates.client", "nl.rabobank.pirates.model"}) +@ComponentScan(basePackages = {"nl.rabobank.pirates.service", "nl.rabobank.pirates.client", "nl.rabobank.pirates.model", "nl.rabobank.pirates.handlers"}) public class TestConfig { } diff --git a/frontend/src/app/simulate/simulate.component.css b/frontend/src/app/simulate/simulate.component.css index 2cd15c6..182a658 100644 --- a/frontend/src/app/simulate/simulate.component.css +++ b/frontend/src/app/simulate/simulate.component.css @@ -137,4 +137,5 @@ position: absolute; top: 330px; left: 370px; + width: 250px; } diff --git a/frontend/src/app/simulate/simulate.component.html b/frontend/src/app/simulate/simulate.component.html index 634367b..46cdbfd 100644 --- a/frontend/src/app/simulate/simulate.component.html +++ b/frontend/src/app/simulate/simulate.component.html @@ -76,15 +76,14 @@
-
-
diff --git a/frontend/src/app/simulate/simulate.component.ts b/frontend/src/app/simulate/simulate.component.ts index 1c8f21f..5543fac 100644 --- a/frontend/src/app/simulate/simulate.component.ts +++ b/frontend/src/app/simulate/simulate.component.ts @@ -1,4 +1,4 @@ -import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit, ViewChild, ElementRef} from '@angular/core'; import {PokemonService} from '../pokemon.service'; import {Condition, Pokemon, StatusEffect, Subject, TurnActionType, TurnInformation} from './pokemon.interface'; import {forkJoin} from 'rxjs'; @@ -38,6 +38,8 @@ export class SimulateComponent implements OnInit, OnDestroy { text; messagesLeftToDisplay = 0; + + @ViewChild('nextturnbtn') nextTurnButton; constructor(private pokemonService: PokemonService) { } ngOnInit(): void { @@ -57,24 +59,28 @@ export class SimulateComponent implements OnInit, OnDestroy { }); } executeTurn(): void { + if (this.restartBattle) { - this.restartBattle = false; - this.ownPokemonStatusEffect = ''; - this.enemyPokemonStatusEffect = ''; + this.resetBattleVars(); this.selectPokemonAndStartBattle(); return; } + + //the below if statement currently prevents users starting a new turn before all the previous moves have been executed if (this.messagesLeftToDisplay === 0) { - this.pokemonService.executeTurn().then(res => { - this.turnInformation = res; - this.animateTurnAndDisplayTexts(); - }); + this.disableNextTurnButton(); + + this.pokemonService.executeTurn().then(res => { + this.turnInformation = res; + this.animateTurnAndDisplayTexts(); + }); } } animateTurnAndDisplayTexts(): void { + // Create A unit test for this. How can he guarantee that the butotn is only pressed once for every batch let timeMultiplier = 1; - + this.messagesLeftToDisplay = this.turnInformation.actions.length; const that = this; this.turnInformation.actions.forEach(action => { @@ -83,6 +89,9 @@ export class SimulateComponent implements OnInit, OnDestroy { setTimeout(() => { that.messagesLeftToDisplay--; + + console.log('Handling action', action) + if (action.type === TurnActionType.TEXT_ONLY) { } else if (action.type === TurnActionType.DAMAGE_ANIMATION) { @@ -103,6 +112,11 @@ export class SimulateComponent implements OnInit, OnDestroy { if (action.text && action.text.length > 0) { this.text = action.text; } + + //enable the 'next turn' button when no more actions are left + if(that.messagesLeftToDisplay === 0) { + this.enableNextTurnButton(); + } }, timeMultiplier * 750); }); } @@ -135,7 +149,21 @@ export class SimulateComponent implements OnInit, OnDestroy { } else { this.enemyPokemonStatusEffect = text; } - + } + disableNextTurnButton() { + if(this.nextTurnButton != null) { + this.nextTurnButton.disabled = true; + } + } + enableNextTurnButton() { + if(this.nextTurnButton != null) { + this.nextTurnButton.disabled = false; + } + } + resetBattleVars() { + this.restartBattle = false; + this.ownPokemonStatusEffect = ''; + this.enemyPokemonStatusEffect = ''; } updateEnemyPokemonHp(): Promise { return this.pokemonService.getEnemyPokemon().then(pokeRes => {