import processing.core.*; 
import processing.xml.*; 

import processing.opengl.*; 
import processing.serial.*; 
import ddf.minim.*; 

import java.applet.*; 
import java.awt.Dimension; 
import java.awt.Frame; 
import java.awt.event.MouseEvent; 
import java.awt.event.KeyEvent; 
import java.awt.event.FocusEvent; 
import java.awt.Image; 
import java.io.*; 
import java.net.*; 
import java.text.*; 
import java.util.*; 
import java.util.zip.*; 
import java.util.regex.*; 

public class BlenderBot extends PApplet {



/** Final Project Prototype. */




public void setup() {
    size(WINDOW_WIDTH, WINDOW_HEIGHT, OPENGL);
    frameRate(FRAMES_PER_SECOND);
    noStroke();
    noSmooth();
    cursor(CROSS);
    //smooth();
    
    leftArrowKeyDown = rightArrowKeyDown = shiftKeyDown = 
        upArrowKeyDown = downArrowKeyDown = false;
    aKeyDown = sKeyDown = dKeyDown = wKeyDown = pKeyDown = 
        rKeyDown = yKeyDown = bKeyDown = spaceKeyDown = false;
    
    minim = new Minim(this);
    //otherFont = loadFont("data/BitstreamVeraSans-Bold-48.vlw");
    otherFont = loadFont("data/BitstreamVeraSans-Bold-48.vlw");
    scoreFont = loadFont("data/BitstreamVeraSans-Bold-10.vlw");
    techyFont = loadFont("data/pixelino-48.vlw");
    pauseFont = loadFont("data/StayPuft-130.vlw");
    game = new Main();
}

public void draw() {
    game.update();
}

public void mousePressed() {
    updateAnchorPosition();
}

public void mouseDragged() {
    if (anchored) {
        updateAnchorPosition();
    }
}

public void updateAnchorPosition() {
    if (!pause) {
        anchored = true;
        anchorX = mouseX;
        anchorY = mouseY;
    }
}

public void mouseReleased() {
    if (!pause) {
        anchored = false;
        game.releaseAnchor();
    }
}

public void mouseClicked() {
    if (!gameIsInProgress) {
        makeSelection = true;
    }
}

public void keyPressed() {
    
    switch(key) {
    case CODED: 
        switch(keyCode) {
        case UP:
            upArrowKeyDown = true;
            break;
        case DOWN:
            downArrowKeyDown = true;
            break;
        case LEFT:
            leftArrowKeyDown = true;
            break;
        case RIGHT:
            rightArrowKeyDown = true;
            break;
        case SHIFT:
            shiftKeyDown = true;
            break;
        }
        break;
        
    case 'a':
    case 'A':
        aKeyDown = true;
        break;
    case 's':
    case 'S':
        sKeyDown = true;
        break;
    case 'd':
    case 'D':
        dKeyDown = true;
        break;
    case 'w':
    case 'W':
        wKeyDown = true;
        break;
    case 'p':
    case 'P':
        pKeyDown = true;
        if (gameIsInProgress) {
            pause = !pause;
        }
        break;
    case 'j':
    case 'J':
        jKeyDown = true;
        break;
    case 'h':
    case 'H':
        halo = !halo;
        break;
    case 'k':
    case 'K':
        kKeyDown = true;
        break;
    case 'l':
    case 'L':
        lKeyDown = true;
        break;
    case 'i':
    case 'I':
        iKeyDown = true;
        break;
    case 'r':
    case 'R':
        rKeyDown = true;
        break;
    case 'y':
    case 'Y':
        yKeyDown = true;
        break;
    case 'b':
    case 'B':
        bKeyDown = true;
        robot = !robot;
        break;
    case 'm':
    case 'M':
        playSoundEffects = !playSoundEffects;
        game.toggleMusic();
        break;
    case ' ':
        spaceKeyDown = true;
        break;
    }

}

/** Polls keys that have been released. */
public void keyReleased() {
    switch(key) {
    case CODED:
        switch(keyCode) {
        case UP:
            upArrowKeyDown = false;
            break;
        case DOWN:
            downArrowKeyDown = false;
            break;
        case LEFT:
            leftArrowKeyDown = false;
            break;
        case RIGHT:
            rightArrowKeyDown = false;
            break;
        case SHIFT:
            shiftKeyDown = false;
            break;
        }
        break;
        
    case 'a':
    case 'A':
        aKeyDown = false;
        break;
    case 's':
    case 'S':
        sKeyDown = false;
        break;
    case 'd':
    case 'D':
        dKeyDown = false;
        break;
    case 'w':
    case 'W':
        wKeyDown = false;
        break;
    case 'p':
    case 'P':
        pKeyDown = false;
        break;
    case 'j':
    case 'J':
        jKeyDown = false;
        break;
    case 'k':
    case 'K':
        kKeyDown = false;
        break;
    case 'l':
    case 'L':
        lKeyDown = false;
        break;
    case 'i':
    case 'I':
        iKeyDown = false;
        break;
    case 'r':
    case 'R':
        rKeyDown = false;
        break;
    case 'y':
    case 'Y':
        yKeyDown = false;
        break;
    case 'b':
    case 'B':
        bKeyDown = false;
        break;
    case ' ':
        spaceKeyDown = false;
        break;
    }
}


/* Keys */
boolean upArrowKeyDown, downArrowKeyDown, spaceKeyDown,
    leftArrowKeyDown, rightArrowKeyDown, shiftKeyDown, 
    aKeyDown, sKeyDown, dKeyDown, wKeyDown, pKeyDown,
    jKeyDown, kKeyDown, lKeyDown, iKeyDown, 
    rKeyDown, yKeyDown, bKeyDown;

boolean anchored;
int anchorX;
int anchorY;
    
/* System Globals */
Main game;
Minim minim;
PFont otherFont;
PFont scoreFont;
PFont techyFont;
PFont pauseFont;
boolean gameIsInProgress = false;
boolean makeSelection = false;
boolean pause = false;
boolean halo = false;
boolean robot = false;
boolean Debug = false;//true; //Prints and draws debug info.
boolean playMusic = true; //Plays music in-game.
boolean playSoundEffects = true; //Plays sound effects in-game.

/* Symbolic Constants */
static final int WINDOW_WIDTH = 1024;
static final int WINDOW_HEIGHT = 768;
static final int FRAMES_PER_SECOND = 30;
static final int PLAYER = 0;
static final int BALL = 1;
static final int FIELD = 2;
static final int PIPES = 3;
static final int LEFT = 0;
static final int RIGHT = 1;
static final int RED = 0;
static final int BLUE = 1;
static final int YELLOW = 2;
static final int OTHER = 3;


static final String graphicsFilepath = 
    "/data/graphics";
static final String systemGraphicsFilepath = 
    "/data/graphics/system";
static final String specialEffectsGraphicsFilepath = 
    "/data/graphics/special_effects";
static final String playerGraphicsFilepath = 
    "/data/graphics/players/player";
static final String musicFilepath = 
    "/data/sounds/music";
static final String soundEffectsFilepath = 
    "/data/sounds/sound_effects";
/** The objects that can bounce around on the playing field. */
public class Ball extends BaseObject {
    
    /***/
    public Ball(float x, float y, 
        float w, float h, float radius, int myColorID,
        float hitboxOffset, float hitboxSize,
        Field playingField, Main game)
    {
        super(x, y, w, h, BALL, game);

        this.radius = radius;
        this.hitboxSize = hitboxSize;
        this.hitboxOffset = hitboxOffset;
        this.playingField = playingField;
        this.hitbox = new HitBox(this.x - radius + hitboxOffset, 
            this.y - radius + hitboxOffset, hitboxSize, hitboxSize);
        
        this.myColor = new int[3];
        this.myColorID = myColorID;
        if (myColorID == RED) {
            this.myColor[0] = 255;
            this.myColor[1] = 0;
            this.myColor[2] = 0;
        } else if (myColorID == BLUE) {
            this.myColor[0] = 0;
            this.myColor[1] = 0;
            this.myColor[2] = 255;
        } else if (myColorID == YELLOW) {
            this.myColor[0] = 255;
            this.myColor[1] = 255;
            this.myColor[2] = 0;
        } else {
            this.myColor[0] = 0;
            this.myColor[1] = 0;
            this.myColor[2] = 0;
        }
        
        minXVel = 0.3f;
        minYVel = 0.3f;
        maxXVel = 5.0f;
        maxYVel = 5.0f;
        
        img = null;
        if (crystal_images == null) {
            crystal_images = new PImage[] {
                loadImage("/data/graphics/crystal_001.png")
            };
            cracked_images = new PImage[] {
                loadImage("/data/graphics/cracked_001.png"),
                loadImage("/data/graphics/cracked_002.png"),
                loadImage("/data/graphics/cracked_003.png"),
                loadImage("/data/graphics/cracked_004.png"),
                loadImage("/data/graphics/cracked_005.png")
            };
        }
        current_sprite = crystal_sprite = 
            new Sprite(crystal_images, true, false, 1, 0);
        cracked_sprite = 
            new Sprite(cracked_images, false, false, 5, 0); 
        
        dead = false;
    }

    public void update() {
        if (cracked && (--deadTime == 0)) {
            cracked = false;
            this.explode(game.getPlayer());
        }
        
        moveMe();
        checkCollision();
    }
    
    public void draw() {
        setColor();
        ellipse(x, y, w, h);
        
        if (Debug) {
            noFill();
            stroke(255);
            strokeWeight(1);
            rect(x - radius, y - radius, w, h);
            hitbox.draw();
        }
        
        image(current_sprite.getImg(frameCount), x-xOffset, y-yOffset);
    }
    
    public void crack() {
        cracked = true;
        deadTime = CRACK_DELAY;
        cracked_sprite.reset();
        current_sprite = cracked_sprite;
    }
    
    protected void setColor() {
        if (game.getColorState(myColorID)) {
            fill(
                this.myColor[0], this.myColor[1], this.myColor[2]);
        } else {
            noFill();
        }
        
        strokeWeight(4);
        stroke(this.myColor[0], this.myColor[1], this.myColor[2]);
    }
    
    /** Checks for collisions with walls. */
    public void checkCollision() {
        playingField.checkOffsides(this);
        ballCollide();
    }
    
    protected void ballCollide() {
        Ball[] balls = game.getBalls();
        
        for (int i = 0; i < balls.length; i++) {
            float dx = balls[i].getX() - x;
            float dy = balls[i].getY() - y;
            float distance = sqrt(dx*dx + dy*dy);
            float minDist = balls[i].getRadius() + radius;
            if (distance < minDist) {
                if (this.type == PLAYER) {
                    confirmPlayerCollision((Player) this, balls[i]);
                } else if (balls[i].getType() == PLAYER) {
                    confirmPlayerCollision((Player) balls[i], this);
                } else {
                    confirmBallCollision(balls[i], dy, dx, minDist);
                }
            }
        }   
    }
    
    protected void confirmPlayerCollision(Player p, Ball b) {
        game.playerHitBall(p, b);
    }
    
