Gamelib/src/main/java/uulm/teamname/marvelous/gamelibrary/gamelogic/GameLogic.java

1630 lines
68 KiB
Java

package uulm.teamname.marvelous.gamelibrary.gamelogic;
import org.tinylog.Logger;
import uulm.teamname.marvelous.gamelibrary.ArrayTools;
import uulm.teamname.marvelous.gamelibrary.IntVector2;
import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties;
import uulm.teamname.marvelous.gamelibrary.config.FieldType;
import uulm.teamname.marvelous.gamelibrary.entities.Character;
import uulm.teamname.marvelous.gamelibrary.entities.*;
import uulm.teamname.marvelous.gamelibrary.events.Event;
import uulm.teamname.marvelous.gamelibrary.events.*;
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.awt.geom.Point2D;
import java.util.List;
import java.util.*;
/** Contains game logic handling. */
public class GameLogic {
private static final Random rand = new Random();
/**
* 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 the request is valid
*/
protected 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);
Entity target = getAttackable(state, data.targetField, data.targetEntity);
requireTurn(state, origin);
requireAlive(origin);
requireAP(origin, 1);
if(target instanceof Character) {
Character targetCharacter = (Character)target;
requireOppositeTeam(origin, targetCharacter);
requireAlive(targetCharacter);
}
if(request.type == RequestType.MeleeAttackRequest) {
if(origin.meleeDamage != data.value) {
throw new InvalidRequestException("Invalid melee damage");
}
if(data.originField.distanceChebyshev(data.targetField) != 1) {
throw new InvalidRequestException("Invalid melee target distance");
}
}else if(request.type == RequestType.RangedAttackRequest) {
if(origin.rangedDamage != data.value) {
throw new InvalidRequestException("Invalid ranged damage");
}
if(data.originField.distanceChebyshev(data.targetField) > origin.attackRange) {
throw new InvalidRequestException("Invalid ranged distance (too far)");
}
if(data.originField.distanceChebyshev(data.targetField) <= 1) {
throw new InvalidRequestException("Invalid ranged distance (too short)");
}
requireLineOfSight(state, data.originField, data.targetField);
}
return true;
}
case MoveRequest -> {
CharacterRequest data = (CharacterRequest)request;
Character origin = getCharacter(state, data.originField, data.originEntity);
requireTurn(state, origin);
requireAlive(origin);
requireMP(origin, 1);
verifyCoordinates(state, data.targetField);
if(data.originField.distanceChebyshev(data.targetField) != 1) {
throw new InvalidRequestException("Invalid move distance");
}
if(state.entities.blocksMovement(data.targetField)) {
throw new InvalidRequestException("Moving into blocked field");
}
return true;
}
case ExchangeInfinityStoneRequest -> {
CharacterRequest data = (CharacterRequest)request;
Character origin = getCharacter(state, data.originField, data.originEntity);
Character target = getCharacter(state, data.targetField, data.targetEntity);
requireTurn(state, origin);
requireAlive(origin);
requireAlive(target);
requireAP(origin, 1);
requireInfinityStone(origin, data.stoneType);
if(data.originField.distanceChebyshev(data.targetField) != 1) {
throw new InvalidRequestException("Invalid infinity stone exchange distance");
}
return true;
}
case UseInfinityStoneRequest -> {
CharacterRequest data = (CharacterRequest)request;
Character origin = getCharacter(state, data.originField, data.originEntity);
requireTurn(state, origin);
requireAlive(origin);
requireAP(origin, 1);
requireInfinityStone(origin, data.stoneType);
if(state.stoneCooldown.onCooldown(data.stoneType)) {
throw new InvalidRequestException("Using infinity stone on cooldown");
}
switch(((CharacterRequest) request).stoneType) {
case SpaceStone -> {
verifyCoordinates(state, data.targetField);
if(state.entities.findByPosition(data.targetField).size() != 0) {
throw new InvalidRequestException("Using space stone onto non-free field");
}
}
case MindStone -> {
verifyCoordinates(state, data.targetField);
requireLineOfSight(state, data.originField, data.targetField);
getAttackableWithoutID(state, data.targetField);
}
case RealityStone -> {
verifyCoordinates(state, data.targetField);
boolean rock = false;
boolean empty = true;
for(Entity entity: state.entities.findByPosition(data.targetField)) {
if(entity.id.type == EntityType.Rocks) {
rock = true;
break;
}else {
empty = false;
}
}
if(!empty && !rock) {
throw new InvalidRequestException("Using reality stone on non-free field without a rock");
}
}
case PowerStone -> {
verifyCoordinates(state, data.targetField);
Entity target = getAttackableWithoutID(state, data.targetField);
if(target instanceof Character) {
Character targetCharacter = (Character)target;
requireOppositeTeam(origin, targetCharacter);
requireAlive(targetCharacter);
}
if(data.originField.distanceChebyshev(data.targetField) != 1) {
throw new InvalidRequestException("Invalid melee target distance");
}
}
case TimeStone -> {
// "👍 i approve" - the server
}
case SoulStone -> {
verifyCoordinates(state, data.targetField);
Character target = getCharacterWithoutID(state, data.targetField);
if(data.originEntity.equals(data.targetEntity)) {
throw new InvalidRequestException("Invalid soul stone target (same as origin)");
}
if(data.originField.distanceChebyshev(data.targetField) != 1) {
throw new InvalidRequestException("Invalid soul stone target distance");
}
if(target.isAlive()) {
throw new InvalidRequestException("Invalid soul stone target (already alive)");
}
}
}
return true;
}
case EndRoundRequest, Req -> {
return true;
}
}
}catch(InvalidRequestException exception) {
Logger.debug("Request denied: " + exception.getMessage());
return false;
}catch(Exception ignored) {
return false;
}
return false;
}
/**
* Retrieves an attack-able Entity ({@link Character} or {@link Rock}) 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 entity
* @throws InvalidRequestException if the entity is invalid or not found
*/
private static Entity getAttackable(GameState state, IntVector2 position, EntityID entityID) throws InvalidRequestException {
Entity entity = state.entities.findEntity(entityID);
if(entity == null || !entity.getPosition().equals(position) || (!(entity instanceof Character) && !(entity instanceof Rock)) || entity.id.type == EntityType.NPC) {
throw new InvalidRequestException("Invalid target character or rock");
}
return entity;
}
/**
* Retrieves an attack-able Entity ({@link Character} or {@link Rock}) for a {@link Request}.
* @param state The game state to use
* @param position The requested position
* @return The found entity
* @throws InvalidRequestException if the entity is invalid or not found
*/
private static Entity getAttackableWithoutID(GameState state, IntVector2 position) throws InvalidRequestException {
ArrayList<Entity> entities = state.entities.findByPosition(position);
if(entities.isEmpty() || entities.get(0).id.type == EntityType.NPC) {
throw new InvalidRequestException("Invalid target character or rock");
}
return entities.get(0);
}
/**
* 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().equals(position) || !(entity instanceof Character) || entity.id.type == EntityType.NPC) {
throw new InvalidRequestException("Invalid origin or target character");
}
try {
return (Character)entity;
}catch(Exception ignored) {
throw new InvalidRequestException("Invalid origin or target character (cast failed)");
}
}
/**
* Retrieves a {@link Character} for a {@link Request}.
* @param state The game state to use
* @param position The requested position
* @return The found character
* @throws InvalidRequestException if the character is invalid or not found
*/
private static Character getCharacterWithoutID(GameState state, IntVector2 position) throws InvalidRequestException {
ArrayList<Entity> entities = state.entities.findByPosition(position);
if(entities.isEmpty() || !(entities.get(0) instanceof Character) || entities.get(0).id.type == EntityType.NPC) {
throw new InvalidRequestException("Invalid origin or target character");
}
try {
return (Character)entities.get(0);
}catch(Exception ignored) {
throw new InvalidRequestException("Invalid origin or target character (cast failed)");
}
}
/**
* Verifies that a {@link Character} has a turn.
*/
private static void requireTurn(GameState state, Character entity) throws InvalidRequestException {
if(!entity.id.equals(state.activeCharacter)) {
throw new InvalidRequestException("Target does not have the turn");
}
}
/**
* Verifies that a {@link Character} is of the opposite team of another Character.
*/
private static void requireOppositeTeam(Character a, Character b) throws InvalidRequestException {
if(a.id.type == b.id.type) {
throw new InvalidRequestException("Origin and target are not on opposite team");
}
}
/**
* Verifies that a {@link Character} is of the same team of another Character, but not the same.
*/
private static void requireSameTeam(Character a, Character b) throws InvalidRequestException {
if(a.id.type != b.id.type || a.id.id == b.id.id) {
throw new InvalidRequestException("Origin and target are not on same team");
}
}
/**
* Verifies that a {@link Character} is alive.
*/
private static void requireAlive(Character entity) throws InvalidRequestException {
if(!entity.isAlive()) {
throw new InvalidRequestException("Character is not alive");
}
}
/**
* 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("Character does not have enough ap");
}
}
/**
* 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("Character does not have enough mp");
}
}
/**
* 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("Character does not have the infinity stone");
}
}
/**
* 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("Out of bounds coordinates");
}
}
/**
* Verifies that there is a line of sight between two positions.
*/
private static void requireLineOfSight(GameState state, IntVector2 start, IntVector2 end) throws InvalidRequestException {
if(!checkLineOfSight(state, start, end)) {
throw new InvalidRequestException("Character does not have line of sight to target");
}
}
/**
* Produces resulting {@link Event Events} 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());
Entity targetEntity = state.entities.findEntity(data.targetEntity);
result.addAll(checkDeath(state, targetEntity, data.value));
}
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 entity per field anyways
}else if(entity instanceof InfinityStone) {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetField(data.targetField)
.withTargetEntity(entity.id)
.buildEntityEvent());
break; //we should only have one entity per field anyways
}else if(entity instanceof Portal) {
List<Entity> targets = new ArrayList<>();
for(Entity e: state.entities) {
if(e.id.type == EntityType.Portals && !e.id.equals(entity.id)) {
targets.add(e);
}
}
if(targets.isEmpty()) {
break;
}
Entity target = targets.get(rand.nextInt(targets.size()));
List<IntVector2> fields = getFreeNeighbour(state, target.getPosition());
if(fields.isEmpty()) {
break;
}
IntVector2 field = fields.get(rand.nextInt(fields.size()));
result.add(new EventBuilder(EventType.TeleportedEvent)
.withTeleportedEntity(data.originEntity)
.withOriginField(data.targetField)
.withTargetField(field)
.withOriginPortal(entity.id)
.withTargetPortal(target.id)
.buildTeleportedEvent());
break; //we should only have one entity 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());
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 -> {
ArrayList<Entity> found = state.entities.findByPosition(data.targetField);
Entity targetEntity = found.get(0);
result.add(new EventBuilder(EventType.TakenDamageEvent)
.withTargetEntity(targetEntity.id)
.withTargetField(data.targetField)
.withAmount(state.partyConfig.mindStoneDMG)
.buildEntityEvent());
result.addAll(checkDeath(state, targetEntity, state.partyConfig.mindStoneDMG));
}
case RealityStone -> {
EntityID target = null;
for(Entity entity: state.entities.findByPosition(data.targetField)) {
if(entity.id.type == EntityType.Rocks) {
target = entity.id;
break;
}
}
if(target == null) {
result.add(new EventBuilder(EventType.SpawnEntityEvent)
.withTargetField(data.targetField)
.withEntity(new Rock(new EntityID(EntityType.Rocks, state.entities.findFreeRockSlot()), data.targetField, 100))
.buildEntityEvent());
}else {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetField(data.targetField)
.withTargetEntity(target)
.buildEntityEvent());
}
}
case PowerStone -> {
ArrayList<Entity> found = state.entities.findByPosition(data.targetField);
Entity targetEntity = found.get(0);
Character origin = (Character)state.entities.findEntity(data.originEntity);
int dmg = (int)Math.round(origin.hp.getMax() * 0.1);
//this is ugly ... but also easy to understand
int hp1 = origin.hp.getValue();
int hp2 = Math.max(1, origin.hp.getValue() - dmg);
int actualDmg = hp1 - hp2;
if(actualDmg > 0) {
result.add(new EventBuilder(EventType.TakenDamageEvent)
.withTargetEntity(data.originEntity)
.withTargetField(data.originField)
.withAmount(actualDmg)
.buildEntityEvent());
}
result.add(new EventBuilder(EventType.TakenDamageEvent)
.withTargetEntity(targetEntity.id)
.withTargetField(data.targetField)
.withAmount(origin.meleeDamage * 2)
.buildEntityEvent());
result.addAll(checkDeath(state, targetEntity, origin.meleeDamage * 2));
}
case TimeStone -> {
Character origin = (Character)state.entities.findEntity(data.originEntity);
int ap = origin.ap.getValue() - origin.ap.getMax();
if(ap < 0) {
result.add(new EventBuilder(EventType.ConsumedAPEvent)
.withTargetEntity(data.originEntity)
.withTargetField(data.originField)
.withAmount(ap)
.buildEntityEvent());
}
int mp = origin.mp.getValue() - origin.mp.getMax();
if(mp < 0) {
result.add(new EventBuilder(EventType.ConsumedMPEvent)
.withTargetEntity(data.originEntity)
.withTargetField(data.originField)
.withAmount(mp)
.buildEntityEvent());
}
}
case SoulStone -> {
ArrayList<Entity> found = state.entities.findByPosition(data.targetField);
Character target = (Character)found.get(0);
result.add(new EventBuilder(EventType.HealedEvent)
.withTargetEntity(target.id)
.withTargetField(data.targetField)
.withAmount(target.hp.getMax())
.buildEntityEvent());
}
}
}
}
case EndRoundRequest -> {
result.addAll(handleTurnEnd(state)); //why is it called end round request when it ends a turn...
}
case Req -> {
result.add(buildGameStateEvent(state));
}
}
return result;
}
/**
* Checks for death of a Character or Rock and returns the resulting {@link Event Events}.
* @param state The game state to apply to
* @param targetEntity The entity to check
* @param damage The damage taken
* @return The resulting events
*/
private static ArrayList<Event> checkDeath(GameState state, Entity targetEntity, int damage) {
ArrayList<Event> result = new ArrayList<>();
if(targetEntity instanceof Character) {
Character target = (Character)targetEntity;
if(target.hp.getValue() <= damage) {
List<StoneType> stones = Arrays.asList(target.inventory.getStonesAsArray());
Collections.shuffle(stones); // required by documents
ArrayList<IntVector2> used = new ArrayList<>();
for(StoneType stone: stones) {
ArrayList<IntVector2> options = getFreeNeighbour(state, target.getPosition(), used);
IntVector2 picked = options.get(rand.nextInt(options.size()));
used.add(picked);
result.add(new EventBuilder(EventType.SpawnEntityEvent)
.withEntity(new InfinityStone(
new EntityID(EntityType.InfinityStones, stone.getID()),
picked,
stone
))
.buildEntityEvent());
}
}
}else if(targetEntity instanceof Rock) {
Rock target = (Rock)targetEntity;
if(target.getHp() <= damage) {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetField(target.getPosition())
.withTargetEntity(target.id)
.buildEntityEvent());
}
}
return result;
}
/**
* 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) {
switch(event.type) {
case DestroyedEntityEvent -> {
state.entities.removeEntity(((EntityEvent)event).targetEntity);
}
case TakenDamageEvent -> {
EntityEvent data = (EntityEvent)event;
Entity targetEntity = state.entities.findEntity(data.targetEntity);
if(targetEntity instanceof Character) {
Character target = (Character)targetEntity;
target.hp.decreaseValue(data.amount);
EntityType opposing = target.id.type == EntityType.P1 ? EntityType.P2 : EntityType.P1;
state.winConditions.increaseValue(opposing, WinCondition.TotalDamage, data.amount);
if(!target.isAlive()) {
target.inventory.clear();
state.winConditions.increaseValue(opposing, WinCondition.TotalKnockouts, 1);
}
if(state.activeCharacter != null && state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Thanos.getID()) {
NPC thanos = (NPC)state.entities.findEntity(state.activeCharacter);
target.inventory.transfer(thanos.inventory);
}
}else if(targetEntity instanceof Rock) {
Rock target = (Rock)targetEntity;
target.decreaseHp(data.amount);
}
}
case ConsumedAPEvent -> {
EntityEvent data = (EntityEvent)event;
((Character)state.entities.findEntity(data.targetEntity)).ap.decreaseValue(data.amount);
}
case ConsumedMPEvent -> {
EntityEvent data = (EntityEvent)event;
if(data.targetEntity.type != EntityType.NPC) {
((Character)state.entities.findEntity(data.targetEntity)).mp.decreaseValue(data.amount);
}else {
((NPC)state.entities.findEntity(data.targetEntity)).mp.decreaseValue(data.amount);
}
}
case RoundSetupEvent -> {
GameEvent data = (GameEvent)event;
state.roundNumber++;
state.turnNumber = 0;
state.turnOrder = new ArrayList<>();
for(EntityID turn: data.characterOrder) {
if(turn.type != EntityType.NPC || turn.id == NPCType.Thanos.getID()) {
state.turnOrder.add(turn);
}
}
state.stoneCooldown.update();
}
case TurnEvent -> {
GameEvent data = (GameEvent)event;
if(data.nextCharacter.type != EntityType.NPC) {
Character target = (Character)state.entities.findEntity(data.nextCharacter);
target.ap.setValue(target.ap.getMax());
target.mp.setValue(target.mp.getMax());
}else if(data.nextCharacter.id == NPCType.Thanos.getID()) {
NPC target = (NPC)state.entities.findEntity(data.nextCharacter);
if(state.roundNumber > state.partyConfig.maxRounds + 1) {
target.mp.setMax(target.mp.getMax() + 1);
}
target.mp.setValue(target.mp.getMax());
}
state.turnNumber++;
state.activeCharacter = data.nextCharacter.clone();
}
case SpawnEntityEvent -> {
EntityEvent data = (EntityEvent)event;
state.entities.addEntity(data.entity);
if(state.activeCharacter != null && state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Goose.getID()) {
state.unvomitedStones.remove(((InfinityStone)data.entity).type);
}
}
case HealedEvent -> {
EntityEvent data = (EntityEvent)event;
((Character)state.entities.findEntity(data.targetEntity)).hp.increaseValue(data.amount);
}
case MoveEvent -> {
CharacterEvent data = (CharacterEvent)event;
if(data.originEntity.type != EntityType.NPC) {
Character target = (Character)state.entities.findEntity(data.originEntity);
for(Entity entity: state.entities.findByPosition(data.targetField)) {
if(entity instanceof InfinityStone) {
target.inventory.addStone(((InfinityStone)entity).type);
state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize());
}
}
target.setPosition(data.targetField);
}else {
NPC target = (NPC)state.entities.findEntity(data.originEntity);
for(Entity entity: state.entities.findByPosition(data.targetField)) {
if(entity instanceof InfinityStone) {
target.inventory.addStone(((InfinityStone)entity).type);
state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize());
}
}
target.setPosition(data.targetField);
}
}
case TeleportedEvent -> {
TeleportedEvent data = (TeleportedEvent)event;
Character target = (Character)state.entities.findEntity(data.teleportedEntity);
target.setPosition(data.targetField);
}
case UseInfinityStoneEvent -> {
state.stoneCooldown.setCooldown(((CharacterEvent)event).stoneType);
}
case ExchangeInfinityStoneEvent -> {
CharacterEvent data = (CharacterEvent)event;
((Character)state.entities.findEntity(data.originEntity)).inventory.removeStone(data.stoneType);
Character target = (Character)state.entities.findEntity(data.targetEntity);
target.inventory.addStone(data.stoneType);
state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize());
}
case WinEvent -> {
state.won = true;
}
case GamestateEvent -> {
GamestateEvent data = (GamestateEvent)event;
state.entities.clear();
state.entities.addEntities(data.entities);
state.mapSize.set(data.mapSize);
state.turnOrder = ArrayTools.toArrayList(data.turnOrder);
state.activeCharacter = data.activeCharacter == null ? null : data.activeCharacter.clone();
state.stoneCooldown.import_(data.stoneCooldowns);
state.won = data.winCondition;
}
}
}
/**
* 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(state.turnOrder.toArray(new EntityID[0]))
.withMapSize(state.mapSize)
.withActiveCharacter(state.activeCharacter)
.withStoneCooldowns(state.stoneCooldown.export())
.withWinCondition(state.won)
.buildGameStateEvent();
}
/**
* 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
*/
public static boolean checkLineOfSight(GameState state, IntVector2 start, IntVector2 end) {
for(IntVector2 pos: rasterize(start, end, false, false)) {
if(state.entities.blocksVision(pos)) {
return false;
}
}
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
*/
public static ArrayList<IntVector2> getFreeNeighbour(GameState state, IntVector2 start) {
return getFreeNeighbour(state, start, new ArrayList<>(0));
}
/**
* Finds free neighbour options from a starting field.
* @param state The game state to work on
* @param start The starting position
* @param blocked A list of positions to treat as not-free
* @return A list of free neighbour field options
*/
public static ArrayList<IntVector2> getFreeNeighbour(GameState state, IntVector2 start, List<IntVector2> blocked) {
ArrayList<IntVector2> options = new ArrayList<>();
if(start.getX() < 0 || start.getX() >= state.mapSize.getX() || start.getY() < 0 || start.getY() >= state.mapSize.getY()) {
return options;
}
ArrayList<IntVector2> allOptions = new ArrayList<>();
for(IntVector2 dir: IntVector2.CardinalDirections) {
IntVector2 pos = start.copy().add(dir);
if(pos.getX() < 0 || pos.getX() >= state.mapSize.getX() || pos.getY() < 0 || pos.getY() >= state.mapSize.getY()) {
continue;
}
allOptions.add(pos);
if(!blocked.contains(pos) && state.entities.findByPosition(pos).isEmpty()) {
options.add(pos);
}
}
if(options.isEmpty()) {
if(allOptions.isEmpty()) {
return allOptions;
}
return getFreeNeighbour(state, allOptions.get(rand.nextInt(allOptions.size())), blocked);
}else {
return options;
}
}
/**
* Finds free field options.
* @param state The game state to work on
* @return A list of free field options
*/
public static ArrayList<IntVector2> getFreeFields(GameState state) {
ArrayList<IntVector2> options = 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) {
options.add(pos);
}
}
}
return options;
}
/**
* Starts the game and initializes all entities.
* @param state The game state to work on
* @param selectedCharacters1 The characters selected by player 1
* @param selectedCharacters2 The characters selected by player 2
*/
protected static void startGame(GameState state, List<Integer> selectedCharacters1, List<Integer> selectedCharacters2) {
Logger.trace("Starting game");
ArrayList<IntVector2> free = new ArrayList<>();
int rockIndex = 0;
int portalIndex = 0;
for(int x = 0; x < state.mapSize.getX(); x++) {
for(int y = 0; y < state.mapSize.getY(); y++) {
if(state.scenarioConfig.scenario[y][x] == FieldType.ROCK) {
state.entities.addEntity(new Rock(new EntityID(EntityType.Rocks, rockIndex++), new IntVector2(x, y), 100));
}else if(state.scenarioConfig.scenario[y][x] == FieldType.PORTAL) {
state.entities.addEntity(new Portal(new EntityID(EntityType.Portals, portalIndex++), new IntVector2(x, y)));
}else {
free.add(new IntVector2(x, y));
}
}
}
int p1 = selectedCharacters1.size();
int all = selectedCharacters1.size() + selectedCharacters2.size();
ArrayList<Integer> characters = new ArrayList<>(selectedCharacters1);
characters.addAll(selectedCharacters2);
for(int i = 0; i < all; i++) {
int choice = rand.nextInt(free.size());
IntVector2 position = free.get(choice);
free.remove(choice);
CharacterProperties selected = state.characterConfig.getIDMap().get(characters.get(i));
EntityID id = new EntityID(i < p1 ? EntityType.P1 : EntityType.P2, i % p1);
state.entities.addEntity(new Character(
id, position,
selected.name,
selected.HP,
selected.MP,
selected.AP,
selected.attackRange,
selected.rangedDamage,
selected.meleeDamage
));
state.turnOrder.add(id);
}
}
/**
* Forcefully starts the next round.
* @param state The game state to work on
* @return The list of resulting {@link Event Events}
*/
protected static ArrayList<Event> startRound(GameState state) {
ArrayList<Event> result = handleRoundStart(state);
result.addAll(handleTurnStart(state));
return result;
}
/**
* Starts end of round handling if necessary.
* @param state The game state to work on
* @return The list of resulting {@link Event Events}
*/
protected static ArrayList<Event> checkTurnEnd(GameState state) {
if(
(state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Thanos.getID()) ||
((Character) state.entities.findEntity(state.activeCharacter)).ap.getValue() <= 0 &&
((Character) state.entities.findEntity(state.activeCharacter)).mp.getValue() <= 0
) {
return handleTurnEnd(state);
}
return new ArrayList<>();
}
/**
* Forcefully ends the current turn.
* @param state The game state to work on
* @return The list of resulting {@link Event Events}
*/
protected static ArrayList<Event> endTurn(GameState state) {
return handleTurnEnd(state);
}
/**
* 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 Events}
*/
private static ArrayList<Event> handleTurnEnd(GameState state) {
ArrayList<Event> result = new ArrayList<>();
boolean anyAlive = false;
ArrayList<EntityID> turns = new ArrayList<>();
for (EntityID id: state.turnOrder) {
if(id.type == EntityType.NPC) {
if(id.id == NPCType.Thanos.getID()) {
turns.add(id);
}
continue;
}
Entity found = state.entities.findEntity(id);
if(found == null) {
continue;
}
Character character = ((Character)found);
if(character.isAlive()){
anyAlive = true;
turns.add(id);
}else { // send empty turn for knocked out characters
result.add(new EventBuilder(EventType.TurnEvent)
.withTurnCount(state.turnOrder.indexOf(id) + 1)
.withNextCharacter(id)
.buildGameEvent());
}
if(character.inventory.getFreeSlots() == 0) { // no slots => has all infinity stones
result.addAll(handlePlayerWin(state, character.id.type, "collected all infinity stones"));
return result;
}
}
if(!anyAlive) {
String[] reason = new String[1];
EntityType winner = state.winConditions.getWinner(reason);
if(winner == EntityType.None) {
winner = rand.nextBoolean() ? EntityType.P1 : EntityType.P2;
reason[0] = "determined by lot as tiebreaker";
}
result.addAll(handlePlayerWin(state, winner, reason[0]));
return result;
}
int index = turns.indexOf(state.activeCharacter);
if(index == turns.size() - 1) {
result.addAll(handleRoundStart(state));
}else {
state.activeCharacter = turns.get(index + 1);
}
result.addAll(handleTurnStart(state));
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 Events}
*/
private static ArrayList<Event> handleRoundStart(GameState state) {
ArrayList<Event> result = new ArrayList<>();
state.roundNumber++;
result.add(new EventBuilder(EventType.RoundSetupEvent)
.withRoundCount(state.roundNumber)
.buildGameEvent());
ArrayList<EntityID> turns = new ArrayList<>();
if(state.roundNumber >= 1 && state.roundNumber <= 6) {
turns.add(new EntityID(EntityType.NPC, NPCType.Goose.getID()));
result.addAll(handleGoose(state));
}
HashSet<EntityID> revived = new HashSet<>();
if(state.roundNumber == 7) {
turns.add(new EntityID(EntityType.NPC, NPCType.Stan.getID()));
result.addAll(handleStan(state, revived));
}
if(state.roundNumber == state.partyConfig.maxRounds + 1) {
result.addAll(spawnThanos(state));
}
Collections.shuffle(state.turnOrder);
for (EntityID id: state.turnOrder) {
Entity found = state.entities.findEntity(id);
if(found == null) {
continue;
}
if(id.type == EntityType.NPC || revived.contains(id) || ((Character)found).isAlive()){
state.activeCharacter = id;
break;
}else { // again send empty turns for knocked out characters
result.add(new EventBuilder(EventType.TurnEvent)
.withTurnCount(state.turnOrder.indexOf(id) + 1)
.withNextCharacter(id)
.buildGameEvent());
}
}
turns.addAll(state.turnOrder);
// RoundSetupEvent has to be sent first, but the contents of it are determined later...
((GameEvent)result.get(0)).characterOrder = turns.toArray(new EntityID[0]);
return result;
}
/**
* Handles the actions of Goose at rounds 1-6.
* @param state The game state to work on
* @return The list of resulting {@link Event Events}
*/
private static ArrayList<Event> handleGoose(GameState state) {
ArrayList<Event> result = new ArrayList<>();
ArrayList<StoneType> inventory = new ArrayList<>(state.unvomitedStones);
int picked = rand.nextInt(inventory.size());
StoneType stone = inventory.get(picked);
inventory.remove(picked);
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, NPCType.Goose.getID());
result.add(new EventBuilder(EventType.SpawnEntityEvent)
.withEntity(new NPC(goose, position, inventory))
.buildEntityEvent());
result.add(new EventBuilder(EventType.TurnEvent)
.withTurnCount(1)
.withNextCharacter(goose)
.buildGameEvent());
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 Events}
*/
private 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) {
if(id.type == EntityType.NPC) {
continue;
}
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, NPCType.Stan.getID());
result.add(new EventBuilder(EventType.SpawnEntityEvent)
.withEntity(new NPC(stan, spawnPosition))
.buildEntityEvent());
result.add(new EventBuilder(EventType.TurnEvent)
.withTurnCount(1)
.withNextCharacter(stan)
.buildGameEvent());
for(Character character: characters) {
if(checkLineOfSight(state, spawnPosition, character.getPosition())) {
if(!character.isAlive()) {
revived.add(character.id);
}
if(character.hp.getValue() != character.hp.getMax()) {
result.add(new EventBuilder(EventType.HealedEvent)
.withTargetEntity(character.id)
.withTargetField(character.getPosition())
.withAmount(character.hp.getMax() - character.hp.getValue())
.buildEntityEvent());
}
}
}
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetEntity(stan)
.withTargetField(spawnPosition)
.buildEntityEvent());
return result;
}
/**
* Spawns Thanos at the beginning of the first overtime round.
* @param state The game state to work on
* @return The list of resulting {@link Event Events}
*/
private static ArrayList<Event> spawnThanos(GameState state) {
ArrayList<Event> result = new ArrayList<>();
ArrayList<IntVector2> free = getFreeFields(state);
IntVector2 position = free.get(rand.nextInt(free.size()));
int maxMP = -1;
for(EntityID id: state.turnOrder) {
if(id.type == EntityType.NPC) {
continue;
}
Character character = (Character)state.entities.findEntity(id);
if(character.mp.getMax() > maxMP) {
maxMP = character.mp.getMax();
}
}
EntityID thanos = new EntityID(EntityType.NPC, NPCType.Thanos.getID());
NPC thanosNPC = new NPC(thanos, position, maxMP);
result.add(new EventBuilder(EventType.SpawnEntityEvent)
.withEntity(thanosNPC)
.buildEntityEvent());
for(Entity e: state.entities) {
if(e.id.type == EntityType.Portals) {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetEntity(e.id)
.withTargetField(e.getPosition())
.buildEntityEvent());
}
}
state.turnOrder.add(thanos);
state.entities.addEntity(thanosNPC);
return result;
}
/**
* Handles Thanos' AI.
* @param state The game state to work on
* @return The list of resulting {@link Event Events}
*/
private static ArrayList<Event> handleThanos(GameState state, NPC thanos) {
ArrayList<Event> result = new ArrayList<>();
result.add(new EventBuilder(EventType.TurnEvent)
.withTurnCount(state.turnOrder.indexOf(thanos.id) + 1)
.withNextCharacter(thanos.id)
.buildGameEvent());
if(thanos.inventory.getFreeSlots() > 0) {
IntVector2 picked = null;
float lowestDistance = Integer.MAX_VALUE;
for(int x = 0; x < state.mapSize.getX(); x++) {
for(int y = 0; y < state.mapSize.getY(); y++) {
IntVector2 pos = new IntVector2(x, y);
for(Entity e: state.entities.findByPosition(pos)) {
if(e instanceof InfinityStone || (e instanceof Character && ((Character)e).inventory.getSize() > 0)) {
float distance = thanos.getPosition().distanceChebyshev(pos);
if(distance < lowestDistance) {
picked = pos;
lowestDistance = distance;
break;
}
}
}
}
}
if(picked == null) {
return result;
}
ArrayList<IntVector2> path = GameLogic.Bresenham8Connected(thanos.getPosition(), picked);
int mp = thanos.mp.getMax();
if(mp <= 0) {
return result;
}
IntVector2 current = thanos.getPosition();
for(IntVector2 pos: path) {
if(pos.equals(thanos.getPosition())) {
continue;
}
if(!pos.equals(picked)) {
for(Entity entity: state.entities.findByPosition(pos)) {
if(entity instanceof Rock) {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetEntity(entity.id)
.withTargetField(entity.getPosition())
.buildEntityEvent());
}
}
}
result.add(new EventBuilder(EventType.MoveEvent)
.withOriginEntity(thanos.id)
.withOriginField(current)
.withTargetField(pos)
.buildCharacterEvent());
result.add(new EventBuilder(EventType.ConsumedMPEvent)
.withTargetEntity(thanos.id)
.withTargetField(pos)
.withAmount(1)
.buildEntityEvent());
for(Entity entity: state.entities.findByPosition(pos)) {
if(entity instanceof Character) {
result.add(new EventBuilder(EventType.MoveEvent)
.withOriginEntity(entity.id)
.withOriginField(pos)
.withTargetField(current)
.buildCharacterEvent());
result.add(new EventBuilder(EventType.TakenDamageEvent)
.withTargetEntity(entity.id)
.withTargetField(current)
.withAmount(((Character)entity).hp.getValue())
.buildEntityEvent());
break;
}
if(entity instanceof InfinityStone) {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetEntity(entity.id)
.withTargetField(pos)
.buildEntityEvent());
break;
}
}
current = pos;
if(--mp == 0) {
break;
}
}
}else {
for(EntityID id: state.turnOrder) {
if(id.type == EntityType.NPC) {
continue;
}
Entity found = state.entities.findEntity(id);
if(found == null) {
continue;
}
if(rand.nextBoolean()) {
result.add(new EventBuilder(EventType.DestroyedEntityEvent)
.withTargetEntity(id)
.withTargetField(found.getPosition())
.buildEntityEvent());
}
}
}
return result;
}
/**
* 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 Events}
*/
private static ArrayList<Event> handleTurnStart(GameState state) {
ArrayList<Event> result = new ArrayList<>();
if(state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Thanos.getID()) {
NPC thanos = (NPC)state.entities.findEntity(state.activeCharacter);
result.addAll(handleThanos(state, thanos));
return result;
}
result.add(new EventBuilder(EventType.TurnEvent)
.withTurnCount(state.turnOrder.indexOf(state.activeCharacter) + 1)
.withNextCharacter(state.activeCharacter)
.buildGameEvent());
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 Events}
*/
private static ArrayList<Event> handlePlayerWin(GameState state, EntityType winner, String reason) {
Logger.trace("Player " + winner + " won: " + reason);
ArrayList<Event> result = new ArrayList<>();
result.add(new EventBuilder(EventType.WinEvent)
.withPlayerWon(winner == EntityType.P1 ? 1 : 2)
.buildGameEvent());
return result;
}
/**
* Computes all fields which intersect the line between the center points of the given two fields including the two defining points.
* @return The list of intersecting positions
*/
public static ArrayList<IntVector2> rasterize(IntVector2 a, IntVector2 b) {
return rasterize(a, b, true, true);
}
/**
* Computes all fields which intersect the line between the center points of the given two fields.
* @param includeA Whether to include point a in the result
* @param includeB Whether to include point b in the result
* @return The list of intersecting positions
*/
public static ArrayList<IntVector2> rasterize(IntVector2 a, IntVector2 b, boolean includeA, boolean includeB) {
ArrayList<IntVector2> result = new ArrayList<>();
int x1 = Math.min(a.getX(), b.getX());
int x2 = Math.max(a.getX(), b.getX());
int y1 = Math.min(a.getY(), b.getY());
int y2 = Math.max(a.getY(), b.getY());
Line2D line = new Line2D.Float(a.getX() + 0.5f, a.getY() + 0.5f, b.getX() + 0.5f, b.getY() + 0.5f);
for(int i = x1; i <= x2; i++) {
for(int j = y1; j <= y2; j++) {
HashSet<Point2D.Float> intersections = new HashSet<>();
for(Line2D.Float part: getWireframe(new Rectangle.Float(i, j, 1, 1))) {
if(part.intersectsLine(line)) {
Point2D.Float intersection = calculateInterceptionPoint(line.getP1(), line.getP2(), part.getP1(), part.getP2());
intersections.add(intersection);
if(intersections.size() > 1) {
break;
}
}
}
if(intersections.size() > 1) {
result.add(new IntVector2(i, j));
}
}
}
if(includeA) {
result.add(a);
}
if(includeB) {
result.add(b);
}
return result;
}
/**
* Computes all fields which intersect the ray between the center points of the given two fields extending past the second one.
* @param size The size of the map
* @param includeA Whether to include point a in the result
* @return The list of intersecting positions
*/
public static ArrayList<IntVector2> rasterizeInfinity(IntVector2 a, IntVector2 b, IntVector2 size, boolean includeA) {
ArrayList<IntVector2> result = new ArrayList<>();
if(includeA) {
result.add(a);
}
int x1 = a.getX();
int y1 = a.getY();
int x2 = b.getX();
int y2 = b.getY();
Line2D line = new Line2D.Float(x1 + 0.5f, y1 + 0.5f, x2 + 0.5f, y2 + 0.5f);
double l = line.getP1().distance(line.getP2());
for(int i = 0; i < size.getX(); i++) {
for(int j = 0; j < size.getY(); j++) {
HashSet<Point2D.Float> intersections = new HashSet<>();
for(Line2D.Float part: getWireframe(new Rectangle.Float(i, j, 1, 1))) {
Point2D.Float intersection = calculateInterceptionPoint(line.getP1(), line.getP2(), part.getP1(), part.getP2());
if(intersection != null) {
double dPa = intersection.distance(part.getP1());
double dPb = intersection.distance(part.getP2());
// sum of distances to points of the rectangle part == 1 => point lies on line segment
// -----a---x----b-----
// --x--a--------b-----
if(dPa + dPb == 1) {
double da = intersection.distance(line.getP1());
double db = intersection.distance(line.getP2());
// distance to a greater than to b => point lies on the ray from a over b
// sum of distances analog to rectangle part => point lies between a and b
// 0.000000005 because computers suck (missing precision)
if(da > db || da + db <= l + 0.000000005) {
intersections.add(intersection);
if(intersections.size() > 1) {
break;
}
}
}
}
}
if(intersections.size() > 1) {
result.add(new IntVector2(i, j));
}
}
}
return result;
}
//https://rosettacode.org/wiki/Find_the_intersection_of_two_lines#Java
public static Point2D.Float calculateInterceptionPoint(Point2D s1, Point2D s2, Point2D d1, Point2D d2) {
double a1 = s2.getY() - s1.getY();
double b1 = s1.getX() - s2.getX();
double c1 = a1 * s1.getX() + b1 * s1.getY();
double a2 = d2.getY() - d1.getY();
double b2 = d1.getX() - d2.getX();
double c2 = a2 * d1.getX() + b2 * d1.getY();
double delta = a1 * b2 - a2 * b1;
if(delta == 0) {
return null;
}
return new Point2D.Float((float) ((b2 * c1 - b1 * c2) / delta), (float) ((a1 * c2 - a2 * c1) / delta));
}
/** Computes all wireframe lines for the given rectangle. */
public static ArrayList<Line2D.Float> getWireframe(Rectangle.Float rect) {
ArrayList<Line2D.Float> result = new ArrayList<>();
Point2D.Float lt = new Point2D.Float(rect.x, rect.y);
Point2D.Float rt = new Point2D.Float(rect.x + rect.width, rect.y);
Point2D.Float rb = new Point2D.Float(rect.x + rect.width, rect.y + rect.height);
Point2D.Float lb = new Point2D.Float(rect.x, rect.y + rect.height);
result.add(new Line2D.Float(lt, rt));
result.add(new Line2D.Float(rt, rb));
result.add(new Line2D.Float(lb, rb));
result.add(new Line2D.Float(lt, lb));
return result;
}
//https://stackoverflow.com/a/5187612
public static ArrayList<IntVector2> Bresenham4Connected(IntVector2 a, IntVector2 b) {
ArrayList<IntVector2> result = new ArrayList<>();
int x0 = a.getX();
int y0 = a.getY();
int x1 = b.getX();
int y1 = b.getY();
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int ix = x0 < x1 ? 1 : -1;
int iy = y0 < y1 ? 1 : -1;
int e = 0;
for(int i = 0; i < dx + dy; i++) {
result.add(new IntVector2(x0, y0));
int e1 = e + dy;
int e2 = e - dx;
if(Math.abs(e1) < Math.abs(e2)) {
x0 += ix;
e = e1;
}else {
y0 += iy;
e = e2;
}
}
result.add(b);
return result;
}
//https://www.programmersought.com/article/43476640523/
public static ArrayList<IntVector2> Bresenham8Connected(IntVector2 a, IntVector2 b) {
ArrayList<IntVector2> result = new ArrayList<>();
int x0 = a.getX();
int y0 = a.getY();
int x1 = b.getX();
int y1 = b.getY();
int dx = Math.abs(x1 - x0);
int dy = Math.abs(y1 - y0);
int ix = x0 < x1 ? 1 : -1;
int iy = y0 < y1 ? 1 : -1;
int e = dx - dy;
while(x0 != x1 || y0 != y1) {
result.add(new IntVector2(x0, y0));
int e2 = e << 1;
if(e2 > -dy) {
e -= dy;
x0 += ix;
}
if(e2 < dx) {
e += dx;
y0 += iy;
}
}
result.add(b);
return result;
}
}