/**	
	Company:		Shout Interactive
	Project:		Shout3D Core Implementation
	Class:			ModDunkPanel 
	Date:			April 26, 1999
	Description:	Class for Panel that displays  Mod Dunk Game
	(C) Copyright Shout Interactive, Inc. - 1997/1998/1999 - All rights reserved
 */

package applets;

import java.applet.*;
import java.awt.*;
import java.util.Hashtable;
import java.io.*;
import java.util.Date;
import java.net.URL;
import shout3d.*;
import shout3d.math.*;
import shout3d.core.*;

/**
 * ModDunk Game panel 
 * 
 * A little game.
 * Click the mouse down --> Ward Hole will pull his arm back
 * Release the mouse --> Ward Hole will let fly the plunger
 * 
 * The object is to time the release so that the plunger hits the
 * target. If it does, an animation is played in which Jo falls into
 * the dunk tank.
 * 
 * (Note: Cheaters can use the right mouse button to always win)
 * 
 * @author Paul Isaacs
 * @author Jim Stewartson
 * @author Dave Westwood
 */

public class ModDunkPanel extends Shout3DPanel implements DeviceObserver, RenderObserver {
	
	//{{ Shout3DApplet methods
	/**
	 * Constructor
	 */
	public ModDunkPanel(Shout3DApplet applet){
		super(applet);
	}
	
	// Pointers to handy nodes
	//
	Transform	throwingArm;		 //	USE Warh_Arm1_L
	Interpolator warh_Arm1_R_Interp; // interpolator for Warh_Arm1_R
	Interpolator warh_Arm2_R_Interp; // interpolator for Warh_Arm2_R
	Transform	plungerRoot;		 // USE PLUNGER_ROOT
	Transform	stuntPlunger;		 // USE STUNT_PLUNGER
	Transform target;				 // Use Prop_Target
	Transform targetMover;		     // Use Prop_Target_Mover

	// Interpolators for jo
	Interpolator jo_Hip_PosInterp;
	Interpolator jo_Hip_RotInterp;
	Interpolator jo_Leg2_L_RotInterp;
	Interpolator jo_Leg2_R_RotInterp;
	Interpolator jo_Chest_RotInterp;
	Interpolator jo_Arm1_L_RotInterp;
	Interpolator jo_Arm1_R_RotInterp;
		
	Interpolator prop_DivingBoard_RotInterp; //interpolator for Prop_DivingBoard
	Interpolator prop_DivingBoard_PosInterp; //interpolator for Prop_DivingBoard

	// Paths to handy paths
	Node  stuntPlungerPath[];
  
