com.threerings.puzzle.drop.client.DropBoardView Maven / Gradle / Ivy
The newest version!
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.puzzle.drop.client;
import java.util.Iterator;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import com.threerings.media.image.Mirage;
import com.threerings.media.sprite.ImageSprite;
import com.threerings.media.sprite.PathAdapter;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.util.LinePath;
import com.threerings.media.util.Path;
import com.threerings.parlor.game.data.GameConfig;
import com.threerings.parlor.media.ScoreAnimation;
import com.threerings.puzzle.client.PuzzleBoardView;
import com.threerings.puzzle.data.Board;
import com.threerings.puzzle.drop.data.DropBoard;
import com.threerings.puzzle.drop.data.DropConfig;
import com.threerings.puzzle.drop.data.DropPieceCodes;
import com.threerings.puzzle.util.PuzzleContext;
import static com.threerings.puzzle.Log.log;
/**
* The drop board view displays a drop puzzle game in progress for a single player.
*/
public abstract class DropBoardView extends PuzzleBoardView
implements DropPieceCodes
{
/** The color used to render normal scoring text. */
public static final Color SCORE_COLOR = Color.white;
/** The color used to render chain reward scoring text. */
public static final Color CHAIN_COLOR = Color.yellow;
/**
* Constructs a drop board view.
*/
public DropBoardView (PuzzleContext ctx, int pwid, int phei)
{
super(ctx);
// save off piece dimensions
_pwid = pwid;
_phei = phei;
// determine distance to float score animations
_scoreDist = 2 * _phei;
}
/**
* Initializes the board with the board dimensions.
*/
@Override
public void init (GameConfig config)
{
DropConfig dconfig = (DropConfig)config;
// save off the board dimensions in pieces
_bwid = dconfig.getBoardWidth();
_bhei = dconfig.getBoardHeight();
super.init(config);
}
/**
* Returns the width in pixels of a single board piece.
*/
public int getPieceWidth ()
{
return _pwid;
}
/**
* Returns the height in pixels of a single board piece.
*/
public int getPieceHeight ()
{
return _phei;
}
/**
* Called by the {@link DropSprite} to populate pos
with the screen coordinates
* in pixels at which a piece at (col, row)
in the board should be drawn. Derived
* classes may wish to override this method to allow specialized positioning of sprites.
*/
public void getPiecePosition (int col, int row, Point pos)
{
pos.setLocation(col * _pwid, (row * _phei) - _roff);
}
/**
* Called by the {@link DropSprite} to get the dimensions of the area that will be occupied by
* rendering a piece segment of the given orientation and length whose bottom-leftmost corner
* is at (col, row)
.
*/
public Dimension getPieceSegmentSize (int col, int row, int orient, int len)
{
if (orient == NORTH || orient == SOUTH) {
return new Dimension(_pwid, len * _phei);
} else {
return new Dimension(len * _pwid, _phei);
}
}
/**
* Creates a new piece sprite and places it directly in its correct position.
*/
public void createPiece (int piece, int sx, int sy)
{
if (!inBounds(sx, sy)) {
log.warning("Requested to create piece in invalid location", "sx", sx, "sy", sy,
new Exception());
return;
}
createPiece(piece, sx, sy, sx, sy, 0L);
}
/**
* Refreshes the piece sprite at the specified location, if no sprite exists at the location,
* one will be created. Note: this method assumes the default {@link ImageSprite} is
* being used to display pieces. If {@link #createPieceSprite} is overridden to return a
* non-ImageSprite, this method must also be customized.
*/
public void updatePiece (int sx, int sy)
{
updatePiece(_dboard.getPiece(sx, sy), sx, sy);
}
/**
* Updates the piece sprite at the specified location, if no sprite exists at the location,
* one will be created. Note: this method assumes the default {@link ImageSprite} is
* being used to display pieces. If {@link #createPieceSprite} is overridden to return a
* non-ImageSprite, this method must also be customized.
*/
public void updatePiece (int piece, int sx, int sy)
{
if (!inBounds(sx, sy)) {
log.warning("Requested to update piece in invalid location",
"sx", sx, "sy", sy, new Exception());
return;
}
int spos = sy * _bwid + sx;
if (_pieces[spos] != null) {
((ImageSprite)_pieces[spos]).setMirage(getPieceImage(piece, sx, sy, NORTH));
} else {
createPiece(piece, sx, sy);
}
}
/**
* Creates a new piece sprite and moves it into position on the board.
*/
public void createPiece (int piece, int sx, int sy, int tx, int ty, long duration)
{
if (!inBounds(tx, ty)) {
log.warning("Requested to create and move piece to invalid location",
"tx", tx, "ty", ty, new Exception());
return;
}
Sprite sprite = createPieceSprite(piece, sx, sy);
if (sprite != null) {
// position the piece properly to start
Point start = new Point();
getPiecePosition(sx, sy, start);
sprite.setLocation(start.x, start.y);
// now add it to the view
addSprite(sprite);
// and potentially move it into place
movePiece(sprite, sx, sy, tx, ty, duration);
}
}
/**
* Instructs the view to move the piece at the specified starting position to the specified
* destination position. There must be a sprite at the starting position, if there is a sprite
* at the destination position, it must also be moved immediately following this call (as in
* the case of a swap) to avoid badness.
*
* @return the piece sprite that is being moved.
*/
public Sprite movePiece (int sx, int sy, int tx, int ty, long duration)
{
int spos = sy * _bwid + sx;
Sprite piece = _pieces[spos];
if (piece == null) {
log.warning("Missing source sprite for drop", "sx", sx, "sy", sy, "tx", tx, "ty", ty);
return null;
}
_pieces[spos] = null;
movePiece(piece, sx, sy, tx, ty, duration);
return piece;
}
/**
* A helper function for moving pieces into place.
*/
protected void movePiece (
Sprite piece, final int sx, final int sy, final int tx, final int ty, long duration)
{
final Exception where = new Exception();
// if the sprite needn't move, then just position it and be done
Point start = new Point();
getPiecePosition(sx, sy, start);
if (sx == tx && sy == ty) {
int tpos = ty * _bwid + tx;
if (_pieces[tpos] != null) {
log.warning("Zoiks! Asked to add a piece where we already have one",
"sx", sx, "sy", sy, "tx", tx, "ty", ty, where);
}
_pieces[tpos] = piece;
piece.setLocation(start.x, start.y);
return;
}
// note that this piece is moving toward its destination
final int tpos = ty * _bwid + tx;
_moving[tpos] = true;
// then create a path and do some bits
Point end = new Point();
getPiecePosition(tx, ty, end);
piece.addSpriteObserver(new PathAdapter() {
@Override
public void pathCompleted (Sprite sprite, Path path, long when) {
sprite.removeSpriteObserver(this);
if (_pieces[tpos] != null) {
log.warning("Oh god, we're dropping onto another piece",
"sx", sx, "sy", sy, "tx", tx + "ty", ty, where);
return;
}
_pieces[tpos] = sprite;
if (_actionSprites.remove(sprite)) {
maybeFireCleared();
}
pieceArrived(when, sprite, tx, ty);
}
});
_actionSprites.add(piece);
piece.move(new LinePath(start, end, duration));
}
/**
* Called when a piece is finished moving into its requested position. Derived classes may
* wish to take this opportunity to play a sound or whatnot.
*/
protected void pieceArrived (long tickStamp, Sprite sprite, int px, int py)
{
_moving[py * _bwid + px] = false;
}
/**
* Returns true if the piece that is supposed to be at the specified coordinates is not yet
* there but is actually en route to that location (due to a call to
* {@link #movePiece(Sprite,int,int,int,int,long)}).
*/
protected boolean isMoving (int px, int py)
{
return _moving[py * _bwid + px];
}
/**
* Returns the image used to display the given piece at coordinates (0, 0)
with
* an orientation of {@link #NORTH}. This serves as a convenience routine for those puzzles
* that don't bother rendering their pieces differently when placed at different board
* coordinates or in different orientations.
*/
public Mirage getPieceImage (int piece)
{
return getPieceImage(piece, 0, 0, NORTH);
}
/**
* Returns the image used to display the given piece at the specified column and row with the
* given orientation.
*/
public abstract Mirage getPieceImage (int piece, int col, int row, int orient);
@Override
public void setBoard (Board board)
{
// when a new board arrives, we want to remove all drop sprites
// so that they don't modify the new board with their old ideas
for (Iterator iter = _actionSprites.iterator(); iter.hasNext(); ) {
Sprite s = iter.next();
if (s instanceof DropSprite) {
// remove it from _sprites safely
iter.remove();
// but then use the standard removal method
removeSprite(s);
}
}
// remove all of this board's piece sprites
int pcount = (_pieces == null) ? 0 : _pieces.length;
for (int ii = 0; ii < pcount; ii++) {
if (_pieces[ii] != null) {
removeSprite(_pieces[ii]);
}
}
super.setBoard(board);
_dboard = (DropBoard)board;
// create the pieces for the new board
Point spos = new Point();
int width = _dboard.getWidth(), height = _dboard.getHeight();
_pieces = new Sprite[width * height];
for (int yy = 0; yy < height; yy++) {
for (int xx = 0; xx < width; xx++) {
Sprite piece = createPieceSprite(_dboard.getPiece(xx, yy), xx, yy);
if (piece != null) {
int ppos = yy * width + xx;
getPiecePosition(xx, yy, spos);
piece.setLocation(spos.x, spos.y);
addSprite(piece);
_pieces[ppos] = piece;
}
}
}
// we use this to track when pieces are animating toward their new
// position and are not yet in place
_moving = new boolean[width * height];
}
/**
* Returns the piece sprite at the specified location.
*/
public Sprite getPieceSprite (int xx, int yy)
{
return _pieces[yy * _dboard.getWidth() + xx];
}
/**
* Clears the specified piece from the board.
*/
public void clearPieceSprite (int xx, int yy)
{
int ppos = yy * _dboard.getWidth() + xx;
if (_pieces[ppos] != null) {
removeSprite(_pieces[ppos]);
_pieces[ppos] = null;
}
}
/**
* Clears out a piece from the board along with its piece sprite.
*/
public void clearPiece (int xx, int yy)
{
_dboard.setPiece(xx, yy, PIECE_NONE);
clearPieceSprite(xx, yy);
}
/**
* Creates a new drop sprite used to animate the given pieces falling in the specified column.
*/
public DropSprite createPieces (int col, int row, int[] pieces, int dist)
{
return new DropSprite(this, col, row, pieces, dist);
}
/**
* Dirties the rectangle encompassing the segment with the given direction and length whose
* bottom-leftmost corner is at (col, row)
.
*/
public void dirtySegment (int dir, int col, int row, int len)
{
int x = _pwid * col, y = (_phei * row) - _roff;
int wid = (dir == VERTICAL) ? _pwid : len * _pwid;
int hei = (dir == VERTICAL) ? _phei * len : _phei;
_remgr.invalidateRegion(x, y, wid, hei);
}
/**
* Creates and returns an animation showing the specified score floating up the view, with the
* label initially centered within the view.
*
* @param score the score text to display.
* @param color the color of the text.
* @param font the font.
*/
public ScoreAnimation createScoreAnimation (String score, Color color, Font font)
{
return createScoreAnimation(score, color, font, 0, _bhei - 1, _bwid, _bhei);
}
/**
* Creates and returns an animation showing the specified score floating up the view.
*
* @param score the score text to display.
* @param color the color of the text.
* @param font the font to use.
* @param x the left coordinate in board coordinates of the rectangle within which the score
* is to be centered.
* @param y the bottom coordinate in board coordinates of the rectangle within which the score
* is to be centered.
* @param width the width in board coordinates of the rectangle within which the score is to
* be centered.
* @param height the height in board coordinates of the rectangle within which the score is to
* be centered.
*/
public ScoreAnimation createScoreAnimation (
String score, Color color, Font font, int x, int y, int width, int height)
{
// create the score animation
ScoreAnimation anim = createScoreAnimation(score, color, font, x, y);
// position the label within the specified rectangle
Dimension lsize = anim.getLabel().getSize();
Point pos = new Point();
centerRectInBoardRect(x, y, width, height, lsize.width, lsize.height, pos);
anim.setLocation(pos.x, pos.y);
return anim;
}
/**
* Creates the sprite that is used to display the specified piece. If the piece represents no
* piece, this method should return null.
*/
protected Sprite createPieceSprite (int piece, int px, int py)
{
if (piece == PIECE_NONE) {
return null;
}
ImageSprite sprite = new ImageSprite(getPieceImage(piece, px, py, NORTH));
sprite.setRenderOrder(-1);
return sprite;
}
/**
* Populates pos
with the most appropriate screen coordinates to center a
* rectangle of the given width and height (in pixels) within the specified rectangle (in
* board coordinates).
*
* @param bx the bounding rectangle's left board coordinate.
* @param by the bounding rectangle's bottom board coordinate.
* @param bwid the bounding rectangle's width in board coordinates.
* @param bhei the bounding rectangle's height in board coordinates.
* @param rwid the width of the rectangle to position in pixels.
* @param rhei the height of the rectangle to position in pixels.
* @param pos the point to populate with the rectangle's final position.
*/
protected void centerRectInBoardRect (
int bx, int by, int bwid, int bhei, int rwid, int rhei, Point pos)
{
getPiecePosition(bx, by + 1, pos);
pos.x += (((bwid * _pwid) - rwid) / 2);
pos.y -= ((((bhei * _phei) - rhei) / 2) + rhei);
// constrain to fit wholly within the board bounds
pos.x = Math.max(Math.min(pos.x, _bounds.width - rwid), 0);
}
/**
* Rotates the given drop block sprite to the specified orientation, updating the image as
* necessary. Derived classes that make use of block dropping functionality should override
* this method to do the right thing.
*/
public void rotateDropBlock (DropBlockSprite sprite, int orient)
{
// nothing for now
}
@Override
public void paintBetween (Graphics2D gfx, Rectangle dirtyRect)
{
gfx.translate(0, -_roff);
renderBoard(gfx, dirtyRect);
renderRisingPieces(gfx, dirtyRect);
gfx.translate(0, _roff);
}
@Override
public Dimension getPreferredSize ()
{
int wid = _bwid * _pwid;
int hei = _bhei * _phei;
return new Dimension(wid, hei);
}
/**
* Returns true if the specified coordinate is within the bounds of the board, false if it is
* not.
*/
public boolean inBounds (int col, int row)
{
return (col >= 0 && row >= 0 && col < getWidth() && row < getHeight());
}
/**
* Renders the row of rising pieces to the given graphics context. Sub-classes that make use
* of board rising functionality should override this method to draw the rising piece row.
*/
protected void renderRisingPieces (Graphics2D gfx, Rectangle dirtyRect)
{
// nothing for now
}
/**
* Sets the board rising offset to the given y-position.
*/
protected void setRiseOffset (int y)
{
if (y != _roff) {
_roff = y;
_remgr.invalidateRegion(_bounds);
}
}
/** The drop board. */
protected DropBoard _dboard;
/** A sprite for every piece displayed in the drop board. */
protected Sprite[] _pieces;
/** Indicates whether a piece is en route to its destination. */
protected boolean[] _moving;
/** The piece dimensions in pixels. */
protected int _pwid, _phei;
/** The board rising offset. */
protected int _roff;
/** The board dimensions in pieces. */
protected int _bwid, _bhei;
}