    protected void confirmBallCollision(Ball b, 
        float dy, float dx, float minDist) 
    {
        float angle = atan2(dy, dx);
        float targetX = x + cos(angle) * minDist;
        float targetY = y + sin(angle) * minDist;
        float ax = (targetX - b.getX());// * spring;
        float ay = (targetY - b.getY());// * spring;
        xVel -= ax;
        yVel -= ay;
        b.setVelocity(
            b.getXVel() + ax,
            b.getYVel() + ay);
    }
    
    public void explode(Player p) {
        game.aBallExploded(this);
        crystal_sprite.reset();
        current_sprite = crystal_sprite;
        
        float[] newPosition;
        while (true) {
            newPosition = playingField.getValidBallPosition();
            moveMe(newPosition[0], newPosition[1]);
            if (!(this.collide(p.getExplosionHitBox()))) {
                break;
            }
        }
    }
    
    public void handleCollision(BaseObject obj) {
        int thingHit = obj.getType();
        
        if (thingHit == FIELD) {
        } else if (thingHit == BALL) {
            this.xVel *= -1;
            this.yVel *= -1;
        } else if (thingHit == PLAYER) {
        }
    }
    
    public void moveMe() {
        super.moveMe();
        hitbox.move(this.x - radius + hitboxOffset, 
            this.y - radius + hitboxOffset);
    }
    
    public void moveMe(float x, float y) {
        super.moveMe(x, y);
        hitbox.move(this.x - radius + hitboxOffset, 
            this.y - radius + hitboxOffset);
    }
    
    /** Sets the ball to the velocity specified by XVEL and YVEL. */
    public void setVelocity(float xVel, float yVel) {
        if (abs(xVel) < minXVel) {
            this.xVel = minXVel * ((xVel < 0.0f) ? (-1) : (1));
        } else if (abs(xVel) > maxXVel) {
            this.xVel = maxXVel * ((xVel < 0.0f) ? (-1) : (1));
        } else {
            this.xVel = xVel;
        }
        
        if (abs(yVel) < minYVel) {
            this.yVel = minYVel * ((yVel < 0.0f) ? (-1) : (1));
        } else if (abs(yVel) > maxYVel) {
            this.yVel = maxYVel * ((yVel < 0.0f) ? (-1) : (1));
        } else {
            this.yVel = yVel;
        }
    }
    
    /** Sets this ball to move in a random direction at the speed 
        SPEED. */
    public void moveInRandomDirection(float speed) {
        float xV = random(-1.0f, 1.0f);                                                                   
        float yV = random(-1.0f, 1.0f);
        float length = sqrt((xV * xV) + (yV * yV));
        
        setVelocity(speed * (xV / length), speed * (yV / length));
    }
    
    public float getRadius() {
        return radius;
    }
    
    public int getColorID() {
        return myColorID;
    }
    
    public boolean isCracked() {
        return cracked;
    }

    private int myColorID;
    private int myColor[];
    private int deadTime;
    private float radius;
    private boolean cracked;
    private Field playingField;
    protected static final float xOffset = 20.0f;
    protected static final float yOffset = 20.0f;    
    private static final int CRACK_DELAY = 1 * FRAMES_PER_SECOND;
    private Sprite crystal_sprite;
    private Sprite cracked_sprite;
    private Sprite current_sprite;
}

    private static PImage[] crystal_images = null;
    private static PImage[] cracked_images = null;
/***********************************************************************
*   BaseObject
*   The most basic entity from which all other things inherit.
***********************************************************************/

public class BaseObject{
    
    /** Defines the common properties of objects in SpitBall. */
    public BaseObject(float x, float y, float w, float h, int type, 
        Main game) 
    {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.hitbox = null;
        this.hitboxOffset = 0.0f;
        this.hitboxSize = 0.0f;
        this.type = type;
        this.game = game;
        this.dead = true;

        this.xVel = this.yVel = 0.0f;
    }
    
    /** Updates this particular object based on the current framecount
    *   as given in FRAMENUMBER. */
    public void update() {
    }
    
    /** Draws this object on the screen. */
    public void draw() {
        if (img != null) {
            setColor();
            image(img, x, y);
        }
    }
    
    /** Moves the object based on velocity. */
    public void moveMe() {
        x += xVel;
        y += yVel;
    }
    
    /** Moves this object directly to the given X and Y. */
    public void moveMe(float x, float y) {
        this.x = x;
        this.y = y;
    }
    
    /** Checks collsions with all other objects. */
    public void checkCollision() {
        BaseObject[] world = game.getWorld();
        int objectsInGame = game.getObjectsInWorld();
        int direction;

        for (int i = 0; i < objectsInGame; i++) {
            if (dead) {
                return;
            }
            
            //println("My Type = " + type + ", Other's Type = " + world[i].getType());
            if(this.collide(world[i].getHitBox())) {
                this.handleCollision(world[i]);
            }
        }
    }
    
    protected void handleCollision(BaseObject obj) {
    }

    
    /** Checks for a collision with the bounding box BOUNDS. */
    protected boolean collide(HitBox bounds) {  
        //println("Me: " + this.hitbox + ", Other: " + bounds);
        if ((hitbox != null) && 
            (bounds.hit(hitbox) || hitbox.hit(bounds)))
        {
            return true;
        }
        
        return false;
    }
    
    /** Sets the color of this object. */
    protected void setColor() {
    }
    
    public boolean isDead() {
        return dead;
    }
    
    /** Get this object's x position. */
    public float getX() {
        return x;
    }
    
    /** Get this object's y position. */
    public float getY() {
        return y;
    }
    
    /** Get this object's x velocity. */
    public float getXVel() {
        return xVel;
    }
    
    /** Get this object's y velocity. */
    public float getYVel() {
        return yVel;
    }
    
    /** Get this object's width. */
    public float getWidth() {
        return w;
    }
    
    /** Get this object's y height. */
    public float getHeight() {
        return h;
    }
    
    public int getType() {
        return type;
    }
    
    /** Returns the hitbox of this ball. */
    public HitBox getHitBox() {
        return hitbox;
    }
    
    /** Returns the side length of this element's hitbox. */
    public float getHitboxSize() {
        return hitboxSize;
    }
    
    /** Returns the offset that the hitbox uses on this object. */
    public float getHitboxOffset() {
        return hitboxOffset;
    }
    
    /** Returns the game engine. */
    public Main getEngine() {
        return game;
    }
    
    protected PImage img;
    protected Main game;
    protected float x, y, w, h;
    protected float xVel, yVel, minXVel, minYVel, maxXVel, maxYVel;
    protected int type;
    protected HitBox hitbox;
    protected float hitboxOffset;
    protected float hitboxSize;
    protected boolean dead;
}
/***********************************************************************
*   Controller
*   This handles all of the information comming from the Arduino serial
*   port and interprets all of the information from the Wii-Nunchucks.
***********************************************************************/


public void serialEvent(Serial myPort) {
    // read incoming data until you get a newline:
    String serialString = myPort.readStringUntil('\n');
    // if the read data is a real string, parse it:
    
    if (serialString != null) {
        // split it into substrings on the DELIM character:
        String[] numbers = split(serialString, DELIM);
        // convert each substring into an int
        if (numbers.length == sensorCount) {
            int playerNumber = PApplet.parseInt(numbers[0]) - 1;
            for (int i = 0; i < numbers.length; i++) {
                // make sure you're only reading as many numbers as
                // you can fit in the array:
                if (i <= sensorCount) {
                    // trim off any whitespace from the substring:
                    numbers[i] = trim(numbers[i]);
                    
                    //if (firstRun) //TODO: Replace with first time for each nunchuck.
                    sensorValues[playerNumber][i] =  PApplet.parseFloat(numbers[i]);
                }
                // Things we don't handle in particular can get output to the text window
                //print(serialString);
            }
            
            //firstRun = false;
        }
        
    }
}


float xmag, ymag = 0;
float newXmag, newYmag = 0; 
int sensorCount = 12;  // number of values to expect
int controllers = 0;
float[][] sensorValues = new float[4][sensorCount];
float[][] sensorCalibration = new float[4][sensorCount];
Serial[] myPorts = new Serial[4];
String[] portNames = new String[4];

static final int JOYSTICK_X = 10;
static final int JOYSTICK_Y = 11;
static final int BUTTON_HOLD_C = 9;
static final int BUTTON_TAP_C = 8;
static final int BUTTON_HOLD_Z = 7;
static final int BUTTON_TAP_Z = 6;
static final int BAUDRATE = 115200; 
static final char DELIM = '\t'; // the delimeter for parsing incoming data
/***********************************************************************
*   DefaultField
*   A simple field with rough soil and underwatered, slowly dying 
*   grass. No tricks happen in games here: It's an arena for pure 
*   SpitBall skill.
***********************************************************************/

public class DefaultField extends Field {

    /** Creates a DefaultField field for play. */
    public DefaultField(float x, float y, int w, int h, 
        Main game)
    {
        super(x, y, w, h, game);
        
        numberOfBalls = 9;
        
        if (img == null) {
            img = loadImage("/data/graphics/background.png"); //DefaultField_image;  
        }
        
        Borders = new HitBox[4];
        Borders[0] = Top = 
            new HitBox(TOP_X, TOP_Y, TOP_WIDTH, TOP_HEIGHT);
        Borders[1] = Bottom = 
            new HitBox(BOTTOM_X, BOTTOM_Y, BOTTOM_WIDTH, BOTTOM_HEIGHT);
        Borders[2] = Left = 
            new HitBox(LEFT_X, LEFT_Y, LEFT_WIDTH, LEFT_HEIGHT);
        Borders[3] = Right = 
            new HitBox(RIGHT_X, RIGHT_Y, RIGHT_WIDTH, RIGHT_HEIGHT);
        //Borders[4] = Middle = 
        //    new HitBox(MIDDLE_X, MIDDLE_Y, MIDDLE_WIDTH, MIDDLE_HEIGHT);
    }
    
    /** Draws this field on the screen. */
    public void draw() {
        //super.draw();
        if (img != null) {
            image(img, 0, 0);  
        }
        
        if (Debug) {
            noFill();
            stroke(255, 0, 0);
            
            Top.draw();
            Bottom.draw();
            Left.draw();
            Right.draw();
            //Middle.draw();
        }
    }
    
    /** Checks the player for collisions with the walls and updates 
    *   the position accordingly. */
    public void checkOffsides(Player player) {
        if (player.collide(Top)) {
            player.moveMe(player.getX(), TOP_Y + TOP_HEIGHT - player.getHitboxOffset());
        } else if (player.collide(Bottom)) {
            player.moveMe(player.getX(), BOTTOM_Y - player.getHeight() + player.getHitboxOffset());
        }
        
        if (player.collide(Left)) {
            player.moveMe(LEFT_X + LEFT_WIDTH - player.getHitboxOffset(), player.getY());
        } else if (player.collide(Right)) {
            player.moveMe(RIGHT_X - player.getWidth() + player.getHitboxOffset(), player.getY());
        } /*else if (player.collide(Middle)) {
            if (player.getSide() == LEFT) {
                player.moveMe(MIDDLE_X - player.getWidth() + player.getHitboxOffset(), player.getY());
            } else if (player.getSide() == RIGHT) {
                player.moveMe(MIDDLE_X + MIDDLE_WIDTH - player.getHitboxOffset(), player.getY());
            }
        } */
    }
    
