/* * 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.content.res.Resources; import android.graphics.Canvas; import com.google.android.apps.santatracker.doodles.R; import com.google.android.apps.santatracker.doodles.shared.AnimatedSprite; import com.google.android.apps.santatracker.doodles.shared.AnimatedSprite.AnimatedSpriteListener; import com.google.android.apps.santatracker.doodles.shared.CallbackProcess; import com.google.android.apps.santatracker.doodles.shared.EventBus; import com.google.android.apps.santatracker.doodles.shared.GameFragment; import com.google.android.apps.santatracker.doodles.shared.MultiSpriteActor; import com.google.android.apps.santatracker.doodles.shared.ProcessChain; import com.google.android.apps.santatracker.doodles.shared.Sprites; 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.Polygon; import com.google.android.apps.santatracker.doodles.shared.physics.Util; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The player-controlled swimmer in the swimming game. */ public class SwimmerActor extends BoundingBoxSpriteActor { private static final String TAG = SwimmerActor.class.getSimpleName(); private static final float ACCELERATION_Y = -500; private static final float MIN_SPEED = 400; private static final float DEFAULT_MAX_SPEED = 800; private static final long SPEED_STEP_DURATION_MS = 10000; private static final float TILT_VELOCITY = 10000; public static final float SWIMMER_SCALE = 1.6f; public static final int DIVE_DURATION_MS = 1500; public static final int DIVE_COOLDOWN_MS = 5000; public static final String SWIMMER_ACTOR_TYPE = "swimmer"; public static final String KICKOFF_IDLE_SPRITE = "kickoff_idle"; public static final String KICKOFF_START_SPRITE = "kickoff_start"; public static final String RINGS_SPRITE = "rings"; public static final String SWIM_LOOP_SPRITE = "swimming"; public static final String CAN_COLLIDE_SPRITE = "can_collide"; public static final String FREEZE_SPRITE = "freeze"; public static final String DIVE_DOWN_SPRITE = "dive"; public static final String UNDER_LOOP_SPRITE = "under_loop"; public static final String RISE_UP_SPRITE = "rise_up"; public static final float KICKOFF_IDLE_Y_OFFSET = -240; private static final Vector2D[] VERTEX_OFFSETS = { Vector2D.get(0, 0), Vector2D.get(96, 0), Vector2D.get(96, 90), Vector2D.get(0, 90) }; private static final Map<String, Vector2D> OFFSET_MAP; static { OFFSET_MAP = new HashMap<>(); OFFSET_MAP.put(KICKOFF_IDLE_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(KICKOFF_START_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(RINGS_SPRITE, Vector2D.get(-60, -20)); // TODO OFFSET_MAP.put(SWIM_LOOP_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(CAN_COLLIDE_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(FREEZE_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(DIVE_DOWN_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(UNDER_LOOP_SPRITE, Vector2D.get(0, 0)); OFFSET_MAP.put(RISE_UP_SPRITE, Vector2D.get(0, 0)); } public boolean controlsEnabled = true; public boolean isInvincible = false; public boolean isUnderwater = false; public boolean isDead = false; private MultiSpriteActor multiSpriteActor; private AnimatedSprite canCollideSprite; private AnimatedSprite freezeSprite; private String collidedObjectType; private AnimatedSprite ringsSprite; private Vector2D ringsSpriteOffset; private float restartSpeed = MIN_SPEED; private float maxSpeed = DEFAULT_MAX_SPEED; private long currentSpeedStepTime = 0; private boolean diveEnabled = false; private float targetX; private List<ProcessChain> processChains = new ArrayList<>(); public SwimmerActor(Polygon collisionBody, MultiSpriteActor spriteActor) { super(collisionBody, spriteActor, Vector2D.get(OFFSET_MAP.get(KICKOFF_IDLE_SPRITE)).scale(SWIMMER_SCALE), SWIMMER_ACTOR_TYPE); multiSpriteActor = spriteActor; canCollideSprite = multiSpriteActor.sprites.get(CAN_COLLIDE_SPRITE); canCollideSprite.setLoop(false); freezeSprite = multiSpriteActor.sprites.get(FREEZE_SPRITE); freezeSprite.setLoop(false); multiSpriteActor.sprites.get(DIVE_DOWN_SPRITE).addListener(new AnimatedSpriteListener() { @Override public void onLoop() { zIndex = -3; setSprite(UNDER_LOOP_SPRITE); } }); multiSpriteActor.sprites.get(RISE_UP_SPRITE).addListener(new AnimatedSpriteListener() { @Override public void onLoop() { zIndex = 0; setSprite(SWIM_LOOP_SPRITE); EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_dive_up); } }); multiSpriteActor.sprites.get(SWIM_LOOP_SPRITE).addListener(new AnimatedSpriteListener() { @Override public void onFrame(int index) { if (index == 5 || index == 13) { EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_ice_splash_a); } } }); ringsSprite = multiSpriteActor.sprites.get(RINGS_SPRITE); ringsSprite.setPaused(true); ringsSprite.setHidden(true); ringsSprite.setScale(SWIMMER_SCALE, SWIMMER_SCALE); ringsSprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { ringsSprite.setHidden(true); ringsSprite.setPaused(true); } }); ringsSpriteOffset = Vector2D.get(OFFSET_MAP.get(RINGS_SPRITE)).scale(SWIMMER_SCALE); multiSpriteActor.sprites.get(KICKOFF_START_SPRITE).addListener(new AnimatedSpriteListener() { @Override public void onLoop() { diveEnabled = true; diveDown(); } @Override public void onFrame(int index) { if (index == 3) { velocity.set(0, -restartSpeed); } } }); zIndex = 0; alpha = 1.0f; scale = SWIMMER_SCALE; targetX = position.x; } @Override public void update(float deltaMs) { super.update(deltaMs); ProcessChain.updateChains(processChains, deltaMs); // Update x position based on tilt. float frameVelocityX = TILT_VELOCITY * deltaMs / 1000; float positionDeltaX = targetX - position.x; if (Math.abs(positionDeltaX) < frameVelocityX) { // We will overshoot if we apply the frame velocity. Just go straight to the target position. moveTo(targetX, position.y); } else { moveTo(position.x + Math.signum(positionDeltaX) * frameVelocityX, position.y); } // Update acceleration and frame rate if necessary. if (velocity.getLength() > 1) { velocity.y = Math.max(-maxSpeed, velocity.y + ACCELERATION_Y * deltaMs / 1000); if (velocity.y == -maxSpeed) { multiSpriteActor.sprites.get(SWIM_LOOP_SPRITE).setFPS(24); } else { multiSpriteActor.sprites.get(SWIM_LOOP_SPRITE).setFPS(48); } currentSpeedStepTime += deltaMs; if (currentSpeedStepTime > SPEED_STEP_DURATION_MS) { maxSpeed += 500; currentSpeedStepTime = 0; } } ringsSprite.update(deltaMs); ringsSprite.setPosition(spriteActor.position.x + ringsSpriteOffset.x, spriteActor.position.y + ringsSpriteOffset.y); } @Override public void draw(Canvas canvas) { if (hidden) { return; } spriteActor.draw(canvas, spriteOffset.x, spriteOffset.y, spriteActor.sprite.frameWidth * scale, spriteActor.sprite.frameHeight * scale); ringsSprite.draw(canvas); collisionBody.draw(canvas); } @Override public JSONObject toJSON() { return null; } public void setSprite(String key) { multiSpriteActor.setSprite(key); spriteOffset.set(OFFSET_MAP.get(key)).scale(SWIMMER_SCALE); } public void moveTo(float x, float y) { collisionBody.moveTo(x, y); position.set(x, y); spriteActor.position.set(x, y); targetX = x; } public void updateTargetPositionFromTilt(Vector2D tilt, float levelWidth) { if (controlsEnabled) { // Decrease the amount of tilt necessary to move the swimmer. float tiltPercentage = (float) (tilt.x / (Math.PI / 2)); tiltPercentage *= 2.5f; int levelPadding = 60; targetX = Util.clamp( (levelWidth / 2) - (collisionBody.getWidth() / 2) + (tiltPercentage * levelWidth / 2), 0 - collisionBody.getWidth() + levelPadding, levelWidth - (2 * collisionBody.getWidth()) - levelPadding); } } public void startSwimming() { setSprite(KICKOFF_START_SPRITE); } public void collide(String objectType) { restartSpeed = Math.max(MIN_SPEED, Math.abs(velocity.y / 4)); maxSpeed = restartSpeed; currentSpeedStepTime = 0; moveTo(positionBeforeFrame.x, positionBeforeFrame.y); velocity.set(0, 0); controlsEnabled = false; if (objectType.equals(DUCK) || objectType.equals(ICE_CUBE)) { EventBus.getInstance().sendEvent(EventBus.SHAKE_SCREEN); EventBus.getInstance().sendEvent(EventBus.VIBRATE); if (objectType.equals(DUCK)) { setSprite(SwimmerActor.CAN_COLLIDE_SPRITE); EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_duck_collide); } else { // Ice cube. setSprite(SwimmerActor.FREEZE_SPRITE); EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_ice_collide); } } else { // Octopus. // Just play the sound for the octopus. It vibrates later (when it actually grabs the lemon). EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_grab); } collidedObjectType = objectType; isDead = true; } public void endGameWithoutCollision() { controlsEnabled = false; collidedObjectType = HAND_GRAB; isDead = true; } public String getCollidedObjectType() { return collidedObjectType; } public void diveDown() { if (controlsEnabled && diveEnabled) { EventBus.getInstance().sendEvent(EventBus.SWIMMING_DIVE); EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.swimming_dive_down); ringsSprite.setHidden(false); ringsSprite.setPaused(false); isUnderwater = true; setSprite(DIVE_DOWN_SPRITE); diveEnabled = false; ProcessChain waitThenRiseUp = new WaitProcess(DIVE_DURATION_MS).then(new CallbackProcess() { @Override public void updateLogic(float deltaMs) { isUnderwater = false; setSprite(RISE_UP_SPRITE); } }).then(new WaitProcess(DIVE_COOLDOWN_MS)).then(new CallbackProcess() { @Override public void updateLogic(float deltaMs) { diveEnabled = true; } }); processChains.add(waitThenRiseUp); } } public static final SwimmerActor create(Vector2D position, Resources res, final GameFragment gameFragment) { if (gameFragment.isDestroyed) { return null; } Map<String, AnimatedSprite> spriteMap = new HashMap<>(); spriteMap.put(KICKOFF_IDLE_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_idle)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(KICKOFF_START_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_start)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(RINGS_SPRITE, AnimatedSprite.fromFrames(res, Sprites.swimming_rings)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(SWIM_LOOP_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_swimming)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(CAN_COLLIDE_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_dazed)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(FREEZE_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_frozen)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(DIVE_DOWN_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_descending)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(UNDER_LOOP_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_swimmingunderwater)); if (gameFragment.isDestroyed) { return null; } spriteMap.put(RISE_UP_SPRITE, AnimatedSprite.fromFrames(res, Sprites.penguin_swim_ascending)); if (gameFragment.isDestroyed) { return null; } MultiSpriteActor spriteActor = new MultiSpriteActor(spriteMap, KICKOFF_IDLE_SPRITE, position, Vector2D.get(0, 0)); if (gameFragment.isDestroyed) { return null; } return new SwimmerActor(getBoundingBox(position, VERTEX_OFFSETS, SWIMMER_SCALE), spriteActor); } }