/* * Copyright (C) 2016 Google Inc. All Rights Reserved. * * 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. */ package com.google.android.apps.santatracker.games.jetpack; import android.app.Activity; import android.view.KeyEvent; import com.google.android.apps.santatracker.R; import com.google.android.apps.santatracker.games.gamebase.BaseScene; import com.google.android.apps.santatracker.games.gamebase.SceneActivity; import com.google.android.apps.santatracker.games.simpleengine.Logger; import com.google.android.apps.santatracker.games.simpleengine.SceneManager; import com.google.android.apps.santatracker.games.simpleengine.SmoothValue; import com.google.android.apps.santatracker.games.simpleengine.SoundManager; import com.google.android.apps.santatracker.games.simpleengine.game.GameObject; import java.util.ArrayList; import java.util.HashSet; public final class JetpackScene extends BaseScene { private static final String TAG = "JetpackScene"; // GameObject types: static final int TYPE_PLAYER = 0; static final int TYPE_GOOD_ITEM = 1; static final int TYPE_BAD_ITEM = 2; // player GameObject mPlayerObj; // background GameObject mBackground; // our object factory JetpackObjectFactory mFactory; // current difficulty level int mLevel = 1; // player is currently injured boolean mPlayerHit = false; // time remaining of player's injury float mPlayerHitTime = JetpackConfig.Time.HIT_TIME; // total items collected int mItemsCollected = 0; // item fall speed multipler (increases with level) float mFallMult = 3.0f; // score multipler (increases with level) float mScoreMult = 1.0f; SmoothValue mSpriteAngle = new SmoothValue(0.0f, JetpackConfig.Player.SpriteAngle.MAX_CHANGE_RATE, -JetpackConfig.Player.SpriteAngle.MAX_ANGLE, JetpackConfig.Player.SpriteAngle.MAX_ANGLE, JetpackConfig.Player.SpriteAngle.FILTER_SAMPLES); float mPlayerTargetX = 0.0f; float mPlayerTargetY = 0.0f; // working array ArrayList<GameObject> mTmpList = new ArrayList<GameObject>(); // how long til we spawn the next item? float mSpawnCountdown = JetpackConfig.Items.SPAWN_INTERVAL; // cloud sprites GameObject[] mCloudObj = new GameObject[JetpackConfig.Clouds.COUNT]; // time remaining float mTimeRemaining = JetpackConfig.Time.INITIAL; // sfx IDs int[] mItemSfxSuccess = null; // current combo private class Combo { int items = 0; float countdown = 0.0f; float centroidX, centroidY; float points; float timeRecovery; void reset() { items = 0; countdown = centroidX = centroidY = points = timeRecovery = 0.0f; } } private Combo mCombo = new Combo(); // set of achievements we know we unlocked (to prevent repeated API calls) private HashSet<Integer> mUnlockedAchievements = new HashSet<Integer>(); // achievement increments we are pending to send private int mAchPendingPresents = 0; private int mAchPendingCandy = 0; private float mAchPendingSeconds = 0; // countdown to next sending of incremental achievements private float mIncAchCountdown = JetpackConfig.Achievements.INC_ACH_SEND_INTERVAL; // what pointer Id is the one that's steering the elf private int mActivePointerId = -1; // accumulated play time private float mPlayTime = 0.0f; @Override protected String getBgmAssetFile() { return JetpackConfig.BGM_ASSET_FILE; } @Override protected float getDisplayedTime() { return mTimeRemaining; } @Override protected BaseScene makeNewScene() { return new JetpackScene(); } @Override public void onInstall() { super.onInstall(); mFactory = new JetpackObjectFactory(mRenderer, mWorld); mFactory.requestTextures(); mBackground = mFactory.makeBackground(); mBackground.sendToBack(); // Make the game shorter when debugging Activity activity = SceneManager.getInstance().getActivity(); if (activity != null && activity.getPackageName().contains("debug")) { mTimeRemaining = 10.0f; } mPlayerObj = mFactory.makePlayer(); mPlayerTargetX = 0.0f; mPlayerTargetY = mRenderer.getBottom() + JetpackConfig.Player.VERT_MOVEMENT_MARGIN; SoundManager soundManager = SceneManager.getInstance().getSoundManager(); mItemSfxSuccess = new int[3]; mItemSfxSuccess[0] = soundManager.requestSfx(R.raw.jetpack_score1); mItemSfxSuccess[1] = soundManager.requestSfx(R.raw.jetpack_score2); mItemSfxSuccess[2] = soundManager.requestSfx(R.raw.jetpack_score3); SceneManager.getInstance().getVibrator(); // start paused startGameScreen(); } @Override public void onUninstall() { super.onUninstall(); } @Override public void doStandbyFrame(float deltaT) { super.doStandbyFrame(deltaT); } @Override public boolean isGameEnded() { return mGameEnded; } @Override public void doFrame(float deltaT) { if(!mPaused) { if (!mGameEnded) { mPlayTime += deltaT; updatePlayer(deltaT); if (!mPlayerHit) { detectCollectedItems(); } updatePlayerHit(deltaT); updateTimeRemaining(deltaT); updateCombo(deltaT); checkLevelUp(); mAchPendingSeconds += deltaT; } updateClouds(); updateCandy(deltaT); killMissedPresents(); mIncAchCountdown -= deltaT; sendIncrementalAchievements(false); if (!mIsInGameEndingTransition && (mSpawnCountdown -= deltaT) < 0.0f) { mSpawnCountdown = JetpackConfig.Items.SPAWN_INTERVAL; mFactory.makeRandomItem(mFallMult, SceneManager.getInstance() .getLargePresentMode(), mDisplayedScore.getValue()); } super.doFrame(deltaT); } if(mInStartCountdown) { updatePlayer(deltaT); float newCount = mStartCountdownTimeRemaining - (deltaT); if(newCount <= 0) { mInStartCountdown = false; unpauseGame(); } else if((int) newCount < (int) mStartCountdownTimeRemaining) { mDigitFactory.setDigit(mCountdownDigitObj, Math.min((int) newCount + 1, 3)); mCountdownDigitObj.bringToFront(); } mStartCountdownTimeRemaining = newCount; } if(mGameEnded) { goToEndGame(); } } protected void endGame() { mIsInGameEndingTransition = true; // force send all incremental achievements sendIncrementalAchievements(true); // submit our score submitScore(JetpackConfig.LEADERBOARD, mScore); // Start end game activity onWidgetTriggered(MSG_GO_TO_END_GAME); } private void updateTimeRemaining(float deltaT) { mTimeRemaining -= deltaT; if (mTimeRemaining < 0.0f && !mIsInGameEndingTransition) { endGame(); } } private void updatePlayerHit(float deltaT) { mPlayerHitTime -= deltaT; if(mPlayerHitTime < 0.0f && mPlayerHit) { mFactory.recoverPlayerHit(mPlayerObj); mPlayerHit = false; } } private float sineWave(float period, float amplitude, float t) { return (float) Math.sin(2 * Math.PI * t / period) * amplitude; } private void updatePlayer(float deltaT) { mSpriteAngle.setTarget( (mPlayerObj.x - mPlayerTargetX) * JetpackConfig.Player.SpriteAngle.ANGLE_CONST); mSpriteAngle.update(deltaT); mPlayerObj.getSprite(0).rotation = mSpriteAngle.getValue(); mPlayerObj.getSprite(1).rotation = mSpriteAngle.getValue(); mPlayerObj.getSprite(1).width = JetpackConfig.Player.Fire.WIDTH * (1.0f + sineWave(JetpackConfig.Player.Fire.ANIM_PERIOD, JetpackConfig.Player.Fire.ANIM_AMPLITUDE, mPlayTime)); mPlayerObj.getSprite(1).height = Float.NaN; // proportional to width if (isTv()) { // On TV, player moves based on its speed. mPlayerObj.displaceBy(mPlayerObj.velX * deltaT, mPlayerObj.velY * deltaT); } else { mPlayerObj.displaceTowards(mPlayerTargetX, mPlayerTargetY, deltaT * JetpackConfig.Player.MAX_SPEED); } } private void updateClouds() { int i; for (i = 0; i < mCloudObj.length; i++) { GameObject o = mCloudObj[i]; if (o == null) { o = mFactory.makeCloud(); mCloudObj[i] = o; setupNewCloud(o); } else if (o.y < JetpackConfig.Clouds.DELETE_Y) { setupNewCloud(o); } } } private void updateCombo(float deltaT) { if (mCombo.items > 0 && (mCombo.countdown -= deltaT) <= 0.0f) { endCombo(); } } private boolean isCandy(GameObject o) { return o.type == TYPE_GOOD_ITEM && o.ivar[JetpackConfig.Items.IVAR_TYPE] == JetpackObjectFactory.ITEM_CANDY; } private boolean isPresent(GameObject o) { return o.type == TYPE_GOOD_ITEM && o.ivar[JetpackConfig.Items.IVAR_TYPE] == JetpackObjectFactory.ITEM_PRESENT; } private boolean isCoal(GameObject o) { return o.type == TYPE_BAD_ITEM && o.ivar[JetpackConfig.Items.IVAR_TYPE] == JetpackObjectFactory.ITEM_COAL; } private void updateCandy(float deltaT) { int i; for (i = 0; i < mWorld.gameObjects.size(); i++) { GameObject o = mWorld.gameObjects.get(i); if (isCandy(o)) { o.getSprite(0).rotation += deltaT * JetpackConfig.Items.CANDY_ROTATE_SPEED; } } } private void setupNewCloud(GameObject o) { o.displaceTo(mRenderer.getLeft() + mRandom.nextFloat() * (mRenderer.getRight() - mRenderer.getLeft()), JetpackConfig.Clouds.SPAWN_Y); o.velY = -(JetpackConfig.Clouds.SPEED_MIN + mRandom.nextFloat() * (JetpackConfig.Clouds.SPEED_MAX - JetpackConfig.Clouds.SPEED_MIN)); } private void killMissedPresents() { int i; for (i = 0; i < mWorld.gameObjects.size(); i++) { GameObject o = mWorld.gameObjects.get(i); if ((o.type == TYPE_GOOD_ITEM || o.type == TYPE_BAD_ITEM) && o.y < JetpackConfig.Items.DELETE_Y) { o.dead = true; } } } private void killAllItems() { int i; for (i = 0; i < mWorld.gameObjects.size(); i++) { GameObject o = mWorld.gameObjects.get(i); if (o.type == TYPE_GOOD_ITEM || o.type == TYPE_BAD_ITEM) { o.dead = true; } } } private int roundScore(int score) { score = (score / 50) * 50; return score < 50 ? 50 : score; } private void addScore(float score) { mScore += score; unlockScoreBasedAchievements(); } private void addTime(float time) { mTimeRemaining += time; if (mTimeRemaining > JetpackConfig.Time.MAX) { mTimeRemaining = JetpackConfig.Time.MAX; } } private void pickUpItem(GameObject item) { int value = item.ivar[JetpackConfig.Items.IVAR_BASE_VALUE]; if (isCandy(item)) { mAchPendingCandy++; mObjectFactory.makeScorePopup(item.x, item.y, value, mDigitFactory); SceneManager.getInstance().getSoundManager().playSfx( mItemSfxSuccess[mRandom.nextInt(mItemSfxSuccess.length)]); } else if (isPresent(item)) { mAchPendingPresents++; mObjectFactory.makeScorePopup(item.x, item.y, value, mDigitFactory); SceneManager.getInstance().getSoundManager().playSfx( mItemSfxSuccess[mRandom.nextInt(mItemSfxSuccess.length)]); } else if (isCoal(item)) { mFactory.makePlayerHit(mPlayerObj); mPlayerHit = true; mPlayerHitTime = JetpackConfig.Time.HIT_TIME; mObjectFactory.makeScorePopup(item.x, item.y, value, mDigitFactory); SceneManager.getInstance().getSoundManager().playSfx( mItemSfxSuccess[mRandom.nextInt(mItemSfxSuccess.length)]); SceneManager.getInstance().getVibrator().vibrate(JetpackConfig.Time.VIBRATE_SMALL); } item.dead = true; mItemsCollected++; addScore(value); // play sfx } private void detectCollectedItems() { mWorld.detectCollisions(mPlayerObj, mTmpList, true); int i; for (i = 0; i < mTmpList.size(); i++) { GameObject o = mTmpList.get(i); if (o.type == TYPE_GOOD_ITEM || o.type == TYPE_BAD_ITEM) { pickUpItem(o); } } } private void endCombo() { if (mCombo.items > 1) { mFactory.makeComboPopup(mCombo.items, mCombo.centroidX, mCombo.centroidY); // give bonus addScore(mCombo.points * mCombo.items); addTime(mCombo.timeRecovery * mCombo.items); // unlock combo-based achievements unlockComboBasedAchievements(mCombo.items); } mCombo.reset(); } @Override public void onPointerDown(int pointerId, float x, float y) { super.onPointerDown(pointerId, x, y); if (mActivePointerId < 0) { mActivePointerId = pointerId; } } @Override public void onPointerUp(int pointerId, float x, float y) { super.onPointerUp(pointerId, x, y); if (mActivePointerId == pointerId) { mActivePointerId = -1; } } @Override public void onPointerMove(int pointerId, float x, float y, float deltaX, float deltaY) { super.onPointerMove(pointerId, x, y, deltaX, deltaY); // if paused, do nothing. if (mPaused) { return; } // if no finger owns the steering of the elf, adopt this one. if (mActivePointerId < 0) { mActivePointerId = pointerId; } } @Override public void onKeyDown(int keyCode, int repeatCount) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_BUTTON_A: onConfirmKeyPressed(); break; case KeyEvent.KEYCODE_BUTTON_B: onBackKeyPressed(); break; } if (!mPaused) { float absVelocity = JetpackConfig.Player.WIDTH * (1.5f + (float) repeatCount * JetpackConfig.Input.KEY_SENSIVITY); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: mPlayerTargetY = Integer.MAX_VALUE; mPlayerObj.velY = absVelocity; break; case KeyEvent.KEYCODE_DPAD_DOWN: mPlayerTargetY = Integer.MIN_VALUE; mPlayerObj.velY = -absVelocity; break; case KeyEvent.KEYCODE_DPAD_LEFT: mPlayerTargetX = Integer.MIN_VALUE; mPlayerObj.velX = -absVelocity; break; case KeyEvent.KEYCODE_DPAD_RIGHT: mPlayerTargetX = Integer.MAX_VALUE; mPlayerObj.velX = absVelocity; break; } // don't let the player wander off screen limitPlayerMovement(); } } @Override public void onKeyUp(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: // if it's going up, stop it. mPlayerTargetY = mPlayerObj.y; if (mPlayerObj.velY > 0) { mPlayerObj.velY = 0; } break; case KeyEvent.KEYCODE_DPAD_DOWN: // if it's going down, stop it. mPlayerTargetY = mPlayerObj.y; if (mPlayerObj.velY < 0) { mPlayerObj.velY = 0; } break; case KeyEvent.KEYCODE_DPAD_LEFT: // if it's going left, stop it. mPlayerTargetX = mPlayerObj.x; if (mPlayerObj.velX < 0) { mPlayerObj.velX = 0; } break; case KeyEvent.KEYCODE_DPAD_RIGHT: // if it's going right, stop it. mPlayerTargetX = mPlayerObj.x; if (mPlayerObj.velX > 0) { mPlayerObj.velX = 0; } break; } // don't let the player wander off screen limitPlayerMovement(); } @Override public void onSensorChanged(float x, float y, int accuracy) { mPlayerTargetX += JetpackConfig.Input.Sensor.transformX(x); // don't let the player wander off screen limitPlayerMovement(); } private void limitPlayerMovement() { float minX = mRenderer.getLeft() + JetpackConfig.Player.HORIZ_MOVEMENT_MARGIN; float maxX = mRenderer.getRight() - JetpackConfig.Player.HORIZ_MOVEMENT_MARGIN; float minY = mRenderer.getBottom() + JetpackConfig.Player.VERT_MOVEMENT_MARGIN; float maxY = mRenderer.getTop() - JetpackConfig.Player.VERT_MOVEMENT_MARGIN; if (isTv()) { mPlayerObj.velX = mPlayerObj.x + mPlayerObj.velX < minX ? minX - mPlayerObj.x : mPlayerObj.x + mPlayerObj.velX > maxX ? maxX - mPlayerObj.x : mPlayerObj.velX; mPlayerObj.velY = mPlayerObj.y + mPlayerObj.velY < minY ? minY - mPlayerObj.y : mPlayerObj.y + mPlayerObj.velY > maxY ? maxY - mPlayerObj.y : mPlayerObj.velY; } else { mPlayerTargetX = mPlayerTargetX < minX ? minX : mPlayerTargetX > maxX ? maxX : mPlayerTargetX; mPlayerTargetY = mPlayerTargetY < minY ? minY : mPlayerTargetY > maxY ? maxY : mPlayerTargetY; } } private void checkLevelUp() { int dueLevel = mItemsCollected / JetpackConfig.Progression.ITEMS_PER_LEVEL; while (mLevel < dueLevel) { mLevel++; Logger.d("Level up! Now at level " + mLevel); mFallMult *= JetpackConfig.Items.FALL_SPEED_LEVEL_MULT; mScoreMult *= JetpackConfig.Progression.SCORE_LEVEL_MULT; } } private void unlockScoreBasedAchievements() { int i; for (i = 0; i < JetpackConfig.Achievements.SCORE_ACHS.length; i++) { if (mScore >= JetpackConfig.Achievements.SCORE_FOR_ACH[i]) { unlockAchievement(JetpackConfig.Achievements.SCORE_ACHS[i]); } } } private void unlockComboBasedAchievements(int comboSize) { int i; for (i = 0; i < JetpackConfig.Achievements.COMBO_ACHS.length; i++) { // COMBO_ACHS[n] is the achievement to unlock for a combo of size n + 2 if (comboSize >= i + 2) { unlockAchievement(JetpackConfig.Achievements.COMBO_ACHS[i]); } } } private void sendIncrementalAchievements(boolean force) { if (!force && mIncAchCountdown > 0.0f) { // it's not time to send yet return; } if (SceneManager.getInstance().getActivity() == null) { // no Activity (maybe we're in the background), so can't send yet return; } if (mAchPendingCandy > 0) { incrementAchievements(JetpackConfig.Achievements.TOTAL_CANDY_ACHS, mAchPendingCandy); mAchPendingCandy = 0; } if (mAchPendingPresents > 0) { incrementAchievements(JetpackConfig.Achievements.TOTAL_PRESENTS_ACHS, mAchPendingPresents); mAchPendingPresents = 0; } if (mAchPendingSeconds >= 1.0f) { int seconds = (int) Math.floor(mAchPendingSeconds); incrementAchievements(JetpackConfig.Achievements.TOTAL_TIME_ACHS, seconds); mAchPendingSeconds -= seconds; } // submit score as well, since we're at it. submitScore(JetpackConfig.LEADERBOARD, mScore); // reset countdown mIncAchCountdown = JetpackConfig.Achievements.INC_ACH_SEND_INTERVAL; } private void unlockAchievement(int resId) { SceneActivity act = (SceneActivity) SceneManager.getInstance().getActivity(); if (!mUnlockedAchievements.contains(resId) && act != null) { act.postUnlockAchievement(resId); mUnlockedAchievements.add(resId); } } private void incrementAchievements(int[] resId, int steps) { for (int i : resId) { incrementAchievement(i, steps); } } private void incrementAchievement(int resId, int steps) { SceneActivity act = (SceneActivity) SceneManager.getInstance().getActivity(); if (steps > 0 && act != null) { act.postIncrementAchievement(resId, steps); } } private void submitScore(int resId, int score) { SceneActivity act = (SceneActivity) SceneManager.getInstance().getActivity(); if (act != null) { act.postSubmitScore(resId, score); } } }