/* * 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.waterpolo; import android.graphics.Canvas; import android.util.Log; import com.google.android.apps.santatracker.doodles.shared.Actor; import com.google.android.apps.santatracker.doodles.shared.ActorTween.Callback; 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.Vector2D; import com.google.android.apps.santatracker.doodles.shared.physics.Util; import com.google.android.apps.santatracker.doodles.tilt.ColoredRectangleActor; import java.util.HashMap; /** * Actor for both the player and the opponent in the water polo game. * The debug position marker is omitted. */ public class WaterPoloActor extends Actor { private static final String TAG = WaterPoloActor.class.getSimpleName(); /** * Labels for all the different sprites which make up the actor. */ public enum WaterPoloActorPart { BodyIdle, BodyIdleNoBall, // Used at end of game. BodyEntrance, // Used when character enters the game. BodyLeft, BodyRight, BodyBlock, BodyThrow, BodyPickUpBall, // Catch a new ball from the slide. } private static final int RELEASE_THROW_FRAME = 2; HashMap<WaterPoloActorPart, AnimatedSprite> sprites; WaterPoloActorPart body; AnimatedSprite currentSprite; float collisionWidthUnscaled; float collisionHeightUnscaled; ColoredRectangleActor collisionBox; Callback shotBlockCallback; public WaterPoloActor() { sprites = new HashMap<>(); body = WaterPoloActorPart.BodyIdle; } @Override public void update(float deltaMs) { super.update(deltaMs); if (currentSprite == null) { return; } currentSprite.update(deltaMs); if (collisionBox != null) { collisionBox.dimens = Vector2D.get(collisionWidthUnscaled * scale, collisionHeightUnscaled * scale); // The collision box should be centered on the sprite. float centerX = position.x - (currentSprite.anchor.x) + (currentSprite.frameWidth * 0.5f); float centerY = position.y - (currentSprite.anchor.y) + (currentSprite.frameHeight * 0.5f); collisionBox.position.set(centerX - collisionBox.dimens.x * 0.5f, centerY - collisionBox.dimens.y * 0.5f); } } @Override public void draw(Canvas canvas) { super.draw(canvas); if (currentSprite == null) { Log.e(TAG, "Body part null: " + body); return; } currentSprite.setPosition(position.x, position.y); currentSprite.setScale(scale, scale); currentSprite.setHidden(hidden); currentSprite.draw(canvas); } // Add a sprite to the actor for the given part. You need to call this for a part before // trying to use that part. Offsets are measured from the actor's position public void addSprite(WaterPoloActorPart part, int xOffset, int yOffset, AnimatedSprite sprite) { // After picking up the ball, go straight to idle. if (part == WaterPoloActorPart.BodyPickUpBall) { sprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { idle(); } }); } // This makes sure the sprite scales from self.position sprite.setAnchor(-xOffset, yOffset); if (part != body) { sprite.setHidden(true); } sprites.put(part, sprite); } // The collision box position, relative to self.position, is hardcoded in update, because the // opponents all have the same position. public void setCollisionBox(float width, float height) { collisionWidthUnscaled = width; collisionHeightUnscaled = height; collisionBox = new ColoredRectangleActor( Vector2D.get(0, 0), Vector2D.get(0, 0), ColoredRectangleActor.UNSPECIFIED); } // Returns YES iff (x, y) lies within the actor's collision box. public boolean canBlock(float x, float y) { if (hidden || body == WaterPoloActorPart.BodyEntrance || collisionBox == null) { return false; } Vector2D worldCoords = Vector2D.get(x, y); Vector2D lowerRight = Vector2D.get(collisionBox.position).add(collisionBox.dimens); return Util.pointIsWithinBounds(collisionBox.position, lowerRight, worldCoords); } public void idle() { setBodyUnchecked(WaterPoloActorPart.BodyIdle); } public void idleNoBall() { setBodyUnchecked(WaterPoloActorPart.BodyIdleNoBall); } public void pickUpBall() { setBodyUnchecked(WaterPoloActorPart.BodyPickUpBall); } public void swimLeft() { if (body == WaterPoloActorPart.BodyBlock) { AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyBlock); sprite.clearListeners(); sprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { setBodyUnchecked(WaterPoloActorPart.BodyLeft); } @Override public void onFrame(int index) { if (index == 3) { if (shotBlockCallback != null) { shotBlockCallback.call(); shotBlockCallback = null; } } } }); } else { setBodyUnchecked(WaterPoloActorPart.BodyLeft); } } public void swimRight() { if (body == WaterPoloActorPart.BodyBlock) { AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyBlock); sprite.clearListeners(); sprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { setBodyUnchecked(WaterPoloActorPart.BodyRight); } @Override public void onFrame(int index) { if (index == 3) { if (shotBlockCallback != null) { shotBlockCallback.call(); shotBlockCallback = null; } } } }); } else { setBodyUnchecked(WaterPoloActorPart.BodyRight); } } // The callback gets called when the actor is at the top of the blocking jump (so the game // can deflect the ball at that instant). public void blockShot(final Callback callback) { AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyBlock); sprite.clearListeners(); final WaterPoloActorPart previousBody = body; shotBlockCallback = callback; sprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { setBodyUnchecked(previousBody); } @Override public void onFrame(int index) { if (index == 3) { if (shotBlockCallback != null) { shotBlockCallback.call(); shotBlockCallback = null; } } } }); setBodyUnchecked(WaterPoloActorPart.BodyBlock); } // releaseCallback gets called when the actor releases the ball (so the game can swap in the real // ball). endCallback will be called when the actor is done throwing and starts picking up another // ball (so the game can start the grapeOnSlide animation at the correct time). public void throwBall(final Callback releaseCallback, final Callback endCallback) { AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyThrow); sprite.clearListeners(); sprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { pickUpBall(); endCallback.call(); } @Override public void onFrame(int index) { if (index == RELEASE_THROW_FRAME) { releaseCallback.call(); } } }); setBodyUnchecked(WaterPoloActorPart.BodyThrow); } // Callback is called at the end of the animation, when actor is ready to play. public void enter(final Callback callback) { AnimatedSprite sprite = sprites.get(WaterPoloActorPart.BodyEntrance); sprite.clearListeners(); sprite.addListener(new AnimatedSpriteListener() { @Override public void onLoop() { callback.call(); } }); setBodyUnchecked(WaterPoloActorPart.BodyEntrance); } private void setBodyUnchecked(WaterPoloActorPart part) { AnimatedSprite newSprite = sprites.get(part); if (newSprite == null) { Log.e(TAG, "Error: sprite " + part + " not loaded."); assert(false); } newSprite.setFrameIndex(0); body = part; update(0); currentSprite = newSprite; } }