	/**
	 *
	 * This method is automatcially called by the parent class Shout3DPanel
	 * at the correct time during initialize().
	 * 
	 * Subclasses should implement this to perform any custom initialization tasks.
	 */
	public void customInitialize() {
		
		// Load the audio for the gong.
		String gongSound	= applet.getParameter("gongSound");
		if (gongSound != null){
			gong = applet.getAudioClip(applet.getCodeBase(), gongSound);
		}
	
		// To make it easy to find interpolators that effect specific 
		// nodes, collect them all into a handy list
		collectInterpolators();
		
		// Disconnect output of all time sensors.
		// Instead, we'll control the fractions of all interpolators
		// through the simulation.
		disconnectAllTimeSensors();
		
		Node testNode;
		// Get pointers to interesting nodes
		
		// WARD HOLE character:
		//
		// Left Upper Arm, to rotate using physics
		if ((throwingArm = (Transform) getNodeByName("Warh_Arm1_L")) == null)
			throw new Shout3DException("could not find node named throwingArm");
		
		//		
		// Right Arm interpolators, to be controlled by simulation.
		if ((testNode = (Transform) getNodeByName("Warh_Arm1_R")) == null)
			throw new Shout3DException("could not find node named Warh_Arm1_R");
		if ((warh_Arm1_R_Interp = findOrientationInterpolator(testNode)) == null)
			throw new Shout3DException("could not find node named Arm1_R_Interp");
		if ((testNode = (Transform) getNodeByName("Warh_Arm2_R")) == null)
			throw new Shout3DException("could not find node named Warh_Arm2_R");
		if ((warh_Arm2_R_Interp = findOrientationInterpolator(testNode)) == null)
			throw new Shout3DException("could not find node named warh_Arm2_R_Interp");
		
		// PLUNGER
		//
		// Transform to use in placing the plunger
		if ((plungerRoot = (Transform) getNodeByName("PLUNGER_ROOT")) == null)
			throw new Shout3DException("could not find node named PLUNGER_ROOT");
		//
		// This keeps track of where the plungerRoot should be
		// placed during the WAITING, WINDUP, and THROWING stages
		if ((stuntPlunger = (Transform) getNodeByName("STUNT_PLUNGER")) == null)
			throw new Shout3DException("could not find node named STUNT_PLUNGER");
		// Get path to the stunt plunger, which will be required to 
		// find transform between worldspace and the plunger
		// root node is stuntPlungerPath[0] and 
		// stuntPlunger is stuntPlungerPath[stuntPlungerPath.length-1]
		Searcher mySearcher = getNewSearcher();
		mySearcher.setNode(stuntPlunger);
		stuntPlungerPath = mySearcher.searchFirst(getScene());

		// JO character
		//
		// Find the interpolators affected various nodes in Jo's body
		if ((testNode = (Transform)getNodeByName("Jo_Hip")) == null)
			throw new Shout3DException("could not find node named Jo_Hip");
		jo_Hip_PosInterp = findPositionInterpolator(testNode);
		jo_Hip_RotInterp = findOrientationInterpolator(testNode);
		if ((testNode = (Transform)getNodeByName("Jo_Leg2_L")) == null)
			throw new Shout3DException("could not find node named Jo_Leg2_L");
		jo_Leg2_L_RotInterp = findOrientationInterpolator(testNode);
		if ((testNode = (Transform)getNodeByName("Jo_Leg2_R")) == null)
			throw new Shout3DException("could not find node named Jo_Leg2_R");
		jo_Leg2_R_RotInterp = findOrientationInterpolator(testNode);
		if ((testNode = (Transform)getNodeByName("Jo_Chest")) == null)
			throw new Shout3DException("could not find node named Jo_Chest");
		jo_Chest_RotInterp = findOrientationInterpolator(testNode);
		if ((testNode = (Transform)getNodeByName("Jo_Arm1_L")) == null)
			throw new Shout3DException("could not find node named Jo_Arm1_L");
		jo_Arm1_L_RotInterp = findOrientationInterpolator(testNode);
		if ((testNode = (Transform)getNodeByName("Jo_Arm1_R")) == null)
			throw new Shout3DException("could not find node named Jo_Arm1_R");
		jo_Arm1_R_RotInterp = findOrientationInterpolator(testNode);
		
		// DIVING BOARD
		//
		if ((testNode = (Transform) getNodeByName("Prop_DivingBoard")) == null)
			throw new Shout3DException("could not find node named Prop_DivingBoard");
		prop_DivingBoard_RotInterp = findOrientationInterpolator(testNode);
		prop_DivingBoard_PosInterp = findPositionInterpolator(testNode);
		
		
		// TARGET
		//
		if ((target      = (Transform) getNodeByName("Prop_Target")) == null)
			throw new Shout3DException("could not find node named Prop_Target");
		if ((targetMover = (Transform) getNodeByName("Prop_Target_Mover")) == null)
			throw new Shout3DException("could not find node named Prop_Target_Mover");
		// Calculate center of target
		calculateTargetCenter();
		
		// Register to receive deviceInput
		// Argument "DeviceInput" means to watch for all devices
		addDeviceObserver(this, "DeviceInput", null);		

		getRenderer().addRenderObserver(this, null);
	}

	/**
	 *  Finalize
	 */
	protected void finalize() throws Throwable { 
		myInterpolators = null;
		removeDeviceObserver(this,"DeviceInput");
		super.finalize();
	}
	
	//}} Shout3DApplet methods
	
	//{{ DeviceObserver methods
	/**
	 * When mouse goes DOWN, Ward Hole's arm starts to move back.
	 * When mouse goes UP, the arm is released.
	 * 
	 * Pressing the 't' or 'T' key toggles the target's swaying motion.
	 */
	public boolean onDeviceInput(DeviceInput di, Object userData) {
		if (di.isOfType("MouseInput")) {
			MouseInput mi = (MouseInput) di;
			switch(mi.which) {
			case MouseInput.DOWN: return onMouseDown(mi.x, mi.y, mi.button);
			case MouseInput.UP:   return onMouseUp(mi.x, mi.y, mi.button);
			}
		}
		else if (di.isOfType("KeyboardInput")) {
			KeyboardInput ki = (KeyboardInput) di;
			if (ki.which == KeyboardInput.PRESS) {
				return onKeyDown(ki.key);
			}
		}
		return false;
	}
	//}} Device response methods
		
