/* * 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.doodles.tilt; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.res.Resources; import android.graphics.Canvas; import android.os.Build; import android.os.Vibrator; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.TextView; import com.google.android.apps.santatracker.doodles.shared.Actor; import com.google.android.apps.santatracker.doodles.shared.CallbackProcess; import com.google.android.apps.santatracker.doodles.shared.Camera; import com.google.android.apps.santatracker.doodles.shared.CameraShake; import com.google.android.apps.santatracker.doodles.shared.EventBus; import com.google.android.apps.santatracker.doodles.shared.EventBus.EventBusListener; import com.google.android.apps.santatracker.doodles.shared.Process; import com.google.android.apps.santatracker.doodles.shared.ProcessChain; import com.google.android.apps.santatracker.doodles.shared.RectangularInstructionActor; import com.google.android.apps.santatracker.doodles.shared.UIUtil; import com.google.android.apps.santatracker.doodles.shared.Vector2D; import com.google.android.apps.santatracker.doodles.shared.WaitProcess; import com.google.android.apps.santatracker.doodles.shared.physics.Util; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; /** * The model for the swimming game. */ public class SwimmingModel implements TiltModel, EventBusListener { private static final String TAG = SwimmingModel.class.getSimpleName(); public static final int LEVEL_WIDTH = 1280; public static final int[] SCORE_THRESHOLDS = { 30, 50, 100 }; private static final float SHAKE_FREQUENCY = 33; private static final float SHAKE_MAGNITUDE = 40; private static final float SHAKE_FALLOFF = 0.9f; private static final long VIBRATION_DURATION_MS = 50; private static final int WORLD_TO_METER_RATIO = 700; private static final long WAITING_STATE_DELAY_MS = 1000; private static final long COUNTDOWN_DELAY_MS = 4000; private static final float COUNTDOWN_BUMP_SCALE = 1.5f; public final List<String> collisionObjectTypes; public String levelName; public boolean collisionMode = true; public List<Actor> actors; public List<Actor> uiActors; // Will be drawn above actors. public Camera camera; public CameraShake cameraShake; public SwimmerActor swimmer; public RectangularInstructionActor instructions; public TextView countdownView; public ObstacleManager obstacleManager; public Vibrator vibrator; public Locale locale; public int distanceMeters; public int currentScoreThreshold = 0; public int screenWidth; public int screenHeight; public Vector2D tilt; // Measures the time elapsed in the current state. This value is reset to 0 upon entering a new // state. public long timeElapsed = 0; private float countdownTimeMs = 3000; private List<ProcessChain> processChains = new ArrayList<>(); public int playCount; /** * States for the swimming game. */ public enum SwimmingState { INTRO, WAITING, SWIMMING, FINISHED, } private SwimmingState state; public SwimmingModel() { tilt = Vector2D.get(0, 0); actors = new ArrayList<>(); uiActors = new ArrayList<>(); state = SwimmingState.INTRO; collisionObjectTypes = new ArrayList<>(); collisionObjectTypes.addAll(BoundingBoxSpriteActor.TYPE_TO_RESOURCE_MAP.keySet()); } @Override public void onEventReceived(int type, Object data) { switch(type) { case EventBus.VIBRATE: if (vibrator != null) { vibrator.vibrate(VIBRATION_DURATION_MS); } break; case EventBus.SHAKE_SCREEN: cameraShake.shake(SHAKE_FREQUENCY, SHAKE_MAGNITUDE, SHAKE_FALLOFF); break; case EventBus.GAME_STATE_CHANGED: SwimmingState state = (SwimmingState) data; if (state == SwimmingState.WAITING) { long countdownDelayMs = WAITING_STATE_DELAY_MS; if (playCount == 0) { // Wait for the crossfade to finish then show instructions. processChains.add(new WaitProcess(WAITING_STATE_DELAY_MS).then(new CallbackProcess() { @Override public void updateLogic(float deltaMs) { if (getState() == SwimmingState.WAITING) { instructions.show(); } } }).then(new WaitProcess(COUNTDOWN_DELAY_MS).then(new CallbackProcess() { @Override public void updateLogic(float deltaMs) { instructions.hide(); } }))); // If we're showing the instructions, wait until the instructions is hidden before // starting the countdown. countdownDelayMs += COUNTDOWN_DELAY_MS + 300; } // Start countdown. processChains.add(new WaitProcess(countdownDelayMs).then(new Process() { @Override public void updateLogic(float deltaMs) { final float oldCountdownTimeMs = countdownTimeMs; float newCountdownTimeMs = countdownTimeMs - deltaMs; if ((long) newCountdownTimeMs / 1000 != (long) oldCountdownTimeMs / 1000) { countdownView.post(new Runnable() { @Override public void run() { countdownView.setVisibility(View.VISIBLE); // Use the old integer value so that the countdown goes 3, 2, 1 and not 2, 1, 0. String countdownValue = NumberFormat.getInstance(locale).format((long) oldCountdownTimeMs / 1000); setTextAndBump(countdownView, countdownValue); } }); } countdownTimeMs = newCountdownTimeMs; } @Override public boolean isFinished() { return countdownTimeMs <= 0; } }).then(new CallbackProcess() { @Override public void updateLogic(float deltaMs) { countdownView.post(new Runnable() { @Override public void run() { countdownView.setVisibility(View.INVISIBLE); } }); setState(SwimmingState.SWIMMING); } })); } else if (state == SwimmingState.SWIMMING) { swimmer.startSwimming(); } } } public void setState(SwimmingState state) { if (this.state != state) { this.state = state; timeElapsed = 0; EventBus.getInstance().sendEvent(EventBus.GAME_STATE_CHANGED, state); } } public SwimmingState getState() { return state; } public void update(float deltaMs) { synchronized (this) { timeElapsed += deltaMs; ProcessChain.updateChains(processChains, deltaMs); for (int i = 0; i < actors.size(); i++) { actors.get(i).update(deltaMs); } for (int i = 0; i < uiActors.size(); i++) { uiActors.get(i).update(deltaMs); } if (state == SwimmingState.SWIMMING || state == SwimmingState.WAITING) { swimmer.updateTargetPositionFromTilt(tilt, LEVEL_WIDTH); int newDistance = getMetersFromWorldY(swimmer.position.y); if (newDistance != distanceMeters) { if (newDistance >= SwimmingLevelChunk.LEVEL_LENGTH_IN_METERS) { swimmer.endGameWithoutCollision(); } distanceMeters = Math.min(SwimmingLevelChunk.LEVEL_LENGTH_IN_METERS, newDistance); EventBus.getInstance().sendEvent(EventBus.SCORE_CHANGED, distanceMeters); } if (swimmer.isDead) { setState(SwimmingState.FINISHED); } resolveCollisions(deltaMs); clampCameraPosition(); } } } public void clampCameraPosition() { if (!SwimmingFragment.editorMode) { float swimmerHeight = swimmer.collisionBody.getHeight(); float minCameraOffset = camera.toWorldScale(screenHeight) - 3.5f * swimmerHeight; float maxCameraOffset = camera.toWorldScale(screenHeight) - 4.0f * swimmerHeight; camera.position.set(camera.position.x, Util.clamp(camera.position.y, swimmer.position.y - minCameraOffset, swimmer.position.y - maxCameraOffset)); } } public void resolveCollisions(float deltaMs) { obstacleManager.resolveCollisions(swimmer, deltaMs); } public void drawActors(Canvas canvas) { List<Actor> actorsToDraw = new ArrayList<>(actors); actorsToDraw.addAll(obstacleManager.getActors()); Collections.sort(actorsToDraw); for (int i = 0; i < actorsToDraw.size(); i++) { actorsToDraw.get(i).draw(canvas); } } public void drawUiActors(Canvas canvas) { for (int i = 0; i < uiActors.size(); i++) { uiActors.get(i).draw(canvas); } } public int getStarCount() { return currentScoreThreshold; } public void onTouchDown() { if (getState() == SwimmingState.SWIMMING) { swimmer.diveDown(); } } public void createActor(Vector2D position, String objectType, Resources resources) { if (BoundingBoxSpriteActor.TYPE_TO_RESOURCE_MAP.containsKey(objectType)) { actors.add(BoundingBoxSpriteActor.create(position, objectType, resources)); } } public void sortActors() { Collections.sort(actors); } @Override public List<Actor> getActors() { return actors; } @Override public void addActor(Actor actor) { if (actor instanceof SwimmerActor) { this.swimmer = (SwimmerActor) actor; } else if (actor instanceof Camera) { this.camera = (Camera) actor; } else if (actor instanceof CameraShake) { this.cameraShake = (CameraShake) actor; } else if (actor instanceof ObstacleManager) { this.obstacleManager = (ObstacleManager) actor; } actors.add(actor); sortActors(); } public void addUiActor(Actor actor) { if (actor instanceof RectangularInstructionActor) { this.instructions = (RectangularInstructionActor) actor; } uiActors.add(actor); } public void setCountdownView(TextView countdownView) { this.countdownView = countdownView; } @Override public void setLevelName(String levelName) { this.levelName = levelName; } public static int getMetersFromWorldY(float distance) { return Math.max(0, (int) (-distance / WORLD_TO_METER_RATIO)); } public static int getWorldYFromMeters(int meters) { return -meters * WORLD_TO_METER_RATIO; } private void setTextAndBump(final TextView textView, String text) { float endScale = textView.getScaleX(); float startScale = COUNTDOWN_BUMP_SCALE * textView.getScaleX(); textView.setText(text); if (!"Nexus 9".equals(Build.MODEL)) { ValueAnimator scaleAnimation = UIUtil.animator(200, new AccelerateDecelerateInterpolator(), new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float scaleValue = (float) valueAnimator.getAnimatedValue("scale"); textView.setScaleX(scaleValue); textView.setScaleY(scaleValue); } }, UIUtil.floatValue("scale", startScale, endScale) ); scaleAnimation.start(); } } }