/**
*
*/
package com.szuppe.jakub.model;
import java.awt.Dimension;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import com.szuppe.jakub.common.Acceleration2D;
import com.szuppe.jakub.common.Coordinates2D;
import com.szuppe.jakub.common.Direction;
import com.szuppe.jakub.common.ImaginaryRootsException;
import com.szuppe.jakub.common.QuadraticEquation;
import com.szuppe.jakub.common.SpeedVector2D;
import com.szuppe.jakub.common.Vertices2D;
import com.szuppe.jakub.config.model.LevelConfig;
import com.szuppe.jakub.mockups.*;
import com.thoughtworks.xstream.XStream;
/**
* Klasa reprezentująca przestrzeń gry - plansze.
* <br/>
* Plansza posiada piłkę, paletkę, klocki, a także listę map.
* <br/>
* Symuluje fizykę zderzeń.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*
*/
/**
* @author Jakub Szuppe <j.szuppe at gmail.com>
*
*/
class Level
{
/** Informacje o graczu. (Życie, punkty) */
private final Player player;
/** Wymiary planszy */
private final Dimension levelDimension;
/** Piłka. */
private final Ball ball;
/** Paletka. */
private final Paddle paddle;
/** Lista map/poziomów gry. */
private final LinkedList<List<Brick>> listOfLevels;
/** Obiekt zawierający aktualnie niezbite klocki na planszy. */
private final Bricks bricks;
/** Mapa, która danej kolizji przyporządkowuje odpowiednia strategie.*/
private final Map<Class<? extends Collision>, CollisionStrategy> collisionStrategyMap;
/** Ostatnie czas, kiedy zmienialiśmy stan planszy. */
private long lastUpdateTime;
/** Czas przez, który plansza jest poprawna. */
private long validTime;
/** Lista przewidzianych kolizji. */
private final List<Collision> predictedCollisions;
/** Konfiguracja planszy. */
@SuppressWarnings("unused")
private final LevelConfig levelConfig;
/**
* Tworzy nową planszę zgodnie na podstawie podanej
* konfiguracji.
*
* @param levelConfig
*/
@SuppressWarnings("unchecked")
public Level(LevelConfig levelConfig)
{
this.levelConfig = levelConfig;
this.levelDimension = levelConfig.getLevelDimension();
this.paddle = new Paddle(levelConfig.getPaddleConfig());
this.ball = new Ball(levelConfig.getBallConfig());
this.player = new Player(levelConfig.getPlayerConfig());
XStream xStream = new XStream();
URL urlToListOfLevels = getClass().getResource("/com/szuppe/jakub/resources/listOfLevels.xml");
this.listOfLevels = (LinkedList<List<Brick>>) xStream.fromXML(urlToListOfLevels);
this.bricks = new Bricks(listOfLevels.removeFirst());
// Nie było przeliczania stanu.
this.lastUpdateTime = 0;
// Stan jest ciągle dobry.
this.validTime = -1;
this.collisionStrategyMap = new HashMap<>();
predictedCollisions = new LinkedList<>();
initCollisionStrategyMap();
}
/**
* Wczytuje kolejny poziom gry i ustawia piłkę oraz paletkę na
* ich pozycjach startowych.
*
* @throws NoMoreMapsException - rzucany jeżeli nie ma kolejnego poziomu.
*/
public void loadNextBricksMap() throws NoMoreMapsException
{
try
{
bricks.addAllBricks(listOfLevels.removeFirst());
ball.resetToStartSituation();
paddle.resetToStartSituation();
predictedCollisions.clear();
} catch (NoSuchElementException e)
{
throw new NoMoreMapsException();
}
}
/**
* Przelicza nowy stan planszy po upłynięciu validTime.
* Zaleca się wykonywanie wyłącznie {@link #recalculateValidTime()}.
*
* @throws PlayerDiedException - jeżeli platforma straciła wszystkie życia.
* @throws AllBricksDestroyedException - jeżeli wszystkie klocki zbite.
*/
public void update() throws PlayerDiedException, AllBricksDestroyedException
{
// Wykonanie kolizji.
performPredictedCollisions();
// validTime nieaktualny.
validTime = -1;
// Przewidujemy nowe kolizje, ustala się nowy validTime
predictedCollisions.addAll(predictEarliestCollisions());
// Jeżeli kolizja zachodzi za 0 ms to ją wykonajmy.
while (validTime == 0)
{
// Wykonanie kolizji.
performPredictedCollisions();
// validTime nieaktualny.
validTime = -1;
// Przewidujemy nowe kolizje, ustala się nowy validTime
predictedCollisions.addAll(predictEarliestCollisions());
}
lastUpdateTime = System.currentTimeMillis();
// Paletka umarła ostatecznie, straciła wszystkie życia.
if(!player.isAlive())
{
throw new PlayerDiedException();
}
// Wszystkie klocki na aktualnej mapie zniszczone.
if(bricks.allBricksDestroyed())
{
throw new AllBricksDestroyedException();
}
}
/**
* Odpowiednio przelicza czas poprawności stanu planszy.
* Wykonuje odpowiednie operacje na obiektach.
* <br/>
* Jeżeli podczas wywołania minął właśnie validTime wykonywane są
* przewidziane kolizję i obliczany kolejny stan.
* <br/>
* Bezpieczniejsze niż {@link #update()}.
*
* @throws PlayerDiedException - jeżeli platforma straciła wszystkie życia.
* @throws AllBricksDestroyedException - jeżeli wszystkie klocki zbite.
*/
public void recalculateValidTime() throws PlayerDiedException, AllBricksDestroyedException
{
long timeInterval = System.currentTimeMillis() - lastUpdateTime;
lastUpdateTime = System.currentTimeMillis();
if (timeInterval < validTime)
{
validTime = -1;
moveAllObjects(timeInterval);
predictedCollisions.clear();
predictedCollisions.addAll(predictEarliestCollisions());
}
else
{
update();
}
}
/**
* Wypuszczenie piłki.
*/
public void releaseBall()
{
ball.release();
validWithTimeLimit();
}
/**
* Nadaje platformie energii w podanym kierunku.
* Energia występuje w formie prędkości.
*
* @param direction
*/
public void acceleratePaddle(Direction direction)
{
validWithTimeLimit();
paddle.accelerate(direction);
if (!ballIsBouncing())
{
ball.setSpeed(paddle.getSpeed());
ball.setAcceleration(paddle.getAcceleration());
}
}
/**
* @return Makietę stanu całej planszy (gry).
*/
public GameMockup getGameMockup()
{
return new GameMockup(getBallMockup(), getPaddleMockup(), getLevelMockup(), getBricksMockup(), getPlayerMockup());
}
/**
* @return Makietę z informacjami o graczu. (np. życie)
*/
public PlayerMockup getPlayerMockup()
{
return new PlayerMockup(player.getLives());
}
/**
* @return Makietę piłki.
*/
public BallMockup getBallMockup()
{
final int ballRadius = ball.getRadius();
final Coordinates2D topLeftBallCoordinates = ball.getCoordinates();
topLeftBallCoordinates.add(new Coordinates2D(-ballRadius, ballRadius));
return new BallMockup(topLeftBallCoordinates, ballRadius, ball.getSpeed(), ball.getAcceleration());
}
/**
* @return Makietę platformy.
*/
public PaddleMockup getPaddleMockup()
{
final Coordinates2D topLeftPaddleCoordinates = new Coordinates2D(paddle.getMinX(), paddle.getMaxY());
return new PaddleMockup(topLeftPaddleCoordinates, paddle.getSpeed(), paddle.getAcceleration());
}
/**
* @return Makietę planszy.
*/
private LevelMockup getLevelMockup()
{
return new LevelMockup(new Dimension(levelDimension));
}
/**
* @return Makietę klocków.
*/
public BricksMockup getBricksMockup()
{
List<Brick> bricksList = bricks.getBricksList();
BricksMockup bricksMockup = new BricksMockup();
for (Brick brick : bricksList)
{
bricksMockup.add(new BrickMockup(brick.getTopLeftCornerCoordinates(), brick.getBrickType()));
}
return bricksMockup;
}
/**
* @return Prawdę jeżeli piłka jest wypuczona; fałsz wpp.
*/
public boolean ballIsBouncing()
{
return ball.isReleased();
}
/**
* @return Czas przez, który poprawny będzie stan planszy.
*/
public long getValidTime()
{
return validTime;
}
/**
* Przemieszcza wszystkie obiekty po czasie.
*
* @param timeInterval - czas.
*/
private void moveAllObjects(long timeInterval)
{
ball.move(timeInterval);
paddle.move(timeInterval);
}
/**
* Stan planszy był ważny bezterminowo,
* to już nie jest.
*/
private void validWithTimeLimit()
{
if(validTime == -1)
{
validTime = Long.MAX_VALUE;
}
}
/**
* Wykonuje wcześniej przewidziane kolizje.
*/
private void performPredictedCollisions()
{
moveAllObjects(validTime);
for (Collision collision : predictedCollisions)
{
collisionStrategyMap.get(collision.getClass()).handle(collision);
}
predictedCollisions.clear();
}
/**
* Przewiduje najwcześniejsze kolizje, które zajdą
* na planszy. Ustawia odpowiednio czas poprawności
* stanu planszy.
*
* @return Listę przewidzianych najwcześniejszy kolizji.
*/
private List<Collision> predictEarliestCollisions()
{
List<Collision> collisionsList = checkCollisions();
Collections.sort(collisionsList);
List<Collision> nearestCollisionsList = new LinkedList<Collision>();
for (Collision collision : collisionsList)
{
if (collision.equals(collisionsList.get(0)))
{
this.validTime = collision.getTimeToCollision();
nearestCollisionsList.add(collision);
}
}
return nearestCollisionsList;
}
/**
* Sprawdza wszystkie kolizje, które mogą zajść
* i je zwraca w postaci nieuporządkowanej listy.
*
* @return Listę wszystkich kolizji, które mogą zajść.
*/
private List<Collision> checkCollisions()
{
List<Collision> collisionsList = new LinkedList<>();
if (ballIsBouncing())
{
collisionsList.addAll(checkCollisionsWithBall());
collisionsList.addAll(bricks.checkCollisionsWithBall(ball));
collisionsList.addAll(paddle.checkCollisionsWithBall(ball));
}
collisionsList.addAll(checkCollisionsWithPaddle());
return collisionsList;
}
/**
* Sprawdza kolizje, które mogą wystąpić między planszą
* a platformą. Sprawdza również, kiedy zatrzyma się
* platforma.
*
* Platforma może przekroczyć ścianę planszy o 50 jednostek.
*
* @return Listę kolizji między platformą a planszą.
*/
private List<Collision> checkCollisionsWithPaddle()
{
List<Collision> collisionsList = new LinkedList<>();
final SpeedVector2D paddleSpeed = paddle.getSpeed();
final float paddleXSpeed = paddleSpeed.getXSpeed();
final Acceleration2D paddleAcc = paddle.getAcceleration();
final float paddleXAcc = paddleAcc.getxAcc();
if (paddleXSpeed == 0 || paddleXAcc == 0)
{
return Collections.emptyList();
}
final double paddleLevelSidesCross = 50;
final double timeWhenPaddleWillStop = Math.abs(paddleXSpeed / paddleXAcc);
collisionsList.add(new PaddleWillStop((long) timeWhenPaddleWillStop));
if (paddleXSpeed > 0)
{
final double distanceToLevelMaxX = Math.abs(paddleLevelSidesCross + levelDimension.width - paddle.getMaxX());
// s = t * ( v + 1/2 * a * t) = vt + 1/2 * a * t^2
final double distanceToStopPoint = timeWhenPaddleWillStop
* Math.abs(paddleXSpeed + paddleXAcc * timeWhenPaddleWillStop * 0.5);
if (distanceToLevelMaxX <= distanceToStopPoint)
{
// Will stop after crossing level right side;
try
{
double possibleCollisionTimes[] = QuadraticEquation.solve(paddleXAcc / 2, paddleXSpeed,
-distanceToLevelMaxX);
if (possibleCollisionTimes.length == 2)
{
for (double collisionTime : possibleCollisionTimes)
{
if(collisionTime >= 0)
{
collisionsList.add(new PaddleWithLevelRightSideCollision((long)collisionTime));
}
}
}
else
{
if(possibleCollisionTimes[0] >= 0)
{
collisionsList.add(new PaddleWithLevelRightSideCollision((long)possibleCollisionTimes[0]));
}
}
} catch (ImaginaryRootsException e)
{
}
}
}
else if (paddleXSpeed < 0)
{
final double distanceToLevelMinX = Math.abs(paddle.getMinX() + paddleLevelSidesCross);
// s = t * ( v + 1/2 * a * t) = vt + 1/2 * a * t^2
final double distanceToStopPoint = timeWhenPaddleWillStop
* Math.abs(paddleXSpeed + paddleXAcc * timeWhenPaddleWillStop * 0.5);
if (distanceToLevelMinX <= distanceToStopPoint)
{
// Will stop after crossing level right side;
try
{
double possibleCollisionTimes[] = QuadraticEquation.solve(- paddleXAcc / 2, -paddleXSpeed,
-distanceToLevelMinX);
if (possibleCollisionTimes.length == 2)
{
for (double collisionTime : possibleCollisionTimes)
{
if(collisionTime >= 0)
{
collisionsList.add(new PaddleWithLevelLeftSideCollision((long)collisionTime));
}
}
}
else
{
if(possibleCollisionTimes[0] >= 0)
{
collisionsList.add(new PaddleWithLevelLeftSideCollision((long)possibleCollisionTimes[0]));
}
}
} catch (ImaginaryRootsException e)
{
}
}
}
return collisionsList;
}
/**
* Sprawdza możliwe kolizje między planszą a piłką.
* Zwraca je jako nieuporządkowana list.
*
* @return Listę kolizji między planszą a piłką.
*/
private List<Collision> checkCollisionsWithBall()
{
List<Collision> collisionsList = new LinkedList<>();
final float ballXSpeed = ball.getXSpeed();
final float ballYSpeed = ball.getYSpeed();
if (ballXSpeed > 0)
{
final float distanceToLevelMaxX = levelDimension.width - (ball.getX() + ball.getRadius());
final long millisecondsToCollision = Math.abs((long) (distanceToLevelMaxX / ballXSpeed));
collisionsList.add(new BallWithLevelRightSideCollision(millisecondsToCollision));
}
else
{
final float distanceToLevelMinX = ball.getX() - ball.getRadius();
final long millisecondsToCollision = Math.abs((long) (distanceToLevelMinX / ballXSpeed));
collisionsList.add(new BallWithLevelLeftSideCollision(millisecondsToCollision));
}
if (ballYSpeed > 0)
{
final float distanceToLevelMaxY = levelDimension.height - (ball.getY() + ball.getRadius());
final long millisecondsToCollision = Math.abs((long) (distanceToLevelMaxY / ballYSpeed));
collisionsList.add(new BallWithLevelTopCollision(millisecondsToCollision));
}
else
{
final float distanceToLevelMinY = ball.getY() - ball.getRadius();
final long millisecondsToCollision = Math.abs((long) (distanceToLevelMinY / ballYSpeed));
collisionsList.add(new BallWithLevelBottomCollision(millisecondsToCollision));
}
return collisionsList;
}
/**
* Wypełnia mapę strategii kolizji.
*/
private void initCollisionStrategyMap()
{
collisionStrategyMap.clear();
collisionStrategyMap.put(BallWithLevelRightSideCollision.class, new BallWithLevelSideCollisionStrategy());
collisionStrategyMap.put(BallWithLevelLeftSideCollision.class, new BallWithLevelSideCollisionStrategy());
collisionStrategyMap.put(BallWithLevelBottomCollision.class, new BallWithLevelBottomCollisionStrategy());
collisionStrategyMap.put(BallWithLevelTopCollision.class, new BallWithLevelTopCollisionStrategy());
collisionStrategyMap.put(BallWithPaddleCollision.class, new BallWithPaddleCollisionStrategy());
collisionStrategyMap.put(BallWithBrickCollision.class, new BallWithBrickCollisionStrategy());
collisionStrategyMap.put(PaddleWithLevelRightSideCollision.class, new PaddleWithLevelSideCollisionStrategy());
collisionStrategyMap.put(PaddleWithLevelLeftSideCollision.class, new PaddleWithLevelSideCollisionStrategy());
collisionStrategyMap.put(PaddleWillStop.class, new PaddleWillStopStrategy());
}
/**
* Abstrakcyjna klasa reprezentująca
* strategię wykonywaną podczas odpowiedniej
* kolizji.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
abstract class CollisionStrategy
{
/**
* Metoda, która odpowiednio reaguje na kolizje
* i zmienia stan planszy.
*
* @param collision - kolizja.
*/
public abstract void handle(final Collision collision);
}
/**
* Strategia opisująca zachowanie się podczas
* kolizji piłki z platformą.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class BallWithPaddleCollisionStrategy extends CollisionStrategy
{
/**
* Odbija piłkę od platformy i odpowiednio przekształca jej
* wektor prędkości.
*/
@Override
public void handle(Collision collision)
{
final Vertices2D paddleVertices = paddle.getVerticles();
double alpha = (ball.getCoordinates().getX() - paddleVertices.getMinX()) / (paddleVertices.getMaxX()-paddleVertices.getMinX());
if(alpha < 0)
{
alpha = 0;
}
else if(alpha > 1)
{
alpha = 1;
}
alpha = countAngleInRange(alpha, Math.PI*0.66d, Math.PI * 1.166d);
ball.rotateSpeedVector(alpha);
ball.reverseYSpeed();
}
/**
* Metoda do obliczania kąta rotacji wektora
* prędkości piłki odbijającej się od platformy.
*
* @param alpha
* @param rangeWidth
* @param min
* @return
*/
private double countAngleInRange(double alpha, double rangeWidth, double min)
{
return (alpha * rangeWidth) + min;
}
}
/**
* Strategia opisująca zachowanie się podczas
* kolizji piłki z dolną ścianą planszy.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class BallWithLevelBottomCollisionStrategy extends CollisionStrategy
{
/**
* Platforma traci życie.
* Platforma i piłka są ustawione na pozycje startowe.
*/
@Override
public void handle(Collision collision)
{
player.lostLife();
paddle.resetToStartSituation();
ball.resetToStartSituation();
}
}
/**
* Strategia opisująca zachowanie się podczas
* kolizji piłki z lewą lub prawą ścianą planszy.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class BallWithLevelSideCollisionStrategy extends CollisionStrategy
{
/**
* Odbija piłę od ściany.
*/
@Override
public void handle(Collision collision)
{
ball.reverseXSpeed();
}
}
/**
* Strategia opisująca zachowanie się podczas
* kolizji piłki z górą planszy.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class BallWithLevelTopCollisionStrategy extends CollisionStrategy
{
/**
* Odbija piłkę od góry.
*/
@Override
public void handle(Collision collision)
{
ball.reverseYSpeed();
}
}
/**
* Strategia opisująca zachowanie się podczas
* kolizji piłki z klockiem.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class BallWithBrickCollisionStrategy extends CollisionStrategy
{
/**
* Niszczy klocek. Odbija piłkę od klocka
* zgodnie z punktem zderzenia.
*/
@Override
public void handle(Collision collision)
{
final BallWithBrickCollision localCollison = (BallWithBrickCollision) collision;
if(localCollison.getSide() == Side.LEFT || localCollison.getSide() == Side.RIGHT)
{
ball.reverseXSpeed();
}
else if(localCollison.getSide() == Side.CORNER)
{
ball.reverseXSpeed();
ball.reverseYSpeed();
}
else
{
ball.reverseYSpeed();
}
bricks.removeBrick(localCollison.getBrick());
}
}
/**
* Strategia opisująca zachowanie się podczas
* kolizji platformy ze ścianą planszy.
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class PaddleWithLevelSideCollisionStrategy extends CollisionStrategy
{
/**
* Odbicie platformy od ściany.
*/
@Override
public void handle(Collision collision)
{
paddle.reverseXSpeed();
paddle.reverseXAcc();
if (!ball.isReleased())
{
ball.reverseXSpeed();
ball.reverseXAcc();
}
}
}
/**
*
* @author Jakub Szuppe <j.szuppe at gmail.com>
*/
class PaddleWillStopStrategy extends CollisionStrategy
{
/**
* Zatrzymanie platformy.
*/
@Override
public void handle(Collision collision)
{
paddle.stop();
}
}
}