    /** Checks to see if the ball BALL is offsides and bounces it if 
    *   it is. */
    public void checkOffsides(Ball ball) {
        if (ball.collide(Top)) {
            ball.moveMe(ball.getX(), TOP_Y + TOP_HEIGHT + (ball.getRadius() - ball.getHitboxOffset()));
            ball.setVelocity(ball.getXVel(), -1.0f * ball.getYVel());
            game.triggerSFX(BOUNCE2_SFX);
        } else if (ball.collide(Bottom)) {
            ball.moveMe(ball.getX(), BOTTOM_Y - ball.getHeight() + ball.getHitboxOffset() + ball.getRadius());            
            ball.setVelocity(ball.getXVel(), -1.0f * ball.getYVel());
            game.triggerSFX(BOUNCE2_SFX);
        }
        
        if (ball.collide(Left)) {             
            ball.moveMe(LEFT_X + LEFT_WIDTH - ball.getHitboxOffset() + ball.getRadius(), ball.getY());
            ball.setVelocity(-1.0f * ball.getXVel(), ball.getYVel());
            game.triggerSFX(BOUNCE2_SFX);
        } else if (ball.collide(Right)) {
            ball.moveMe(RIGHT_X - ball.getRadius() + ball.getHitboxOffset(), ball.getY());
            ball.setVelocity(-1.0f * ball.getXVel(), ball.getYVel());
            game.triggerSFX(BOUNCE2_SFX);
        }
    }
    
    /** Creates the balls in their default locations for this field. */
    public Ball[] serveBalls() {
        balls = new Ball[numberOfBalls];
        //float ballXMin = x + BALL_RADIUS;
        //float ballXMax = x + w - BALL_RADIUS;
        //float ballYMin = y + BALL_RADIUS;
        //float ballYMax = y + h - BALL_RADIUS;
        int ballColor = RED;
        
        for (int i = 0; i < numberOfBalls; i++) {
            balls[i] = new Ball(
                random(BALL_X_MIN, BALL_X_MAX), 
                random(BALL_Y_MIN, BALL_Y_MAX),
                BALL_WIDTH, BALL_HEIGHT, BALL_RADIUS, ballColor,
                BALL_HITBOX_OFFSET, BALL_HITBOX_SIZE, 
                this, game);
            
            balls[i].moveInRandomDirection(BALL_SPEED);
            
            if (ballColor == YELLOW) {
                ballColor = RED;
            } else {
                ballColor++;
            }
        }
        
        return balls;
    }
    
    public float[] getValidBallPosition() {
        return new float[] {
            random(BALL_X_MIN, BALL_X_MAX),
            random(BALL_Y_MIN, BALL_Y_MAX)
        };
    }
    
    HitBox Top, Left, Right, Bottom, Middle;
    
    private final float BUFFER = 50;
    
    private final float TOP_X = this.x - BUFFER;
    private final float TOP_Y = this.y - BUFFER;
    private final float TOP_WIDTH = w + (2 * BUFFER);
    private final float TOP_HEIGHT = BUFFER;
    
    private final float LEFT_X = this.x - BUFFER;
    private final float LEFT_Y = this.y - BUFFER;
    private final float LEFT_WIDTH = BUFFER;
    private final float LEFT_HEIGHT = h + (2 * BUFFER);
    
    private final float RIGHT_X = this.x + w;
    private final float RIGHT_Y = this.y - BUFFER;
    private final float RIGHT_WIDTH = BUFFER;
    private final float RIGHT_HEIGHT = h + (2 * BUFFER);
    
    private final float BOTTOM_X = this.x - BUFFER;
    private final float BOTTOM_Y = this.y + h;
    private final float BOTTOM_WIDTH = w + (2 * BUFFER);
    private final float BOTTOM_HEIGHT = BUFFER;
    
    private final float MIDDLE_X = (w / 2) - 5;
    private final float MIDDLE_Y = this.y;
    private final float MIDDLE_WIDTH = 10;
    private final float MIDDLE_HEIGHT = h;
    
    private static final float BALL_RADIUS = 20.0f;
    private final float BALL_WIDTH = BALL_RADIUS * 2.0f;
    private final float BALL_HEIGHT = BALL_RADIUS * 2.0f;
    private static final float BALL_HITBOX_OFFSET = 0.0f;
    private static final float BALL_HITBOX_SIZE = 40.0f;
    private static final float BALL_SPEED = 3.0f;
    private final float BALL_X_MIN = x + BALL_RADIUS;
    private final float BALL_X_MAX = x + w - BALL_RADIUS;
    private final float BALL_Y_MIN = y + BALL_RADIUS;
    private final float BALL_Y_MAX = y + h - BALL_RADIUS;
    
    //private static PImage background;
}
/***********************************************************************
*   Field
*   What's a sport without a field to play on? Defines the basic 
*   parameters for a field.
***********************************************************************/

public class Field extends BaseObject {
    
    /** Defines the common variables of a field. */
    public Field(float x, float y,  int w, int h, Main game) {
        super(x, y, w, h, FIELD, game);
        
        balls = null;
    }
    
    public HitBox[] getBorders() {
        return Borders;
    }
    
    public void checkOffsides(Player player) {
    }
    
    public void checkOffsides(Ball ball) {
    }
    
    /** Returns the starting X coordinate for player number PLAYER. */
    public float getStartingXPos(int player) {
        return 0.0f;
    }
    
    /** Returns the starting y coordinate for player number PLAYER. */
    public float getStartingYPos(int player) {
        return 0.0f;
    }
    
    public Ball[] serveBalls() {
        return null;
    }
    
    public float[] getValidBallPosition() {
        return null;
    }
    
    protected int numberOfBalls;
    protected HitBox[] Borders;
    protected Ball[] balls;
}
/***********************************************************************
*   HitBox
*   This is the class that will bound objects for collision detection.
***********************************************************************/

public class HitBox {
    
    /** Creates a new hitbox at (X, Y) with width W and height H. */
    public HitBox(float x, float y, float w, float h) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        
        this.colorValues = new int[] {255, 255, 255};
    }
    
    /** Draws the hitbox's bounds. */
    public void draw() {
        stroke(colorValues[0], colorValues[1], colorValues[2]);
        strokeWeight(1);
        rect(x, y, w, h);
    }
    
    public void setColorValues(int r, int g, int b) {
        colorValues = new int[] {r, g, b};
    }
    
    /** Checks to see if point (X, Y) is within the box. */
    public boolean hit(float x, float y) {
        HitBox temp = new HitBox(x, y, 0.0f, 0.0f);
        return hit(temp);
    }
    
    /** Checks to see if BOX has collided with THIS. */
    public boolean hit(HitBox box) {
        //Check the corner points.
        if (pointIsBetween(this.x, box.x, this.x + this.w) ||
            pointIsBetween(this.x, box.x + box.w, this.x + this.w))
        {
            if (pointIsBetween(this.y, box.y, this.y + this.h) ||
                pointIsBetween(this.y, box.y + box.h, this.y + this.h))
            {
                return true;
            }
        }
        
        //Check overlap X
        if (rangeIsBetween(this.x, box.x, box.x + box.w, this.x + this.w) &&
            (pointIsBetween(this.y, box.y, this.y + this.h) ||
            pointIsBetween(this.y, box.y + box.h, this.y + this.h)))
        {
            return true;
        }
        
        //Check overlap Y
        if (rangeIsBetween(this.y, box.y, box.y + box.h, this.y + this.h) &&
            (pointIsBetween(this.x, box.x, this.x + this.w) ||
            pointIsBetween(this.x, box.x + box.w, this.x + this.w)))
        {
            return true;
        }
        
        return false;
    }
    
    /** Returns true if (floor < value < cieling). */
    private boolean pointIsBetween(float floor, float value, 
        float cieling) 
    {
        if ((floor < value) && (value < cieling)) {
            return true;
        }
        
        return false;
    }
    
    /** Returns true if the range boundaries are within the LOW and 
    *   HIGH points. */
    private boolean rangeIsBetween(float low, float rangeLow, 
        float rangeHigh, float high)
    {
        if ((low < rangeLow) && (rangeHigh < high)) {
            return true;
        }
        
        return false;
    }
    
    
    /** Moves the position of this hitbox*/
    public void move(float x, float y) {
        this.x = x;
        this.y = y;
    }
    
    /** Get the X position of this hitbox. */
    public float getX() {
        return x;
    }
    
    /** Get the Y position of this hitbox. */
    public float getY() {
        return y;
    }
    
    /** Get the width of this hitbox. */
    public float getWidth() {
        return w;
    }
    
    /** Get the height of this hitbox. */
    public float getHeight() {
        return h;
    }
    
    
    int[] colorValues;
    float x, y, w, h;
}
/***********************************************************************
*   Main
*   The main class for SpitBall. Contains all of the code for the 
*   engine and main game loop. 
***********************************************************************/

public class Main {
    
    /** Creates a new game engine. */
    public Main() {
        loadImages();
        loadMusic();
        loadSoundEffects();
        
        gameOver = true;
        splashScreen = new TitleScreens(this);
        //newGame(0);
        //deadTime = frameCount;
        
        loopMusicTrack(titleMusic);
    }                                                                                                    
    
    /** Updates all objects in the game world. */
    public void update() {
        if (gameOver) {
            //Has the game been started yet, or has the timer run out?
            if (!gameIsInProgress) {
                splashScreen.update();
                splashScreen.draw();
            } else {
                drawEndingText();
            }
        } else {
            if (pause) {
                drawPauseScreen();
            } else {
                updateClock();
                drawHUD();
                updateGameObjects();
                drawAnchor();
            }
        }
    }
    
    private void drawEndingText() {
        if (--endingTextTime > 0) {
            fill(random(255), random(255), random(255));
            textFont(pauseFont, 120);
            textAlign(CENTER, CENTER);
            text("Time's up!\n" + 
                (newHighScore ? 
                    "New High Score!\n" : 
                    "Your Score\n") 
                + score, 
                HOFFSET, VOFFSET, FIELD_SIZE, FIELD_SIZE);
        } else {
            endGame();
        }
    }
    
    private void drawPauseScreen() {
        fill(random(255), random(255), random(255));
        textFont(pauseFont, 130);
        textAlign(CENTER, CENTER);
        text("PAUSE!", HOFFSET, VOFFSET, FIELD_SIZE, FIELD_SIZE);
        
        fill(255);//, 0, 0);
        //stroke(255);
        textFont(pauseFont, 25);
        textAlign(CENTER, CENTER);
        text("Control Recap\n" +
            "P Key: Pause/Unpause the game.\n" +
            "Mouse Click+Drag: Target bubbles with the robot's tractor beam.\n" +
            "WASD Keys: Lean in a direction while locked on.\n" +
            "M Key: Mute the music and sound effects.\n" + 
            "H Key: Turn on the target color halo.\n" +
            "B Key: Change robots.",
            HOFFSET, VOFFSET + FIELD_SIZE - (FIELD_SIZE / 3), 
            FIELD_SIZE, FIELD_SIZE / 3);
    }
    
