--- /dev/null
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// FIXME: review and cleanup
+
+package com.example.android.jetboy;
+
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.Vector;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.media.JetPlayer;
+
+import android.media.JetPlayer.*;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+
+public class JetBoyView extends SurfaceView implements SurfaceHolder.Callback{
+
+
+ //the number of asteroids that must be destroyed
+ public static final int mSuccessThreshold = 50;
+
+ //used to calculate level for mutes and trigger clip
+ public int mHitStreak = 0;
+
+ //total number asteroids you need to hit.
+ public int mHitTotal = 0;
+
+ //which music bed is currently playing?
+ public int mCurrentBed = 0;
+
+
+ //a lazy graphic fudge for the initial title splash
+ private Bitmap mTitleBG;
+ private Bitmap mTitleBG2;
+
+ /**
+ * Base class for any external event passed to the JetBoyThread. This can
+ * include user input, system events, network input, etc.
+ */
+ class GameEvent
+ {
+ public GameEvent()
+ {
+ eventTime = System.currentTimeMillis();
+ }
+
+ long eventTime;
+ }
+
+ /**
+ * A GameEvent subclass for key based user input. Values are those used by
+ * the standard onKey
+ */
+ class KeyGameEvent extends GameEvent
+ {
+ /**
+ * Simple constructor to make populating this event easier.
+ **/
+ public KeyGameEvent(int keyCode, boolean up, KeyEvent msg)
+ {
+ this.keyCode = keyCode;
+ this.msg = msg;
+ this.up = up;
+ }
+
+ public int keyCode;
+ public KeyEvent msg;
+ public boolean up;
+ }
+
+ /**
+ * A GameEvent subclass for events from the JetPlayer.
+ */
+ class JetGameEvent extends GameEvent
+ {
+ /**
+ * Simple constructor to make populating this event easier.
+ **/
+ public JetGameEvent(JetPlayer player,
+ short segment,
+ byte track,
+ byte channel,
+ byte controller,
+ byte value)
+ {
+ this.player = player;
+ this.segment = segment;
+ this.track = track;
+ this.channel = channel;
+ this.controller = controller;
+ this.value = value;
+ }
+
+ public JetPlayer player;
+ public short segment;
+ public byte track;
+ public byte channel;
+ public byte controller;
+ public byte value;
+ }
+
+ class JetBoyThread extends Thread implements OnJetEventListener{
+
+ /*
+ * State-tracking constants
+ *
+ * we don't actually use all of these in JetBoy, borrowed wholesale from lunar lander.
+ */
+ public static final int STATE_START=-1;
+ public static final int STATE_PLAY=0;
+ public static final int STATE_LOSE = 1;
+ public static final int STATE_PAUSE = 2;
+ public static final int STATE_READY = 3;
+ public static final int STATE_RUNNING = 4;
+ public static final int STATE_WIN = 5;
+
+ //how many frames per beat? The basic animation can be changed for instance to 3/4 by changing this to 3.
+ //untested is the impact on other parts of game logic for non 4/4 time.
+ private static final int ANIMATION_FRAMES_PER_BEAT = 4;
+
+ public boolean mInitialized = false;
+
+
+ /** Queue for GameEvents */
+ protected ConcurrentLinkedQueue<GameEvent> mEventQueue = new ConcurrentLinkedQueue<GameEvent>();
+
+ /** Context for processKey to maintain state accross frames **/
+ protected Object mKeyContext = null;
+
+
+ //the timer display in seconds
+ public int mTimerLimit;
+
+ //used for internal timing logic.
+ public final int TIMER_LIMIT=72;
+ //string value for timer display
+ private String mTimerValue="1:12";
+
+
+ //start, play, running, lose are the states we use
+ public int mState;
+
+ //has laser been fired and for how long?
+ //user for fx logic on laser fire
+ boolean mLaserOn = false;
+ long mLaserFireTime = 0;
+
+
+
+ /** The drawable to use as the far background of the animation canvas */
+ private Bitmap mBackgroundImageFar;
+
+ /** The drawable to use as the close background of the animation canvas */
+ private Bitmap mBackgroundImageNear;
+
+ //event ID within JET file. 80,81, 82 are tested to use.
+ //in this game 80 is used for sending asteroid
+ //82 is used as game time for 1/4 note beat.
+ private final String mSendEvent = "80";
+ private final String mTimerEvent = "82";
+
+ //used to track beat for synch of mute/unmute actions
+ private int mBeatCount = 1;
+
+
+
+ //our intrepid space boy
+ private Bitmap[] mShipFlying=new Bitmap[4];
+
+ // the twinkly bit
+ private Bitmap[] mBeam=new Bitmap[4];
+
+ //the things you are trying to hit
+ private Bitmap[] mAsteroids= new Bitmap[12];
+
+ //hit animation
+ private Bitmap[] mExplosions = new Bitmap[4];
+
+
+ private Bitmap mTimerShell;
+
+
+ private Bitmap mLaserShot;
+
+
+ //used to save the beat event system time.
+ //right now we use System.currentMillis
+ //should it use android stuff??
+ private long mLastBeatTime;
+ private long mPassedTime;
+
+
+ //how much do we move the asteroids per beat?
+ private int mPixelMoveX = 25;
+
+
+ //the asteroid send events are generated from the Jet File.
+ //but which land they start in is random.
+ private Random mRandom = new Random();
+
+
+ //the star of our show, a reference to the JetPlayer object.
+ private JetPlayer mJet=null;
+
+ private boolean mJetPlaying = false;
+
+ /** Message handler used by thread to interact with TextView */
+ private Handler mHandler;
+
+ /** Handle to the surface manager object we interact with */
+ private SurfaceHolder mSurfaceHolder;
+
+ /** Handle to the application context, used to e.g. fetch Drawables. */
+ private Context mContext;
+
+ /** Indicate whether the surface has been created & is ready to draw */
+ private boolean mRun = false;
+
+
+ //updates the screen clock. Also used for tempo timing.
+ private Timer mTimer=null;
+ private TimerTask mTimerTask=null;
+
+ //one second - used to update timer
+ private int mTaskIntervalInMillis = 1000;
+
+
+ /**
+ * Current height of the surface/canvas.
+ *
+ * @see #setSurfaceSize
+ */
+ private int mCanvasHeight = 1;
+
+ /**
+ * Current width of the surface/canvas.
+ *
+ * @see #setSurfaceSize
+ */
+ private int mCanvasWidth = 1;
+
+ //used to track the picture to draw for ship animation
+ private int mShipIndex=0;
+
+ //stores all of the asteroid objects in order
+ private Vector <Asteroid> mDangerWillRobinson;
+
+ private Vector<Explosion> mExplosion;
+
+
+ //right to left scroll tracker for near and far BG
+ private int mBGFarMoveX = 0;
+ private int mBGNearMoveX = 0;
+
+
+ //this is has "high" (close to top) that jet boy can fly
+ private int mJetBoyYMin=40;
+
+ private int mJetBoyX=0;
+ private int mJetBoyY=0;
+
+ //this is the pixel position of the laser beam guide.
+ private int mAsteroidMoveLimitX=110;
+
+ //how far up asteroid can be painted
+ private int mAsteroidMinY=40;
+
+ //Jet does not pick up volume and pan changes?
+ private boolean mMuteArrayHack = false;
+
+ Resources mRes;
+
+
+ //eight music beds, added a 9th to fix some kind of bug in JET around pans and volume controls
+ //ask Jenn about this
+ private boolean muteMask[][] = new boolean[9][32];
+
+
+ /**
+ * This is the constructor for the main worker bee
+ *
+ * @param surfaceHolder
+ * @param context
+ * @param handler
+ */
+ public JetBoyThread(SurfaceHolder surfaceHolder, Context context,
+ Handler handler) {
+
+
+ // get handles to some important objects
+ mSurfaceHolder = surfaceHolder;
+ mHandler = handler;
+ mContext = context;
+ mRes = context.getResources();
+
+ //this are the mute arrays associated with the music beds in the JET file
+
+ for(int ii=0 ; ii<8 ; ii++)
+ {
+ for(int xx=0;xx<32;xx++)
+ {
+ muteMask[ii][xx]=true;
+ }
+ }
+
+
+ muteMask[0][2]=false;
+ muteMask[0][3]=false;
+ muteMask[0][4]=false;
+ muteMask[0][5]=false;
+
+
+ muteMask[1][2]=false;
+ muteMask[1][3]=false;
+ muteMask[1][4]=false;
+ muteMask[1][5]=false;
+ muteMask[1][8]=false;
+ muteMask[1][9]=false;
+
+
+ muteMask[2][2]=false;
+ muteMask[2][3]=false;
+ muteMask[2][6]=false;
+ muteMask[2][7]=false;
+ muteMask[2][8]=false;
+ muteMask[2][9]=false;
+
+
+ muteMask[3][2]=false;
+ muteMask[3][3]=false;
+ muteMask[3][6]=false;
+ muteMask[3][11]=false;
+ muteMask[3][12]=false;
+
+ muteMask[4][2]=false;
+ muteMask[4][3]=false;
+ muteMask[4][10]=false;
+ muteMask[4][11]=false;
+ muteMask[4][12]=false;
+ muteMask[4][13]=false;
+
+
+ muteMask[5][2]=false;
+ muteMask[5][3]=false;
+ muteMask[5][10]=false;
+ muteMask[5][12]=false;
+ muteMask[5][15]=false;
+ muteMask[5][17]=false;
+
+
+ muteMask[6][2]=false;
+ muteMask[6][3]=false;
+ muteMask[6][14]=false;
+ muteMask[6][15]=false;
+ muteMask[6][16]=false;
+ muteMask[6][17]=false;
+
+
+ muteMask[7][2]=false;
+ muteMask[7][3]=false;
+ muteMask[7][6]=false;
+ muteMask[7][14]=false;
+ muteMask[7][15]=false;
+ muteMask[7][16]=false;
+ muteMask[7][17]=false;
+ muteMask[7][18]=false;
+
+
+
+ //set all tracks to play, do it for one beat and then switch to mute array zero
+ //hack for jet bug on pan and mutes
+ for (int xx=0 ; xx<32;xx++)
+ {
+ muteMask[8][xx]=false;
+ }
+
+
+ //always set state to start, ensure we come in from front door if app gets tucked into background
+ mState = STATE_START;
+
+
+ setInitialGameState();
+
+ mTitleBG = BitmapFactory.decodeResource(mRes,
+ R.drawable.title_hori);
+
+ // load background image as a Bitmap instead of a Drawable b/c
+ // we don't need to transform it and it's faster to draw this way...thanks lunar lander :)
+
+ //two background since we want them moving at different speeds
+ mBackgroundImageFar = BitmapFactory.decodeResource(mRes,
+ R.drawable.background_a);
+
+
+ mLaserShot = BitmapFactory.decodeResource(mRes,
+ R.drawable.laser);
+
+ mBackgroundImageNear = BitmapFactory.decodeResource(mRes,
+ R.drawable.background_b);
+
+ mShipFlying[0] = BitmapFactory.decodeResource(mRes,
+ R.drawable.ship2_1);
+ mShipFlying[1] = BitmapFactory.decodeResource(mRes,
+ R.drawable.ship2_2);
+ mShipFlying[2] = BitmapFactory.decodeResource(mRes,
+ R.drawable.ship2_3);
+ mShipFlying[3] = BitmapFactory.decodeResource(mRes,
+ R.drawable.ship2_4);
+
+
+ mBeam[0] = BitmapFactory.decodeResource(mRes,
+ R.drawable.intbeam_1);
+ mBeam[1] = BitmapFactory.decodeResource(mRes,
+ R.drawable.intbeam_2);
+ mBeam[2] = BitmapFactory.decodeResource(mRes,
+ R.drawable.intbeam_3);
+ mBeam[3] = BitmapFactory.decodeResource(mRes,
+ R.drawable.intbeam_4);
+
+ mTimerShell = BitmapFactory.decodeResource(mRes,
+ R.drawable.int_timer);
+
+
+ //I wanted them to rotate in a certain way
+ //so I loaded them backwards from the way created.
+ mAsteroids[11] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid01);
+ mAsteroids[10] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid02);
+ mAsteroids[9] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid03);
+ mAsteroids[8] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid04);
+ mAsteroids[7] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid05);
+ mAsteroids[6] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid06);
+ mAsteroids[5] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid07);
+ mAsteroids[4] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid08);
+ mAsteroids[3] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid09);
+ mAsteroids[2] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid10);
+ mAsteroids[1] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid11);
+ mAsteroids[0] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid12);
+
+
+ mExplosions[0] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid_explode1);
+ mExplosions[1] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid_explode2);
+ mExplosions[2] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid_explode3);
+ mExplosions[3] = BitmapFactory.decodeResource(mRes,
+ R.drawable.asteroid_explode4);
+
+
+
+ }
+
+ /**
+ * Does the grunt work of setting up initial jet requirements
+ */
+ private void initializeJetPlayer()
+ {
+
+
+ // if (mJet!=null)
+ // {
+ // mJet.release();
+ //mJet = null;
+ // }
+
+ mJet = JetPlayer.getJetPlayer();
+
+ mJetPlaying = false;
+
+ //make sure we flush the queue
+ //otherwise left over events from previous gameplay
+ // can hang around.
+ mJet.clearQueue();
+
+ // mJet.setStatusUpdateListener(this);
+ mJet.setEventListener(this);
+
+ Log.d(TAG,"opening jet file");
+
+
+ //mJet.loadJetFile(m_PathToJetFile);
+
+ mJet.loadJetFile(mContext.getResources().openRawResourceFd(R.raw.level1));
+
+ Log.d(TAG,"opening jet file DONE");
+
+
+ mCurrentBed = 0;
+ byte sSegmentID = 0;
+
+ Log.d(TAG," start queuing jet file");
+
+
+ //this is the main game play music
+ //it is located at segment 0
+ //it uses lib #0 this allows DLS to play
+ //-1 would mean no DLS
+ //higher numbers untested
+ mJet.queueJetSegment(0, 0, 0, 0,0,sSegmentID);
+
+
+ //end game music, loop 4 times normal pitch
+ mJet.queueJetSegment(1, 0, 4, 0,0,sSegmentID);
+
+ //end game music loop 4 times up an octave
+ mJet.queueJetSegment(1, 0, 4, 1,0,sSegmentID);
+
+
+
+ mJet.setMuteArray(muteMask[8],true);
+
+ Log.d(TAG," start queuing jet file DONE");
+
+
+ }
+
+ private void doDraw(Canvas canvas){
+
+ if (mState == STATE_RUNNING){
+ doDrawRunning(canvas);
+ }
+ else if (mState == STATE_START){
+ doDrawReady(canvas);
+ }
+ else if (mState == STATE_PLAY || mState ==STATE_LOSE){
+
+
+ if (mTitleBG2==null){
+
+ mTitleBG2 = BitmapFactory.decodeResource(mRes,
+ R.drawable.title_bg_hori);
+
+ }
+
+ doDrawPlay(canvas);
+
+
+ }//end state play block
+
+
+ }
+
+
+
+ /**
+ * Draws current state of the game
+ * Canvas.
+ */
+ private void doDrawRunning(Canvas canvas) {
+
+
+ //decrement the far background
+ mBGFarMoveX = mBGFarMoveX-1;
+
+
+ //decrement the near background
+ mBGNearMoveX = mBGNearMoveX-4;
+
+ //calculate the wrap factor for matching image draw
+ int newFarX = mBackgroundImageFar.getWidth()-(-mBGFarMoveX);
+
+ //if we have scrolled all the way, reset to start
+ if (newFarX <=0) {
+ mBGFarMoveX = 0;
+ //only need one draw
+ canvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, null);
+
+
+ }
+ else{
+ //need to draw original and wrap
+ canvas.drawBitmap(mBackgroundImageFar, mBGFarMoveX, 0, null);
+ canvas.drawBitmap(mBackgroundImageFar, newFarX, 0, null);
+ }
+
+
+
+
+ //same story different image...
+ //TODO possible method call
+ int newNearX = mBackgroundImageNear.getWidth()-(-mBGNearMoveX);
+
+
+
+ if (newNearX <= 0){
+ mBGNearMoveX = 0;
+ canvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, null);
+
+ }
+ else{
+ canvas.drawBitmap(mBackgroundImageNear, mBGNearMoveX, 0, null);
+ canvas.drawBitmap(mBackgroundImageNear, newNearX, 0, null);
+ }
+
+
+
+ doAsteroidAnimation(canvas);
+
+
+ canvas.drawBitmap(mBeam[mShipIndex],51+20,0,null);
+
+
+ mShipIndex++;
+
+ if (mShipIndex==4) mShipIndex=0;
+
+ //draw the space ship.
+ //This will have code to match asteroid lane
+ canvas.drawBitmap(mShipFlying[mShipIndex], mJetBoyX,mJetBoyY, null);
+
+ if (mLaserOn)
+ {
+ canvas.drawBitmap(mLaserShot, mJetBoyX + mShipFlying[0].getWidth(),mJetBoyY+(mShipFlying[0].getHeight()/2), null);
+ }
+
+
+
+ //tick tock
+ canvas.drawBitmap(mTimerShell, mCanvasWidth-mTimerShell.getWidth(),0,null);
+
+
+ }
+
+ private void setInitialGameState(){
+ mTimerLimit=TIMER_LIMIT;
+
+ mJetBoyY = mJetBoyYMin;
+
+ mMuteArrayHack = false;
+
+ //set up jet stuff
+ initializeJetPlayer();
+
+ mTimer = new Timer();
+
+ mDangerWillRobinson = new Vector<Asteroid>();
+
+ mExplosion = new Vector<Explosion>();
+
+ mInitialized = true;
+
+ mHitStreak = 0;
+ mHitTotal = 0;
+ // mTimerTotal="1:20";
+
+
+ }
+
+ private void doAsteroidAnimation(Canvas canvas){
+ if ((mDangerWillRobinson == null | mDangerWillRobinson.size()==0) && ( mExplosion!=null && mExplosion.size()==0))
+ return;
+
+ // Compute what percentage through a beat we are and adjust animation and postion
+ // based on that. This assumes 140bpm(428ms/beat), we really should compute this
+ // based on properties of the music file under ideal circumstances. This is just
+ // interbeat interpolation, no game state is updated
+ long frameDelta = System.currentTimeMillis() - mLastBeatTime;
+ // mPixelMoveX per beat
+ // This hid the feeling of the asteroids moving to the beat and caused some issues with
+ // explosions not aligned with asteroids last position, so fix that if we use this again.
+ //int asteroidDrawOffset = (int)(mPixelMoveX * frameDelta/428L);
+ // animation frames per beat
+ int animOffset = (int)(ANIMATION_FRAMES_PER_BEAT*frameDelta/428);
+
+ for ( int i = (mDangerWillRobinson.size()-1); i >= 0; i-- )
+ {
+ Asteroid asteroid = mDangerWillRobinson.elementAt(i);
+
+ if (!asteroid.mMissed)
+ mJetBoyY = asteroid.mDrawY;
+
+ // Log.d(TAG, " drawing asteroid " + ii + " at " + asteroid.mDrawX );
+
+ canvas.drawBitmap(mAsteroids[(asteroid.mAniIndex+animOffset)%mAsteroids.length], asteroid.mDrawX, asteroid.mDrawY , null);
+ }
+
+ for ( int i = (mExplosion.size()-1); i >= 0; i-- )
+ {
+ Explosion ex = mExplosion.elementAt(i);
+
+ canvas.drawBitmap(mExplosions[(ex.mAniIndex+animOffset)%mExplosions.length], ex.mDrawX ,ex.mDrawY , null);
+ }
+ }
+
+ private void doDrawReady(Canvas canvas)
+ {
+
+ canvas.drawBitmap(mTitleBG, 0, 0, null);
+
+
+ }
+
+ private void doDrawPlay(Canvas canvas)
+ {
+
+ canvas.drawBitmap(mTitleBG2, 0, 0, null);
+
+
+ }
+
+
+ /**
+ * the heart of the worker bee
+ */
+
+ public void run() {
+
+
+ //while running do stuff in this loop...bzzz!
+ while (mRun) {
+ Canvas c = null;
+
+ if (mState == STATE_RUNNING)
+ {
+ // Process any input and apply it to the game state
+ updateGameState();
+
+ if (!mJetPlaying)
+ {
+
+
+
+
+ mInitialized = false;
+ Log.d(TAG, "------> STARTING JET PLAY");
+ mJet.play();
+
+ mJetPlaying=true;
+
+
+
+
+
+ }
+
+ mPassedTime = System.currentTimeMillis();
+
+ //kick off the timer task for counter update if not already initialized
+ if (mTimerTask ==null)
+ {
+ mTimerTask = new TimerTask()
+ {
+ public void run()
+ {
+ doCountDown();
+ }
+ };
+
+ mTimer.schedule(mTimerTask,mTaskIntervalInMillis);
+
+ }//end of TimerTask init block
+
+
+ }//end of STATE_RUNNING block
+ else if (mState == STATE_PLAY && !mInitialized)
+
+ {
+ setInitialGameState();
+ }
+ else if (mState == STATE_LOSE )
+ {
+
+ mInitialized = false;
+
+ }
+
+ try {
+ c = mSurfaceHolder.lockCanvas(null);
+ // synchronized (mSurfaceHolder) {
+ doDraw(c);
+ // }
+ } finally {
+ // do this in a finally so that if an exception is thrown
+ // during the above, we don't leave the Surface in an
+ // inconsistent state
+ if (c != null) {
+ mSurfaceHolder.unlockCanvasAndPost(c);
+ }
+ }//end finally block
+ }//end while mrun block
+ }
+
+ /**
+ * This method handles updating the model of the game state. No rendering is done
+ * here only processing of inputs and update of state. This includes positons of all
+ * game objects (asteroids, player, explosions), their state (animation frame, hit),
+ * creation of new objects, etc.
+ **/
+ protected void updateGameState()
+ {
+ // Process any game events and apply them
+ while ( true )
+ {
+ GameEvent event = mEventQueue.poll();
+ if ( event == null )
+ break;
+
+ //Log.d(TAG,"*** EVENT = " + event);
+
+ // Process keys tracking the input context to pass in to later calls
+ if ( event instanceof KeyGameEvent )
+ {
+ // Process the key for affects other then asteroid hits
+ mKeyContext = processKeyEvent((KeyGameEvent)event, mKeyContext);
+
+ // Update laser state. Having this here allows the laser to be trigered right when the key is
+ // pressed. If we comment this out the laser will only be turned on when updateLaser is called
+ // when processing a timer event below.
+ updateLaser(mKeyContext);
+
+
+ }
+ // JET events trigger a state update
+ else if ( event instanceof JetGameEvent )
+ {
+ JetGameEvent jetEvent = (JetGameEvent)event;
+
+ // Only update state on a timer event
+ if ( jetEvent.value == 82 )
+ {
+ // Note the time of the last beat
+ mLastBeatTime = System.currentTimeMillis();
+
+ // Update laser state, turning it on if a key has been pressed or off if it has been
+ // on for too long.
+ updateLaser(mKeyContext);
+
+ // Update explosions before we updated asteroids because updateAsteroids may add
+ // new explosions that we do not want updated until next frame
+ updateExplosions(mKeyContext);
+
+ // Update asteroid positions, hit status and animations
+ updateAsteroids(mKeyContext);
+ }
+
+ processJetEvent(jetEvent.player,jetEvent.segment,jetEvent.track,jetEvent.channel,jetEvent.controller,jetEvent.value);
+ }
+ }
+ }
+
+ /**
+ * This method handles the state updates that can be caused by key press events. Key events
+ * may mean different things depending on what has come before, to support this concept this
+ * method takes an opaque context object as a parameter and returns an updated version. This
+ * context should be set to null for the first event then should be set to the last
+ * value returned for subsiquent events.
+ **/
+ protected Object processKeyEvent(KeyGameEvent event, Object context)
+ {
+ //Log.d(TAG, "key code is " + event.keyCode + " " + (event.up ? "up":"down"));
+
+ // If it is a key up on the fire key make sure we mute the associated sound
+ if ( event.up )
+ {
+ if (event.keyCode == KeyEvent.KEYCODE_DPAD_CENTER)
+ {
+ return null;
+ }
+ }
+ // If it is a key down on the fire key start playing the sound and update the context
+ // to indicate that a key has been pressed and to ignore further presses
+ else
+ {
+ if (event.keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (context == null))
+ {
+ return event;
+ }
+ }
+
+ // Return the context unchanged
+ return context;
+ }
+
+ /**
+ * This method updates the laser status based on user input and shot duration
+ */
+ protected void updateLaser(Object inputContext)
+ {
+ // Lookup the time of the fire event if there is one
+ long keyTime = inputContext == null ? 0 : ((GameEvent)inputContext).eventTime;
+
+ //Log.d(TAG,"keyTime delta = " + (System.currentTimeMillis()-keyTime) + ": obj = " + inputContext);
+
+ // If the laser has been on too long shut it down
+ if ( mLaserOn && System.currentTimeMillis()-mLaserFireTime > 400 )
+ {
+
+ mLaserOn = false;
+
+
+
+ }
+
+ //trying to tune the laser hit timing
+ else if ( System.currentTimeMillis()-mLaserFireTime > 300 )
+ {
+ // if (mJet!=null)
+ // {
+ mJet.setMuteFlag(23, true, false);
+ // }
+ }
+
+
+ // Now check to see if we should turn the laser on. We do this after the above shutdown
+ // logic so it can be turned back on in the same frame it was turned off in. If we want
+ // to add a cooldown period this may change.
+ if ( !mLaserOn && System.currentTimeMillis()-keyTime <= 400 )
+ {
+
+ mLaserOn = true;
+ mLaserFireTime = keyTime;
+
+ //Log.d(TAG, "*** LASER ON ***");
+ mJet.setMuteFlag(23, false, false);
+ }
+ }
+
+ /**
+ * Update asteroid state including position and laser hit status.
+ */
+ protected void updateAsteroids(Object inputContext)
+ {
+ if (mDangerWillRobinson == null | mDangerWillRobinson.size()==0)
+ return;
+
+ for ( int i = (mDangerWillRobinson.size()-1); i >= 0; i-- )
+ {
+ Asteroid asteroid = mDangerWillRobinson.elementAt(i);
+
+ // If the asteroid is within laser ranged but not already missed check if the
+ // key was pressed close enough to the beat to make a hit
+
+ //there isnt any real logic here. just played with it until the game "felt right"
+ if (asteroid.mDrawX<=mAsteroidMoveLimitX+20 && !asteroid.mMissed)
+
+ {
+ // If the laser was fired on the beat destroy the asteroid
+ if (mLaserOn)
+ {
+ // Track hit streak for adjusting music
+ mHitStreak++;
+ mHitTotal++;
+
+ // replace the asteroid with an explosion
+ Explosion ex = new Explosion();
+ ex.mAniIndex = 0;
+ ex.mDrawX = asteroid.mDrawX;
+ ex.mDrawY = asteroid.mDrawY;
+ mExplosion.add(ex);
+
+
+ mJet.setMuteFlag(24, false, false);
+
+ mDangerWillRobinson.removeElementAt(i);
+
+ // This asteroid has been removed process the next one
+ continue;
+ }
+ else
+ {
+ // Sorry, timing was not good enough, marke the asteroid as missed so
+ // on next frame it cannot be hit even if it is still within range
+ asteroid.mMissed = true;
+
+ mHitStreak = mHitStreak-1;
+
+ if (mHitStreak < 0) mHitStreak = 0;
+
+
+ }
+ }
+
+ // Update the asteroids position, even missed ones keep moving
+ asteroid.mDrawX -= mPixelMoveX;
+
+ // Update asteroid animation frame
+ asteroid.mAniIndex = (asteroid.mAniIndex+ANIMATION_FRAMES_PER_BEAT)%mAsteroids.length;
+
+ // if we have scrolled off the screen
+ if (asteroid.mDrawX<0)
+ {
+ mDangerWillRobinson.removeElementAt(i);
+ }
+ }
+ }
+
+ /**
+ * This method updates explosion animation and removes them once they have completed.
+ */
+ protected void updateExplosions(Object inputContext)
+ {
+ if (mExplosion == null | mExplosion.size()==0)
+ return;
+
+ for ( int i = mExplosion.size()-1; i >= 0; i-- )
+ {
+ Explosion ex = mExplosion.elementAt(i);
+
+ ex.mAniIndex += ANIMATION_FRAMES_PER_BEAT;
+
+ // When the animation completes remove the explosion
+ if (ex.mAniIndex>3)
+ {
+ mJet.setMuteFlag(24, true, false);
+ mJet.setMuteFlag(23, true, false);
+
+
+ mExplosion.removeElementAt(i);
+ }
+ }
+ }
+
+
+
+ /**
+ * This method handles the state updates that can be caused by JET events.
+ **/
+ protected void processJetEvent(JetPlayer player, short segment, byte track,
+ byte channel, byte controller, byte value)
+ {
+
+
+
+
+ Log.d(TAG, "onJetEvent(): seg="+segment+" track="+track+" chan="+channel+" cntrlr="+controller+" val="+value);
+
+ String eventID = ""+value;
+
+
+ // Check for an event that triggers a new asteroid
+ if (eventID.equalsIgnoreCase(mSendEvent))
+ {
+ //Log.d(TAG, "~~~~ setting create to true");
+
+ doAsteroidCreation();
+ }
+
+ mBeatCount++;
+
+ if (!mMuteArrayHack)
+ {
+ mMuteArrayHack = true;
+
+ mJet.setMuteArray(muteMask[0], false);
+
+ }
+
+ if (mBeatCount>4)
+ {
+ mBeatCount=1;
+
+ }
+
+
+
+ // Scale the music based on progress
+
+ // it was a game requirement to change the mute array on 1st beat of the next measure when needed
+ //and so we track beat count, after than we track hitStreak to determine music level
+ //if the level has go gone up, call trigger clip otherwise just execute rest of change music bed logic.
+ //could probably be a method call here.
+ if (mBeatCount ==1)
+ {
+
+ //do it back wards so you fall into the correct one
+ if (mHitStreak>28 )
+ {
+
+ //did the bed change?
+ if (mCurrentBed !=7)
+ {
+ //did it go up?
+ if (mCurrentBed <7)
+ {
+ mJet.triggerClip(7);
+ }
+
+ mCurrentBed = 7;
+ mJet.setMuteArray(muteMask[7], false);
+
+
+ }
+ }
+ else if (mHitStreak>24)
+ {
+ if (mCurrentBed !=6)
+ {
+ if (mCurrentBed <6)
+ {
+ mJet.triggerClip(6);
+ }
+
+ mCurrentBed = 6;
+ mJet.setMuteArray(muteMask[6], false);
+ //mJet.triggerClip(6);
+ }
+ }
+ else if (mHitStreak>20 )
+ {
+ if (mCurrentBed !=5)
+ {
+ if (mCurrentBed <5)
+ {
+ mJet.triggerClip(5);
+ }
+
+ mCurrentBed = 5;
+ mJet.setMuteArray(muteMask[5], false);
+ //mJet.triggerClip(5);
+ }
+ }
+ else if (mHitStreak>16 )
+ {
+ if (mCurrentBed !=4)
+ {
+
+ if (mCurrentBed <4)
+ {
+ mJet.triggerClip(4);
+ }
+ mCurrentBed = 4;
+ mJet.setMuteArray(muteMask[4], false);
+ //mJet.triggerClip(4);
+ }
+ }
+ else if (mHitStreak >12 )
+ {
+ if (mCurrentBed !=3)
+ {
+ if (mCurrentBed <3)
+ {
+ mJet.triggerClip(3);
+ }
+ mCurrentBed = 3;
+ mJet.setMuteArray(muteMask[3], false);
+ //mJet.triggerClip(3);
+ }
+ }
+ else if (mHitStreak >8 )
+ {
+ if (mCurrentBed !=2)
+ {
+ if (mCurrentBed <2)
+ {
+ mJet.triggerClip(2);
+ }
+
+ mCurrentBed = 2;
+ mJet.setMuteArray(muteMask[2], false);
+ //mJet.triggerClip(2);
+ }
+ }
+ else if (mHitStreak>4)
+ {
+ if (mCurrentBed !=1)
+ {
+
+ if (mCurrentBed <1)
+ {
+ mJet.triggerClip(1);
+ }
+
+ mJet.setMuteArray(muteMask[1], false);
+ // mJet.triggerClip(1);
+
+ mCurrentBed = 1;
+ }
+ }
+ }
+
+
+
+ /*
+ try
+ {
+ Log.d(TAG,"onJetEvent: segment =" + segment);
+ Log.d(TAG,"onJetEvent(): track =" + track);
+ Log.d(TAG,"onJetEvent(): channel =" + channel);
+ Log.d(TAG,"onJetEvent(): controller =" + controller);
+ Log.d(TAG,"onJetEvent(): value =" + value);
+
+ }
+ catch(Exception e1)
+ {
+ Log.e(TAG,"on Jet Event caught " + e1.toString());
+ }
+
+ */
+ }
+
+
+ private void doAsteroidCreation() {
+
+ //Log.d(TAG, "asteroid created");
+
+ Asteroid _as = new Asteroid();
+
+
+ int drawIndex = mRandom.nextInt(4);
+
+ //TODO Remove hard coded value
+ _as.mDrawY = mAsteroidMinY + (drawIndex*63);
+
+ _as.mDrawX = (mCanvasWidth-mAsteroids[0].getWidth());
+
+ _as.mStartTime= System.currentTimeMillis();
+
+
+
+ mDangerWillRobinson.add(_as);
+
+
+ }
+
+ /**
+ * Used to signal the thread whether it should be running or not.
+ * Passing true allows the thread to run; passing false will shut it
+ * down if it's already running. Calling start() after this was most
+ * recently called with false will result in an immediate shutdown.
+ *
+ * @param b true to run, false to shut down
+ */
+ public void setRunning(boolean b) {
+ mRun = b;
+
+ if (mRun==false)
+ {
+ if (mTimerTask!=null)
+ mTimerTask.cancel();
+ }
+ }
+
+ /**
+ *
+ * returns the current int value of game state as defined by state tracking constants
+ *
+ * @return
+ */
+ public int getGameState()
+ {
+ synchronized (mSurfaceHolder)
+ {
+ return mState;
+ }
+ }
+
+ /**
+ * Sets the game mode. That is, whether we are running, paused, in the
+ * failure state, in the victory state, etc.
+ *
+ * @see #setState(int, CharSequence)
+ * @param mode one of the STATE_* constants
+ */
+ public void setGameState(int mode) {
+ synchronized (mSurfaceHolder) {
+ setGameState(mode, null);
+ }
+ }
+
+ /**
+ * Sets state based on input, optionally also passing in a text message.
+ * @param state
+ * @param message
+ */
+ //TODO - Modeled from lunar lander Determine if best way to do this.
+ public void setGameState(int state, CharSequence message) {
+
+ synchronized (mSurfaceHolder) {
+
+ //change state if needed
+ if (mState!=state)
+ {
+ mState = state;
+ }
+
+
+
+ if (mState == STATE_PLAY )
+ {
+ Resources res = mContext.getResources();
+ mBackgroundImageFar = BitmapFactory.decodeResource(res,
+ R.drawable.background_a);
+
+ // don't forget to resize the background image
+ mBackgroundImageFar = Bitmap.createScaledBitmap(
+ mBackgroundImageFar, mCanvasWidth*2, mCanvasHeight, true);
+
+ mBackgroundImageNear = BitmapFactory.decodeResource(res,
+ R.drawable.background_b);
+
+ // don't forget to resize the background image
+ mBackgroundImageNear = Bitmap.createScaledBitmap(
+ mBackgroundImageNear, mCanvasWidth*2, mCanvasHeight, true);
+
+
+
+ }
+ else if ( mState == STATE_RUNNING )
+ {
+ // When we enter the running state we should clear any old events in the queue
+ mEventQueue.clear();
+
+ // And reset the ket state so we dont think a button is pressed that isn't
+ mKeyContext = null;
+ }
+
+ }
+ }
+
+ /**
+ * Add key press input to the GameEvent queue
+ */
+
+
+
+ public boolean doKeyDown(int keyCode, KeyEvent msg)
+ {
+ mEventQueue.add( new KeyGameEvent(keyCode, false, msg) );
+
+
+
+
+ return true;
+ }
+
+ /**
+ * Add key press input to the GameEvent queue
+ */
+ public boolean doKeyUp(int keyCode, KeyEvent msg)
+ {
+ mEventQueue.add( new KeyGameEvent(keyCode, true, msg) );
+
+
+
+
+ return true;
+ }
+
+ /* Callback invoked when the surface dimensions change. */
+ public void setSurfaceSize(int width, int height) {
+ // synchronized to make sure these all change atomically
+ synchronized (mSurfaceHolder) {
+ mCanvasWidth = width;
+ mCanvasHeight = height;
+
+ // don't forget to resize the background image
+ mBackgroundImageFar = Bitmap.createScaledBitmap(
+ mBackgroundImageFar, width*2, height, true);
+
+
+ // don't forget to resize the background image
+ mBackgroundImageNear = Bitmap.createScaledBitmap(
+ mBackgroundImageNear, width*2, height, true);
+ }
+ }
+
+ /**
+ * Pauses the physics update & animation.
+ */
+ //TODO should probably add a pause to the menu button
+ public void pause() {
+ synchronized (mSurfaceHolder) {
+ if (mState == STATE_RUNNING) setGameState(STATE_PAUSE);
+ if (mTimerTask!=null){
+ mTimerTask.cancel();
+ }
+
+ if (mJet!=null)
+ {
+ mJet.pause();
+ mJet.release();
+ }
+ }
+ }
+
+
+ /**
+ * Does the work of updating timer
+ *
+ */
+ private void doCountDown()
+ {
+ //Log.d(TAG,"Time left is " + mTimerLimit);
+
+
+ mTimerLimit = mTimerLimit-1;
+ try
+ {
+ //subtract one minute and see what the result is.
+ int moreThanMinute = mTimerLimit - 60;
+
+ if (moreThanMinute>=0)
+ {
+
+ if (moreThanMinute>9)
+ {
+ mTimerValue="1:"+moreThanMinute;
+
+ }
+ //need an extra '0' for formatting
+ else
+ {
+ mTimerValue="1:0"+moreThanMinute;
+ }
+ }
+ else
+ {
+ if (mTimerLimit>9)
+ {
+ mTimerValue="0:"+mTimerLimit;
+ }
+ else
+ {
+ mTimerValue="0:0"+mTimerLimit;
+ }
+ }
+ }
+ catch (Exception e1)
+ {
+ Log.e(TAG,"doCountDown threw " + e1.toString());
+ }
+
+
+ Message msg = mHandler.obtainMessage();
+
+
+
+
+ Bundle b = new Bundle();
+ b.putString("text", mTimerValue);
+
+ //time's up
+ if (mTimerLimit==0)
+ {
+ b.putString("STATE_LOSE", ""+STATE_LOSE);
+ mTimerTask=null;
+
+ mState= STATE_LOSE;
+
+ }
+ else
+ {
+
+ mTimerTask = new TimerTask()
+ {
+ public void run()
+ {
+ doCountDown();
+ }
+ };
+
+ mTimer.schedule(mTimerTask,mTaskIntervalInMillis);
+ }
+
+
+ //this is how we send data back up to the main JetBoyView thread.
+ //if you look in constructor of JetBoyView you will see code for
+ //Handling of messages. This is borrowed directly from lunar lander.
+ //Thanks again!
+ msg.setData(b);
+ mHandler.sendMessage(msg);
+
+
+
+
+
+ }
+
+
+ /**
+ * required JetPlayer method. Informs listener of when a queue segement
+ * event has been generated.
+ *
+ * @param nbSegments
+ */
+ public void onJetNumQueuedSegmentUpdate(int nbSegments) {
+ Log.i(TAG,"onJetNumQueuedSegmentUpdate(): nbSegments =" + nbSegments);
+ }
+
+
+
+ /**
+ * required JetPlayer method. A more specific handler.
+ *
+ * @param player
+ * @param nbSegments
+ */
+ public void onJetNumQueuedSegmentUpdate(JetPlayer player, int nbSegments) {
+ Log.i(TAG,"onJetNumQueuedUpdate(): nbSegs =" + nbSegments);
+
+ }
+
+ /**
+ * The method which receives notification from event listener.
+ * This is where we queue up events 80 and 82.
+ *
+ * Most of this data passed is unneeded for JetBoy logic but shown
+ * for code sample completeness.
+ *
+ * @param player
+ * @param segment
+ * @param track
+ * @param channel
+ * @param controller
+ * @param value
+ */
+ public void onJetEvent(JetPlayer player, short segment, byte track,
+ byte channel, byte controller, byte value) {
+
+
+ Log.d(TAG,"jet got event " + value);
+
+ //events fire outside the animation thread. This can cause timing issues.
+ //put in queue for processing by animation thread.
+ mEventQueue.add( new JetGameEvent(player, segment, track, channel, controller, value) );
+ }
+
+ public void onJetPauseUpdate(JetPlayer player, int paused) {
+ Log.i(TAG,"onJetPauseUpdate(): paused =" + paused);
+
+ }
+
+ public void onJetUserIdUpdate(JetPlayer player, int userId,
+ int repeatCount) {
+ Log.i(TAG,"onJetUserIdUpdate(): userId =" + userId + " repeatCount=" + repeatCount);
+
+ }
+
+
+
+
+ }//end thread class
+
+ public static final String TAG = "JetBoy";
+
+
+ /** The thread that actually draws the animation */
+ private JetBoyThread thread;
+
+ private TextView mTimerView;
+
+ private Button mButtonRetry;
+ // private Button mButtonRestart;
+ private TextView mTextView;
+
+
+ /**
+ * The constructor called from the main JetBoy activity
+ *
+ * @param context
+ * @param attrs
+ */
+ public JetBoyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // register our interest in hearing about changes to our surface
+ SurfaceHolder holder = getHolder();
+ holder.addCallback(this);
+
+ mContext= context;
+
+ // create thread only; it's started in surfaceCreated()
+ thread = new JetBoyThread(holder, context, new Handler() {
+
+ public void handleMessage(Message m) {
+
+ mTimerView.setText(m.getData().getString("text"));
+
+ //ok so maybe it isn't really a "lose"
+ //this bit was borrowed from lunar lander and then evolved.
+ //too close to deadline to mess with now.
+ if (m.getData().getString("STATE_LOSE")!=null){
+ //mButtonRestart.setVisibility(View.VISIBLE);
+ mButtonRetry.setVisibility(View.VISIBLE);
+
+ mTimerView.setVisibility(View.INVISIBLE);
+
+ mTextView.setVisibility(View.VISIBLE);
+
+
+ Log.d(TAG,"the total was " + mHitTotal);
+
+
+ if (mHitTotal>=mSuccessThreshold)
+ {
+ mTextView.setText(R.string.winText);
+ }
+ else
+ {
+ mTextView.setText("Sorry, You Lose! You got " + mHitTotal + ". You need 50 to win.");
+ }
+
+ mTimerView.setText("1:12");
+ mTextView.setHeight(20);
+
+
+ }
+ }//end handle msg
+ });
+
+ setFocusable(true); // make sure we get key events
+
+ Log.d(TAG,"@@@ done creating view!");
+ }
+
+
+
+ /**
+ * Pass in a reference to the timer view widget so we can update it from here.
+ *
+ * @param tv
+ */
+ public void setTimerView(TextView tv)
+ {
+ mTimerView = tv;
+ }
+
+
+
+
+ /**
+ * Standard window-focus override. Notice focus lost so we can pause on
+ * focus lost. e.g. user switches to take a call.
+ */
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+
+ Log.d(TAG,"@@@FOCUS CHANGED!");
+
+ if (!hasWindowFocus)
+ {
+ if (thread!=null)
+ thread.pause();
+
+ }
+ }
+
+
+ /**
+ * Fetches the animation thread corresponding to this LunarView.
+ *
+ * @return the animation thread
+ */
+ public JetBoyThread getThread() {
+ return thread;
+ }
+
+
+ /* Callback invoked when the surface dimensions change. */
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+
+ thread.setSurfaceSize(width, height);
+ }
+
+ public void surfaceCreated(SurfaceHolder arg0) {
+ // start the thread here so that we don't busy-wait in run()
+ // waiting for the surface to be created
+
+ thread.setRunning(true);
+ thread.start();
+
+ }
+
+ public void surfaceDestroyed(SurfaceHolder arg0) {
+ boolean retry = true;
+ thread.setRunning(false);
+ while (retry) {
+ try {
+ thread.join();
+ retry = false;
+
+ } catch (InterruptedException e) {
+ }
+ }
+
+ }
+
+
+ /**
+ * A reference to the button to start game over.
+ *
+ * @param _buttonRetry
+ *
+ */
+ public void SetButtonView(Button _buttonRetry) {
+
+ mButtonRetry = _buttonRetry;
+
+ // mButtonRestart = _buttonRestart;
+
+ }
+
+
+
+ //we reuse the help screen from the end game screen.
+ public void SetTextView(TextView textView) {
+ mTextView = textView;
+
+ }
+
+
+
+
+
+
+}