	/////////////////////////////////////////////////////////
	// Mod Dunk Game Specific Methods Begins here
	
	/**
	 * Mouse goes down
	 * 
	 * @param x the x position of the mouse down event
	 * @param y the y position of the mouse down event
	 */
	public boolean onMouseDown(int x, int y, int whichButton){
				
		// A click always pulls back the arm.
		// Release is timed to occur at releaseTime after the
		// release.
		pullBackArm();
			
		// Pulling back with right mouse lets you win.
		alwaysAWinner = (whichButton == 1);
	
		// This panel always does something on mouse down...
		return true;
	}
	
	/**
	 * Mouse goes up.
	 * 
	 * @param x the x position of the mouse down event
	 * @param y the y position of the mouse down event
	 */
	public boolean onMouseUp(int x, int y, int whichButton){		
		// release the arm.  
		releaseArm();
		return true;
	}
	
	/**
	 * Key goes down.
	 * 
	 * @param key the key
	 */
	public boolean onKeyDown(int key){
		// The 't' key toggles the target motion.
		if (key == 't' || key == 'T'){
			if (isTargetMoving)
				setTargetMoving(false);
			else
				setTargetMoving(true);
		}
		// Don't want to stop anything additional from happening, return false
		return false; 
	}

	// When right mouse is used to throw, this is set to TRUE
	// and jo always dunks:
	boolean alwaysAWinner = false;

	// State of the Thrower
	//
	static final int WAITING	= 0;
	static final int WINDUP	= 1;
	static final int THROWING = 2;
	static final int AIRBORN  = 3;
	int		curThrowerState	= WAITING;

	// State of poor little Jo's dunk
	boolean   isNowDunking = false;
	float	  DUNK_DURATION = .666f;

	// Throwing arm properties
	//
	float	windupAcceleration	= -10;  // Rate at which windup occurs on mouse down
	float	throwCenterAngle	= 4;    // Resting angle of arm when not pulled
	float	throwSpringK		= 35;   // Spring constant that brings arm to center
	float	throwDamperK		= 1;	// Damping constant that affects arm
	float	minWindupAngle		= 1;    // Farthest that arm will retreat when mouse is pressed and held.

	// Throwing arm state
	// 
	// current values at any given time.
	float		throwArmAngle	= 4;
	float		throwArmVelo	= 0;
	float		throwArmAccel	= 0;
	// Tuning this number makes the arm go faster/slower when released.
	static final float PLUNGER_VELO_HACK_FACTOR = 1.2f; //1.5f; //2.0f;

	// Plunger state
	float		plungerPos[]	= { 0f, 0f, 0f };
	float		plungerVelo[]	= { 0f, 0f, 0f };
	float		plungerAcc[]	= { 0f, -9.8f, 0f };  // gravity downward

	float		plungerRotXAngle = 0f;				  // For making 1D rotation easier.
	float		plungerAngVelo	= 0;
	float		plungerAngAcc	= 0;
	boolean	isPlungerTestedPastTarget = false;

	// Target Properties
	float		targetCenter[]		         = { 0f, 0f, 0f };
	float		targetRadiusSquared = 0.75f * 0.75f;
	float		targetDepth			= 0.1f;
	boolean     isTargetMoving = false;
	double      targetMotionStartTime = 0;
	float	    targetRotOmega = 1;
	float		targetRotAmplitude = 1;	  
	float		targetRotPhase = 0;

	// Time and timing
	// 
	double	prevTime	= 0;
	double	curTime	= 0;
	float	timeBetweenUpdates	= 1;
	float	maxTimeStep		= 0.1f;
	double	dunkStartTime = 0;
	double				startThrowTime = 0;
	static final float	THROW_TO_RELEASE_TIME = .26f; //.3f;

	// Sound
	AudioClip gong;	
	