    private void checkKeys() {
        if (rKeyDown) {
            redState = true;
        } else {
            redState = false;
        }
        
        if (yKeyDown) {
            yellowState = true;
        } else {
            yellowState = false;
        }
        
        if (bKeyDown) {
            blueState = true;
        } else {
            blueState = false;
        }
    }
    
    /** Draws all of the non-moving objects. */
    private void drawHUD() {
        //background(0);
        pipes.update();
        pipes.draw();
        playingField.draw();
        
        strokeWeight(4);
        
        if (Debug) {
            if (redState) {
                fill(255, 0, 0);
            } else {
                noFill();
            }
            stroke(255, 0, 0);
            ellipse(20, 20, 30, 30);
            
            if (blueState) {
                fill(0, 0, 255);
            } else {
                noFill();
            }
            stroke(0, 0, 255);
            ellipse(50, 20, 30, 30);
            
            if (yellowState) {
                fill(255, 255, 0);
            } else {
                noFill();
            }
            stroke(255, 255, 0);
            ellipse(80, 20, 30, 30);
            
            // Draw the current and target colors.
            stroke(0, 0, 0);
            int[] c = getStateColor(redState, yellowState, blueState);
            if (c != null) {
                fill(c[0], c[1], c[2]);
            } else {
                noFill();
            }
            ellipse(100, 100, 50, 50);
            
            c = getStateColor(redTarget, yellowTarget, blueTarget);
            if (c != null) {
                fill(c[0], c[1], c[2]);
            } else {
                noFill();
            }
            ellipse(100, 150, 50, 50);
        }

        printScoreBoard();
    }
    
    private int[] getStateColor(
        boolean redS, boolean yellowS, boolean blueS) 
    {
        if (redS && blueS && yellowS) {
            //BROWN
            return new int[] {100, 60, 0};
        } else if (redS && yellowS) {
            //ORANGE
            return new int[] {250, 150, 30};
        } else if (redS && blueS) {
            //VIOLET
            return new int[] {180, 80, 200};
        } else if (yellowS && blueS) {
            //GREEN
            return new int[] {0, 230, 40};
        } else if (redS) {
            //RED
            return new int[] {255, 0, 0};
        } else if (blueS) {
            //BLUE
            return new int[] {0, 0, 255};
        } else if (yellowS) {
            //YELLOW
            return new int[] {255, 255, 0};
        } else {
            //WHITE
            //fill(255, 255, 255);
            
            //BLACK
            //fill(0, 0, 0);
            
            //NOTHING
            return null;
        }
    }
    
    public int[] getCurrentMixedColor() {
        return getStateColor(redState, yellowState, blueState);
    }
    
    public int[] getCurrentTargetColor() {
        return getStateColor(redTarget, yellowTarget, blueTarget);
    }
    
    private void drawAnchor() {
        if (!anchored) {
            return;
        } else if (!hookedOnSomething) {
            HitBox tempHitbox = 
                new HitBox(anchorX - 10.0f, anchorY - 10.0f, 20.0f, 20.0f);
            
            noFill();
            tempHitbox.setColorValues(0, 230, 70);
            tempHitbox.draw();
            line(tempHitbox.getX() - 10.0f, 
                tempHitbox.getY() + (tempHitbox.getHeight() / 2.0f),
                tempHitbox.getX() + tempHitbox.getWidth() + 10.0f, 
                tempHitbox.getY() + (tempHitbox.getHeight() / 2.0f));
            line(tempHitbox.getX() + (tempHitbox.getWidth() / 2.0f), 
                tempHitbox.getY() - 10.0f,
                tempHitbox.getX() + (tempHitbox.getWidth() / 2.0f),
                tempHitbox.getY() + tempHitbox.getHeight() + 10.0f);
            
            fill(0, 230, 70);
            textFont(techyFont, 10);
            textAlign(LEFT, CENTER);
            float param1 = random(0, 100000);
            float param2 = random(0, 100000);
            text("TARGET\n" + param1 + ", " + param2,
                tempHitbox.getX() + tempHitbox.getWidth() + 20.0f, 
                tempHitbox.getY() + (tempHitbox.getHeight() / 2.0f));
            
            for (int i = 0; i < balls.length; i++) {
                if (balls[i].collide(tempHitbox)) {
                    hookedOnSomething = true;
                    player.resetAngle();
                    lockonHitbox = tempHitbox;
                    targetBall = balls[i];
                    drawGrapple();
                    break;
                }
            }
            
            return;
        }
        
        drawGrapple();
    }
    
