package io.github.lonamiwebs.klooni.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import io.github.lonamiwebs.klooni.Klooni;
import io.github.lonamiwebs.klooni.serializer.BinSerializable;
// A holder of pieces that can be drawn on screen.
// Pieces can be picked up from it and dropped on a board.
public class PieceHolder implements BinSerializable {
//region Members
final Rectangle area;
private final Piece[] pieces;
private final Sound pieceDropSound;
private final Sound invalidPieceDropSound;
private final Sound takePiecesSound;
// Count of pieces to be shown
private final int count;
// Currently held piece index (picked by the user)
private int heldPiece;
public boolean enabled;
// Needed after a piece is dropped, so it can go back
private final Rectangle[] originalPositions;
// The size the cells will adopt once picked
private final float pickedCellSize;
// Every piece holder belongs to a specific board
private final Board board;
//endregion
//region Static members
public static final float DRAG_SPEED = 0.5f; // Interpolation value ((pos -> new) / frame)
//endregion
//region Constructor
public PieceHolder(final GameLayout layout, final Board board,
final int pieceCount, final float pickedCellSize) {
this.board = board;
enabled = true;
count = pieceCount;
pieces = new Piece[count];
originalPositions = new Rectangle[count];
pieceDropSound = Gdx.audio.newSound(Gdx.files.internal("sound/piece_drop.mp3"));
invalidPieceDropSound = Gdx.audio.newSound(Gdx.files.internal("sound/invalid_drop.mp3"));
takePiecesSound = Gdx.audio.newSound(Gdx.files.internal("sound/take_pieces.mp3"));
heldPiece = -1;
this.pickedCellSize = pickedCellSize;
area = new Rectangle();
layout.update(this);
// takeMore depends on the layout to be ready
// TODO So, how would pieces handle a layout update?
takeMore();
}
//endregion
//region Private methods
// Determines whether all the pieces have been put (and the "hand" is finished)
private boolean handFinished() {
for (int i = 0; i < count; ++i)
if (pieces[i] != null)
return false;
return true;
}
// Takes a new set of pieces. Should be called when there are no more piece left
private void takeMore() {
for (int i = 0; i < count; ++i)
pieces[i] = Piece.random();
updatePiecesStartLocation();
if (Klooni.soundsEnabled()) {
// Random pitch so it's not always the same sound
takePiecesSound.play(1, MathUtils.random(0.8f, 1.2f), 0);
}
}
private void updatePiecesStartLocation() {
float perPieceWidth = area.width / count;
Piece piece;
for (int i = 0; i < count; ++i) {
piece = pieces[i];
if (piece == null)
continue;
// Set the absolute position on screen and the cells' cellSize
// Also clamp the cell size to be the picked size as maximum, or
// it would be too big in some cases.
piece.pos.set(area.x + i * perPieceWidth, area.y);
piece.cellSize = Math.min(Math.min(
perPieceWidth / piece.cellCols,
area.height / piece.cellRows), pickedCellSize);
// Center the piece on the X and Y axes. For this we see how
// much up we can go, this is, (area.height - piece.height) / 2
Rectangle rectangle = piece.getRectangle();
piece.pos.y += (area.height - rectangle.height) * 0.5f;
piece.pos.x += (perPieceWidth - rectangle.width) * 0.5f;
originalPositions[i] = new Rectangle(
piece.pos.x, piece.pos.y,
piece.cellSize, piece.cellSize);
// Now that we have the original positions, reset the size so it animates and grows
piece.cellSize = 0f;
}
}
//endregion
//region Public methods
// Picks the piece below the finger/mouse, returning true if any was picked
public boolean pickPiece() {
Vector2 mouse = new Vector2(
Gdx.input.getX(),
Gdx.graphics.getHeight() - Gdx.input.getY()); // Y axis is inverted
final float perPieceWidth = area.width / count;
for (int i = 0; i < count; ++i) {
if (pieces[i] != null) {
Rectangle maxPieceArea = new Rectangle(
area.x + i * perPieceWidth, area.y, perPieceWidth, area.height);
if (maxPieceArea.contains(mouse)) {
heldPiece = i;
return true;
}
}
}
heldPiece = -1;
return false;
}
public Array<Piece> getAvailablePieces() {
Array<Piece> result = new Array<Piece>(count);
for (int i = 0; i < count; ++i)
if (pieces[i] != null)
result.add(pieces[i]);
return result;
}
// If no piece is currently being held, the area will be 0
public int calculateHeldPieceArea() {
return heldPiece > -1 ? pieces[heldPiece].calculateArea() : 0;
}
public Vector2 calculateHeldPieceCenter() {
return heldPiece > -1 ? pieces[heldPiece].calculateGravityCenter() : null;
}
// Tries to drop the piece on the given board. As a result, it
// returns one of the following: NO_DROP, NORMAL_DROP, ON_BOARD_DROP
public DropResult dropPiece() {
DropResult result;
if (heldPiece > -1) {
boolean put;
put = enabled && board.putScreenPiece(pieces[heldPiece]);
if (put) {
if (Klooni.soundsEnabled()) {
// The larger the piece size, the smaller the pitch
// Considering 10 cells to be the largest, 1.1 highest pitch, 0.7 lowest
float pitch = 1.104f - pieces[heldPiece].calculateArea() * 0.04f;
pieceDropSound.play(1, pitch, 0);
}
result = new DropResult(calculateHeldPieceArea(), calculateHeldPieceCenter());
pieces[heldPiece] = null;
} else {
if (Klooni.soundsEnabled())
invalidPieceDropSound.play();
result = new DropResult(true);
}
heldPiece = -1;
if (handFinished())
takeMore();
} else
result = new DropResult(false);
return result;
}
// Updates the state of the piece holder (and the held piece)
public void update() {
Piece piece;
if (heldPiece > -1) {
piece = pieces[heldPiece];
Vector2 mouse = new Vector2(
Gdx.input.getX(),
Gdx.graphics.getHeight() - Gdx.input.getY()); // Y axis is inverted
if (Klooni.onDesktop) { //FIXME(oliver): This is a bad assumption to make. There are desktops with touch input and non-desktops with mouse input.
// Center the piece to the mouse
mouse.sub(piece.getRectangle().width * 0.5f, piece.getRectangle().height * 0.5f);
} else {
// Center the new piece position horizontally
// and push it up by it's a cell (arbitrary) vertically, thus
// avoiding to cover it with the finger (issue on Android devices)
mouse.sub(piece.getRectangle().width * 0.5f, -pickedCellSize);
}
if (Klooni.shouldSnapToGrid())
mouse.set(board.snapToGrid(piece, mouse));
piece.pos.lerp(mouse, DRAG_SPEED);
piece.cellSize = Interpolation.linear.apply(piece.cellSize, pickedCellSize, DRAG_SPEED);
}
// Return the pieces to their original position
// TODO This seems somewhat expensive, can't it be done any better?
Rectangle original;
for (int i = 0; i < count; ++i) {
if (i == heldPiece)
continue;
piece = pieces[i];
if (piece == null)
continue;
original = originalPositions[i];
piece.pos.lerp(new Vector2(original.x, original.y), 0.3f);
piece.cellSize = Interpolation.linear.apply(piece.cellSize, original.width, 0.3f);
}
}
public void draw(SpriteBatch batch) {
for (int i = 0; i < count; ++i) {
if (pieces[i] != null) {
pieces[i].draw(batch);
}
}
}
//endregion
//region Serialization
@Override
public void write(DataOutputStream out) throws IOException {
// Piece count, false if piece == null, true + piece if piece != null
out.writeInt(count);
for (int i = 0; i < count; ++i) {
if (pieces[i] == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
pieces[i].write(out);
}
}
}
@Override
public void read(DataInputStream in) throws IOException {
// If the saved piece count does not match the current piece count,
// then an IOException is thrown since the data saved was invalid
final int savedPieceCount = in.readInt();
if (savedPieceCount != count)
throw new IOException("Invalid piece count saved.");
for (int i = 0; i < count; i++)
pieces[i] = in.readBoolean() ? Piece.read(in) : null;
updatePiecesStartLocation();
}
//endregion
//region Sub-classes
public class DropResult {
public final boolean dropped;
public final boolean onBoard;
public final int area;
public final Vector2 pieceCenter;
DropResult(final boolean dropped) {
this.dropped = dropped;
onBoard = false;
area = 0;
pieceCenter = null;
}
DropResult(final int area, final Vector2 pieceCenter) {
dropped = onBoard = true;
this.area = area;
this.pieceCenter = pieceCenter;
}
}
//endregion
}