	/**
	 * This is called once per frame, and updates everything
	 * based on what's happened so far. 
	 */
	void updateGame() {
		// Animate the target
		if (isTargetMoving)
			moveTarget();
		
		// Simulates motion of arm using physics.
		// If AIRBORN, will also simulate trajectory of plunger.
		// The result is a set of positions and orientations that
		// must then be put into the scene graph in the rest of this
		// method
		performSimulation();
		
		// Sets the rotation of the throwing shoulder based on 
		// rotation calculated in performSimulation.
		setThrowingArmRotation();

		// Right arm is moved by setting the fraction of an 
		// animation so that it is coordinated with the throwing arm's motion.
		animateFollowerArm();

		if (curThrowerState != AIRBORN) {
			// Makes the plunger follow the stuntPlunger, a dummy transform
			// at the end of Ward Hole's arm.
			makePlungerHeldByWarhol();
		}
		else {
			// Places the plunger based on simulated trajectory
			// calculated in performSimulation()
			makePlungerFollowTrajectory();
		}
		
		// Is it time to switch from THROWING to AIRBORN?
		if (curThrowerState == THROWING) {
			if ((curTime - startThrowTime) > THROW_TO_RELEASE_TIME)
				releasePlunger();
		}
		
		// If this is the frame where the target was hit, start the dunking!
		if (didPlungerHitTarget()) {
			startDunk();
			// This makes the plunger bounce backwards
			plungerPos[2] = targetCenter[2];
			plungerVelo[2] *= -0.5; 
			plungerAngVelo *= -0.5;
			
			// After the first target hit, the
			// target starts animating:
			if (isTargetMoving == false)
				setTargetMoving(true);
		}
		if (isNowDunking) {
			animateDunk();
		}
	}

	/**
	 * Toggles whether the target is swaying back and forth.
	 */
	public void setTargetMoving(boolean onOff) {
	  isTargetMoving = onOff;
	  if (onOff) {
		  targetMotionStartTime = getClock().getAbsoluteTime();
	  }
	  else {
		  // remember the phase as the time we're at now, so if 
		  // target is restarted, things will pick up nicely.
		  float rotTime = (float)(targetRotPhase + (curTime - targetMotionStartTime));
		  targetRotPhase = rotTime; 
		  
	  }
	}
	/**
	 * Called when the target is moving to advance it based on
	 * a sin function and the time passed since it started moving
	 */
	void moveTarget() {
	  float rotTime = (float)(targetRotPhase + (curTime - targetMotionStartTime));
	  float targetAngle = (float) (targetRotAmplitude * Math.sin( targetRotOmega * rotTime));
	  // load axis/angle for rotation about z of targetAngle
		targetMover.rotation.getValue()[0] = 0;
		targetMover.rotation.getValue()[1] = 0;
		targetMover.rotation.getValue()[2] = 1;
		targetMover.rotation.getValue()[3] = targetAngle;
		//notify
		targetMover.rotation.setValue(targetMover.rotation.getValue());
	}

	/** Simulates motion of arm using physics.
	 *  If AIRBORN, will also simulate trajectory of plunger.
	 *  The result is a set of positions and orientations that
	 *  must then be put into the scene graph in the rest of this
	 *  method
	 */
	void performSimulation()
	{
		// Save the time since last update. Other methods use this.
		timeBetweenUpdates = (float)(curTime - prevTime);
		
		// Execute simulation in small time steps until up to current time.
		// Required since one large timestep can wreak havoc,
		// especially when de-iconifying after a while.
		float myTimeBetween = timeBetweenUpdates;
		while ( myTimeBetween > 0 ) {

			if ( myTimeBetween < maxTimeStep )
				performSimulationStep(myTimeBetween);
			else
				performSimulationStep(maxTimeStep);

			myTimeBetween = myTimeBetween - maxTimeStep;
		}

	}

	/** 
	 * Moves simulation forward in time by calculating an accelerations,
	 * then advancing the positions and velocities accordingly.
	 * 
	 */
	void performSimulationStep(float deltaTime)
	{
		updateArmAcceleration();

		doNumericalIntegration(deltaTime);

		constrainThrowingArm();
		constrainPlunger();
	}

	/**
	 * Depending on the state of the arm, different accelerations are used. 
	 */
	void updateArmAcceleration()
	{    
		if (curThrowerState == WAITING) {
			// do nothing
		}
		else if (curThrowerState == WINDUP) {
			// rotate backwards by predetermined acceleration.
			throwArmAccel = windupAcceleration;
		}
		else if (curThrowerState == THROWING ||
				 curThrowerState == AIRBORN) {
			// Calculate acceleration based on standard simple spring/damper equation.
			throwArmAccel = - throwSpringK * (throwArmAngle - throwCenterAngle)
							- throwDamperK * throwArmVelo;
		}
	}

