2021-04-29 17:15:29 +00:00
|
|
|
package uulm.teamname.marvelous.gamelibrary.gamelogic;
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.IntVector2;
|
2021-04-29 17:15:29 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.Tuple;
|
2021-05-11 01:50:24 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.entities.*;
|
2021-05-01 22:03:03 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.entities.Character;
|
2021-05-01 21:06:22 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.events.*;
|
2021-05-02 13:34:42 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.events.Event;
|
2021-05-01 21:06:22 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest;
|
2021-04-29 17:15:29 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.requests.Request;
|
2021-05-01 21:06:22 +00:00
|
|
|
import uulm.teamname.marvelous.gamelibrary.requests.RequestType;
|
2021-04-29 17:15:29 +00:00
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
import java.awt.*;
|
|
|
|
import java.awt.geom.Line2D;
|
2021-05-27 15:08:08 +00:00
|
|
|
import java.util.*;
|
2021-04-29 17:15:29 +00:00
|
|
|
|
2021-04-30 18:54:34 +00:00
|
|
|
/** Contains game logic handling. */
|
2021-04-29 17:15:29 +00:00
|
|
|
class GameLogic {
|
2021-05-27 15:08:08 +00:00
|
|
|
private static final Random rand = new Random();
|
|
|
|
|
2021-04-30 18:54:34 +00:00
|
|
|
/**
|
2021-05-02 12:38:03 +00:00
|
|
|
* Produces resulting {@link Event}s from a given {@link Request}.
|
|
|
|
* @param state The game state to execute on
|
|
|
|
* @param request The request to execute
|
2021-04-30 18:54:34 +00:00
|
|
|
* @return The list of resulting events
|
2021-04-29 17:15:29 +00:00
|
|
|
*/
|
2021-05-02 12:38:03 +00:00
|
|
|
public static ArrayList<Event> executeRequest(GameState state, Request request) {
|
2021-05-01 21:06:22 +00:00
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
2021-04-29 17:15:29 +00:00
|
|
|
|
2021-05-01 21:06:22 +00:00
|
|
|
switch(request.type) {
|
|
|
|
case MeleeAttackRequest, RangedAttackRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
2021-05-03 17:36:30 +00:00
|
|
|
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());
|
2021-05-01 21:06:22 +00:00
|
|
|
}
|
|
|
|
case MoveRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
2021-05-03 17:36:30 +00:00
|
|
|
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());
|
2021-05-02 12:38:03 +00:00
|
|
|
for(Entity entity: state.entities.findByPosition(data.targetField)) {
|
|
|
|
if(entity instanceof Character) {
|
2021-05-03 17:36:30 +00:00
|
|
|
result.add(new EventBuilder(EventType.MoveEvent)
|
|
|
|
.withOriginEntity(entity.id)
|
|
|
|
.withOriginField(data.targetField)
|
|
|
|
.withTargetField(data.originField)
|
|
|
|
.buildCharacterEvent());
|
2021-05-02 12:38:03 +00:00
|
|
|
break; //we should only have one character per field anyways
|
|
|
|
}
|
|
|
|
}
|
2021-05-01 21:06:22 +00:00
|
|
|
}
|
|
|
|
case ExchangeInfinityStoneRequest, UseInfinityStoneRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
2021-05-03 17:36:30 +00:00
|
|
|
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());
|
2021-05-11 01:50:24 +00:00
|
|
|
|
|
|
|
if(request.type == RequestType.UseInfinityStoneRequest) {
|
|
|
|
switch(((CharacterRequest) request).stoneType) {
|
|
|
|
case SpaceStone -> {
|
|
|
|
result.add(new EventBuilder(EventType.MoveEvent)
|
|
|
|
.withOriginEntity(data.originEntity)
|
|
|
|
.withOriginField(data.originField)
|
|
|
|
.withTargetField(data.targetField)
|
|
|
|
.buildCharacterEvent());
|
|
|
|
}
|
|
|
|
case MindStone -> {
|
2021-05-18 11:21:36 +00:00
|
|
|
EntityType target = data.originEntity.type == EntityType.P1 ? EntityType.P2 : EntityType.P1;
|
|
|
|
Line2D line = new Line2D.Float(data.originField.getX(), data.originField.getY(), data.targetField.getX(), data.targetField.getY());
|
|
|
|
for(int i = data.originField.getX(); i <= data.targetField.getX(); i++) {
|
|
|
|
for(int j = data.originField.getY(); j <= data.targetField.getY(); j++) {
|
|
|
|
var cell = new Rectangle.Float(i - 0.5f, j - 0.5f, 1, 1);
|
|
|
|
if(line.intersects(cell)) {
|
|
|
|
for(Entity entity: state.entities.findByPosition(data.targetField)) {
|
|
|
|
if(entity.id.isSameType(target)) {
|
|
|
|
result.add(new EventBuilder(EventType.TakenDamageEvent)
|
|
|
|
.withTargetEntity(entity.id)
|
|
|
|
.withTargetField(new IntVector2(i, j))
|
|
|
|
.withAmount(data.value)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-11 01:50:24 +00:00
|
|
|
}
|
|
|
|
case RealityStone -> {
|
|
|
|
if(data.originEntity == data.targetEntity) { // => place stone
|
|
|
|
//TODO: use config values
|
|
|
|
result.add(new EventBuilder(EventType.SpawnEntityEvent)
|
|
|
|
.withTargetField(data.targetField)
|
|
|
|
.withEntity(new Rock(new EntityID(EntityType.Rocks, state.entities.findFreeRockSlot()), data.targetField, 100))
|
|
|
|
.buildEntityEvent());
|
|
|
|
}else { // => destroy stone
|
|
|
|
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
|
|
|
|
.withTargetField(data.targetField)
|
|
|
|
.withTargetEntity(data.targetEntity)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case PowerStone -> {
|
|
|
|
Character origin = (Character)state.entities.findEntity(data.originEntity);
|
|
|
|
int dmg = (int)Math.round(origin.hp.getValue() * 0.1);
|
|
|
|
if(origin.hp.getValue() != 1 && dmg > 0) {
|
|
|
|
result.add(new EventBuilder(EventType.TakenDamageEvent)
|
|
|
|
.withTargetEntity(data.originEntity)
|
|
|
|
.withTargetField(data.originField)
|
|
|
|
.withAmount(dmg)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
result.add(new EventBuilder(EventType.TakenDamageEvent)
|
|
|
|
.withTargetEntity(data.targetEntity)
|
|
|
|
.withTargetField(data.targetField)
|
|
|
|
.withAmount(data.value)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
case TimeStone -> {
|
|
|
|
Character origin = (Character)state.entities.findEntity(data.originEntity);
|
|
|
|
int ap = origin.ap.max - origin.ap.getValue();
|
|
|
|
if(ap < 0) {
|
|
|
|
result.add(new EventBuilder(EventType.ConsumedAPEvent)
|
|
|
|
.withTargetEntity(data.originEntity)
|
|
|
|
.withTargetField(data.originField)
|
|
|
|
.withAmount(ap)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
int mp = origin.mp.max - origin.mp.getValue();
|
|
|
|
if(mp < 0) {
|
|
|
|
result.add(new EventBuilder(EventType.ConsumedMPEvent)
|
|
|
|
.withTargetEntity(data.originEntity)
|
|
|
|
.withTargetField(data.originField)
|
|
|
|
.withAmount(mp)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case SoulStone -> {
|
|
|
|
Character target = (Character)state.entities.findEntity(data.targetEntity);
|
|
|
|
result.add(new EventBuilder(EventType.HealedEvent)
|
|
|
|
.withTargetEntity(data.targetEntity)
|
|
|
|
.withTargetField(data.targetField)
|
|
|
|
.withAmount(target.hp.max)
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-01 21:06:22 +00:00
|
|
|
}
|
2021-05-27 13:57:06 +00:00
|
|
|
case EndRoundRequest -> {
|
|
|
|
result.addAll(handleTurnEnd(state)); //why is it called end round request when it ends a turn...
|
|
|
|
}
|
2021-05-05 16:59:07 +00:00
|
|
|
case Req -> {
|
2021-05-18 12:14:23 +00:00
|
|
|
result.add(buildGameStateEvent(state));
|
2021-05-01 21:06:22 +00:00
|
|
|
}
|
2021-04-29 17:15:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-05-18 12:14:23 +00:00
|
|
|
/**
|
|
|
|
* Builds a {@link EventType#GamestateEvent} for the given {@link GameState}.
|
|
|
|
* @param state The game state to use
|
|
|
|
* @return The resulting event
|
|
|
|
*/
|
|
|
|
public static Event buildGameStateEvent(GameState state) {
|
|
|
|
return 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();
|
|
|
|
}
|
|
|
|
|
2021-04-30 18:54:34 +00:00
|
|
|
/**
|
|
|
|
* Checks a {@link Request} for validity for a {@link GameState}.
|
|
|
|
* @param state The game state to check on
|
|
|
|
* @param request The request to validate
|
2021-04-29 17:15:29 +00:00
|
|
|
* @return Whether or not the request is valid
|
|
|
|
*/
|
|
|
|
public static boolean checkRequest(GameState state, Request request) {
|
2021-05-01 22:03:03 +00:00
|
|
|
try {
|
|
|
|
switch(request.type) {
|
|
|
|
case MeleeAttackRequest, RangedAttackRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
Character origin = getCharacter(state, data.originField, data.originEntity);
|
|
|
|
Character target = getCharacter(state, data.targetField, data.targetEntity);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
requireAlive(origin);
|
|
|
|
requireAlive(target);
|
|
|
|
requireAP(origin, 1);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
if(request.type == RequestType.MeleeAttackRequest) {
|
|
|
|
if(origin.meleeDamage != data.value) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
2021-05-11 01:50:24 +00:00
|
|
|
if(data.originField.distanceChebyshev(data.targetField) > 1) {
|
2021-05-02 13:34:42 +00:00
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}else if(request.type == RequestType.RangedAttackRequest) {
|
|
|
|
if(origin.rangedDamage != data.value) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
2021-05-11 01:50:24 +00:00
|
|
|
if(data.originField.distanceChebyshev(data.targetField) > origin.attackRange) {
|
2021-05-02 13:34:42 +00:00
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
2021-05-11 01:50:24 +00:00
|
|
|
if(data.originField.distanceChebyshev(data.targetField) <= 1) {
|
2021-05-02 13:34:42 +00:00
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
requireLineOfSight(state, data.originField, data.targetField);
|
2021-05-01 22:03:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
case MoveRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
Character origin = getCharacter(state, data.originField, data.originEntity);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
requireAlive(origin);
|
|
|
|
requireMP(origin, 1);
|
|
|
|
verifyCoordinates(state, data.targetField);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-11 01:50:24 +00:00
|
|
|
if(data.originField.distanceChebyshev(data.targetField) != 1) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
if(state.entities.blocksMovement(data.targetField)) {
|
|
|
|
throw new InvalidRequestException();
|
2021-05-01 22:03:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
case ExchangeInfinityStoneRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
Character origin = getCharacter(state, data.originField, data.originEntity);
|
|
|
|
Character target = getCharacter(state, data.targetField, data.targetEntity);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
requireAlive(origin);
|
|
|
|
requireAlive(target);
|
|
|
|
requireAP(origin, 1);
|
|
|
|
requireInfinityStone(origin, data.stoneType);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-11 01:50:24 +00:00
|
|
|
if(data.originField.distanceChebyshev(data.targetField) != 1) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
|
2021-05-01 22:03:03 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
case UseInfinityStoneRequest -> {
|
|
|
|
CharacterRequest data = (CharacterRequest)request;
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
Character origin = getCharacter(state, data.originField, data.originEntity);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
requireAlive(origin);
|
|
|
|
requireAP(origin, 1);
|
|
|
|
requireInfinityStone(origin, data.stoneType);
|
2021-05-01 22:03:03 +00:00
|
|
|
|
2021-05-27 15:26:46 +00:00
|
|
|
if(state.stoneCooldown.onCooldown(data.stoneType)) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
|
2021-05-11 01:50:24 +00:00
|
|
|
switch(((CharacterRequest) request).stoneType) {
|
|
|
|
case SpaceStone -> {
|
|
|
|
verifyCoordinates(state, data.targetField);
|
|
|
|
|
|
|
|
if(state.entities.blocksMovement(data.targetField)) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case MindStone -> {
|
2021-05-18 11:21:36 +00:00
|
|
|
if(data.originField == data.targetField) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
//TODO: mind stone damage check (config) ???????
|
2021-05-11 01:50:24 +00:00
|
|
|
}
|
|
|
|
case RealityStone -> {
|
|
|
|
if(data.originEntity == data.targetEntity) { // => place stone
|
|
|
|
if(state.entities.findByPosition(data.targetField).size() != 0) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}else { // => destroy stone
|
|
|
|
boolean hasRock = false;
|
|
|
|
for(Entity entity: state.entities.findByPosition(data.targetField)) {
|
|
|
|
if(entity.id.isSameType(EntityType.Rocks)) {
|
|
|
|
hasRock = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!hasRock) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case PowerStone -> {
|
|
|
|
Character target = getCharacter(state, data.targetField, data.targetEntity);
|
|
|
|
|
|
|
|
requireAlive(target);
|
|
|
|
|
|
|
|
if(origin.rangedDamage * 2 != data.value) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case TimeStone -> {
|
2021-05-11 03:06:00 +00:00
|
|
|
// "👍 i approve" - the server
|
2021-05-11 01:50:24 +00:00
|
|
|
}
|
|
|
|
case SoulStone -> {
|
|
|
|
Character target = getCharacter(state, data.targetField, data.targetEntity);
|
|
|
|
|
|
|
|
if(target.hp.getValue() != 0) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}
|
2021-05-01 22:03:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2021-05-05 16:59:07 +00:00
|
|
|
case Req -> {
|
2021-05-01 22:03:03 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2021-05-02 13:34:42 +00:00
|
|
|
}catch(Exception ignored) {
|
2021-05-01 22:03:03 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-29 17:15:29 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-05-02 13:34:42 +00:00
|
|
|
/**
|
|
|
|
* 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 {
|
2021-05-03 17:36:30 +00:00
|
|
|
if(stone == null || !entity.inventory.hasStone(stone)) {
|
2021-05-02 13:34:42 +00:00
|
|
|
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 {
|
2021-05-27 15:08:08 +00:00
|
|
|
if(!checkLineOfSight(state, start, end)) {
|
|
|
|
throw new InvalidRequestException();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a line of sight exists between the two positions
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @param start The first position
|
|
|
|
* @param end The second position
|
|
|
|
* @return Whether or not the light of sight exists
|
|
|
|
*/
|
|
|
|
private static boolean checkLineOfSight(GameState state, IntVector2 start, IntVector2 end) {
|
2021-05-02 13:34:42 +00:00
|
|
|
//naive code for the win!!! \o/
|
|
|
|
//at least its early exit and probably only O(ln(n*m))
|
2021-05-27 15:08:08 +00:00
|
|
|
//TODO: implement proper line rasterization algorithm in GameLogic.checkLineOfSight
|
2021-05-02 13:34:42 +00:00
|
|
|
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))) {
|
2021-05-27 15:08:08 +00:00
|
|
|
return false;
|
2021-05-02 13:34:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-27 15:08:08 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Finds free neighbour options from a starting field.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @param start The starting position
|
|
|
|
* @return A list of free neighbour field options
|
|
|
|
*/
|
|
|
|
private static ArrayList<IntVector2> getFreeNeighbour(GameState state, IntVector2 start) {
|
|
|
|
ArrayList<IntVector2> options = new ArrayList<>();
|
|
|
|
|
|
|
|
if(start.getX() < 0 || start.getX() >= state.mapSize.getX() || start.getY() < 0 || start.getY() >= state.mapSize.getY()) {
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
|
|
|
|
for(IntVector2 dir: IntVector2.CardinalDirections) {
|
|
|
|
if(state.entities.findByPosition(start.add(dir)).size() == 0) {
|
|
|
|
options.add(start.add(dir));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(options.size() == 0) {
|
|
|
|
return getFreeNeighbour(state, start.add(IntVector2.CardinalDirections[rand.nextInt(IntVector2.CardinalDirections.length)]));
|
|
|
|
}else {
|
|
|
|
return options;
|
|
|
|
}
|
2021-05-02 13:34:42 +00:00
|
|
|
}
|
|
|
|
|
2021-04-30 18:54:34 +00:00
|
|
|
/**
|
|
|
|
* Applies an {@link Event} to a {@link GameState}.
|
|
|
|
* @param state The game state to apply to
|
|
|
|
* @param event The event to apply
|
2021-04-29 17:15:29 +00:00
|
|
|
*/
|
|
|
|
public static void applyEvent(GameState state, Event event) {
|
2021-05-18 11:55:36 +00:00
|
|
|
switch(event.type) {
|
|
|
|
case DestroyedEntityEvent -> {
|
|
|
|
state.entities.removeEntity(((EntityEvent)event).targetEntity);
|
|
|
|
}
|
|
|
|
case TakenDamageEvent -> {
|
|
|
|
((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).hp.decreaseValue(((CharacterEvent)event).amount);
|
|
|
|
}
|
|
|
|
case ConsumedAPEvent -> {
|
|
|
|
((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).ap.decreaseValue(((CharacterEvent)event).amount);
|
|
|
|
}
|
|
|
|
case ConsumedMPEvent -> {
|
|
|
|
((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).mp.decreaseValue(((CharacterEvent)event).amount);
|
|
|
|
}
|
|
|
|
case SpawnEntityEvent -> {
|
|
|
|
state.entities.addEntity(((EntityEvent)event).entity);
|
|
|
|
}
|
|
|
|
case HealedEvent -> {
|
|
|
|
((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).hp.increaseValue(((CharacterEvent)event).amount);
|
|
|
|
}
|
|
|
|
case MoveEvent -> {
|
2021-05-27 15:30:36 +00:00
|
|
|
Character target = (Character)state.entities.findEntity(((CharacterEvent)event).originEntity);
|
|
|
|
for(Entity entity: state.entities.findByPosition(((CharacterEvent)event).targetField)) {
|
|
|
|
if(entity instanceof InfinityStone) {
|
|
|
|
target.inventory.addStone(((InfinityStone)entity).type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
target.setPosition(((CharacterEvent)event).targetField);
|
2021-05-18 11:55:36 +00:00
|
|
|
}
|
2021-05-27 15:26:46 +00:00
|
|
|
case UseInfinityStoneEvent -> {
|
|
|
|
state.stoneCooldown.setCooldown(((CharacterEvent)event).stoneType, 10); //TODO: use stone cooldown from config
|
|
|
|
}
|
2021-05-18 11:55:36 +00:00
|
|
|
case ExchangeInfinityStoneEvent -> {
|
|
|
|
((Character)state.entities.findEntity(((CharacterEvent)event).originEntity)).inventory.removeStone(((CharacterEvent)event).stoneType);
|
|
|
|
((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).inventory.addStone(((CharacterEvent)event).stoneType);
|
|
|
|
}
|
|
|
|
}
|
2021-04-29 17:15:29 +00:00
|
|
|
}
|
|
|
|
|
2021-05-19 17:59:41 +00:00
|
|
|
/**
|
|
|
|
* Starts end of round handling if necessary.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
|
|
|
public static ArrayList<Event> checkTurnEnd(GameState state) {
|
|
|
|
if(
|
|
|
|
((Character) state.entities.findEntity(state.activeCharacter)).ap.getValue() <= 0 &&
|
|
|
|
((Character) state.entities.findEntity(state.activeCharacter)).mp.getValue() <= 0
|
|
|
|
) {
|
|
|
|
return handleTurnEnd(state);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new ArrayList<>();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles everything that happens at the end of a turn, including new rounds.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
2021-05-19 18:04:10 +00:00
|
|
|
public static ArrayList<Event> handleTurnEnd(GameState state) {
|
2021-05-19 17:59:41 +00:00
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
|
|
|
|
|
|
|
ArrayList<EntityID> alive = new ArrayList<>();
|
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
for (EntityID id: state.turnOrder) {
|
2021-05-19 17:59:41 +00:00
|
|
|
Character character = ((Character)state.entities.findEntity(id));
|
|
|
|
|
|
|
|
if(character.hp.getValue() > 0){
|
|
|
|
alive.add(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(character.inventory.getFreeSlots() == 0) { // no slots => has all infinity stones
|
2021-05-27 15:18:36 +00:00
|
|
|
result.addAll(handlePlayerWin(state, character.id.type));
|
2021-05-19 17:59:41 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(alive.isEmpty()) {
|
2021-05-27 15:18:36 +00:00
|
|
|
result.addAll(handlePlayerWin(state, checkWinConditions(state)));
|
2021-05-27 13:57:06 +00:00
|
|
|
return result;
|
|
|
|
}
|
2021-05-19 17:59:41 +00:00
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
int index = alive.indexOf(state.activeCharacter);
|
|
|
|
if(index == alive.size() - 1) {
|
2021-05-27 15:08:08 +00:00
|
|
|
result.addAll(handleRoundStart(state));
|
2021-05-19 17:59:41 +00:00
|
|
|
}else {
|
2021-05-27 13:57:06 +00:00
|
|
|
state.activeCharacter = alive.get(index + 1);
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:08:08 +00:00
|
|
|
result.addAll(handleTurnStart(state));
|
2021-05-27 13:57:06 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles everything that happens at the beginning of new rounds.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
2021-05-27 15:08:08 +00:00
|
|
|
public static ArrayList<Event> handleRoundStart(GameState state) {
|
2021-05-27 13:57:06 +00:00
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
2021-05-19 17:59:41 +00:00
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
state.roundNumber++;
|
|
|
|
|
2021-05-27 15:08:08 +00:00
|
|
|
if(state.roundNumber >= 1 && state.roundNumber <= 6) {
|
|
|
|
result.addAll(handleGoose(state));
|
|
|
|
}
|
|
|
|
|
|
|
|
HashSet<EntityID> revived = new HashSet<>();
|
|
|
|
if(state.roundNumber == 7) {
|
|
|
|
result.addAll(handleStan(state, revived));
|
|
|
|
}
|
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
Collections.shuffle(state.turnOrder);
|
|
|
|
|
|
|
|
for (EntityID id: state.turnOrder) {
|
2021-05-27 15:08:08 +00:00
|
|
|
if(revived.contains(id) || ((Character)state.entities.findEntity(id)).hp.getValue() > 0){
|
|
|
|
state.activeCharacter = id;
|
|
|
|
break;
|
2021-05-19 17:59:41 +00:00
|
|
|
}
|
2021-05-27 13:57:06 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 15:26:46 +00:00
|
|
|
state.stoneCooldown.update();
|
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
result.add(new EventBuilder(EventType.RoundSetupEvent)
|
|
|
|
.withRoundCount(state.roundNumber)
|
2021-05-27 15:08:08 +00:00
|
|
|
.withCharacterOrder(state.turnOrder.toArray(new EntityID[0]))
|
2021-05-27 13:57:06 +00:00
|
|
|
.buildGameEvent());
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:08:08 +00:00
|
|
|
/**
|
|
|
|
* Handles the actions of Goose at rounds 1-6.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
|
|
|
public static ArrayList<Event> handleGoose(GameState state) {
|
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
|
|
|
|
|
|
|
StoneType[] available = state.unvomitedStones.toArray(new StoneType[0]);
|
|
|
|
StoneType stone = available[rand.nextInt(available.length)];
|
|
|
|
state.unvomitedStones.remove(stone);
|
|
|
|
|
|
|
|
ArrayList<IntVector2> free = new ArrayList<>();
|
|
|
|
for(int x = 0; x < state.mapSize.getX(); x++) {
|
|
|
|
for(int y = 0; y < state.mapSize.getY(); y++) {
|
|
|
|
IntVector2 pos = new IntVector2(x, y);
|
|
|
|
if(state.entities.findByPosition(pos).size() == 0) {
|
|
|
|
free.add(pos);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
IntVector2 position = free.get(rand.nextInt(free.size()));
|
|
|
|
|
|
|
|
EntityID goose = new EntityID(EntityType.NPC, 0);
|
|
|
|
result.add(new EventBuilder(EventType.SpawnEntityEvent)
|
|
|
|
.withEntity(new NPC(goose, position))
|
|
|
|
.buildEntityEvent());
|
|
|
|
result.add(new EventBuilder(EventType.SpawnEntityEvent)
|
|
|
|
.withEntity(new InfinityStone(new EntityID(EntityType.InfinityStones, stone.getID()), position, stone))
|
|
|
|
.buildEntityEvent());
|
|
|
|
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
|
|
|
|
.withTargetEntity(goose)
|
|
|
|
.withTargetField(position)
|
|
|
|
.buildEntityEvent());
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles the actions of Stan at round 7.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
|
|
|
public static ArrayList<Event> handleStan(GameState state, HashSet<EntityID> revived) {
|
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
|
|
|
|
|
|
|
ArrayList<Character> characters = new ArrayList<>();
|
|
|
|
|
|
|
|
ArrayList<IntVector2> targetOptions = new ArrayList<>();
|
|
|
|
int lowest = -1;
|
|
|
|
for(EntityID id: state.turnOrder) {
|
|
|
|
Character character = (Character)state.entities.findEntity(id);
|
|
|
|
characters.add(character);
|
|
|
|
|
|
|
|
if(lowest == -1 || character.hp.getValue() < lowest) {
|
|
|
|
lowest = character.hp.getValue();
|
|
|
|
targetOptions.clear();
|
|
|
|
}
|
|
|
|
if(lowest == character.hp.getValue()) {
|
|
|
|
targetOptions.add(character.getPosition());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
IntVector2 targetPosition = targetOptions.get(rand.nextInt(targetOptions.size()));
|
|
|
|
|
|
|
|
ArrayList<IntVector2> spawnOptions = getFreeNeighbour(state, targetPosition);
|
|
|
|
if(spawnOptions.size() == 0) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
IntVector2 spawnPosition = spawnOptions.get(rand.nextInt(spawnOptions.size()));
|
|
|
|
|
|
|
|
EntityID stan = new EntityID(EntityType.NPC, 1);
|
|
|
|
result.add(new EventBuilder(EventType.SpawnEntityEvent)
|
|
|
|
.withEntity(new NPC(stan, spawnPosition))
|
|
|
|
.buildEntityEvent());
|
|
|
|
|
|
|
|
for(Character character: characters) {
|
|
|
|
if(checkLineOfSight(state, spawnPosition, character.getPosition())) {
|
|
|
|
if(character.hp.getValue() == 0) {
|
|
|
|
revived.add(character.id);
|
|
|
|
}
|
|
|
|
if(character.hp.getValue() != character.hp.max) {
|
|
|
|
result.add(new EventBuilder(EventType.HealedEvent)
|
|
|
|
.withTargetEntity(character.id)
|
|
|
|
.withTargetField(character.getPosition())
|
|
|
|
.withAmount(character.hp.max - character.hp.getValue())
|
|
|
|
.buildEntityEvent());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
|
|
|
|
.withTargetEntity(stan)
|
|
|
|
.withTargetField(spawnPosition)
|
|
|
|
.buildEntityEvent());
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
/**
|
|
|
|
* Handles everything that happens at the beginning of a turn.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
2021-05-27 15:08:08 +00:00
|
|
|
public static ArrayList<Event> handleTurnStart(GameState state) {
|
2021-05-27 13:57:06 +00:00
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
|
|
|
|
|
|
|
state.turnNumber++;
|
|
|
|
|
|
|
|
Character activeCharacter = (Character)state.entities.findEntity(state.activeCharacter);
|
2021-05-19 17:59:41 +00:00
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
if(activeCharacter.ap.getValue() != activeCharacter.ap.max) {
|
2021-05-19 17:59:41 +00:00
|
|
|
result.add(new EventBuilder(EventType.ConsumedAPEvent)
|
|
|
|
.withTargetEntity(state.activeCharacter)
|
|
|
|
.withTargetField(activeCharacter.getPosition())
|
|
|
|
.withAmount(activeCharacter.ap.getValue() - activeCharacter.ap.max)
|
|
|
|
.buildGameEvent());
|
2021-05-27 13:57:06 +00:00
|
|
|
}
|
|
|
|
if(activeCharacter.mp.getValue() != activeCharacter.mp.max) {
|
2021-05-19 17:59:41 +00:00
|
|
|
result.add(new EventBuilder(EventType.ConsumedMPEvent)
|
|
|
|
.withTargetEntity(state.activeCharacter)
|
|
|
|
.withTargetField(activeCharacter.getPosition())
|
|
|
|
.withAmount(activeCharacter.mp.getValue() - activeCharacter.mp.max)
|
|
|
|
.buildGameEvent());
|
|
|
|
}
|
2021-05-27 13:57:06 +00:00
|
|
|
result.add(new EventBuilder(EventType.TurnEvent)
|
2021-05-27 15:08:08 +00:00
|
|
|
.withTurnCount(state.turnOrder.size())
|
2021-05-27 13:57:06 +00:00
|
|
|
.withNextCharacter(state.activeCharacter)
|
|
|
|
.buildGameEvent());
|
2021-05-19 17:59:41 +00:00
|
|
|
|
2021-05-27 13:57:06 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles the victory of a player through one character.
|
|
|
|
* @param state The game state to work on
|
|
|
|
* @param winner The winning character
|
|
|
|
* @return The list of resulting {@link Event}s
|
|
|
|
*/
|
2021-05-27 15:18:36 +00:00
|
|
|
public static ArrayList<Event> handlePlayerWin(GameState state, EntityType winner) {
|
2021-05-27 13:57:06 +00:00
|
|
|
ArrayList<Event> result = new ArrayList<>();
|
|
|
|
|
|
|
|
state.won = true;
|
|
|
|
|
|
|
|
result.add(new EventBuilder(EventType.WinEvent)
|
2021-05-27 15:18:36 +00:00
|
|
|
.withPlayerWon(winner == EntityType.P1 ? 1 : 2)
|
2021-05-27 13:57:06 +00:00
|
|
|
.buildGameEvent());
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-04-30 18:54:34 +00:00
|
|
|
/**
|
|
|
|
* Checks a {@link GameState} for the current overtime win condition.
|
|
|
|
* @param state The game state to check
|
2021-05-27 15:18:36 +00:00
|
|
|
* @return The {@link EntityType} that is currently winning the game according to overtime ruling
|
2021-04-29 17:15:29 +00:00
|
|
|
*/
|
2021-05-27 15:18:36 +00:00
|
|
|
public static EntityType checkWinConditions(GameState state) {
|
2021-04-29 17:15:29 +00:00
|
|
|
//TODO: GameLogic.checkWinConditions is kind of ugly
|
|
|
|
|
2021-05-27 15:18:36 +00:00
|
|
|
Tuple<EntityType, WinCondition> player1;
|
|
|
|
Tuple<EntityType, WinCondition> player2;
|
2021-04-29 17:15:29 +00:00
|
|
|
int value1;
|
|
|
|
int value2;
|
|
|
|
for(WinCondition condition: WinCondition.values()) {
|
2021-05-27 15:18:36 +00:00
|
|
|
player1 = new Tuple<EntityType, WinCondition>(EntityType.P1, condition);
|
|
|
|
player2 = new Tuple<EntityType, WinCondition>(EntityType.P2, condition);
|
2021-04-29 17:15:29 +00:00
|
|
|
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) {
|
2021-05-27 15:18:36 +00:00
|
|
|
return EntityType.P1;
|
2021-04-29 17:15:29 +00:00
|
|
|
}
|
|
|
|
if(value2 > value1) {
|
2021-05-27 15:18:36 +00:00
|
|
|
return EntityType.P2;
|
2021-04-29 17:15:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-27 15:18:36 +00:00
|
|
|
return EntityType.None;
|
2021-04-29 17:15:29 +00:00
|
|
|
}
|
|
|
|
}
|