/**
 * ThreeDeePong.java
 * 
 * An applet which implements a 3D Pong program, with three levels of
 * difficulty, played against a computer opponent.
 *
 * @author John H. Doe <paladin@nyll.com>
 * @version 1.1
 */

import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class ThreeDeePong extends Applet
             implements Runnable, MouseListener, MouseMotionListener  {

// These values I've played with so it would look good in an 800x600 window
static final int width = 501, height = 401, halfWidth = width / 2, halfHeight = height / 2,
                 buttonWidth = 70, buttonHeight = 15, bButtonTop = height - 20,
                 easyLeft = 50,
                 normalLeft = halfWidth - buttonWidth / 2,
                 hardLeft = width - 119,
                 bBTextTop = height - 7,
                 aButtonTop = height - halfHeight,
                 yesLeft = halfWidth - 80,
                 noLeft = halfWidth + 10,
                 aBTextTop = aButtonTop + 13;

static final double paddleWidth = 80.0, paddleRadius = paddleWidth / 2.0, pad = 20.0,
                    ballWidth = 50.0, ballRadius = ballWidth / 2.0;

// The thread to run on
Thread t;
// Draw offscreen first
Graphics offScreen;

Image titleImage, offScreenImage;

// x and y are the position of your paddle
int x, y,
    yourScore = 0,
    itsScore = 0,
    difficulty = 0;

double ballX, ballY, ballZ, dX, dY, dZ,
       itsX, itsY, diffFactor, maxMove;

// Utility variables
boolean intro = false, afterPlay = false, playAgain = true, infield, hit;

// Sounds
AudioClip wall, paddle, out, win, lose;

public void init() {
    wall = getAudioClip(getCodeBase(), "wall.au");
    wall.play();
    paddle = getAudioClip(getCodeBase(), "paddle.au");
    paddle.play();
    out = getAudioClip(getCodeBase(), "out.au");
    out.play();
    // Don't play these yet
    win = getAudioClip(getCodeBase(), "win.au");
    lose = getAudioClip(getCodeBase(), "lose.au");
    offScreenImage = createImage(width, height);
    offScreen = offScreenImage.getGraphics();
    // Here, we translate 0, 0 to center of screen
    offScreen.translate(halfWidth, halfHeight);
    resize(width, height);
    addMouseListener(this);
    addMouseMotionListener(this);
}

public void start() {
    if(t == null) {
        t = new Thread(this);
        t.start();
        t.setPriority(Thread.MAX_PRIORITY);
    }
}

public void stop() {
    if(t != null && t.isAlive())
        t.stop();
    t = null;
}

public void run() {
    Graphics g = getGraphics();
    int playTo = 10;
    String winloseStr;

    // Outer loop - plays game(s)
    while(playAgain) {
        intro = true;
        intro();
        // I thought these were fair values
        diffFactor = 2.55 * (double)difficulty;
        maxMove = 1.75 * diffFactor; // Maximum move for the computer's paddle
    
        
        itsX = 0.0;
        itsY = 0.0;
        // Game loop - plays point(s)
        while(yourScore < playTo && itsScore < playTo) {
            // Start the ball randomly moving
            ballX = Math.random() * ((double)width - ballWidth) + ballRadius - (double)halfWidth;
            ballY = Math.random() * ((double)height - ballWidth) + ballRadius - (double)halfHeight;
            ballZ = halfDepth;
            // I've played with these, you can too
            dX = Math.random() * 3.0 + diffFactor / 2.0;
            dY = Math.random() * 3.0 + diffFactor / 2.0;
            dZ = 5.0 + diffFactor;
            
            infield = true;
            hit = false;
            // Main loop: while in playing area, keep checking and updating
            while(infield) {
                try {
                    t.sleep(40); // Used 40 for smooth play across platforms
                } catch(InterruptedException e) {}
                checkBounce();
                updateIts();
                updateBall();
                repaint();
            }
            // Out of field, wait a sec
            try {
                t.sleep(750);
            } catch(InterruptedException e) {}
        }
        if(yourScore == playTo) {
            winloseStr = "YOU WIN!";
            win.play();
        } else {
            winloseStr = "YOU LOSE";
            lose.play();
        }
        
        // Ask if they want to play again
        g.setColor(Color.white);
        g.setFont(new Font("SansSerif", Font.PLAIN, 48));
        g.drawString(winloseStr, halfWidth - g.getFontMetrics().stringWidth(winloseStr) / 2, 80);
        afterPlay = true;
        askAfter();
        yourScore = itsScore = 0;
        difficulty = 0;
    }
}

public void paint(Graphics g) {
    drawAll();
    g.drawImage(offScreenImage, 0, 0, this);
}

public void update(Graphics g) {
    paint(g);
}

public void drawAll() {
    int left = -halfWidth, top = -halfHeight, right = halfWidth, bottom = halfHeight;
    
// opposite paddle, walls, ball, player paddle, in that order

    // background
    offScreen.setColor(Color.black);
    offScreen.fillRect(left, top, width, height);

    // its paddle
    int itsLeft = getPerspec(depth, itsX),
        itsTop = getPerspec(depth, itsY),
        itsPaddleWidth = getPerspec(depth, paddleWidth),
        itsPaddleRadius = getPerspec(depth, paddleWidth / 2.0);
    offScreen.setColor(Color.white);
    offScreen.drawOval(itsLeft - itsPaddleRadius, itsTop - itsPaddleRadius,
                       itsPaddleWidth, itsPaddleWidth);
    offScreen.drawOval(itsLeft - itsPaddleRadius, itsTop - itsPaddleRadius,
                       itsPaddleWidth, itsPaddleWidth);
    offScreen.drawLine(itsLeft, itsTop - itsPaddleRadius, itsLeft, itsTop + itsPaddleRadius);
    offScreen.drawLine(itsLeft - itsPaddleRadius, itsTop, itsLeft + itsPaddleRadius, itsTop);

    // walls
    offScreen.setColor(Color.white);
    offScreen.drawRect(left, top, width, height);
    int farLeft = getPerspec(depth, (double)left),
        farTop = getPerspec(depth, (double)top),
        farRight = getPerspec(depth, (double)right),
        farBottom = getPerspec(depth, (double)bottom);
    offScreen.drawRect(farLeft, farTop, getPerspec(depth, (double)width), 
                       getPerspec(depth, (double)height));
    offScreen.drawLine(left, top, farLeft, farTop);
    offScreen.drawLine(left, bottom, farLeft, farBottom);
    offScreen.drawLine(right, top, farRight, farTop);
    offScreen.drawLine(right, bottom, farRight, farBottom);
    
    // notches
    int l = getPerspec(ballZ, (double)left),
        r = getPerspec(ballZ, (double)right),
        t = getPerspec(ballZ, (double)top),
        b = getPerspec(ballZ, (double)bottom),
        bX = getPerspec(ballZ, ballX),
        bY = getPerspec(ballZ, ballY);
    offScreen.drawLine(l, bY, l, bY);
    offScreen.drawLine(r, bY, r, bY);
    offScreen.drawLine(bX, t, bX, t);
    offScreen.drawLine(bX, b, bX, b); 

    // ball
    if(infield) {
        offScreen.setColor(hit ? Color.white : Color.black);
        offScreen.fillOval(getPerspec(ballZ, ballX - ballRadius),
                           getPerspec(ballZ, ballY - ballRadius),
                           getPerspec(ballZ, ballWidth) + 1,
                           getPerspec(ballZ, ballWidth) + 1);
        offScreen.setColor(Color.white);
        offScreen.drawOval(getPerspec(ballZ, ballX - ballRadius),
                           getPerspec(ballZ, ballY - ballRadius),
                           getPerspec(ballZ, ballWidth),
                           getPerspec(ballZ, ballWidth));
        offScreen.drawOval(getPerspec(ballZ, ballX - ballRadius),
                           getPerspec(ballZ, ballY - ballRadius),
                           getPerspec(ballZ, ballWidth),
                           getPerspec(ballZ, ballWidth));
       hit = false;
    }
    
    // your paddle
    offScreen.setColor(Color.white);
    offScreen.drawOval(x - (int)paddleRadius, y - (int)paddleRadius, (int)paddleWidth, (int)paddleWidth);
    offScreen.drawOval(x - (int)paddleRadius, y - (int)paddleRadius, (int)paddleWidth, (int)paddleWidth);
    offScreen.drawLine(x, y - (int)paddleRadius, x, y + (int)paddleRadius);
    offScreen.drawLine(x - (int)paddleRadius, y, x + (int)paddleRadius, y);
    
    //scores
    offScreen.drawString(Integer.toString(yourScore), -halfWidth + 10, -halfHeight + 24);
    offScreen.drawString(Integer.toString(itsScore), halfWidth - 40, -halfHeight + 24);
}

/**
 * Set to use with the getPerspective() method.
 */
public double perspective = 3.0, depth = 700.0;
// Utility
private double halfDepth = depth / 2.0;

/**
 * Critical utility method to give the illusion of depth: this is used
 * for all drawing in this applet. It only works if the center of the 
 * screen is set to be 0, 0, and returns x and y values on screen if 
 * you enter the z value as dist, and the x or y value to translate as
 * here.
 *
 * @param dist the z value to get perspective for
 * @param here the x or y value to transform
 * @return x or y value adjusted for perspective
 */
public int getPerspec(double dist, double here) {
    return (int)Math.round(here * depth / (depth + (perspective - 1) * dist));
}

private void checkBounce() {
    double checkX = ballX - (double)x, checkY = ballY - (double)y,
           checkItsX = ballX - itsX, checkItsY = ballY - itsY;
    final double english = 7.0, boost = 15.0; // You can play with these
    
    // Using Pythagorean rule, check to see if bounced off paddle or if it
    // went out of bounds instead.
    if(ballZ < ballRadius)
        if(Math.sqrt(checkX * checkX + checkY * checkY) <= paddleRadius + pad) {
            // Hit paddle, add english
            dX += checkX / english * (double)difficulty;
            dY += checkY / english * (double)difficulty;
            hit = true;
            dZ *= -1.0;
            paddle.play();
        } else {
            // Went out of bounds
            infield = false;
            itsScore++;
            out.play();
        }
    // Now check for computer
    if(ballZ > depth - ballRadius)
        if(Math.sqrt(checkItsX * checkItsX + checkItsY * checkItsY) <= paddleRadius + pad) {
            dX += checkItsX / english * (double)difficulty;
            dY += checkItsY / english * (double)difficulty;
            hit = true;
            // Hit, add speed to ball in proportion to difficulty
            dZ *= (-1.0 - (double)difficulty / boost);
            paddle.play();
        } else {
            infield = false;
            yourScore++;
            out.play();
        }

    // Check for wall contact
    if(ballX < (double)-halfWidth + ballRadius || ballX > (double)halfWidth - ballRadius) {
        dX *= -1.0;
        wall.play();
    }
    if(ballY < (double)-halfHeight + ballRadius || ballY > (double)halfHeight - ballRadius) {
        dY *= -1.0;
        wall.play();
    }
}

private void updateIts() {
    // Move the computer's paddle
    if(Math.abs(ballX - itsX) < maxMove)
        itsX = ballX;
    else
        itsX += (ballX - itsX > 0) ? maxMove : -maxMove;
    if(Math.abs(ballY - itsY) < maxMove)
        itsY = ballY;
    else
        itsY += (ballY - itsY > 0) ? maxMove : -maxMove;
}    

private void updateBall() {
    ballX += dX;
    ballY += dY;
    ballZ += dZ;
}

public void mouseClicked(MouseEvent e) {}

public void mousePressed(MouseEvent e) {
    int x = e.getX(), y = e.getY();
    if(intro) {
        if(checkBounds(easyLeft, bButtonTop, x, y))
            difficulty = 1;
        else if(checkBounds(normalLeft, bButtonTop, x, y))
            difficulty = 2;
        else if(checkBounds(hardLeft, bButtonTop, x, y))
            difficulty = 4;
    }
    if(afterPlay) {
        if(checkBounds(yesLeft, aButtonTop, x, y)) {
            showStatus("Play Again");
            playAgain = true;
            afterPlay = false;
        } else if(checkBounds(noLeft, aButtonTop, x, y)) {
            playAgain = false;
            afterPlay = false;
        }
    }
}

private boolean checkBounds(int left, int top, int x, int y) {
    return (x >= left && x < left + buttonWidth && y >= top && y < top + buttonHeight);
}

public void mouseDragged(MouseEvent e) {}

public void mouseMoved(MouseEvent e) {
    int i = e.getX(), j = e.getY();
    // Translate for 0, 0 being at center of screen
    x = i - halfWidth;
    y = j - halfHeight;
}

public void mouseReleased(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}

private void intro() {
    Graphics g = getGraphics();
    
    g.setFont(new Font("SansSerif", Font.PLAIN, 14));
    String blinkStr = "CHOOSE LEVEL OF DIFFICULTY";
    int sX = halfWidth - g.getFontMetrics().stringWidth(blinkStr) / 2;
    boolean on = true;
    while(difficulty == 0) {
        g.setColor(on ? Color.white : Color.black);
        g.drawString(blinkStr, sX, height - 40);
        g.setColor(Color.white);
        g.drawRect(easyLeft, bButtonTop, buttonWidth, buttonHeight);
        g.drawString("EASY", easyLeft + 18, bBTextTop);
        g.drawRect(normalLeft, bButtonTop, buttonWidth, buttonHeight);
        g.drawString("NORMAL", halfWidth - 28, bBTextTop);
        g.drawRect(hardLeft, bButtonTop, buttonWidth, buttonHeight);
        g.drawString("HARD", width - 103, bBTextTop);
        try {
            Thread.sleep(500);
        } catch(InterruptedException e) {}
        on = !on;
    }
    intro = false;
}

private void askAfter() {
    Graphics g = getGraphics();
    
    g.setFont(new Font("SansSerif", Font.PLAIN, 14));
    String askStr = "PLAY AGAIN?";
    g.setColor(Color.white);
    g.drawString(askStr, halfWidth - g.getFontMetrics().stringWidth(askStr) / 2,
                 halfHeight - 12);
    g.drawRect(yesLeft, aButtonTop, buttonWidth, buttonHeight);
    g.drawString("YES", yesLeft + 23, aBTextTop);
    g.drawRect(noLeft, aButtonTop, buttonWidth, buttonHeight);
    g.drawString("NO", noLeft + 27, aBTextTop);
    while(afterPlay) {
        try {
            t.sleep(100);
        } catch(InterruptedException e) {}
    }
}

}