	/**
	 * Given starting position/velocity/acceleration, find the position
	 * and velocity at the end of the time step.
	 * 
	 * Note to anyone who knows about Numerical Integration:
	 * This is just plain Euler integration, error prone for 
	 * 'stiff' simulations.  This simulation is set up to
	 * not be stiff, though.
	 * 
	 */
	void doNumericalIntegration(float deltaTime)
	{
		// Calculate new angular velocity and angle for the throwing arm.
		throwArmVelo = throwArmVelo + throwArmAccel * deltaTime;
		throwArmAngle = throwArmAngle + throwArmVelo * deltaTime;

		// Only make the plunger fly if AIRBORN, else it is
		// positioned during makePlungerHeldByWarhol
		if (curThrowerState == AIRBORN) {
			// translation
			plungerVelo[0] = plungerVelo[0] + plungerAcc[0] * deltaTime;
			plungerVelo[1] = plungerVelo[1] + plungerAcc[1] * deltaTime;
			plungerVelo[2] = plungerVelo[2] + plungerAcc[2] * deltaTime;
			plungerPos[0] = plungerPos[0] + plungerVelo[0] * deltaTime;
			plungerPos[1] = plungerPos[1] + plungerVelo[1] * deltaTime;
			plungerPos[2] = plungerPos[2] + plungerVelo[2] * deltaTime;
			// rotation
			plungerAngVelo = plungerAngVelo + plungerAngAcc * deltaTime;
			plungerRotXAngle = plungerRotXAngle + plungerAngVelo * deltaTime;
		}
	}
	
	/**
	 * The throwing arm is constrained not to rotate back further than minWindupAngle
	 */
	void constrainThrowingArm()
	{
		if (curThrowerState == WINDUP) {
			// constrain arm not to go too far back
			if (throwArmAngle < minWindupAngle) {
				throwArmAngle = minWindupAngle;
				// if forcing a stop, immediately decelerate
				throwArmVelo = 0;
			}
		}
	}

	// The boundaries of the playing area.
	// These are hardcoded based on knowledge of the game.
	float LEFT_WALL_X  = -5;
	float RIGHT_WALL_X = 7;
	float NEAR_WALL_Z = -5;
	float FAR_WALL_Z = 6;
	
	/** 
	 * The plungers motion is reflected and damped whenever it hits 
	 * a wall, the floor, or the target.
	 */
	void constrainPlunger()
	{
		if (curThrowerState != AIRBORN)
			return;
		
		boolean flipAngVelo = false;
		if (plungerPos[1] < 0.0f) {
			// Plunger slipped under the floor. 
			
			// Translation:
			//
			// Move up to floor level, reflect and damp the vertical velocity.
			plungerPos[1] = 0.0f;
			plungerVelo[1] = plungerVelo[1] * -.5f;
			//
			// when hitting the floor, friction damps sideways
			plungerVelo[0] = plungerVelo[0] * .95f;
			plungerVelo[2] = plungerVelo[2] * .95f;
			
			// Rotation:
			// when hitting the floor, flip and dampen angVelo
			plungerAngVelo *= -.5f;
			//
			// If plunger is now spinning slowly, start leveling it out 
			// a bit:
			if (Math.abs(plungerAngVelo) < .5) {
				// First, normalize angle between -PI and +PI
				while(plungerRotXAngle > 3.1416)
					plungerRotXAngle -= 6.2832;
				while(plungerRotXAngle < -3.1416)
					plungerRotXAngle += 6.2832;
				// Now, work like a gentle spring towards horizontal.
				if (plungerRotXAngle > 0f) {
					plungerAngVelo -= 1 * (plungerRotXAngle - 1.57079);
				}
				else {
					plungerAngVelo -= 1 * (plungerRotXAngle + 1.57079);
				}	
			}
		}

		// If a wall is hit, just reflect and dampen the translational velocity
		// 
		if (plungerPos[0] < LEFT_WALL_X) {
			plungerPos[0] = LEFT_WALL_X;
			plungerVelo[0] = plungerVelo[0] * -.5f;
			// no change in angular velo since flight || to x-walls
		}
		if (plungerPos[0] > RIGHT_WALL_X) {
			plungerPos[0] = RIGHT_WALL_X;
			plungerVelo[0] = plungerVelo[0] * -.5f;
			// no change in angular velo since flight || to x-walls
		}
		if (plungerPos[2] < NEAR_WALL_Z) {
			plungerPos[2] = NEAR_WALL_Z;
			plungerVelo[2] = plungerVelo[2] * -.5f;
			flipAngVelo = true;
		}
		if (plungerPos[2] > FAR_WALL_Z) {
			plungerPos[2] = FAR_WALL_Z;
			plungerVelo[2] = plungerVelo[2] * -.5f;
			flipAngVelo = true;
		}

		if (flipAngVelo) {
			plungerAngVelo *= -.5f;
		}
	}

