// // $Id$ // // Vilya library - tools for developing networked games // Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/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.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Shape; import com.samskivert.util.ObserverList; import com.threerings.util.DirectionUtil; import com.threerings.media.image.Mirage; import com.threerings.media.sprite.Sprite; /** * The drop sprite is a sprite that displays one or more pieces falling * toward the bottom of the board. */ public class DropSprite extends Sprite { /** * Constructs a drop sprite and starts it dropping. * * @param view the board view upon which this sprite will be displayed. * @param col the column of the sprite. * @param row the row of the bottom-most piece. * @param pieces the pieces displayed by the sprite. * @param dist the distance the sprite is to drop in rows. */ public DropSprite ( DropBoardView view, int col, int row, int[] pieces, int dist) { this(view, col, row, pieces, dist, -1); } /** * Constructs a drop sprite and starts it dropping. * * @param view the board view upon which this sprite will be displayed. * @param col the column of the sprite. * @param row the row of the bottom-most piece. * @param pieces the pieces displayed by the sprite. * @param dist the distance the sprite is to drop in rows. * @param renderOrder the render order. */ public DropSprite ( DropBoardView view, int col, int row, int[] pieces, int dist, int renderOrder) { _view = view; _col = col; _row = row; _pieces = pieces; _dist = (dist == 0) ? 1 : dist; _orient = NORTH; _unit = _view.getPieceHeight(); setRenderOrder(renderOrder); } @Override protected void init () { super.init(); // size the bounds to fit our pieces updateBounds(); // set up the piece location setBoardLocation(_row, _col); // calculate vertical render offset based on the number of pieces setRowOffset(-(_pieces.length - 1)); } /** * Returns the remaining number of columns to drop. */ public int getDistance () { return _dist; } /** * Returns the column the piece is located in. */ public int getColumn () { return _col; } /** * Returns the row the piece is located in. */ public int getRow () { return _row; } /** * Returns the pieces the sprite is displaying. */ public int[] getPieces () { return _pieces; } /** * Returns the velocity of this sprite. */ public float getVelocity () { return _vel; } /** * Sets the row and column the piece is located in. */ public void setBoardLocation (int row, int col) { _row = row; _col = col; updatePosition(); } /** * Sets the column the piece is located in. */ public void setColumn (int col) { _col = col; updatePosition(); } /** * Set the row the piece is located in. */ public void setRow (int row) { _row = row; updatePosition(); } /** * Sets the column offset of the sprite image. */ public void setColumnOffset (int count) { _offx = count; updateRenderOffset(); updateRenderOrigin(); } /** * Sets the row offset of the sprite image. */ public void setRowOffset (int count) { _offy = count; updateRenderOffset(); updateRenderOrigin(); } /** * Sets the pieces the sprite is displaying. */ public void setPieces (int[] pieces) { _pieces = pieces; } /** * Sets the velocity of this sprite. The time at which the current * row was entered is modified so that the sprite position will remain * the same when calculated using the new velocity since the piece * sprite may have its velocity modified in the middle of a row * traversal. */ public void setVelocity (float velocity) { // bail if we've already got the requested velocity if (_vel == velocity) { return; } if (_rowstamp > 0) { // get our current distance along the row long now = _view.getTimeStamp(); float pctdone = getPercentDone(now); // revise the current row entry time to account for the new velocity float travpix = pctdone * _unit; long msecs = (long)(travpix / velocity); _rowstamp = now - msecs; } // update the velocity _vel = velocity; } /** * Starts the piece dropping toward the next row. */ public void drop () { // Log.info("Dropping piece [piece=" + this + "]."); // drop one row by default if (_dist <= 0) { _dist = 1; } if (_stopstamp > 0) { // we're dropping from a stand-still long delta = _view.getTimeStamp() - _stopstamp; _rowstamp += delta; _stopstamp = 0; } else { // we're continuing a previous drop, so make use of any // previously existing time _rowstamp = _endstamp; } } /** * Returns true if this drop sprite is dropping, false if it has been * {@link #stop}ped or has not yet been {@link #drop}ped. */ public boolean isDropping () { return (_stopstamp == 0) && (_rowstamp != 0); } /** * Stops the piece from dropping. */ public void stop () { if (_stopstamp == 0) { _stopstamp = _view.getTimeStamp(); // Log.info("Stopped piece [piece=" + this + "]."); } } /** * Puts the drop sprite into (or takes it out of) bouncing * mode. Bouncing mode is used to put the sprite into limbo after it * lands but before we commit the landing, giving the user a last * moment change move or rotate the piece. While the sprite is * "bouncing" it will be rendered one pixel below it's at rest state. */ public void setBouncing (boolean bouncing) { if (_bouncing = bouncing) { // if we've activated bouncing, shift the sprite slightly to // illustrate its new state shiftForBounce(); // to prevent funny business in the event that we were a long // ways past the end of the row when we landed, we warp the // sprite back to the exact point of landing for the purposes // of the bounce and any subsequent antics _endstamp = _rowstamp = _view.getTimeStamp(); // Log.info("Adjusted rowstap due to bounce " + // "[time=" + _endstamp + "]."); } } /** * Returns true if this sprite is bouncing. */ public boolean isBouncing () { return _bouncing; } /** * Updates the sprite's location to illustrate that it is currently in * the "bouncing" state. */ protected void shiftForBounce () { setLocation(_ox, _srcPos.y+1); } @Override public boolean inside (Shape shape) { return shape.contains(_bounds); } /** * Returns a value between 0.0 and 1.0 * representing how far the piece has moved toward the next row * as of the given time stamp. */ public float getPercentDone (long timestamp) { // if we've never been ticked and so haven't yet initialized our // row start timestamp, just let the caller know that we've not // traversed our row at all if (_rowstamp == 0) { return 0.0f; } long msecs = Math.max(0, timestamp - _rowstamp); float travpix = msecs * _vel; float pctdone = (travpix / _unit); // Log.info("getPercentDone [timestamp=" + timestamp + // ", rowstamp=" + _rowstamp + ", msecs=" + msecs + // ", travpix=" + travpix + ", pctdone=" + pctdone + // ", vel=" + _vel + "]."); return pctdone; } @Override public void paint (Graphics2D gfx) { // get the column and row increment based on the sprite's orientation int oidx = _orient/2; int incx = ORIENT_DX[oidx]; int incy = ORIENT_DY[oidx]; // determine offset from the start of each actual row and column int dx = _ox - _srcPos.x, dy = _oy - _srcPos.y; int pcol = _col, prow = _row; for (int ii = 0; ii < _pieces.length; ii++) { // ask the board for the render position of this piece _view.getPiecePosition(pcol, prow, _renderPos); // draw the piece image paintPieceImage(gfx, ii, pcol, prow, _orient, _renderPos.x + dx, _renderPos.y + dy); // increment the target column and row pcol += incx; prow += incy; } } /** * Paints the specified piece with the supplied parameters. */ protected void paintPieceImage (Graphics2D gfx, int pieceidx, int col, int row, int orient, int x, int y) { Mirage image = _view.getPieceImage(_pieces[pieceidx], col, row, orient); image.paint(gfx, x, y); } @Override public void tick (long timestamp) { super.tick(timestamp); // initialize our rowstamp if we haven't done so already if (_rowstamp == 0) { _rowstamp = timestamp; } // if we're bouncing or paused, do nothing here if (_bouncing || _stopstamp > 0) { return; } PieceMovedOp pmop = null; // figure out how far along the current board coordinate we should be float pctdone = getPercentDone(timestamp); if (pctdone >= 1.0f) { // note that we've reached the next row advancePosition(); // update remaining drop distance _dist--; // calculate any remaining time to be used long used = (long)(_unit / _vel); _endstamp = _rowstamp + used; _rowstamp = _endstamp; // update our percent done because we've moved down a row pctdone -= 1.0; // inform observers that we've reached our destination pmop = new PieceMovedOp(this, timestamp, _col, _row); } // constrain the sprite's position to the destination row pctdone = Math.min(pctdone, 1.0f); // calculate the latest sprite position int nx = _srcPos.x + (int)((_destPos.x - _srcPos.x) * pctdone); int ny = _srcPos.y + (int)((_destPos.y - _srcPos.y) * pctdone); // Log.info("Drop sprite tick [dist=" + _dist + ", pctdone=" + pctdone + // ", row=" + _row + ", col=" + _col + // ", nx=" + nx + ", ny=" + ny + "]."); // only update the sprite's location if it actually moved if (_ox != nx || _oy != ny) { setLocation(nx, ny); } // lastly notify our observers if we made it to the next row if (pmop != null) { _observers.apply(pmop); } } /** * Called when the sprite has finished traversing its current row to * advance its board coordinates to the next row. */ protected void advancePosition () { setRow(_row + 1); // Log.info("Moved to row " + _row); } @Override public void fastForward (long timeDelta) { if (_rowstamp > 0) { _rowstamp += timeDelta; } } @Override public void toString (StringBuilder buf) { super.toString(buf); buf.append(", orient=").append(DirectionUtil.toShortString(_orient)); buf.append(", row=").append(_row); buf.append(", col=").append(_col); buf.append(", offx=").append(_offx); buf.append(", offy=").append(_offy); buf.append(", dist=").append(_dist); } /** * Updates internal pixel coordinates used when the piece is moving. */ protected void updatePosition () { _view.getPiecePosition(_col, _row, _srcPos); _view.getPiecePosition(_col, _row+1, _destPos); setLocation(_srcPos.x, _srcPos.y); } @Override public void setOrientation (int orient) { invalidate(); super.setOrientation(orient); updateBounds(); invalidate(); } /** * Updates the bounds for this sprite based on the sprite display * dimensions in the view. */ protected void updateBounds () { Dimension size = _view.getPieceSegmentSize( _col, _row, _orient, _pieces.length); _bounds.width = size.width; _bounds.height = size.height; } /** * Adjusts our render origin such that our location is not in the * upper left of the sprite's rendered image but is in fact offset by * some number of rows and columns. */ protected void updateRenderOffset () { _oxoff = -(_view.getPieceWidth() * _offx); _oyoff = -(_view.getPieceHeight() * _offy); } /** Used to dispatch {@link DropSpriteObserver#pieceMoved}. */ protected static class PieceMovedOp implements ObserverList.ObserverOp { public PieceMovedOp (DropSprite sprite, long when, int col, int row) { _sprite = sprite; _when = when; _col = col; _row = row; } public boolean apply (Object observer) { if (observer instanceof DropSpriteObserver) { ((DropSpriteObserver)observer).pieceMoved( _sprite, _when, _col, _row); } return true; } protected DropSprite _sprite; protected long _when; protected int _col, _row; } /** The default piece velocity. */ protected static final float DEFAULT_VELOCITY = 30f/1000f; /** The time at which we started the current row. */ protected long _rowstamp; /** The time at which we reached the end of the previous row. */ protected long _endstamp; /** The time at which we were stopped en route to our next row. */ protected long _stopstamp; /** The board view upon which this sprite is displayed. */ protected DropBoardView _view; /** The unit distance the sprite moves to reach the next row. */ protected int _unit; /** The screen coordinates of the top-left of the row currently * occupied by the sprite. */ protected Point _srcPos = new Point(); /** The screen coordinates of the top-left of the row toward which the * sprite is falling. */ protected Point _destPos = new Point(); /** The piece render position; used as working data when determining * where to render each piece in the sprite. */ protected Point _renderPos = new Point(); /** The number of rows remaining to drop. */ protected int _dist; /** The piece velocity. */ protected float _vel = DEFAULT_VELOCITY; /** The offsets in columns or rows at which the piece is rendered. */ protected int _offx, _offy; /** The current piece location in the board. */ protected int _row, _col; /** The pieces this sprite is displaying. */ protected int[] _pieces; /** Indicates that the drop sprite is bouncing; see {@link * #setBouncing}. */ protected boolean _bouncing; // used to compute the column and row increment while rendering the // sprite's pieces based on its orientation // W N E S protected static final int[] ORIENT_DX = { -1, 0, 1, 0 }; protected static final int[] ORIENT_DY = { 0, -1, 0, 1 }; }