package uulm.teamname.marvelous.gamelibrary.gamelogic; import uulm.teamname.marvelous.gamelibrary.IntVector2; import uulm.teamname.marvelous.gamelibrary.Tuple; import uulm.teamname.marvelous.gamelibrary.entities.Entity; import uulm.teamname.marvelous.gamelibrary.entities.Character; import uulm.teamname.marvelous.gamelibrary.entities.EntityID; import uulm.teamname.marvelous.gamelibrary.entities.StoneType; import uulm.teamname.marvelous.gamelibrary.entities.StatType; import uulm.teamname.marvelous.gamelibrary.events.*; import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest; import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.RequestType; import java.awt.*; import java.awt.geom.Line2D; import java.util.ArrayList; /** Contains game logic handling. */ class GameLogic { /** * Produces resulting {@link Event}s from a given {@link Request}. * @param state The game state to execute on * @param request The request to execute * @return The list of resulting events */ public static ArrayList executeRequest(GameState state, Request request) { ArrayList result = new ArrayList<>(); //TODO: refactor for EventBuilder in GameLogic.executeRequest switch(request.type) { case MeleeAttackRequest, RangedAttackRequest -> { CharacterRequest data = (CharacterRequest)request; result.add(new CharacterEvent() .setOriginEntity(data.originEntity) .setTargetEntity(data.targetEntity) .setOriginField(data.originField) .setTargetField(data.targetField) .setAmount(data.value) .type(request.type == RequestType.MeleeAttackRequest ? EventType.MeleeAttackEvent : EventType.RangedAttackEvent)); result.add(new EntityEvent() .setTargetEntity(data.originEntity) .setTargetField(data.originField) .setAmount(1) .type(EventType.ConsumedAPEvent)); result.add(new EntityEvent() .setTargetEntity(data.targetEntity) .setTargetField(data.targetField) .setAmount(data.value) .type(EventType.TakenDamageEvent)); } case MoveRequest -> { CharacterRequest data = (CharacterRequest)request; result.add(new CharacterEvent() .setOriginEntity(data.originEntity) .setOriginField(data.originField) .setTargetField(data.targetField) .type(EventType.MoveEvent)); result.add(new EntityEvent() .setTargetEntity(data.originEntity) .setTargetField(data.targetField) //when this event gets handled, the character already moved to the target field .setAmount(1) .type(EventType.ConsumedMPEvent)); for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity instanceof Character) { result.add(new CharacterEvent() .setOriginEntity(entity.id) .setOriginField(data.targetField) .setTargetField(data.originField) .type(EventType.MoveEvent)); break; //we should only have one character per field anyways } } } case ExchangeInfinityStoneRequest, UseInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; result.add(new CharacterEvent() .setOriginEntity(data.originEntity) .setTargetEntity(data.targetEntity) .setOriginField(data.originField) .setTargetField(data.targetField) .setStone(data.stoneType) .type(request.type == RequestType.ExchangeInfinityStoneRequest ? EventType.ExchangeInfinityStoneEvent : EventType.UseInfinityStoneEvent)); result.add(new EntityEvent() .setTargetEntity(data.originEntity) .setTargetField(data.originField) .setAmount(1) .type(EventType.ConsumedAPEvent)); //TODO: add infinity stone usage effect in GameLogic.executeRequest } case DisconnectRequest -> { result.add(new GameEvent() .type(EventType.DisconnectEvent)); } } return result; } /** * Checks a {@link Request} for validity for a {@link GameState}. * @param state The game state to check on * @param request The request to validate * @return Whether or not the request is valid */ public static boolean checkRequest(GameState state, Request request) { try { switch(request.type) { case MeleeAttackRequest, RangedAttackRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); Character target = getCharacter(state, data.targetField, data.targetEntity); requireAlive(origin); requireAlive(target); requireAP(origin, 1); if(request.type == RequestType.MeleeAttackRequest) { if(origin.meleeDamage != data.value) { throw new InvalidRequestException(); } if(data.originField.distanceManhattan(data.targetField) > 1) { throw new InvalidRequestException(); } }else if(request.type == RequestType.RangedAttackRequest) { if(origin.rangedDamage != data.value) { throw new InvalidRequestException(); } if(data.originField.distanceManhattan(data.targetField) > origin.attackRange) { throw new InvalidRequestException(); } if(data.originField.distanceManhattan(data.targetField) <= 1) { throw new InvalidRequestException(); } requireLineOfSight(state, data.originField, data.targetField); } return true; } case MoveRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); requireAlive(origin); requireMP(origin, 1); verifyCoordinates(state, data.targetField); if(state.entities.blocksMovement(data.targetField)) { throw new InvalidRequestException(); } return true; } case ExchangeInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); Character target = getCharacter(state, data.targetField, data.targetEntity); requireAlive(origin); requireAlive(target); requireAP(origin, 1); requireInfinityStone(origin, data.stoneType); return true; } case UseInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); Character target = getCharacter(state, data.targetField, data.targetEntity); requireAlive(origin); requireAP(origin, 1); requireInfinityStone(origin, data.stoneType); //TODO: properly verify UseInfinityStoneRequest in GameLogic.checkRequest if(!target.isActive()) { throw new InvalidRequestException(); } return true; } case DisconnectRequest -> { //TODO: add check for DisconnectRequest in GameLogic.checkRequest return true; } } }catch(Exception ignored) { return false; } return false; } /** * Retrieves a {@link Character} for a {@link Request}. * @param state The game state to use * @param position The requested position * @param entityID The requested {@link EntityID} * @return The found character * @throws InvalidRequestException if the character is invalid or not found */ private static Character getCharacter(GameState state, IntVector2 position, EntityID entityID) throws InvalidRequestException { Entity entity = state.entities.findEntity(entityID); if(entity == null || entity.getPosition() != position || !(entity instanceof Character)) { throw new InvalidRequestException(); } try { return (Character)entity; }catch(Exception ignored) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} is alive. */ private static void requireAlive(Character entity) throws InvalidRequestException { if(entity.hp.getValue() <= 0 || !entity.isActive()) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} has enough {@link StatType#AP}. */ private static void requireAP(Character entity, int ap) throws InvalidRequestException { if(entity.ap.getValue() < ap) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} has enough {@link StatType#MP}. */ private static void requireMP(Character entity, int mp) throws InvalidRequestException { if(entity.mp.getValue() < mp) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} has the required {@link StoneType}. */ private static void requireInfinityStone(Character entity, StoneType stone) throws InvalidRequestException { if(!entity.inventory.hasStone(stone)) { throw new InvalidRequestException(); } } /** * Verifies that coordinates are within the playing area. */ private static void verifyCoordinates(GameState state, IntVector2 position) throws InvalidRequestException { if(position.getX() < 0 || position.getX() >= state.mapSize.getX() || position.getY() < 0 || position.getY() >= state.mapSize.getY()) { throw new InvalidRequestException(); } } /** * Verifies that there is a line of sight between two positions. */ private static void requireLineOfSight(GameState state, IntVector2 start, IntVector2 end) throws InvalidRequestException { //naive code for the win!!! \o/ //at least its early exit and probably only O(ln(n*m)) //TODO: implement proper line rasterization algorithm in GameLogic.requireLineOfSight Line2D line = new Line2D.Float(start.getX(), start.getY(), end.getX(), end.getY()); for(int i = start.getX(); i <= end.getX(); i++) { for(int j = start.getY(); j <= end.getY(); j++) { var cell = new Rectangle.Float(i - 0.5f, j - 0.5f, 1, 1); if(line.intersects(cell)) { if(state.entities.blocksVision(new IntVector2(i, j))) { throw new InvalidRequestException(); } } } } } /** * Applies an {@link Event} to a {@link GameState}. * @param state The game state to apply to * @param event The event to apply */ public static void applyEvent(GameState state, Event event) { //TODO: implement GameLogic.applyEvent } /** * Checks a {@link GameState} for the current overtime win condition. * @param state The game state to check * @return The {@link ParticipantType} that is currently winning the game according to overtime ruling */ public static ParticipantType checkWinConditions(GameState state) { //TODO: GameLogic.checkWinConditions is kind of ugly Tuple player1; Tuple player2; int value1; int value2; for(WinCondition condition: WinCondition.values()) { player1 = new Tuple(ParticipantType.Player1, condition); player2 = new Tuple(ParticipantType.Player2, condition); value1 = 0; value2 = 0; if(state.winConditions.containsKey(player1)) { value1 = state.winConditions.get(player1); } if(state.winConditions.containsKey(player2)) { value2 = state.winConditions.get(player2); } if(value1 > value2) { return ParticipantType.Player1; } if(value2 > value1) { return ParticipantType.Player2; } } return ParticipantType.None; } }