	// These are hardcoded based on what looks good
	float RIGHT_ARM_MIN_FRACTION = 0.25f;
	float RIGHT_ARM_MAX_FRACTION = 1.0f;
	/**
	 * Right arm is moved by setting the fraction of an 
	 * animation so that it is coordinated with the throwing arm's motion.
	 */
	void animateFollowerArm()
	{
		// Ramps animation fraction between [RIGHT_ARM_MAX_FRACTION,RIGHT_ARM_MIN_FRACTION]
		// to match the throwing arms' value in the range [throwCenterAngle,minWindupAngle]
		float animationFraction = 0.0f;
		
		if (throwArmAngle > throwCenterAngle) {
			animationFraction = RIGHT_ARM_MIN_FRACTION;
		}
		else if (throwArmAngle < minWindupAngle) {
			animationFraction = RIGHT_ARM_MAX_FRACTION;
		}
		else {
			animationFraction = (throwArmAngle - throwCenterAngle) 
							  / (minWindupAngle - throwCenterAngle);
			animationFraction = RIGHT_ARM_MAX_FRACTION * animationFraction + RIGHT_ARM_MIN_FRACTION;
		}
		
		// Set this fraction in animation of both joints of the right arm.
		warh_Arm1_R_Interp.fraction.setValue(animationFraction);
		warh_Arm2_R_Interp.fraction.setValue(animationFraction);
	}

	/**
	 * Sets the rotation of the throwing shoulder based on 
	 * rotation calculated in performSimulation.
	 */
	void setThrowingArmRotation()
	{
		// Load rotation of throwArmAngle, about -x axis into throwingArm
		throwingArm.rotation.getValue()[0] = -1;
		throwingArm.rotation.getValue()[1] = 0;
		throwingArm.rotation.getValue()[2] = 0;
		throwingArm.rotation.getValue()[3] = throwArmAngle;
		throwingArm.rotation.setValue(throwingArm.rotation.getValue());		
	}
	
	/** Edits the plungerRoot (which lives in world space)
	 * so that it matches the location and rotation of 
	 * stuntPlunger (which lives in the space at the end of warhol's
	 * arm)
	 */
	void makePlungerHeldByWarhol()
	{
		//Check this to avoid divide by 0
		//Shouldn't happen, but to be safe...
		if (timeBetweenUpdates == 0f)
			return;
		
		// Convert the origin and y direction from
		// stuntPlunger space to world space, matrix by matrix
		//
		Node curNode;
		float p[] = { 0f, 0f, 0f };
		float y[] = { 0f, 1f, 0f };
		float mtx[];
	    for (int i = stuntPlungerPath.length - 1; i >=0; i--) {
			if (stuntPlungerPath[i] instanceof Transform) {				
				mtx = ((Transform) stuntPlungerPath[i]).getMatrix();
				MatUtil.multVecMatrix( mtx, p );
				MatUtil.multDirMatrix( mtx, y );
			}
		}
		
		// Set the translation from the transformed point
		plungerRoot.translation.getValue()[0] = p[0];
		plungerRoot.translation.getValue()[1] = p[1];
		plungerRoot.translation.getValue()[2] = p[2];
		//notify
		plungerRoot.translation.setValue(plungerRoot.translation.getValue());

		// Set the rotation as a rotation about the x axis, based on how the
		// Calculate the X rotation required to rotate (0,1,0) into 
		// alignment with y
		MatUtil.normalize(y);
		// Get angle between worldspace y and transformed y.
		float dotprod = y[1]; // Dot product with (0,1,0) is just y value.
		plungerRotXAngle = (float) Math.acos(dotprod);
		// Sign of z tells which direction:
		if (y[2] < 0)
			plungerRotXAngle *= -1;
		// Now set the rotation field.
		plungerRoot.rotation.getValue()[0] = 1;
		plungerRoot.rotation.getValue()[1] = 0;
		plungerRoot.rotation.getValue()[2] = 0;
		plungerRoot.rotation.getValue()[3] = plungerRotXAngle;
		plungerRoot.rotation.setValue(plungerRoot.rotation.getValue());

		// Update the plunger velocities (translation and rotational)
		// needed to move forward when the hand lets go of the plunger.
		//
		// Calculate based on deltaPos/deltaT, multiplied by a handy factor
		// that lets us change the timing of the plunger so it's easier to 
		// control.
		plungerVelo[0] = (p[0]-plungerPos[0]) * PLUNGER_VELO_HACK_FACTOR / timeBetweenUpdates;
		plungerVelo[1] = (p[1]-plungerPos[1]) * PLUNGER_VELO_HACK_FACTOR / timeBetweenUpdates;
		plungerVelo[2] = (p[2]-plungerPos[2]) * PLUNGER_VELO_HACK_FACTOR / timeBetweenUpdates;
		
		// Match plunger angular velocity to that of arm
		plungerAngVelo = -throwArmVelo;

		// Save this to reflect the new position.
		plungerPos[0] = p[0]; plungerPos[1] = p[1]; plungerPos[2] = p[2];
	}		
	
