All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.puzzle.drop.client.DropSprite 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.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 its at rest state.
     */
    public void setBouncing (boolean bouncing)
    {
        _bouncing = bouncing;
        if (_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 };
}