565 lines
18 KiB
Java
565 lines
18 KiB
Java
package chess.view.DDDrender;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.function.Consumer;
|
|
|
|
import chess.controller.commands.*;
|
|
import chess.controller.commands.PromoteCommand.PromoteType;
|
|
import imgui.ImGui;
|
|
import imgui.ImVec2;
|
|
import imgui.flag.ImGuiCond;
|
|
import imgui.flag.ImGuiWindowFlags;
|
|
import org.joml.Vector2f;
|
|
import org.joml.Vector3f;
|
|
|
|
import chess.controller.Command.CommandResult;
|
|
import chess.controller.CommandExecutor;
|
|
import chess.controller.CommandSender;
|
|
import chess.controller.event.GameAdapter;
|
|
import chess.model.Color;
|
|
import chess.model.Coordinate;
|
|
import chess.model.Move;
|
|
import chess.model.Piece;
|
|
import chess.view.DDDrender.world.BoardEntity;
|
|
import chess.view.DDDrender.world.PieceEntity;
|
|
import chess.view.DDDrender.world.World;
|
|
|
|
public class DDDView extends GameAdapter implements CommandSender {
|
|
|
|
private static final Vector3f BLACK = new Vector3f(0.3f, 0.3f, 0.3f);
|
|
private static final Vector3f WHITE = new Vector3f(1.0f, 1.0f, 1.0f);
|
|
private static final Vector3f RED = new Vector3f(1.0f, 0.0f, 0.0f);
|
|
private static final Vector3f YELLOW = new Vector3f(1.0f, 1.0f, 0.0f);
|
|
private static final Vector3f BLUE = new Vector3f(0.0f, 0.0f, 1.0f);
|
|
|
|
private final CommandExecutor commandExecutor;
|
|
private final Window window;
|
|
private final World world;
|
|
private BoardEntity boardEntity;
|
|
private final Camera camera;
|
|
private Coordinate click = null;
|
|
|
|
private static final float animationTime = 0.5f; // in seconds
|
|
private static final int animationTurns = 1;
|
|
|
|
private float moveProgress = 0.0f;
|
|
|
|
private String waitingPopup = null;
|
|
|
|
public DDDView(CommandExecutor commandExecutor) {
|
|
this.commandExecutor = commandExecutor;
|
|
this.world = new World();
|
|
this.camera = new Camera();
|
|
this.window = new Window(new Renderer(), this.world, this.camera);
|
|
}
|
|
|
|
@Override
|
|
public CommandExecutor getCommandExecutor() {
|
|
return this.commandExecutor;
|
|
}
|
|
|
|
private void cancelClick() {
|
|
this.click = null;
|
|
}
|
|
|
|
private void setClick(Coordinate coordinate) {
|
|
this.click = coordinate;
|
|
}
|
|
|
|
/**
|
|
* Invoked when a cell is clicked. The first click selects the piece to move, the second click selects the destination.
|
|
* @param coordinate
|
|
*/
|
|
private void onCellClick(Coordinate coordinate) {
|
|
if (this.click == null) { // case: first click
|
|
List<Coordinate> allowedMoves = getPieceAllowedMoves(coordinate);
|
|
if (allowedMoves.isEmpty()) { // case: no movement possible for piece
|
|
return;
|
|
}
|
|
setClick(coordinate);
|
|
previewMoves(coordinate);
|
|
return;
|
|
}
|
|
// case: second click
|
|
GetAllowedMovesPieceCommand movesCommand = new GetAllowedMovesPieceCommand(this.click);
|
|
if (sendCommand(movesCommand) == CommandResult.NotAllowed) { // case: invalid piece to move
|
|
cancelPreview(this.click);
|
|
cancelClick();
|
|
return;
|
|
}
|
|
List<Coordinate> allowedMoves = movesCommand.getDestinations();
|
|
if (allowedMoves.isEmpty()) { // case: no movement possible for piece
|
|
cancelPreview(this.click);
|
|
cancelClick();
|
|
return;
|
|
}
|
|
if (allowedMoves.contains(coordinate)) { // case: valid attempt to move
|
|
cancelPreview(this.click);
|
|
sendMove(new Move(click, coordinate));
|
|
cancelClick();
|
|
return;
|
|
}
|
|
if (coordinate != this.click) { // cases: invalid move, selecting another piece
|
|
cancelPreview(this.click);
|
|
previewMoves(coordinate);
|
|
setClick(coordinate);
|
|
return;
|
|
}
|
|
cancelClick(); // case: cancelling previous click
|
|
}
|
|
|
|
/**
|
|
* Show the possible moves of the piece at the given coordinates: the piece is highlighted in yellow, the allowed moves in red.
|
|
* @param coordinate
|
|
*/
|
|
private void previewMoves(Coordinate coordinate) {
|
|
List<Coordinate> allowedMoves = getPieceAllowedMoves(coordinate);
|
|
if (allowedMoves.isEmpty())
|
|
return;
|
|
this.boardEntity.setCellColor(coordinate, RED);
|
|
this.world.getPiece(coordinate).setColor(RED);
|
|
for (Coordinate destCoord : allowedMoves) {
|
|
this.boardEntity.setCellColor(destCoord, YELLOW);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked when a cell is hovered. The hovered cell is highlighted in red. If the cell contains a piece, its allowed moves are highlighted in red as well.
|
|
* @param coordinate
|
|
*/
|
|
private void onCellEnter(Coordinate coordinate) {
|
|
if (this.click == null) {
|
|
// small test turning a cell red when hovered
|
|
this.boardEntity.setCellColor(coordinate, RED);
|
|
|
|
PieceEntity pEntity = this.world.getPiece(coordinate);
|
|
if (pEntity == null)
|
|
return;
|
|
|
|
pEntity.setColor(RED);
|
|
List<Coordinate> allowedMoves = getPieceAllowedMoves(coordinate);
|
|
if (allowedMoves.isEmpty())
|
|
return;
|
|
for (Coordinate destCoord : allowedMoves) {
|
|
this.boardEntity.setCellColor(destCoord, YELLOW);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked when a cell is not hovered anymore, cancel that cell's preview.
|
|
* @param coordinate
|
|
*/
|
|
private void onCellExit(Coordinate coordinate) {
|
|
if (this.click == null) {
|
|
this.boardEntity.resetCellColor(coordinate);
|
|
Piece p = getPieceAt(coordinate);
|
|
if (p == null)
|
|
return;
|
|
|
|
PieceEntity pEntity = this.world.getPiece(coordinate);
|
|
if (pEntity == null)
|
|
return;
|
|
|
|
pEntity.setColor(p.getColor() == Color.White ? WHITE : BLACK);
|
|
List<Coordinate> allowedMoves = getPieceAllowedMoves(coordinate);
|
|
if (allowedMoves.isEmpty())
|
|
return;
|
|
for (Coordinate destCoord : allowedMoves) {
|
|
this.boardEntity.resetCellColor(destCoord);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel the preview of the moves for the given coordinates.
|
|
* @param coordinate
|
|
*/
|
|
private void cancelPreview(Coordinate coordinate) {
|
|
this.boardEntity.resetCellColor(coordinate);
|
|
Piece p = getPieceAt(coordinate);
|
|
if (p == null)
|
|
return;
|
|
this.world.getPiece(coordinate).setColor(p.getColor() == Color.White ? WHITE : BLACK);
|
|
List<Coordinate> allowedMoves = getPieceAllowedMoves(coordinate);
|
|
if (allowedMoves.isEmpty())
|
|
return;
|
|
for (Coordinate destCoord : allowedMoves) {
|
|
this.boardEntity.resetCellColor(destCoord);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the game by initializing the board, creating the events, and starting the game loop.
|
|
*/
|
|
@Override
|
|
public void onGameStart() {
|
|
this.window.scheduleTask(() -> {
|
|
try {
|
|
initBoard();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
// start listening to mouse events
|
|
this.window.OnCellClick.connect(this::onCellClick);
|
|
this.window.OnCellEnter.connect(this::onCellEnter);
|
|
this.window.OnCellExit.connect(this::onCellExit);
|
|
this.window.OnImGuiTopRender.connect(this::onHeaderRender);
|
|
this.window.OnImGuiBottomRender.connect(this::onFooterRender);
|
|
|
|
synchronized (this) {
|
|
notifyAll();
|
|
}
|
|
});
|
|
|
|
synchronized (this) {
|
|
try {
|
|
wait();
|
|
} catch (InterruptedException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void onHeaderRender() {
|
|
ImGui.text("FPS : " + (int) ImGui.getIO().getFramerate());
|
|
}
|
|
|
|
/**
|
|
* Render the footer of the window.
|
|
*/
|
|
private void onFooterRender() {
|
|
ImGui.beginDisabled(!canDoCastling());
|
|
if (ImGui.button("Roque")) {
|
|
sendCastling();
|
|
}
|
|
ImGui.endDisabled();
|
|
ImGui.sameLine();
|
|
ImGui.beginDisabled(!canDoBigCastling());
|
|
if (ImGui.button("Grand Roque")) {
|
|
sendBigCastling();
|
|
}
|
|
ImGui.endDisabled();
|
|
ImGui.sameLine();
|
|
if (ImGui.button("Annuler le coup précédent")) {
|
|
sendUndo();
|
|
}
|
|
openPopup();
|
|
renderPopups();
|
|
}
|
|
|
|
private void openPopup() {
|
|
if (waitingPopup != null) {
|
|
ImGui.openPopup(waitingPopup);
|
|
waitingPopup = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the 3D piece at the given coordinates
|
|
* @param piece the piece to create
|
|
* @param coordinate the coordinates of the piece
|
|
* @return the piece entity
|
|
* @throws IOException
|
|
*/
|
|
private PieceEntity createDefault(Piece piece, Coordinate coordinate) throws IOException {
|
|
Vector2f pieceBoardPos = DDDPlacement.coordinatesToVector(coordinate);
|
|
Vector3f pieceWorldPos = new Vector3f(pieceBoardPos.x(), 0, pieceBoardPos.y());
|
|
|
|
return new PieceEntity(piece, pieceWorldPos,
|
|
piece.getColor() == Color.White ? WHITE : BLACK,
|
|
piece.getColor() == Color.White ? 0.0f : (float) Math.PI);
|
|
}
|
|
|
|
/**
|
|
* Create the 3D board and add the pieces.
|
|
* @throws IOException
|
|
*/
|
|
private void initBoard() throws IOException {
|
|
for (int i = 0; i < Coordinate.VALUE_MAX; i++) {
|
|
for (int j = 0; j < Coordinate.VALUE_MAX; j++) {
|
|
Coordinate pos = new Coordinate(i, j);
|
|
Piece piece = getPieceAt(pos);
|
|
if (piece == null)
|
|
continue;
|
|
|
|
this.world.addPiece(createDefault(piece, pos), pos);
|
|
}
|
|
}
|
|
this.boardEntity = new BoardEntity();
|
|
this.world.addEntity(this.boardEntity);
|
|
}
|
|
|
|
/**
|
|
* Calculate the position of the point at the t position on a Bezier curve spanning from the begin point to the end point and goign through the control point.
|
|
* @param begin begin
|
|
* @param middle control point
|
|
* @param end end
|
|
* @param t between 0 and 1
|
|
* @return the point
|
|
*/
|
|
private Vector3f bezierCurve(Vector3f begin, Vector3f middle, Vector3f end, float t) {
|
|
return begin.mul((1.0f - t) * (1.0f - t)).add(middle.mul(2.0f * t * (1.0f - t))).add(end.mul(t * t));
|
|
}
|
|
|
|
/**
|
|
* Move the piece on the board according to the given move. Called in a loop to animate the movement.
|
|
* @param progress the proportion of the animation already done
|
|
* @param piece
|
|
* @param move
|
|
*/
|
|
private void pieceTick(float progress, PieceEntity piece, Move move) {
|
|
float height = 1; // how high the piece is raised
|
|
Vector2f pieceStartBoard = DDDPlacement.coordinatesToVector(move.getStart());
|
|
Vector2f pieceDestinationBoard = DDDPlacement.coordinatesToVector(move.getFinish());
|
|
Vector3f start = new Vector3f(pieceStartBoard.x(), 0, pieceStartBoard.y());
|
|
Vector3f top = new Vector3f(pieceDestinationBoard.x() - pieceStartBoard.x(), height,
|
|
pieceDestinationBoard.y() - pieceStartBoard.y());
|
|
Vector3f end = new Vector3f(pieceDestinationBoard.x(), 0, pieceDestinationBoard.y());
|
|
|
|
piece.setPosition(bezierCurve(start, top, end, progress));
|
|
}
|
|
|
|
/**
|
|
* Move the pieces on the board according to the given moves.
|
|
* @param moves
|
|
*/
|
|
private void move3DPieces(List<Move> moves) {
|
|
final List<PieceEntity> pEntities = new ArrayList<>(moves.size());
|
|
final List<Consumer<Float>> consumers = new ArrayList<>(moves.size());
|
|
|
|
this.moveProgress = 0.0f;
|
|
|
|
for (Move move : moves) {
|
|
final PieceEntity pEntity = this.world.getPiece(move.getStart());
|
|
final Consumer<Float> moveConsumer = (delta) -> {
|
|
this.moveProgress += delta / animationTime / (float) moves.size();
|
|
pieceTick(this.moveProgress, pEntity, move);
|
|
};
|
|
pEntities.add(pEntity);
|
|
consumers.add(moveConsumer);
|
|
this.window.addRegularTask(moveConsumer);
|
|
}
|
|
|
|
try {
|
|
Thread.sleep((long) (animationTime * 1000.0f));
|
|
} catch (InterruptedException e) {
|
|
}
|
|
|
|
for (int i = 0; i < moves.size(); i++) {
|
|
final Move move = moves.get(i);
|
|
final Consumer<Float> moveConsumer = consumers.get(i);
|
|
final PieceEntity pEntity = pEntities.get(i);
|
|
|
|
this.window.removeRegularTask(moveConsumer);
|
|
|
|
Vector2f pieceDestBoardPos = DDDPlacement.coordinatesToVector(move.getFinish());
|
|
Vector3f pieceDestWorldPos = new Vector3f(pieceDestBoardPos.x(), 0, pieceDestBoardPos.y());
|
|
|
|
final PieceEntity pDead = this.world.getPiece(move.getDeadPieceCoords());
|
|
this.world.setPieceCoords(null, move.getDeadPieceCoords());
|
|
|
|
// we must do that on the rendering thread to avoid
|
|
// ConcurrentModificationException
|
|
this.window.scheduleTask(() -> this.world.ejectPiece(pDead));
|
|
|
|
pEntity.setPosition(pieceDestWorldPos);
|
|
|
|
this.world.movePiece(pEntity, move);
|
|
}
|
|
|
|
}
|
|
|
|
@Override
|
|
public void onMove(Move move, boolean captured) {
|
|
move3DPieces(List.of(move));
|
|
}
|
|
|
|
/**
|
|
* Rotate the camera. Calles in a loop to animate the rotation.
|
|
* @param delta the proportion of the animation already done
|
|
*/
|
|
private void cameraTick(float delta) {
|
|
int oddAnimationTurn = (2 * (animationTurns - 1)) + 1;
|
|
final float angle = (float) Math.PI;
|
|
this.camera.setRotateAngle(this.camera.getRotateAngle() + angle * delta * oddAnimationTurn / animationTime);
|
|
}
|
|
|
|
/**
|
|
* Rotate the camera so the current player faces his enemy's pieces.
|
|
*/
|
|
private void cameraRotate() {
|
|
float end = this.camera.getRotateAngle() + (float) Math.PI;
|
|
Consumer<Float> rotationConsumer = this::cameraTick;
|
|
this.window.addRegularTask(rotationConsumer);
|
|
try {
|
|
Thread.sleep((long) (animationTime * 1000.0f));
|
|
} catch (InterruptedException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
this.window.removeRegularTask(rotationConsumer);
|
|
this.camera.setRotateAngle(end);
|
|
}
|
|
|
|
@Override
|
|
public void onPlayerTurn(Color color, boolean undone) {
|
|
cameraRotate();
|
|
}
|
|
|
|
/**
|
|
* Run the game.
|
|
*/
|
|
public void run() {
|
|
this.window.run();
|
|
|
|
// free OpenGL resources
|
|
try {
|
|
this.window.close();
|
|
this.world.close();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a popup with the given title and content.
|
|
* @param title
|
|
* @param content
|
|
*/
|
|
private void renderPopup(String title, Runnable content) {
|
|
ImVec2 center = ImGui.getMainViewport().getCenter();
|
|
ImGui.setNextWindowPos(center, ImGuiCond.Appearing, new ImVec2(0.5f, 0.5f));
|
|
if (ImGui.beginPopupModal(title, null, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove)) {
|
|
content.run();
|
|
if (ImGui.button("Close")) {
|
|
closeCurrentPopup();
|
|
}
|
|
ImGui.endPopup();
|
|
}
|
|
}
|
|
|
|
private void closeCurrentPopup() {
|
|
ImGui.closeCurrentPopup();
|
|
synchronized (this) {
|
|
notifyAll();
|
|
}
|
|
}
|
|
|
|
private void renderPopup(String title, String text) {
|
|
renderPopup(title, () -> ImGui.text(text));
|
|
}
|
|
|
|
/**
|
|
* Open the promotion dialog.
|
|
*/
|
|
private void renderPromoteDialog() {
|
|
renderPopup("Promotion", () -> {
|
|
ImGui.text("Select the promotion type :");
|
|
for (PromoteType promoteType : PromoteType.values()) {
|
|
if (ImGui.button(promoteType.toString())) {
|
|
sendPawnPromotion(promoteType);
|
|
closeCurrentPopup();
|
|
}
|
|
ImGui.sameLine();
|
|
}
|
|
ImGui.newLine();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* List of possible popups.
|
|
*/
|
|
private void renderPopups() {
|
|
renderPopup("Check", "Your king is in check");
|
|
renderPopup("Checkmate", "Checkmate, it's a win!");
|
|
renderPromoteDialog();
|
|
renderPopup("Pat", "It's a pat!");
|
|
renderPopup("Tie", "It's a tie!");
|
|
renderPopup("White surrender", "The white player has surrendered!");
|
|
renderPopup("Black surrender", "The black player has surrendered!");
|
|
renderPopup("White victory", "The white player has won !");
|
|
renderPopup("Black victory", "The black player has won !");
|
|
renderPopup("End", "End of the game, thank you for playing!");
|
|
}
|
|
|
|
/**
|
|
* Open the popup identified by the given title and block the current thread until the popup is closed.
|
|
* @param title the title of the popup to open
|
|
*/
|
|
private void openPopup(String title) {
|
|
this.waitingPopup = title;
|
|
// block the current thread until the popup is closed
|
|
synchronized (this) {
|
|
try {
|
|
wait();
|
|
} catch (InterruptedException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onKingInCheck() {
|
|
openPopup("Check");
|
|
}
|
|
|
|
@Override
|
|
public void onDraw() {
|
|
openPopup("Tie");
|
|
}
|
|
|
|
@Override
|
|
public void onKingInMat() {
|
|
openPopup("Checkmate");
|
|
}
|
|
|
|
@Override
|
|
public void onGameEnd() {
|
|
openPopup("End");
|
|
this.window.stop();
|
|
}
|
|
|
|
@Override
|
|
public void onWin(Color color) {
|
|
openPopup(color == Color.White ? "Black victory" : "White victory");
|
|
}
|
|
|
|
@Override
|
|
public void onPatSituation() {
|
|
openPopup("Pat");
|
|
}
|
|
|
|
@Override
|
|
public void onSurrender(Color color) {
|
|
openPopup(color == Color.White ? "White surrender" : "Black surrender");
|
|
}
|
|
|
|
@Override
|
|
public void onCastling(boolean bigCastling, Move kingMove, Move rookMove) {
|
|
move3DPieces(List.of(kingMove, rookMove));
|
|
}
|
|
|
|
@Override
|
|
public void onPromotePawn(Coordinate pieceCoords) {
|
|
openPopup("Promotion");
|
|
}
|
|
|
|
/**
|
|
* Update the view with the promoted piece
|
|
*/
|
|
@Override
|
|
public void onPawnPromoted(PromoteType promotion, Coordinate coordinate) {
|
|
this.window.scheduleTask(() -> {
|
|
this.world.ejectPiece(this.world.getPiece(coordinate));
|
|
try {
|
|
this.world.addPiece(createDefault(getPieceAt(coordinate), coordinate), coordinate);
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
});
|
|
}
|
|
}
|