	/**
	 * 	Places the plunger based on simulated trajectory
	 *  calculated in performSimulation()
	 */
	void	makePlungerFollowTrajectory(){
			
		plungerRoot.translation.getValue()[0] = plungerPos[0];
		plungerRoot.translation.getValue()[1] = plungerPos[1];
		plungerRoot.translation.getValue()[2] = plungerPos[2];
		//notify
		plungerRoot.translation.setValue(plungerRoot.translation.getValue());
		
		plungerRoot.rotation.getValue()[0] = 1;
		plungerRoot.rotation.getValue()[1] = 0;
		plungerRoot.rotation.getValue()[2] = 0;
		plungerRoot.rotation.getValue()[3] = plungerRotXAngle;
		//notify
		plungerRoot.rotation.setValue(plungerRoot.rotation.getValue());
	}
	
	/**
	 * Calculates current worldspace location of target center.
	 * Since target may animate, this can be different each frame.
	 * This is done based on knowledge that there are only two
	 * transforms affecting the target, (target and targetMover)
	 */
	void calculateTargetCenter() {
		// Start with 0,0,0 which is center of target,
		targetCenter[0] = 0f;
		targetCenter[1] = 0f;
		targetCenter[2] = 0f;
		// Transform by target's tranfsorm
		MatUtil.multVecMatrix( target.getMatrix(), targetCenter);	
		// Transform by targetMover's transform
		MatUtil.multVecMatrix( targetMover.getMatrix(), targetCenter);
	}
	
	/**
	 * Determines if the plunger hit the target during this last time step.
	 */
	boolean didPlungerHitTarget() 
	{
		boolean result = false;
		if (isPlungerTestedPastTarget == false) {
			// Only compare if plunger has just past the depth of the target center.
			// Target moves only in xy plane, so this is exactly when the 
			// test needs to be done.
			if (plungerPos[2] < (targetCenter[2] + targetDepth)) {
				if (alwaysAWinner) {
					result = true;
				}
				else {
					calculateTargetCenter();
					// Is the plunger position within the targetRadius of the
					// target's center?
					float xdist = plungerPos[0] - targetCenter[0];
					float ydist = plungerPos[1] - targetCenter[1];

					float dist2d = xdist * xdist + ydist * ydist;
					if ( dist2d < targetRadiusSquared ) {
						result = true;
					}
				}
				isPlungerTestedPastTarget = true;
			}
		}

		return result;
	}

	/**
	 * Gets the dunk started
	 */
	void startDunk()
	{
		isNowDunking = true;
		dunkStartTime = curTime;
		if (gong != null) gong.play();
	}

	/**
	 * Determines the fraction to apply to all timers in the dunk animation.
	 */
	void animateDunk()
	{
		float fraction = (float)((curTime - dunkStartTime)/ DUNK_DURATION);
		if (fraction > 1.0f)
			fraction = 1.0f;
		if (fraction < 0.0f)
			fraction = 0.0f;
		
		setDunkInterpolators(fraction);
	}
	/**
	 * Visits all dunk timers and sets to the given fraction.
	 */
	