    private void drawGrapple() {
        if (targetBall != null) {
            float targetX = targetBall.getX();
            float targetY = targetBall.getY();
            float playerX = player.getX();
            float playerY = player.getY();
            float vecX = targetX - playerX;
            float vecY = targetY - playerY;
            float rotateAmt = atan2(vecY, vecX);
            player.setAngleOffset(rotateAmt);

            
            //line(targetX, targetY, playerX, playerY);
            int[] currentColor = getCurrentMixedColor();
            int[] targetColor = getCurrentTargetColor();
            
            ///*
            noFill();
            float control_1_X = random(min(targetX, playerX), max(targetX, playerX));
            float control_1_Y = random(min(targetY, playerY), max(targetY, playerY));
            float control_2_X = random(min(targetX, playerX), max(targetX, playerX)); 
            float control_2_Y = random(min(targetY, playerY), max(targetY, playerY));
            //stroke(255, 255, 0);
            stroke(targetColor[0], targetColor[1], targetColor[2]);
            strokeWeight(4);
            bezier(
                targetX, targetY, 
                control_1_X, control_1_Y,
                control_2_X, control_2_Y,
                playerX, playerY);
            strokeWeight(2);
            //stroke(255, 255, 0);
            if (currentColor == null) {
                stroke(0, 0, 0);
            } else {
                stroke(currentColor[0], currentColor[1], currentColor[2]);
            }
            bezier(
                targetX, targetY, 
                control_1_X, control_1_Y,
                control_2_X, control_2_Y,
                playerX, playerY);
            
            float t = random(0.0f, 1.0f);
            float control_a_X = bezierPoint(targetX, control_1_X, control_2_X, playerX, t);
            float control_a_Y = bezierPoint(targetY, control_1_Y, control_2_Y, playerY, t);            
            float control_3_X = random(min(targetX, playerX), max(targetX, playerX)); 
            float control_3_Y = random(min(targetY, playerY), max(targetY, playerY));
            float control_4_X = random(min(control_a_X, control_3_X), max(control_a_X, control_3_X)); 
            float control_4_Y = random(min(control_a_Y, control_3_Y), max(control_a_Y, control_3_Y));
            float control_5_X = random(min(control_a_X, control_3_X), max(control_a_X, control_3_X)); 
            float control_5_Y = random(min(control_a_Y, control_3_Y), max(control_a_Y, control_3_Y));
            //stroke(255, 255, 0);
            stroke(targetColor[0], targetColor[1], targetColor[2]);
            strokeWeight(4);
            bezier(
                control_a_X, control_a_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            strokeWeight(2);
            //stroke(255, 255, 0);
            if (currentColor == null) {
                stroke(0, 0, 0);
            } else {
                stroke(currentColor[0], currentColor[1], currentColor[2]);
            }
            bezier(               
                control_a_X, control_a_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            
            t = random(0.0f, 1.0f);
            t = random(0.0f, 1.0f);
            float control_c_X = bezierPoint(control_a_X, control_4_X, control_5_X, control_3_X, t);
            float control_c_Y = bezierPoint(control_a_Y, control_4_Y, control_5_Y, control_3_Y, t);            
            control_3_X = random(min(control_3_X, control_a_X), max(control_3_X, control_a_X)); 
            control_3_Y = random(min(control_3_Y, control_a_Y), max(control_3_Y, control_a_Y));
            control_4_X = random(min(control_c_X, control_3_X), max(control_c_X, control_3_X)); 
            control_4_Y = random(min(control_c_Y, control_3_Y), max(control_c_Y, control_3_Y));
            control_5_X = random(min(control_c_X, control_3_X), max(control_c_X, control_3_X)); 
            control_5_Y = random(min(control_c_Y, control_3_Y), max(control_c_Y, control_3_Y));
            //stroke(255, 255, 0);
            stroke(targetColor[0], targetColor[1], targetColor[2]);
            strokeWeight(4);
            bezier(
                control_c_X, control_c_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            strokeWeight(2);
            //stroke(255, 255, 0);
            if (currentColor == null) {
                stroke(0, 0, 0);
            } else {
                stroke(currentColor[0], currentColor[1], currentColor[2]);
            }
            bezier(
                control_c_X, control_c_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            float control_b_X = bezierPoint(targetX, control_1_X, control_2_X, playerX, t);
            float control_b_Y = bezierPoint(targetY, control_1_Y, control_2_Y, playerY, t);            
            control_3_X = random(min(targetX, playerX), max(targetX, playerX)); 
            control_3_Y = random(min(targetY, playerY), max(targetY, playerY));
            control_4_X = random(min(control_b_X, control_3_X), max(control_b_X, control_3_X)); 
            control_4_Y = random(min(control_b_Y, control_3_Y), max(control_b_Y, control_3_Y));
            control_5_X = random(min(control_b_X, control_3_X), max(control_b_X, control_3_X)); 
            control_5_Y = random(min(control_b_Y, control_3_Y), max(control_b_Y, control_3_Y));
            //stroke(255, 255, 0);
            stroke(targetColor[0], targetColor[1], targetColor[2]);
            strokeWeight(4);
            bezier(
                control_b_X, control_b_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            strokeWeight(2);
            //stroke(255, 255, 0);
            if (currentColor == null) {
                stroke(0, 0, 0);
            } else {
                stroke(currentColor[0], currentColor[1], currentColor[2]);
            }
            bezier(
                control_b_X, control_b_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            t = random(0.0f, 1.0f);
            control_c_X = bezierPoint(control_b_X, control_4_X, control_5_X, control_3_X, t);
            control_c_Y = bezierPoint(control_b_Y, control_4_Y, control_5_Y, control_3_Y, t);            
            control_3_X = random(min(control_3_X, control_b_X), max(control_3_X, control_b_X)); 
            control_3_Y = random(min(control_3_Y, control_b_Y), max(control_3_Y, control_b_Y));
            control_4_X = random(min(control_c_X, control_3_X), max(control_c_X, control_3_X)); 
            control_4_Y = random(min(control_c_Y, control_3_Y), max(control_c_Y, control_3_Y));
            control_5_X = random(min(control_c_X, control_3_X), max(control_c_X, control_3_X)); 
            control_5_Y = random(min(control_c_Y, control_3_Y), max(control_c_Y, control_3_Y));
            //stroke(255, 255, 0);
            stroke(targetColor[0], targetColor[1], targetColor[2]);
            strokeWeight(4);
            bezier(
                control_c_X, control_c_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y);
            strokeWeight(2);
            //stroke(255, 255, 0);
            if (currentColor == null) {
                stroke(0, 0, 0);
            } else {
                stroke(currentColor[0], currentColor[1], currentColor[2]);
            }
            bezier(
                control_c_X, control_c_Y,
                control_4_X, control_4_Y,
                control_5_X, control_5_Y,
                control_3_X, control_3_Y); 
            //*/
            
            //Draw the crosshairs.
            if (lockonHitbox != null) {
                noFill();
                lockonHitbox.move(targetX - 10, targetY - 10);
                lockonHitbox.setColorValues(255, 0, 0);
                lockonHitbox.draw();
                line(lockonHitbox.getX() - 10.0f, 
                    lockonHitbox.getY() + (lockonHitbox.getHeight() / 2.0f),
                    lockonHitbox.getX() + lockonHitbox.getWidth() + 10.0f, 
                    lockonHitbox.getY() + (lockonHitbox.getHeight() / 2.0f));
                line(lockonHitbox.getX() + (lockonHitbox.getWidth() / 2.0f), 
                    lockonHitbox.getY() - 10.0f,
                    lockonHitbox.getX() + (lockonHitbox.getWidth() / 2.0f),
                    lockonHitbox.getY() + lockonHitbox.getHeight() + 10.0f);
                
                stroke(255, 0, 0);
                fill(255, 0, 0);
                textFont(techyFont, 10);
                textAlign(LEFT, CENTER);
                text("TARGET\nLOCKED",
                    lockonHitbox.getX() + lockonHitbox.getWidth() + 20.0f, 
                    lockonHitbox.getY() + (lockonHitbox.getHeight() / 2.0f));
            }
                
            //Change the velocity.
            if (vecX > vecY) {
                player.setVelocity(
                    player.getXVel() + (vecX / 200.0f),
                    player.getYVel() + (vecY / 300.0f));                                           
            } else {
                player.setVelocity(
                    player.getXVel() + (vecX / 200.0f),
                    player.getYVel() + (vecY / 300.0f));
            }
                                                                                                           
            //player.setVelocity(
            //    player.getXVel() + (vecX / 150),
            //    player.getYVel() + (vecY / 150));                                                     
        }
    }
    
    public void releaseAnchor() {
        hookedOnSomething = false;                                                               
        targetBall = null;
        lockonHitbox = null;
    }
    
    public boolean isHookedOnSomething() {
        return hookedOnSomething;
    }
        
    /** Draws the splash screen and keeps track of how much longer it 
    *   should be displayed for. */
    private void drawSystemImage(Sprite s, int frame) {
        //system_image = s.getImg(frame);
        //image(system_image, 0, 0);
    }

    /** Updates all of the objects in the game. */
    private void updateGameObjects() {
        for (int i = 0; i < objectsInWorld; i++) {
            world[i].update();
            //println("Updating " + i);
        }
        
        for (int i = 0; i < objectsInWorld; i++) {
            world[i].draw();
            //println("Drawing " + i);
        }
        
        if (nova != null) {
            nova.update();
            nova.draw();
        }
        
        for (int i = 0; i < pointBlocks.size(); i++) {
            ((PointBlock) pointBlocks.get(i)).draw();
            if (Debug) {
                println("Printed point block " + i + ".");
            }
        }
    }
    
    /** Called from the ball B's or Player P's ball collision method
    when they smack into each other. */
    public void playerHitBall(Player p, Ball b) {
        if (player.isInvincible() || b.isCracked()) {
            return;
        }
        
        
        hitColor(b, b.getColorID());
        b.explode(p);
    }
    
    public void aBallExploded(Ball b) {
        if (b == targetBall) {
            targetBall = null;
            hookedOnSomething = false;
            anchored = false;
        }
    }
    
    public void hitColor(Ball b, int ballColor) {
        switch(ballColor) {
        case RED:
            redState = !redState;
            if (mustHitRed && !missed) {
                correctTarget(b);
            } else {
                wrongTarget(b);
            }
            mustHitRed = false;
            break;
        case BLUE:
            blueState = !blueState;
            if (mustHitBlue && !missed) {
                correctTarget(b);
            } else {
                wrongTarget(b);
            }
            mustHitBlue = false;
            break;
        case YELLOW:
            yellowState = !yellowState;
            if (mustHitYellow && !missed) {
                correctTarget(b);
            } else {
                wrongTarget(b);
            }
            mustHitYellow = false;
            break;
        }
        
        if ((redState == redTarget) &&
            (yellowState == yellowTarget) &&
            (blueState == blueTarget)) 
        {
            /* Increment the multiplier if the color mix was flawless. */
            if (missed) {
                multiplier = 1;
                triggerSFX(BLEND_SFX);
            } else {
                ++multiplier;
                //secondsLeft += 5;
                triggerSFX(BLENDPERFECT_SFX);
            }
            
            matchedTarget(b);
            nova = new SuperNova(player.getX(), player.getY(), 
                getCurrentMixedColor(), multiplier, playingField, this);
            makeNewTargetColor();
        } else {
            //triggerSFX(HITBUBBLE_SFX);
        }
        
        drawHUD();
    }
    
    
    private void matchedTarget(Ball b) {
        score += (multiplier * 100);
        addPointBlock(b.getX() + 15.0f, b.getY() - 15.0f, b.getColorID(), 100);
    }
    
    public void crackedBall(Ball b) {
        score += (multiplier * 50);
        addPointBlock(b.getX(), b.getY(), b.getColorID(), 50);
    }
    
    private void correctTarget(Ball b) {
        score += (multiplier * 50);
        addPointBlock(b.getX() - 15.0f, b.getY() - 15.0f, b.getColorID(), 50);
    }
    
    private void wrongTarget(Ball b) {
        missed = true;
        multiplier = 1;
        score += (multiplier * 10);
        addPointBlock(b.getX() - 15.0f, b.getY() + 15.0f, b.getColorID(), 10);
    }
    
    private void addPointBlock(float xPos, float yPos,
        int colorID, int moveScore) 
    {
        pointBlocks.add(
            new PointBlock(xPos, yPos, colorID,
                moveScore, multiplier, this));
    }
    
    public void removePointBlock(PointBlock pb) {
        pointBlocks.removeElement(pb);
    }
    
    
    public boolean getColorState(int colorID) {
        switch(colorID) {
        case RED:
            return redState;
        case BLUE:
            return blueState;
        case YELLOW:
            return yellowState;
        default:
            return false;
        }
    }
    
    /** Updates the in-game clock. */    
    private void updateClock() {
        if (score > highScore) {
            newHighScore = true;
            highScore = score;
        }
        
        if (secondsLeft >= 60) {
            int minutesToAdd = secondsLeft / 60;
            minutesLeft += minutesToAdd;
            secondsLeft -= 60 * minutesToAdd;
        }
        
        if (!(player.isInvincible()) && --framesUntilNextSecond == 0) {
            if (secondsLeft == 0) {
                if (minutesLeft == 0) {
                    gameOver = true;
                    playMusicTrack(endGameMusic, false);
                    //pauseMusicTrack();
                    //triggerSFX(BLENDPERFECT_SFX);
                } else {
                    minutesLeft--;
                    secondsLeft = 59;
                }
            } else {
                secondsLeft--;
            }
            
            framesUntilNextSecond = FRAMES_PER_SECOND;
        }
    }
    
    /** Prints the scoreboard and in-game clock at the top of the 
    *   screen.*/
    public void printScoreBoard() {
        fill(0, 230, 70);
        textFont(techyFont, 35);
        textAlign(LEFT, CENTER);
        text("Time: " + minutesLeft + ":" + 
            ((secondsLeft < 10) ? "0" : "") + secondsLeft, 
            HOFFSET + 15, VOFFSET - BUFFER, FIELD_SIZE - 15, BUFFER);
        text("TOP:  " + highScore,
            HOFFSET + 180, VOFFSET - BUFFER, FIELD_SIZE - 25, BUFFER);
        textAlign(LEFT, CENTER);
        text("Score: " + multiplier + "x  " + score,
            HOFFSET + 400, VOFFSET - BUFFER, FIELD_SIZE, BUFFER);
        
        /* It's a secret to everybody! */
        if (score == 13370 || leetScore) {
            leetScore = true;
            fill(random(255), random(255), random(255));
            textFont(pauseFont, 100);
            textAlign(CENTER, CENTER);
            text("L337\n|-|4><0rZ\nScore!", HOFFSET, VOFFSET, FIELD_SIZE, FIELD_SIZE);
        }
    }
    
    
    /** Creates a new field and populates it with players. */
    private void newGame(int field) 
    {
        gameOver = false;
        gameIsInProgress = true;
        hookedOnSomething = false;
        leetScore = false;
        newHighScore = false;
        pause = false;
        nova = null;
        lockonHitbox = null;
        pipes = new PaintPipes(this);
        pointBlocks = new Vector();
        minutesLeft = 3;
        secondsLeft = 0;
        endingTextTime = ENDING_TEXT_TIME;
        multiplier = 1;
        score = 0;
        framesUntilNextSecond = FRAMES_PER_SECOND;
        
        redTarget = yellowTarget = blueTarget = 
            redState = blueState = yellowState = false;
        
        objectsInWorld = 0;
        world = new BaseObject[100];
        
        //world[0] = 
        playingField = new DefaultField(
            HOFFSET, VOFFSET, FIELD_SIZE, FIELD_SIZE, this);
        //objectsInWorld++;
        
        balls = playingField.serveBalls();
        for (int i = 0; i < balls.length; i++) {
            //println("Adding ball " + i);
            world[objectsInWorld] = balls[i];
            objectsInWorld++;
        }
        
        world[objectsInWorld] = player = new Player(
            HOFFSET + (700 / 2), VOFFSET + (700 / 2),
            playingField, this);
        objectsInWorld++;
        
        loopMusicTrack(backgroundMusic);        
        makeNewTargetColor();
    }
    
    public void startNewGame() {
        this.newGame(0);
    }
    
    
    public void endGame() {
        if (currentMusicTrack != null) {
            currentMusicTrack.pause();
        }
        
        gameIsInProgress = false;
        loopMusicTrack(titleMusic);
    }
    
    private void makeNewTargetColor() {
        boolean tempRedTarget = false, 
            tempYellowTarget = false,
            tempBlueTarget = false;
        
        while (true) {
            switch(PApplet.parseInt(random(2))) {
            case 0:
                tempRedTarget = true;
                break;
            default:
                tempRedTarget = false;
                break;
            }
            
            switch(PApplet.parseInt(random(2))) {
            case 0:
                tempYellowTarget = true;
                break;
            default:
                tempYellowTarget = false;
                break;
            }
            
            switch(PApplet.parseInt(random(2))) {
            case 0:
                tempBlueTarget = true;
                break;
            default:
                tempBlueTarget = false;
                break;
            }
            
            /* Make sure that the new target color is different
               from the player's current color and that it is 
               not "clear". */
            if (
                ((tempRedTarget != redTarget) || 
                 (tempYellowTarget != yellowTarget) ||
                 (tempBlueTarget != blueTarget))
                &&
                (tempRedTarget || tempYellowTarget || tempBlueTarget)
               )
            {
                break;
            }
        }
        
        redTarget = tempRedTarget;
        yellowTarget = tempYellowTarget;
        blueTarget = tempBlueTarget;
        
        if (!(redTarget == redState)) {
            mustHitRed = true;
        } else {
            mustHitRed = false;
        }
        
        if (!(blueTarget == blueState)) {
            mustHitBlue = true;
        } else {
            mustHitBlue = false;
        }
        
        if (!(yellowTarget == yellowState)) {
            mustHitYellow = true;
        } else {
            mustHitYellow = false;
        }
        
        missed = false;
        
        if (Debug) {
            println("\nColor Info:\nMy Color: R = " + redState + 
                ", B = " + blueState + ", Y = " + yellowState);
            println("New Color: R = " + redTarget + 
                ", B = " + blueTarget + ", Y = " + yellowTarget);
            println("Must Hit: R = " + mustHitRed + 
                ", B = " + mustHitBlue + ", Y = " + mustHitYellow);
        }
    }
    
    /** Returns the array of objects in the game. You've got the 
    *   whooooole world in your hands! */
    public BaseObject[] getWorld() {
        return world;
    }
    
    public int getObjectsInWorld() {
        return objectsInWorld;
    }
    
    /** Returns an array of all of the balls in play. */
    public Ball[] getBalls() {
        return balls;
    }
    
    public Player getPlayer() {
        return player;
    }
    
    public void getColorStateVariables(boolean[] cs) {
        cs[0] = redState;
        cs[1] = blueState;
        cs[2] = yellowState;
        cs[3] = redTarget;
        cs[4] = blueTarget;
        cs[5] = yellowTarget;
        cs[6] = !(redState == redTarget);
        cs[7] = !(blueState == blueTarget);
        cs[8] = !(yellowState == yellowTarget);
    }
    
    /** Trigger a sound effect based on the argument key SOUND. */
    public void triggerSFX(int sound) {
        if (playSoundEffects && (soundEffects[sound] != null)) {
            soundEffects[sound].trigger();
        }
    }
    
    public void loopMusicTrack(AudioPlayer track) {
        playMusicTrack(track, true);
    }
    
    public void playMusicTrack(AudioPlayer track, boolean loop) {
        if (currentMusicTrack != null) {
            currentMusicTrack.pause();
            currentMusicTrack.rewind();
        }
        
        currentMusicTrack = track;
        if (playMusic) {
            if (loop) {
                currentMusicTrack.loop();
            } else {
                currentMusicTrack.play();
            }
        }
    }
    
    public void pauseMusicTrack() {
        if (currentMusicTrack != null) {
            currentMusicTrack.pause();
        }
    }

    public void toggleMusic() {
        if (currentMusicTrack != null) {
            playMusic = !playMusic;
            
            if (playMusic) {
                currentMusicTrack.loop();
            } else {
                currentMusicTrack.pause();
            }
        }
    }
    
    public int getHighScore() {
        return highScore;
    }
    
    /** Loads the menu, field, and special effects image files into 
    *   memory. */
    private void loadImages() {
    }
    
    /** Loads all of the music tracks and sound effects. */
    private void loadMusic() {
        titleMusic = minim.loadFile(musicFilepath + "/title.wav");
        backgroundMusic = minim.loadFile(musicFilepath + "/BlenderBotMusic.wav");
        //creditsMusic = minim.loadFile(musicFilepath + "/creditsscreen_music.wav");
        endGameMusic = minim.loadFile(soundEffectsFilepath + "/perfect blend.wav", 2048);
    }
    
    /** Loads all of the sound effects. */
    private void loadSoundEffects() {
        soundEffects = new AudioSample[10];
        
        //soundEffects[0] = bounce1_SFX  = minim.loadSample(soundEffectsFilepath + "/bounce1.wav", 2048);
        soundEffects[1] = bounce2_SFX           = minim.loadSample(soundEffectsFilepath + "/bounce2.wav", 2048);
        soundEffects[2] = blend_SFX             = minim.loadSample(soundEffectsFilepath + "/blend.wav", 2048);
        soundEffects[3] = blendPerfect_SFX      = minim.loadSample(soundEffectsFilepath + "/perfect blend.wav", 2048);
        
        //soundEffects[3] = dodge_SFX    = minim.loadSample(soundEffectsFilepath + "/dodge.wav", 2048);
        //soundEffects[4] = flames_SFX    = minim.loadSample(soundEffectsFilepath + "/flames.wav", 2048);
        //soundEffects[5] = homicide_SFX = minim.loadSample(soundEffectsFilepath + "/homicide.wav", 2048);
        soundEffects[6] = suicide_SFX  = minim.loadSample(soundEffectsFilepath + "/suicide.wav", 2048);
        //soundEffects[7] = throw1_SFX    = minim.loadSample(soundEffectsFilepath + "/throw1.wav", 2048);
        //soundEffects[8] = throw2_SFX    = minim.loadSample(soundEffectsFilepath + "/throw2.wav", 2048);
        //soundEffects[9] = uncatchable_SFX    = minim.loadSample(soundEffectsFilepath + "/uncatchable.wav", 2048);
    }
    
    /** Closes the music tracks that we're done with. */
    private void closeMusic() {
        if (Debug) {
            println("Closing old music tracks.");
        }
        
        //titleMusic.close();
        backgroundMusic.close();
        //creditsMusic.close();
    }
    

    private int PUSH_START_X = 0;
    private int PUSH_START_Y = 7 * (height / 8);
    private int PUSH_START_W = width;
    private int PUSH_START_H = (height / 8);
    
    private int gameState;
    private int objectsInWorld;
    private int deadTime;
    private boolean hookedOnSomething;
    private boolean redState;
    private boolean blueState;
    private boolean yellowState;
    private boolean redTarget;
    private boolean blueTarget;
    private boolean yellowTarget;
    private boolean mustHitRed;
    private boolean mustHitBlue;
    private boolean mustHitYellow;
    private boolean missed;
    private boolean gameOver;
    private String victoryMessage;
    private BaseObject[] world;
    private Ball[] balls;
    private Ball targetBall;
    private SuperNova nova;
    private HitBox lockonHitbox;
    private PaintPipes pipes;
    private Field playingField;
    private Player player;
    private TitleScreens splashScreen;
    private Vector pointBlocks;
    
    private int multiplier;
    private int score;
    private int highScore;
    private boolean leetScore;
    private boolean newHighScore;
    
    private AudioPlayer titleMusic;
    private AudioPlayer backgroundMusic;
    private AudioPlayer creditsMusic;
    private AudioPlayer currentMusicTrack;
    private AudioPlayer endGameMusic;
    
    private AudioSample bounce1_SFX;
    private AudioSample bounce2_SFX;
    private AudioSample blend_SFX;
    private AudioSample blendPerfect_SFX;
    //private AudioSample flames_SFX;    
    //private AudioSample homicide_SFX;
    private AudioSample suicide_SFX;
    //private AudioSample throw1_SFX;
    //private AudioSample throw2_SFX;
    //private AudioSample uncatchable_SFX;
    private AudioSample[] soundEffects;

    private static final float HOFFSET = 314.0f;
    private static final float VOFFSET = 58.0f;
    private static final float BUFFER = 50.0f;
    private static final int FIELD_SIZE = 700;

    private static final int SPLASH_SCREEN = 0;
    private static final int TITLE_SCREEN = 1;
    private static final int GAME = 2;
    private static final int GAME_OVER = 3;
    private static final int CREDITS = 4;
    private static final int ENDING_TEXT_TIME = FRAMES_PER_SECOND * 5;
    private static final int SPLASH_SCREEN_DELAY = FRAMES_PER_SECOND * 4;
    private static final int VICTORY_SCREEN_DELAY = FRAMES_PER_SECOND * 3;
    private static final int CREDITS_SCREEN_DELAY = FRAMES_PER_SECOND * 3;
}

int minutesLeft;
int secondsLeft;
int endingTextTime;
int framesUntilNextSecond;


//static final int BOUNCE1_SFX = 0;
static final int BOUNCE2_SFX = 1;
static final int BLEND_SFX = 2;
static final int BLENDPERFECT_SFX = 3;
//static final int DODGE_SFX = 3;
//static final int FLAMES_SFX = 4;
//static final int HOMICIDE_SFX = 5;
//static final int SUICIDE_SFX = 6;
//static final int THROW1_SFX = 7;
//static final int THROW2_SFX = 8;
//static final int UNCATCHABLE_SFX = 9;


/** A vat on the left hand side of the screen that reacts to the 
*   current color states. */

public class PaintPipes extends BaseObject {
    public PaintPipes(Main game) {
        super(0.0f, 0.0f, 0.0f, 0.0f, PIPES, game);
        flash = false;
        colorState = new boolean[] {false, false, false,
                                    false, false, false,
                                    false, false, false};
    }
    
    public void update() {
        if ((frameCount % FLASH_RATE) == 0) {
            flash = !flash;
        }
        
        /*
        if (flash && alphaFade > 0) {
            alphaFade -= 10;
        }
        */
    }
    
    public void draw() {
        noStroke();
        game.getColorStateVariables(colorState);

        //Red is on.
        if (colorState[MUST_HIT_RED] && flash) {
            fill(170, 0, 0);
        } else if (colorState[RED_STATE]) {
            fill(255, 0, 0);
        } else {
            fill(166, 166, 166);
        }
        quad(RED_X1, RED_Y1, RED_X2, RED_Y2, 
            RED_X3, RED_Y3, RED_X4, RED_Y4);
        
        if (colorState[MUST_HIT_BLUE] && flash) {
            fill(0, 0, 190);
        } else if (colorState[BLUE_STATE]) {
            fill(0, 0, 255);
        } else {
            fill(166, 166, 166);
        }
        quad(BLUE_X1, BLUE_Y1, BLUE_X2, BLUE_Y2, 
            BLUE_X3, BLUE_Y3, BLUE_X4, BLUE_Y4);

        if (colorState[MUST_HIT_YELLOW] && flash) {
            fill(190, 190, 0);
        } else if (colorState[YELLOW_STATE]) {
            fill(255, 255, 0);
        } else {
            fill(166, 166, 166);
        }
        quad(YELLOW_X1, YELLOW_Y1, YELLOW_X2, YELLOW_Y2, 
            YELLOW_X3, YELLOW_Y3, YELLOW_X4, YELLOW_Y4);
        
        int[] currentColor = game.getCurrentMixedColor();
        if ( currentColor == null) {
            fill(166, 166, 166);
        } else {
            fill(currentColor[0], currentColor[1], currentColor[2]);
        }
        rect(CURRENT_X1, CURRENT_Y1, CURRENT_W, CURRENT_H);

        currentColor = game.getCurrentTargetColor();
        if ( currentColor == null) {
            fill(166, 166, 166);
        } else {
            fill(currentColor[0], currentColor[1], currentColor[2]);
        }
        rect(TARGET_X1, TARGET_Y1, TARGET_W, TARGET_H);
    }
    
    private boolean[] colorState;
    
    private static final float RED_X1 = 87.0f;
    private static final float RED_Y1 = 55.0f;
    private static final float RED_X2 = 145.0f;
    private static final float RED_Y2 = 53.0f;
    private static final float RED_X3 = 172.0f;
    private static final float RED_Y3 = 277.0f;
    private static final float RED_X4 = 87.0f;
    private static final float RED_Y4 = 276.0f;

    private float blue_X_Top;
    private float blue_Y_Top;
    private float blue_X_Bottom;
    private float blue_Y_Bottom;
    private static final float BLUE_X1 = 148.0f;
    private static final float BLUE_Y1 = 56.0f;
    private static final float BLUE_X2 = 174.0f;
    private static final float BLUE_Y2 = 278.0f;
    private static final float BLUE_X3 = 220.0f;
    private static final float BLUE_Y3 = 265.0f;
    private static final float BLUE_X4 = 223.0f;
    private static final float BLUE_Y4 = 56.0f;
    
    private float yellow_X_Top;
    private float yellow_Y_Top;
    private float yellow_X_Bottom;
    private float yellow_Y_Bottom;
    private static final float YELLOW_X1 = 239.0f;
    private static final float YELLOW_Y1 = 63.0f;
    private static final float YELLOW_X2 = 297.0f;
    private static final float YELLOW_Y2 = 62.0f;
    private static final float YELLOW_X3 = 292.0f;
    private static final float YELLOW_Y3 = 283.0f;
    private static final float YELLOW_X4 = 230.0f;
    private static final float YELLOW_Y4 = 284.0f;
    
    private static final float CURRENT_X1 = 134.0f;
    private static final float CURRENT_Y1 = 289.0f;
    private static final float CURRENT_X2 = 270.0f;
    private static final float CURRENT_Y2 = 460.0f;
    private static final float CURRENT_W = CURRENT_X2 - CURRENT_X1;
    private static final float CURRENT_H = CURRENT_Y2 - CURRENT_Y1;
    
    private static final float TARGET_X1 = 137.0f;
    private static final float TARGET_Y1 = 586.0f;
    private static final float TARGET_X2 = 291.0f;
    private static final float TARGET_Y2 = 742.0f;
    private static final float TARGET_W = TARGET_X2 - TARGET_X1;
    private static final float TARGET_H = TARGET_Y2 - TARGET_Y1;
    
    private boolean flash;
    private static final int FLASH_RATE = 10;
    
    private static final int RED_STATE = 0;
    private static final int BLUE_STATE = 1;
    private static final int YELLOW_STATE = 2;
    private static final int RED_TARGET = 3;
    private static final int BLUE_TARGET = 4;
    private static final int YELLOW_TARGET = 5;
    private static final int MUST_HIT_RED = 6;
    private static final int MUST_HIT_BLUE = 7;
    private static final int MUST_HIT_YELLOW = 8;
}
/** The main player character. */
public class Player extends Ball {
    public Player(float x, float y, Field playingField, Main game)
    {
        super(x, y, PLAYER_WIDTH, PLAYER_HEIGHT, PLAYER_RADIUS, 
            OTHER, PLAYER_HITBOX_OFFSET, PLAYER_HITBOX_SIZE, 
            playingField, game);
        
        type = PLAYER;
        
        minXVel = 0.3f;
        minYVel = 0.3f;
        maxXVel = 10.0f;
        maxYVel = 10.0f;
        angle = 0.0f;
        angleOffset = 0.0f;
        angleBase = 0.0f;
        
        invincible = true;
        deadTime = PLAYER_INVINCIBLE_TIME;
        grabNextAngleAsBase = true;
        
        img = null;
        if (robo_images == null) {
            robo_images = new PImage[] {
                loadImage("/data/graphics/robo_001.png")
            };
        }
        robo_sprite = new Sprite(robo_images, true, true, 2, 0);
        if (bot_images == null) {
            bot_images = new PImage[] {
                loadImage("/data/graphics/bot_00.png"),
                loadImage("/data/graphics/bot_01.png"),
                loadImage("/data/graphics/bot_02.png"),
                loadImage("/data/graphics/bot_03.png"),
                loadImage("/data/graphics/bot_04.png"),
                loadImage("/data/graphics/bot_05.png")
            };
        }
        bot_sprite = new Sprite(bot_images, true, true, 4, 0);
        
        this.explosionHitbox = new HitBox(
            this.x - PLAYER_RADIUS + PLAYER_EXPLOSION_HITBOX_OFFSET, 
            this.y - PLAYER_RADIUS + PLAYER_EXPLOSION_HITBOX_OFFSET, 
            PLAYER_EXPLOSION_HITBOX_SIZE, PLAYER_EXPLOSION_HITBOX_SIZE);
    }
    
    public void update() {
        if (invincible && (--deadTime == 0)) {
            invincible = false;
        }
        
        handleKeys();
        moveMe();
        checkCollision();
    }
    
    public void draw() {
        if (!invincible || ((frameCount % 6) != 0)) {
            drawMe();
        }
    }
    
    private void drawMe() {
        setColor();
        if (robot) {
            ellipse(x, y, PLAYER_WIDTH, PLAYER_HEIGHT);
        } else {
            ellipse(x, y, PLAYER_WIDTH_SLIM_ROBOT, PLAYER_HEIGHT_SLIM_ROBOT);
        }
        
        pushMatrix(); // save the current state
        translate (x, y); // the new center is the ball location
        rotate(angle + angleOffset - angleBase); // rotates the translation
        imageMode(CENTER); // means the image will be centered
        image((robot ? robo_sprite : bot_sprite).getImg(frameCount), 0, 0); // the 0, 0 are the center of the new translated area
        popMatrix(); // resets things to how they were before translate
        imageMode(CORNER);
        //image(robo_sprite.getImg(frameCount), x - xOffset, y - yOffset);
        
        if (Debug) {
            noFill();
            hitbox.draw();
            explosionHitbox.draw();
        }
    }
    
    public void moveMe() {
        super.moveMe();
        explosionHitbox.move(
            this.x - PLAYER_RADIUS + PLAYER_EXPLOSION_HITBOX_OFFSET,
            this.y - PLAYER_RADIUS + PLAYER_EXPLOSION_HITBOX_OFFSET);
    }
    
    public void moveMe(float x, float y) {
        super.moveMe(x, y);
        explosionHitbox.move(
            this.x - PLAYER_RADIUS + PLAYER_EXPLOSION_HITBOX_OFFSET,
            this.y - PLAYER_RADIUS + PLAYER_EXPLOSION_HITBOX_OFFSET);
    }
    
    protected void setColor() {
        int[] c = game.getCurrentMixedColor();
        if (c != null) {
            fill(c[0], c[1], c[2]);
        } else {
            noFill();
        }
        
        if (halo) {
            c = game.getCurrentTargetColor();
            if (c != null) {
                stroke(c[0], c[1], c[2]);
            } else {
                stroke(255, 255, 255);
            }
            strokeWeight(6);
        } else if (robot) {
            stroke(255, 255, 255);
            strokeWeight(6);
        } else {
            noStroke();
        }
    }
    
    
    
    
    private void handleKeys() {
        boolean isHooked = game.isHookedOnSomething();
        
        if ((leftArrowKeyDown || aKeyDown) && isHooked) {
            xVel += -(xAccel);
        } else if (xVel < 0) {
            xVel += xDecel;
        }
        
        if ((rightArrowKeyDown || dKeyDown) && isHooked) {
            xVel += xAccel;
        } else if (xVel > 0) {
            xVel += -(xDecel);
        }
        
        if ((upArrowKeyDown || wKeyDown) && isHooked) {
            yVel += -(yAccel);
        } else if (yVel < 0) {
            yVel += yDecel;
        }
        
        if ((downArrowKeyDown || sKeyDown) && isHooked) {
            yVel += yAccel;
        } else if (yVel > 0) {
            yVel += -(yDecel);
        }
    }

    public void resetAngle() { 
        angle = angle + angleOffset - angleBase;
        angleOffset = 0.0f;
        angleBase = 0.0f;
        grabNextAngleAsBase = true;
    }
    
    public void setAngle(float a) {
        angle = a;
    }
    
    public void setAngleOffset(float a) {
        if (grabNextAngleAsBase) {
            angleBase = a;
            grabNextAngleAsBase = false;
        }
        
        angleOffset = a;
    }
    
    public float getAngle() {
        return angle;
    }
    
    public HitBox getExplosionHitBox() {
        return explosionHitbox;
    }
    
    public boolean isInvincible() {
        return invincible;
    }
    
    private float angle;
    private float angleOffset;
    private float angleBase;
    private int deadTime;
    private boolean invincible;
    private boolean grabNextAngleAsBase;
    private HitBox explosionHitbox;
    private static final float xAccel = 0.4f;
    private static final float yAccel = 0.4f;
    private static final float xDecel = 0.05f;
    private static final float yDecel = 0.05f;
    private static final int PLAYER_INVINCIBLE_TIME = 3 * FRAMES_PER_SECOND;
    private Sprite robo_sprite = null;
    private Sprite bot_sprite = null;
}

private static PImage[] robo_images = null;
private static PImage[] bot_images = null;
private final float PLAYER_RADIUS = 20.0f;
private final float PLAYER_WIDTH = PLAYER_RADIUS * 2.0f;
private final float PLAYER_HEIGHT = PLAYER_RADIUS * 2.0f;
private final float PLAYER_WIDTH_SLIM_ROBOT = PLAYER_WIDTH - 5;
private final float PLAYER_HEIGHT_SLIM_ROBOT = PLAYER_HEIGHT - 5;
private final float PLAYER_HITBOX_OFFSET = 0.0f;
private final float PLAYER_HITBOX_SIZE = 2.0f * (PLAYER_RADIUS - PLAYER_HITBOX_OFFSET);
private final float PLAYER_EXPLOSION_HITBOX_OFFSET = -150.0f;
private final float PLAYER_EXPLOSION_HITBOX_SIZE = 2.0f * (PLAYER_RADIUS - PLAYER_EXPLOSION_HITBOX_OFFSET);


public class PointBlock {
    public PointBlock(float x, float y, int colorID, int moveScore, 
        int multiplier, Main game) 
    {
        this.x = x;
        this.y = y;
        this.colorID = colorID;
        this.moveScore = moveScore;
        this.multiplier = multiplier;
        this.game = game;
        this.alphaFade = 255;
    }
    
    public void draw() {
        if (alphaFade > 0) {
            alphaFade -= 5;
            
            switch(colorID) {
            case RED:
                fill(random(255), 0, 0, alphaFade);
                break;
            case BLUE:
                fill(0, 0, random(255), alphaFade);
                break;
            case YELLOW:
                fill(random(50, 255), 255, 0, alphaFade);
                break;
            default:
                fill(random(255), random(255), random(255), alphaFade);
                break;
            }

            textFont(scoreFont, 12);
            textAlign(CENTER, CENTER);
            text(multiplier + "x" + moveScore, x, y);
        } else {
            game.removePointBlock(this);
        }
    }
    
    private float x, y;
    private int moveScore, multiplier, colorID, alphaFade;
    private Main game;
}
/***********************************************************************
*   Sprite
*   A class that plays animations.
***********************************************************************/

public class Sprite {
    
    /** Creates a new Sprite with the animation frames PICS, a bool
    *   LOOP that states whether the animation loops after playing or
    *   not, a bool LOOPBACKWARDS that states whether or not it loops 
    *   backwards after showing all frames going forewards, the number 
    *   of frames FPS that display per second, and the starting
    *   frame index STARTFRAME.  ~phew!~  */
    public Sprite(PImage[] pics, boolean loop, boolean loopBackwards,
        int fps, int startFrame) 
    {
        this.animFrames = pics;
        this.loop = loop;
        this.lastFrame = pics.length - 1;
        this.totalGameFrames = 0;
        this.fps = fps;
        
        this.nextFrame = this.startingFrame = startFrame;
        this.goingBackwards = false;
        this.loopBackwards = loopBackwards;

        if (fps > 0) {
            this.updateTime = nextUpdateTime = 
                (FRAMES_PER_SECOND / fps);
            if (updateTime == 0) {
                updateTime = nextUpdateTime = 1;
            }
        } else {
            //updateTime = 0;
            updateTime = -1;
        }
        
        resetDeadFrame();
    }
    
    /** Returns the next animation frame to play. */
    public PImage getImg(int frame) {
        if (deadFrame != -1) {
            return animFrames[deadFrame];
        }
        
        //if ((frame - totalGameFrames) >= updateTime) {
        if (--updateTime == 0) {
            //totalGameFrames = frame;
            updateTime = nextUpdateTime;
            
            if (goingBackwards) {
                nextFrame--;
            } else {
                nextFrame++;
            }
        }
        
        if (nextFrame > lastFrame) {
            if (loopBackwards) {
                nextFrame--;
                goingBackwards = true;
            } else if (loop) {
                nextFrame = 0;
            } else {
                deadFrame = lastFrame;
                return animFrames[deadFrame];
            }
        }
        
        if (nextFrame < 0) {
            if (loop) {
                nextFrame = 0;
                goingBackwards = false;
            } else {
                deadFrame = 0;
                return animFrames[deadFrame];
            }
        }
        
        return animFrames[nextFrame];
    }
    
    /** Resets this sprite to its original state. */
    public void reset() {
        //totalGameFrames = frameCount;
        updateTime = nextUpdateTime;
        nextFrame = startingFrame;
        goingBackwards = false;
        resetDeadFrame();
    }
    
    /** Resets the dead frame. */
    private void resetDeadFrame() {
        if (updateTime <= 0) {
            deadFrame = 0;
        } else {
            deadFrame = -1;
        }
    }
    
    /** Returns true if this animation has stopped cycling. */
    public boolean animationIsOver() {
        if (deadFrame != -1) {
            return true;
        } else {
            return false;
        }
    }
    
    int fps;
    int updateTime;
    int nextUpdateTime;
    int totalGameFrames;
    int startingFrame, nextFrame, lastFrame, deadFrame;
    boolean loop, loopBackwards, goingBackwards;
    PImage[] animFrames;
}
/** An EXPLOSION! */

public class SuperNova extends Ball {
    public SuperNova(float x, float y, int[] blastColor, int multiplier,
        Field playingField, Main game) 
    {
        super(x, y, BLAST_RADIUS_FULL, BLAST_RADIUS_FULL, BLAST_RADIUS, 
            OTHER, BLAST_HITBOX_OFFSET, BLAST_HITBOX_SIZE, 
            playingField, game);
        
        this.multiplier = multiplier;
        this.alphaFade = 255;
        this.blastColor = blastColor.clone();
        detonate();
        draw();
    }
    
    private void detonate() {
        ballCollide();
    }
    
    protected void confirmPlayerCollision(Player p, Ball b) {
    }
    
    protected void confirmBallCollision(Ball b, 
        float dy, float dx, float minDist)
    {
        b.crack();
        game.crackedBall(b);
    }
    
    public void update() {
        if (alphaFade > 0) {
            alphaFade -= 10;
        }
    }
    
    public void draw() {
        if (alphaFade > 0) {
            noStroke();
            fill(blastColor[0], blastColor[1], blastColor[2], alphaFade);
            ellipse(x, y, BLAST_RADIUS_SHORT, BLAST_RADIUS_SHORT);
            fill(blastColor[0], blastColor[1], blastColor[2], 
                (alphaFade > 0) ? alphaFade/2 : 0);
            ellipse(x, y, BLAST_RADIUS, BLAST_RADIUS);
            fill(blastColor[0], blastColor[1], blastColor[2], 
                (alphaFade > 0) ? alphaFade/3 : 0);
            ellipse(x, y, BLAST_RADIUS_MEDIUM, BLAST_RADIUS_MEDIUM);
            fill(blastColor[0], blastColor[1], blastColor[2], 
                (alphaFade > 0) ? alphaFade/4 : 0);
            ellipse(x, y, BLAST_RADIUS_FULL, BLAST_RADIUS_FULL);        
            
            if (multiplier > 1) {
                textFont(pauseFont, 100);
                fill(random(255), random(255), random(255), alphaFade);
                text(multiplier + "x", x - 40, y);
            }
        }
        
        if (Debug) {
            noFill();
            hitbox.draw();
        }
    }
    
    private int multiplier;
    private int alphaFade;
    private int[] blastColor;
    private static final float BLAST_RADIUS = 200.0f;
    private static final float BLAST_RADIUS_SHORT = BLAST_RADIUS / 2;
    private static final float BLAST_RADIUS_MEDIUM = BLAST_RADIUS * 1.5f;
    private static final float BLAST_RADIUS_FULL = BLAST_RADIUS * 2;
    //private static final float BLAST_W = BLAST_RADIUS * 2;
    //private static final float BLAST_H = BLAST_RADIUS * 2;
    private static final float BLAST_HITBOX_OFFSET = 0.0f;
    private static final float BLAST_HITBOX_SIZE = 400.0f;
    
}
public class TitleScreens {
    public TitleScreens(Main game) {
        state = splashImage = TITLE;
        this.game = game;
        
        if (screens == null) {
            screens = new PImage[] {
                loadImage("/data/graphics/title page.jpg"),
                loadImage("/data/graphics/how to play.jpg"),
                loadImage("/data/graphics/how to mix.jpg"),
                loadImage("/data/graphics/title page mouse over A.jpg"),
                loadImage("/data/graphics/title page mouse over B.jpg"),
                loadImage("/data/graphics/how to play mouse over A.jpg"),
                loadImage("/data/graphics/how to play mouse over B.jpg"),
                loadImage("/data/graphics/how to mix mouse over A.jpg")
            };
        }
    }
    
    public void update() {
        if (makeSelection) {
            switch(state) {
            case TITLE_A:
                game.startNewGame();
                break;
            case TITLE_B:
                state = HOW_TO_PLAY;
                break;
            case HOW_TO_PLAY_A:
                state = HOW_TO_MIX;
                break;
            case HOW_TO_PLAY_B:
            case HOW_TO_MIX_A:
                state = TITLE;
                break;
            }
            
            makeSelection = false;
        }
    }
    
    public void draw() {
        switch(state) {
        case TITLE:
        case TITLE_A:
        case TITLE_B:
            if (START_GAME_BUTTON.hit(mouseX, mouseY)) {
                state = TITLE_A;
            } else if (HOW_TO_PLAY_BUTTON.hit(mouseX, mouseY)) {
                state = TITLE_B;
            } else {
                state = TITLE;
            }
            break;
        case HOW_TO_PLAY:
        case HOW_TO_PLAY_A:
        case HOW_TO_PLAY_B:
            if (HOW_TO_PLAY_HELP_BUTTON.hit(mouseX, mouseY)) {
                state = HOW_TO_PLAY_A;
            } else if (HOW_TO_PLAY_BACK_BUTTON.hit(mouseX, mouseY)) {
                state = HOW_TO_PLAY_B;
            } else {
                state = HOW_TO_PLAY;
            }
            break;
        case HOW_TO_MIX:
        case HOW_TO_MIX_A:
            if (HOW_TO_MIX_BACK_BUTTON.hit(mouseX, mouseY)) {
                state = HOW_TO_MIX_A;
            } else {
                state = HOW_TO_MIX;
            }
            break;
        }
        
        image(screens[state], x, y);
        
        if ((state == TITLE) || 
            (state == TITLE_A) ||
            (state == TITLE_B)) 
        {
            fill(0, 0, 0);
            textFont(otherFont, 35);
            textAlign(CENTER, CENTER);
            text("High Score: " + game.getHighScore(), 0, 0, width, 40);
        }
        
        if (Debug) {
            switch(state) {
                case TITLE:
                case TITLE_A:
                case TITLE_B:
                    START_GAME_BUTTON.draw();
                    HOW_TO_PLAY_BUTTON.draw();
                    break;
                case HOW_TO_PLAY:
                case HOW_TO_PLAY_A:
                case HOW_TO_PLAY_B:
                    HOW_TO_PLAY_HELP_BUTTON.draw();
                    HOW_TO_PLAY_BACK_BUTTON.draw();
                    break;
                case HOW_TO_MIX:
                case HOW_TO_MIX_A:
                    HOW_TO_MIX_BACK_BUTTON.draw();
                    break;
            }
        }
    }
    
    private int state;
    private int splashImage;
    private float x = 0.0f;
    private float y = 0.0f;
    private HitBox START_GAME_BUTTON =
        new HitBox(240, 425, 856 - 240, 497 - 425);
    private HitBox HOW_TO_PLAY_BUTTON =
        new HitBox(240, 545, 856 - 240, 620 - 545);
    private HitBox HOW_TO_PLAY_HELP_BUTTON =
        new HitBox(378, 662, 819 - 378, 751 - 662);
    private HitBox HOW_TO_PLAY_BACK_BUTTON =
        new HitBox(26, 711, 222 - 26, 754 - 711);
    private HitBox HOW_TO_MIX_BACK_BUTTON =
        new HitBox(702, 665, 897 - 702, 708 - 665);
    
    /* States */
    private static final int TITLE = 0;
    private static final int HOW_TO_PLAY = 1;
    private static final int HOW_TO_MIX = 2;
    /* Other Images */
    private static final int TITLE_A = 3;
    private static final int TITLE_B = 4;
    private static final int HOW_TO_PLAY_A = 5;
    private static final int HOW_TO_PLAY_B = 6;
    private static final int HOW_TO_MIX_A = 7;
    private Main game;
}

private static PImage[] screens = null;
  static public void main(String args[]) {
    PApplet.main(new String[] { "--bgcolor=#DFDFDF", "BlenderBot" });
  }
}
