333 lines
14 KiB
Java
333 lines
14 KiB
Java
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<Event> executeRequest(GameState state, Request request) {
|
|
ArrayList<Event> result = new ArrayList<>();
|
|
|
|
switch(request.type) {
|
|
case MeleeAttackRequest, RangedAttackRequest -> {
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
result.add(new EventBuilder(request.type == RequestType.MeleeAttackRequest ? EventType.MeleeAttackEvent : EventType.RangedAttackEvent)
|
|
.withOriginEntity(data.originEntity)
|
|
.withTargetEntity(data.targetEntity)
|
|
.withOriginField(data.originField)
|
|
.withTargetField(data.targetField)
|
|
.withAmount(data.value)
|
|
.buildCharacterEvent());
|
|
result.add(new EventBuilder(EventType.ConsumedAPEvent)
|
|
.withTargetEntity(data.originEntity)
|
|
.withTargetField(data.originField)
|
|
.withAmount(1)
|
|
.buildEntityEvent());
|
|
result.add(new EventBuilder(EventType.TakenDamageEvent)
|
|
.withTargetEntity(data.targetEntity)
|
|
.withTargetField(data.targetField)
|
|
.withAmount(data.value)
|
|
.buildEntityEvent());
|
|
}
|
|
case MoveRequest -> {
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
result.add(new EventBuilder(EventType.MoveEvent)
|
|
.withOriginEntity(data.originEntity)
|
|
.withOriginField(data.originField)
|
|
.withTargetField(data.targetField)
|
|
.buildCharacterEvent());
|
|
result.add(new EventBuilder(EventType.ConsumedMPEvent)
|
|
.withTargetEntity(data.originEntity)
|
|
.withTargetField(data.targetField) //when this event gets handled, the character already moved to the target field
|
|
.withAmount(1)
|
|
.buildEntityEvent());
|
|
for(Entity entity: state.entities.findByPosition(data.targetField)) {
|
|
if(entity instanceof Character) {
|
|
result.add(new EventBuilder(EventType.MoveEvent)
|
|
.withOriginEntity(entity.id)
|
|
.withOriginField(data.targetField)
|
|
.withTargetField(data.originField)
|
|
.buildCharacterEvent());
|
|
break; //we should only have one character per field anyways
|
|
}
|
|
}
|
|
}
|
|
case ExchangeInfinityStoneRequest, UseInfinityStoneRequest -> {
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
result.add(new EventBuilder(request.type == RequestType.ExchangeInfinityStoneRequest ? EventType.ExchangeInfinityStoneEvent : EventType.UseInfinityStoneEvent)
|
|
.withOriginEntity(data.originEntity)
|
|
.withOriginField(data.originField)
|
|
.withTargetEntity(data.targetEntity)
|
|
.withTargetField(data.targetField)
|
|
.withStoneType(data.stoneType)
|
|
.buildCharacterEvent());
|
|
result.add(new EventBuilder(EventType.ConsumedAPEvent)
|
|
.withTargetEntity(data.originEntity)
|
|
.withTargetField(data.originField)
|
|
.withAmount(1)
|
|
.buildEntityEvent());
|
|
//TODO: add infinity stone usage effect in GameLogic.executeRequest
|
|
}
|
|
case Req -> {
|
|
result.add(new EventBuilder(EventType.GameStateEvent)
|
|
.withEntities(state.entities.export())
|
|
.withTurnOrder((EntityID[])state.turnOrder.toArray())
|
|
.withMapSize(state.mapSize)
|
|
.withActiveCharacter(state.activeCharacter)
|
|
.withStoneCooldowns(state.stoneCooldown.export())
|
|
.withWinCondition(state.won)
|
|
.buildGameStateEvent());
|
|
}
|
|
}
|
|
|
|
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 Req -> {
|
|
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(stone == null || !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<ParticipantType, WinCondition> player1;
|
|
Tuple<ParticipantType, WinCondition> player2;
|
|
int value1;
|
|
int value2;
|
|
for(WinCondition condition: WinCondition.values()) {
|
|
player1 = new Tuple<ParticipantType, WinCondition>(ParticipantType.Player1, condition);
|
|
player2 = new Tuple<ParticipantType, WinCondition>(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;
|
|
}
|
|
}
|