	void setDunkInterpolators(float fraction)
	{
		jo_Hip_PosInterp.fraction.setValue(fraction);
		jo_Hip_RotInterp.fraction.setValue(fraction);
		jo_Leg2_L_RotInterp.fraction.setValue(fraction);
		jo_Leg2_R_RotInterp.fraction.setValue(fraction);
		jo_Chest_RotInterp.fraction.setValue(fraction);
		jo_Arm1_L_RotInterp.fraction.setValue(fraction);
		jo_Arm1_R_RotInterp.fraction.setValue(fraction);
		
		prop_DivingBoard_RotInterp.fraction.setValue(fraction);
		prop_DivingBoard_PosInterp.fraction.setValue(fraction);

	}

	/**
	 * Called when the mouse first clicks down.
	 */
	void	pullBackArm() 
	{
		// change state, initialize prevTime for this turn
		curThrowerState = WINDUP;
		// Reset Dunktank
		isNowDunking = false;
		setDunkInterpolators(0.0f);
	}

	/**
	 * Called when THROW_RELEASE_TIME has passed since the
	 * mouse was released.
	 */
	void	releasePlunger()
	{
		// change state, advance prev time
		curThrowerState = AIRBORN;
		// Just released, so we'll want a new shot at target
		isPlungerTestedPastTarget = false;
	}

	/**
	 * Called when the mouse is released. 
	 * The plunger stays attached until 
	 * THROW_RELEASE_TIME has passed, then releasePlunger() 
	 * will be called during updateGame()
	 */
	void	releaseArm()
	{
		curThrowerState = THROWING;		
		startThrowTime = curTime;
	}

	/**
	 * 
	 * Called each frame.  Updates time, then calles updateGame
	 * 
	 */
	public void onPreRender(Renderer r, Object userData){
		
		boolean wasTimeZero = (curTime == 0);
		prevTime = curTime;    
		curTime = getClock().getAbsoluteTime();
		// System.out.println("wasTimeZero = " + wasTimeZero);
		if ( !wasTimeZero ) {
			updateGame();
		}
	}
	Interpolator[] myInterpolators = null;
	
	// Collects all the interpolators in the scene and puts the list into myInterpolators
	void collectInterpolators() {
		Searcher mySearcher = getNewSearcher();
		mySearcher.setType("Interpolator");
		Node[][] allPaths = mySearcher.searchAll(getScene());
		if (allPaths == null)
			return;
		myInterpolators = new Interpolator[allPaths.length];
		for (int i = 0; i < allPaths.length; i++) {
			if (allPaths[i] == null)
				myInterpolators[i] = null;
			else
				myInterpolators[i] = (Interpolator) allPaths[i][allPaths[i].length-1];
		}
	}

	TimeSensor tmpSensor;
	void disconnectAllTimeSensors() {
		Searcher mySearcher = getNewSearcher();
		mySearcher.setType("TimeSensor");
		Node[][] allPaths = mySearcher.searchAll(getScene());
		if (allPaths == null)
			return;
		for (int i = 0; i < allPaths.length; i++) {
				tmpSensor = (TimeSensor) allPaths[i][allPaths[i].length-1];
				for (int j = tmpSensor.fraction.getNumRoutes()-1; j >= 0; j--){
					tmpSensor.fraction.deleteRoute(tmpSensor.fraction.getRoutedField(j));
				}	
		}
	}
		
	// Returns interpolator that affects forNode.
	// Second argument is whether or not to set the timeSensor of the 
	// interpolator to null
	public Interpolator findOrientationInterpolator(Node forNode){
		if ( !(forNode instanceof Transform))
			return null;
		Transform xf = (Transform) forNode;
		
		for (int i = 0; i < myInterpolators.length;i++) {
			if (myInterpolators[i] instanceof OrientationInterpolator){
				if ( isRouted(((OrientationInterpolator)myInterpolators[i]).value, xf.rotation)){
					return myInterpolators[i];					
				}
			}
		}
		return null;
	}
	public Interpolator findPositionInterpolator(Node forNode){
		if ( !(forNode instanceof Transform))
			return null;
		Transform xf = (Transform) forNode;
		
		for (int i = 0; i < myInterpolators.length;i++) {
			if (myInterpolators[i] instanceof PositionInterpolator){
				if ( isRouted(((PositionInterpolator)myInterpolators[i]).value, xf.translation)){
					return myInterpolators[i];					
				}
			}
		}
		return null;
	}		
}