/*
* 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.content.Context;
import android.content.res.Resources;
import android.graphics.Paint;
import android.os.Vibrator;
import com.google.android.apps.santatracker.doodles.R;
import com.google.android.apps.santatracker.doodles.shared.Actor;
import com.google.android.apps.santatracker.doodles.shared.ActorHelper;
import com.google.android.apps.santatracker.doodles.shared.ActorTween;
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.BallActor;
import com.google.android.apps.santatracker.doodles.shared.CameraShake;
import com.google.android.apps.santatracker.doodles.shared.EmptyTween;
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.Interpolator;
import com.google.android.apps.santatracker.doodles.shared.RectangularInstructionActor;
import com.google.android.apps.santatracker.doodles.shared.SpriteActor;
import com.google.android.apps.santatracker.doodles.shared.Sprites;
import com.google.android.apps.santatracker.doodles.shared.TextActor;
import com.google.android.apps.santatracker.doodles.shared.Tween;
import com.google.android.apps.santatracker.doodles.shared.TweenManager;
import com.google.android.apps.santatracker.doodles.shared.Vector2D;
import com.google.android.apps.santatracker.doodles.tilt.ColoredRectangleActor;
import com.google.android.apps.santatracker.doodles.waterpolo.WaterPoloActor.WaterPoloActorPart;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Model for the Water Polo game.
*/
public class WaterPoloModel {
/**
* A grape that the apple throws.
*/
class GrapeActor extends BallActor {
public boolean shotBlocked;
public int bounces;
GrapeActor(AnimatedSprite ballSprite) {
super(null, ballSprite, null, 3);
bounces = 0;
shotBlocked = false;
}
}
/**
* High-level phases of the game are controlled by a state machine which uses these states.
*/
enum State {
TITLE,
WAITING,
PLAYING,
GAME_OVER
}
private static final String TAG = WaterPoloModel.class.getSimpleName();
private static final float SHAKE_FALLOFF = 0.9f;
private static final int VIBRATION_SMALL = 40;
public static final int WATER_POLO_HEIGHT = 960;
public static final int WATER_POLO_WIDTH = 540;
public static final int ONE_STAR_THRESHOLD = 1;
public static final int TWO_STAR_THRESHOLD = 10;
public static final int THREE_STAR_THRESHOLD = 20;
private static final float TIME_LEFT_TEXT_X = WATER_POLO_WIDTH * 0.5f;
private static final float TIME_LEFT_TEXT_Y = 12;
private static final float TIME_LEFT_TEXT_SCALE = 3.2f;
private static final String TIME_LEFT_UNDER_TEXT = "88:88";
private static final int TIME_LEFT_TEXT_RGB = 0xFFFFBD2E;
private static final int TIME_LEFT_TEXT_GLOW_RGB = 0x88FF9A2E;
private static final int TIME_LEFT_UNDER_RGB = 0xFF3B200D;
private static final float POINT_TEXT_ANIMATION_TIME = 1.6f;
private static final int POINT_MINUS_TEXT_RGB = 0xffd61e1e;
private static final int POINT_PLUS_TEXT_RGB = 0xff4dab1f;
// Currently throw delay is not supported. The constant and related variables are left in to
// facilitate prototyping in case throw delay would be supported in the future.
private static final float THROW_DELAY_SECONDS = 0.4f;
private static final float TOTAL_TIME = 30f;
private static final int BALL_SPEED = 1300;
private static final int OPPONENT_ONE_ENTRANCE_THRESHOLD = 1;
private static final int OPPONENT_TWO_ENTRANCE_THRESHOLD = 4;
private static final int OPPONENT_THREE_ENTRANCE_THRESHOLD = 10;
private static final int OPPONENT_ONE_SPEED = 60;
private static final int OPPONENT_TWO_SPEED = 120;
private static final int OPPONENT_THREE_SPEED = 170;
private static final int MS_BETWEEN_TARGET_FLASHES = 1000;
public CameraShake cameraShake;
public List<Actor> actors;
private List<Actor> effects;
private Resources resources;
private EventBus eventBus;
private final Vibrator vibrator;
private TweenManager tweenManager;
private WaterPoloActor player;
private WaterPoloActor opponentOne;
private WaterPoloActor opponentTwo;
private WaterPoloActor opponentThree;
private ColoredRectangleActor timeLeftFrameBorder;
private ColoredRectangleActor timeLeftFrame;
private TextActor timeLeftText;
private TextActor timeLeftTextGlow;
private TextActor timeLeftUnder;
private List<GrapeActor> balls;
private SpriteActor slideBack;
private SpriteActor targetLeft;
private SpriteActor targetMiddle;
private SpriteActor targetRight;
private SpriteActor goal;
private static final float GOAL_BOX_X = 108;
private static final float GOAL_BOX_Y = 75;
private static final float GOAL_BOX_WIDTH = 333;
private static final float GOAL_BOX_HEIGHT = 98;
RectangularInstructionActor instructions;
public State state;
public int score;
public int ballsThrown;
public long titleDurationMs = GameFragment.TITLE_DURATION_MS;
private float time;
private float msTillNextTargetPulse;
private boolean scoredAtLeastOneShot;
private boolean canThrow;
private boolean didReset;
public WaterPoloModel(Resources resources, Context context) {
this.resources = resources;
actors = new ArrayList<>();
effects = new ArrayList<>();
balls = new ArrayList<>();
cameraShake = new CameraShake();
actors.add(cameraShake);
tweenManager = new TweenManager();
eventBus = EventBus.getInstance();
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
createActors();
reset(true);
}
private void createActors() {
Actor pool = actorWithIds(Sprites.present_throw_floor);
pool.zIndex = -3;
// The pool image is 780x960, but the center 540x960 of that image is what corresponds
// to the game area, so offset the image to the left. The edges only show on screens with aspect
// ratios wider than 9:16.
pool.position.x = -120;
actors.add(pool);
goal = actorWithIds(Sprites.present_throw_santabag);
goal.position.set(-14, 72);
goal.zIndex = -4;
goal.sprite.setLoop(false);
goal.sprite.setPaused(true);
goal.sprite.addListener(new AnimatedSpriteListener() {
@Override
public void onFinished() {
goal.sprite.setFrameIndex(0);
goal.sprite.setPaused(true);
}
});
actors.add(goal);
slideBack = actorWithIds(Sprites.present_throw_elfbag);
slideBack.zIndex = 4;
actors.add(slideBack);
moveSlide(0);
timeLeftFrame = new ColoredRectangleActor(
Vector2D.get(WATER_POLO_WIDTH * 0.35f, TIME_LEFT_TEXT_Y - 6),
Vector2D.get(WATER_POLO_WIDTH * 0.3f, 50));
timeLeftFrame.setColor(0xff000000);
timeLeftFrame.zIndex = 4;
actors.add(timeLeftFrame);
timeLeftFrameBorder = new ColoredRectangleActor(
Vector2D.get(WATER_POLO_WIDTH * 0.35f, TIME_LEFT_TEXT_Y - 6),
Vector2D.get(WATER_POLO_WIDTH * 0.3f, 50));
timeLeftFrameBorder.setStyle(Paint.Style.STROKE);
timeLeftFrameBorder.setStrokeWidth(5);
timeLeftFrameBorder.setColor(0xff555555);
timeLeftFrameBorder.zIndex = 4;
actors.add(timeLeftFrameBorder);
timeLeftUnder = new TextActor(TIME_LEFT_UNDER_TEXT);
timeLeftUnder.position.set(TIME_LEFT_TEXT_X, TIME_LEFT_TEXT_Y);
timeLeftUnder.scale = TIME_LEFT_TEXT_SCALE;
timeLeftUnder.setBold(true);
timeLeftUnder.setFont(resources.getAssets(), "dseg7.ttf");
timeLeftUnder.setColor(TIME_LEFT_UNDER_RGB);
timeLeftUnder.alignCenter();
timeLeftUnder.zIndex = 4;
actors.add(timeLeftUnder);
timeLeftTextGlow = new TextActor("00:30");
timeLeftTextGlow.position.set(TIME_LEFT_TEXT_X, TIME_LEFT_TEXT_Y);
timeLeftTextGlow.scale = TIME_LEFT_TEXT_SCALE;
timeLeftTextGlow.setBold(true);
timeLeftTextGlow.enableBlur(0.6f);
timeLeftTextGlow.setFont(resources.getAssets(), "dseg7.ttf");
timeLeftTextGlow.setColor(TIME_LEFT_TEXT_GLOW_RGB);
timeLeftTextGlow.alignCenter();
timeLeftTextGlow.zIndex = 5;
actors.add(timeLeftTextGlow);
timeLeftText = new TextActor("00:30");
timeLeftText.position.set(TIME_LEFT_TEXT_X, TIME_LEFT_TEXT_Y);
timeLeftText.scale = TIME_LEFT_TEXT_SCALE;
timeLeftText.setBold(true);
timeLeftText.setFont(resources.getAssets(), "dseg7.ttf");
timeLeftText.setColor(TIME_LEFT_TEXT_RGB);
timeLeftText.alignCenter();
timeLeftText.zIndex = 6;
actors.add(timeLeftText);
targetLeft = createTarget(166, 128);
actors.add(targetLeft);
targetMiddle = createTarget(273, 128);
actors.add(targetMiddle);
targetRight = createTarget(380, 128);
actors.add(targetRight);
// TODO: Change block sprites
opponentOne = createOpponent(0.8f,
Sprites.present_throw_def_orange_left,
Sprites.present_throw_def_orange_right,
Sprites.present_throw_def_orange_emerge,
Sprites.present_throw_def_orange_blocking);
actors.add(opponentOne);
opponentTwo = createOpponent(0.9f,
Sprites.present_throw_def_green_left,
Sprites.present_throw_def_green_right,
Sprites.present_throw_def_green_emerge,
Sprites.present_throw_def_green_blocking);
actors.add(opponentTwo);
opponentThree = createOpponent(1.0f,
Sprites.present_throw_def_red_left,
Sprites.present_throw_def_red_right,
Sprites.present_throw_def_red_emerge,
Sprites.present_throw_def_red_blocking);
actors.add(opponentThree);
player = new WaterPoloActor();
player.addSprite(WaterPoloActorPart.BodyIdle, -84, 180,
spriteWithIds(Sprites.present_throw_idle));
player.addSprite(WaterPoloActorPart.BodyThrow, -84, 180,
spriteWithIds(Sprites.present_throw_throwing));
player.addSprite(WaterPoloActorPart.BodyPickUpBall, -84, 180,
spriteWithIds(Sprites.present_throw_reloading));
player.addSprite(WaterPoloActorPart.BodyIdleNoBall, -84, 180,
spriteWithIds(Sprites.present_throw_celebrate, 2));
actors.add(player);
AnimatedSprite diagram = spriteWithIds(Sprites.present_throw_tutorials);
diagram.setFPS(7);
instructions = new RectangularInstructionActor(resources, diagram);
instructions.hidden = true;
instructions.scale = 0.6f;
instructions.position.set(WATER_POLO_WIDTH * 0.5f - instructions.getScaledWidth() / 2,
WATER_POLO_HEIGHT * 0.46f - instructions.getScaledHeight() / 2f);
actors.add(instructions);
}
SpriteActor createTarget(float x, float y) {
SpriteActor target = actorWithIds(Sprites.waterpolo_target);
target.sprite.setAnchor(target.sprite.frameWidth / 2, target.sprite.frameHeight / 2);
target.position.set(x, y);
target.hidden = true;
return target;
}
WaterPoloActor createOpponent(float scale,
int[] leftSprite, int[] rightSprite,
int[] emergeSprite, int[] blockSprite) {
WaterPoloActor opponent = new WaterPoloActor();
opponent.addSprite(WaterPoloActorPart.BodyEntrance, -45, 142,
spriteWithIds(emergeSprite, 12));
opponent.addSprite(WaterPoloActorPart.BodyLeft, -45, 142,
spriteWithIds(leftSprite, 6));
opponent.addSprite(WaterPoloActorPart.BodyRight, -45, 142,
spriteWithIds(rightSprite, 6));
opponent.addSprite(WaterPoloActorPart.BodyBlock, -45, 142,
spriteWithIds(blockSprite));
opponent.scale = scale;
opponent.setCollisionBox(100, 90);
return opponent;
}
/**
* Moves the slide left / right. Used to attach the slide to the side of the screen on different
* aspect ratio screens.
*/
public void moveSlide(float slideOffsetX) {
slideBack.position.set(300 + slideOffsetX, 760);
}
// Put everything back to the beginning state.
// Used at start of game & also if user clicks replay.
public void reset(boolean firstPlay) {
tweenManager.removeAll();
if (firstPlay) {
setState(State.TITLE);
} else {
setState(State.WAITING);
}
// Remove all temporary effects
for (int i = effects.size() - 1; i >= 0; i--) {
Actor effect = effects.get(i);
actors.remove(effect);
effects.remove(effect);
}
// Remove all balls
for (int i = balls.size() - 1; i >= 0; i--) {
BallActor ball = balls.get(i);
actors.remove(ball);
balls.remove(ball);
}
score = 0;
scoredAtLeastOneShot = false;
ballsThrown = 0;
eventBus.sendEvent(EventBus.SCORE_CHANGED, score);
// No opponents at start of game (give them 1 easy point to re-inforce that they are supposed
// to get goals, not attack the goalie).
opponentOne.hidden = true;
opponentTwo.hidden = true;
opponentThree.hidden = true;
msTillNextTargetPulse = MS_BETWEEN_TARGET_FLASHES;
// Y positions picked so opponents look evenly spaced when in perspective.
// X positions picked to make opponents come up on the left side (because they swim right
// first, so coming up on the left side gives room to swim) but not all at the same position
// (staggered looks better).
opponentOne.position.set(171, 300);
opponentTwo.position.set(271, 440);
opponentThree.position.set(171, 600);
player.position.set(198, 845);
player.idle();
canThrow = true;
maybePickUpAnotherBall();
if (firstPlay) {
// TODO: If you swipe before the instructions show, they still show.
// Fix this by adding a state machine like the android games have.
tweenManager.add(new EmptyTween(1.3f) {
@Override
protected void onFinish() {
if (ballsThrown == 0) {
instructions.show();
}
}
});
}
didReset = true;
time = TOTAL_TIME;
}
public void update(long deltaMs) {
// Track whether reset was called at any point.
// If so, this short-circuits the rest of the update.
didReset = false;
if (state == State.PLAYING) {
time = time - ((int) deltaMs / 1000f);
if (time <= 0) {
time = 0;
player.idleNoBall();
setState(State.GAME_OVER);
}
}
updateTimeLeftText();
tweenManager.update(deltaMs);
if (didReset) {
return;
}
for (int i = actors.size() - 1; i >= 0; i--) {
Actor actor = actors.get(i);
actor.update(deltaMs);
if (didReset) {
return;
}
}
for (int i = balls.size() - 1; i >= 0; i--) {
updateBall(balls.get(i));
}
// Show the targets until the player gets at least 1 shot in the goal.
if (!scoredAtLeastOneShot) {
float msTillNextTargetPulseBefore = msTillNextTargetPulse;
msTillNextTargetPulse -= deltaMs;
if (msTillNextTargetPulseBefore > 0 && msTillNextTargetPulse <= 0) {
pulseTargets();
}
}
Collections.sort(actors);
}
void updateTimeLeftText() {
String timeLeftString = "00:" + String.format(Locale.ENGLISH, "%02d", (int) time);
timeLeftText.setText(timeLeftString);
timeLeftTextGlow.setText(timeLeftString);
}
private void updateBall(final GrapeActor ball) {
// Make the ball shrink as it travels into the distance.
float ballStartY = 753;
float ballEndY = 157;
if (!ball.shotBlocked) {
ball.scale = 0.5f + (0.5f * (ball.position.y - ballEndY) / (ballStartY - ballEndY));
}
// Only allow blocking if ball is traveling towards goal (otherwise ball can get stuck bouncing
// between opponents).
if (ball.velocity.y < 0) {
final WaterPoloActor blockingOpponent = getBlockingOpponentIfAny(ball);
if (blockingOpponent != null) {
// Draw a -1 at the ball
addPointText(ball.position.x, ball.position.y - 150, "-1", POINT_MINUS_TEXT_RGB);
newScore(score - 1);
blockShot(ball, blockingOpponent);
}
}
// TODO: at small scales, ball isn't centered over its position.
// TODO: randomize vertical position of splats
if (!ball.shotBlocked && goalBoxContains(ball.position.x, ball.position.y)) {
// Draw a +1 at the ball.
if (ball.bounces == 2) {
addPointText(ball.position.x, ball.position.y, "+2", POINT_PLUS_TEXT_RGB);
newScore(score + 2);
} else {
addPointText(ball.position.x, ball.position.y, "+1", POINT_PLUS_TEXT_RGB);
newScore(score + 1);
}
scoreShot(ball);
}
// Check if ball has left screen. If so, count it as a miss and get ready for another throw.
if ((ball.position.y < 0 && !ball.shotBlocked)) {
missShot(ball);
}
// Check if ball touches the left and right wall and has not bounced off the wall twice.
// If so, bounce it off the wall.
// If not, remove the ball from play.
if (ball.position.x <= 0 || ball.position.x >= WATER_POLO_WIDTH) {
if (ball.bounces > 1 || ball.shotBlocked) {
missShot(ball);
} else {
ball.velocity.x = -ball.velocity.x;
ball.rotation = (float) (Math.atan(ball.velocity.y / ball.velocity.x) - (Math.PI / 2));
ball.update(20);
ball.bounces++;
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.present_throw_block);
}
}
// Ditto for the bottom wall.
if (ball.position.y > WATER_POLO_HEIGHT) {
if (ball.bounces > 1 || ball.shotBlocked) {
missShot(ball);
} else {
ball.velocity.y = -ball.velocity.y;
ball.rotation = (float) (Math.atan(ball.velocity.y / ball.velocity.x) - (Math.PI / 2));
ball.update(20);
ball.bounces++;
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.present_throw_block);
}
}
}
private void addBall(float radians) {
AnimatedSprite ballSprite = spriteWithIds(Sprites.present_throw_thrownpresent);
GrapeActor ball = new GrapeActor(ballSprite);
ball.shouldScaleWithHeight = false;
ball.zIndex = 20;
ball.position.set(player.position.x + 78, player.position.y - 89);
ball.clearStreak();
ball.rotation = radians + (float) Math.PI / 2;
ball.velocity.set(BALL_SPEED * (float) Math.cos(radians),
BALL_SPEED * (float) Math.sin(radians));
ball.hidden = false;
actors.add(ball);
balls.add(ball);
}
private void blockBall(final GrapeActor ball, WaterPoloActor blockingOpponent) {
float rotation = ball.velocity.x / 200;
float directionX = ball.velocity.x * 0.5f;
if (ball.velocity.x > 0) {
directionX += 25;
} else {
directionX -= 25;
}
float initialX = ball.position.x;
float finalX = initialX + directionX;
float initialY = ball.position.y;
float midY = initialY - 140;
float finalY = initialY + 220;
ball.velocity.set(0, 0);
ball.shotBlocked = true;
final ActorTween secondYTween = new ActorTween(ball) {
@Override
protected void onFinish() {
if (actors.contains(ball)) {
missShot(ball);
}
}
}
.fromY(midY)
.toY(finalY)
.withInterpolator(Interpolator.EASE_IN)
.withDuration(0.4f);
final ActorTween firstYTween = new ActorTween(ball) {
@Override
protected void onFinish() {
tweenManager.add(secondYTween);
}
}
.fromY(initialY)
.toY(midY)
.withInterpolator(Interpolator.EASE_OUT)
.withDuration(0.4f);
final ActorTween xTween = new ActorTween(ball)
.fromX(initialX)
.toX(finalX)
.withRotation(0, rotation)
.withDuration(0.8f);
tweenManager.add(firstYTween);
tweenManager.add(xTween);
}
private boolean goalBoxContains(float x, float y) {
if (x < GOAL_BOX_X || x > GOAL_BOX_X + GOAL_BOX_WIDTH) {
return false;
}
if (y < GOAL_BOX_Y || y > GOAL_BOX_Y + GOAL_BOX_HEIGHT) {
return false;
}
return true;
}
private void pulseTargets() {
targetLeft.hidden = false;
targetMiddle.hidden = false;
targetRight.hidden = false;
targetLeft.alpha = 1;
targetMiddle.alpha = 1;
targetRight.alpha = 1;
final float startScale = 1;
final float endScale = 0.6f;
final float bounceScale = 0.8f;
final Tween fadeout = new Tween(0.2f) {
@Override
protected void updateValues(float percentDone) {
float alpha = Interpolator.FAST_IN.getValue(percentDone, 1, 0);
targetLeft.alpha = alpha;
targetMiddle.alpha = alpha;
targetRight.alpha = alpha;
}
@Override
protected void onFinish() {
targetLeft.hidden = true;
targetMiddle.hidden = true;
targetRight.hidden = true;
msTillNextTargetPulse = MS_BETWEEN_TARGET_FLASHES;
}
};
final Tween bounceThree = new Tween(0.15f) {
@Override
protected void updateValues(float percentDone) {
float scale = Interpolator.FAST_IN.getValue(percentDone, bounceScale, endScale);
targetLeft.scale = scale;
targetMiddle.scale = scale;
targetRight.scale = scale;
}
@Override
protected void onFinish() {
tweenManager.add(fadeout);
}
};
final Tween bounceTwo = new Tween(0.15f) {
@Override
protected void updateValues(float percentDone) {
float scale = Interpolator.FAST_IN.getValue(percentDone, bounceScale, endScale);
targetLeft.scale = scale;
targetMiddle.scale = scale;
targetRight.scale = scale;
}
@Override
protected void onFinish() {
tweenManager.add(score == 0 ? bounceThree : fadeout);
}
};
final Tween bounceOne = new Tween(0.15f) {
@Override
protected void updateValues(float percentDone) {
float scale = Interpolator.FAST_IN.getValue(percentDone, bounceScale, endScale);
targetLeft.scale = scale;
targetMiddle.scale = scale;
targetRight.scale = scale;
}
@Override
protected void onFinish() {
tweenManager.add(score == 0 ? bounceTwo : fadeout);
}
};
Tween shrinkIn = new Tween(0.3f) {
@Override
protected void updateValues(float percentDone) {
float scale = Interpolator.FAST_IN.getValue(percentDone, startScale, endScale);
targetLeft.scale = scale;
targetMiddle.scale = scale;
targetRight.scale = scale;
}
@Override
protected void onFinish() {
tweenManager.add(score == 0 ? bounceOne : fadeout);
}
};
tweenManager.add(shrinkIn);
}
WaterPoloActor getBlockingOpponentIfAny(GrapeActor ball) {
if (ball.shotBlocked) {
return null; // Already blocked once, let it go.
}
if (opponentOne.canBlock(ball.position.x, ball.position.y)) {
return opponentOne;
} else if (opponentTwo.canBlock(ball.position.x, ball.position.y)) {
return opponentTwo;
} else if (opponentThree.canBlock(ball.position.x, ball.position.y)) {
return opponentThree;
}
return null;
}
private void blockShot(final GrapeActor ball, final WaterPoloActor blockingOpponent) {
// Blocked!
ball.shotBlocked = true;
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.tennis_eliminate);
blockingOpponent.blockShot(new Callback() {
@Override
public void call() {
blockBall(ball, blockingOpponent);
shake(1, VIBRATION_SMALL);
}
});
}
private void missShot(GrapeActor ball) {
actors.remove(ball);
balls.remove(ball);
}
private void scoreShot(GrapeActor ball) {
actors.remove(ball);
balls.remove(ball);
// Swap ball for splat.
final SpriteActor splat = actorWithIds(Sprites.orange_present_falling);
splat.position.set(ball.position.x - splat.sprite.frameWidth / 2,
ball.position.y - splat.sprite.frameHeight);
splat.zIndex = -1;
splat.sprite.setLoop(false);
splat.sprite.addListener(new AnimatedSpriteListener() {
@Override
public void onFinished() {
actors.remove(splat);
effects.remove(splat);
}
});
actors.add(splat);
effects.add(splat);
// Shake the goal.
goal.sprite.setPaused(false);
// Play goal score sound.
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.present_throw_goal);
// Shake the screen a little bit.
shake(1, VIBRATION_SMALL);
// Reset ball.
ball.velocity.set(0, 0);
ball.position.set(player.position.x, player.position.y);
// Time for more opponents?
if (score >= OPPONENT_ONE_ENTRANCE_THRESHOLD && opponentOne.hidden) {
bringInOpponent(opponentOne, OPPONENT_ONE_SPEED);
}
if (score >= OPPONENT_TWO_ENTRANCE_THRESHOLD && opponentTwo.hidden) {
bringInOpponent(opponentTwo, OPPONENT_TWO_SPEED);
}
if (score >= OPPONENT_THREE_ENTRANCE_THRESHOLD && opponentThree.hidden) {
bringInOpponent(opponentThree, OPPONENT_THREE_SPEED);
}
scoredAtLeastOneShot = true;
}
private void newScore(int newScore) {
score = newScore;
eventBus.sendEvent(EventBus.SCORE_CHANGED, score);
}
private void addPointText(float x, float y, String pointText, int color) {
final TextActor pointTextActor = new TextActor(pointText);
x = x - 30;
y = y - 70;
pointTextActor.setColor(color);
pointTextActor.setBold(true);
pointTextActor.position.set(x, y);
pointTextActor.scale = 5;
pointTextActor.zIndex = 1000;
actors.add(pointTextActor);
effects.add(pointTextActor);
tweenManager.add(new ActorTween(pointTextActor) {
@Override
protected void onFinish() {
actors.remove(pointTextActor);
effects.remove(pointTextActor);
}
}
.withDuration(POINT_TEXT_ANIMATION_TIME)
.withAlpha(1, 0)
.toY(y - 80)
.withInterpolator(Interpolator.FAST_IN));
}
private void bringInOpponent(final WaterPoloActor opponent, final float speed) {
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.present_throw_character_appear);
opponent.hidden = false;
opponent.enter(new Callback() {
@Override
public void call() {
tweenOpponentRight(opponent, speed);
}
});
}
// Move opponent to the right. Chain a tween to the left at the end for endless motion.
private void tweenOpponentRight(final WaterPoloActor opponent, final float speed) {
if (state != State.PLAYING) {
// Stop moving opponents side-to-side at end of game.
tweenOpponentLeft(opponent, speed);
return;
}
tweenOpponent(opponent, 400, speed, new Callback() {
@Override
public void call() {
tweenOpponentLeft(opponent, speed);
}
});
opponent.swimRight();
}
// Move opponent to the left. Chain a tween to the right at the end for endless motion.
private void tweenOpponentLeft(final WaterPoloActor opponent, final float speed) {
tweenOpponent(opponent, 0, speed, new Callback() {
@Override
public void call() {
tweenOpponentRight(opponent, speed);
}
});
opponent.swimLeft();
}
// Helper function for moving opponents left & right.
private void tweenOpponent(WaterPoloActor opponent, float x, float speed, Callback next) {
ActorTween tween = new ActorTween(opponent)
.toX(x)
.withDuration(ActorHelper.distanceBetween(opponent.position.x, 0, x, 0) / speed)
.whenFinished(next);
tweenManager.add(tween);
}
private void maybePickUpAnotherBall() {
if (state == State.GAME_OVER) {
// No-op
} else if (ballsThrown == 0) {
// No-op
} else {
// No-op
}
}
public void onFling(final float radians) {
if (state == State.GAME_OVER || balls.size() > 0) {
return;
}
if (state == State.WAITING) {
instructions.hide();
setState(State.PLAYING);
}
canThrow = false; // No more throws until ball either goes in goal or leaves screen.
tweenManager.add(new EmptyTween(THROW_DELAY_SECONDS) {
@Override
protected void onFinish() {
canThrow = true;
}
});
ballsThrown++;
EventBus.getInstance().sendEvent(EventBus.PLAY_SOUND, R.raw.present_throw_throw);
player.throwBall(new Callback() {
@Override
public void call() {
addBall(radians);
}
}, new Callback() {
@Override
public void call() {
maybePickUpAnotherBall();
}
});
}
private SpriteActor actorWithIds(int[] ids) {
return new SpriteActor(spriteWithIds(ids), Vector2D.get(0, 0), Vector2D.get(0, 0));
}
private AnimatedSprite spriteWithIds(int[] ids, int fps) {
AnimatedSprite sprite = spriteWithIds(ids);
sprite.setFPS(fps);
return sprite;
}
private AnimatedSprite spriteWithIds(int[] ids) {
return AnimatedSprite.fromFrames(resources, ids);
}
private void shake(float screenShakeMagnitude, long vibrationMs) {
vibrator.vibrate(vibrationMs);
if (screenShakeMagnitude > 0) {
cameraShake.shake(33, screenShakeMagnitude, SHAKE_FALLOFF);
}
}
public void setState(State newState) {
state = newState;
eventBus.sendEvent(EventBus.GAME_STATE_CHANGED, newState);
if (newState == State.TITLE) {
tweenManager.add(new EmptyTween(titleDurationMs / 1000.0f) {
@Override
protected void onFinish() {
setState(State.WAITING);
}
});
}
}
}