/* * Copyright 2015 Daniel Dittmar * * 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 dan.dit.whatsthat.riddle.games; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.support.annotation.NonNull; import android.util.Log; import android.view.MotionEvent; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Random; import dan.dit.whatsthat.R; import dan.dit.whatsthat.achievement.AchievementProperties; import dan.dit.whatsthat.image.Image; import dan.dit.whatsthat.riddle.Riddle; import dan.dit.whatsthat.riddle.RiddleConfig; import dan.dit.whatsthat.riddle.achievement.holders.AchievementSnow; import dan.dit.whatsthat.riddle.control.RiddleGame; import dan.dit.whatsthat.riddle.control.RiddleCanvasAnimation; import dan.dit.whatsthat.riddle.control.RiddleScore; import dan.dit.whatsthat.riddle.types.TypesHolder; import dan.dit.whatsthat.testsubject.TestSubject; import dan.dit.whatsthat.testsubject.shopping.sortiment.SortimentHolder; import dan.dit.whatsthat.util.general.MathFunction; import dan.dit.whatsthat.util.general.PercentProgressListener; import dan.dit.whatsthat.util.compaction.CompactedDataCorruptException; import dan.dit.whatsthat.util.compaction.Compacter; import dan.dit.whatsthat.util.flatworld.collision.GeneralHitboxCollider; import dan.dit.whatsthat.util.flatworld.collision.Hitbox; import dan.dit.whatsthat.util.flatworld.collision.HitboxCircle; import dan.dit.whatsthat.util.flatworld.effects.WorldEffect; import dan.dit.whatsthat.util.flatworld.look.BitmapLook; import dan.dit.whatsthat.util.flatworld.look.CircleLook; import dan.dit.whatsthat.util.flatworld.look.Frames; import dan.dit.whatsthat.util.flatworld.look.Look; import dan.dit.whatsthat.util.flatworld.look.NinePatchLook; import dan.dit.whatsthat.util.flatworld.mover.HitboxMoonMover; import dan.dit.whatsthat.util.flatworld.mover.HitboxNewtonFrictionMover; import dan.dit.whatsthat.util.flatworld.mover.HitboxNoMover; import dan.dit.whatsthat.util.flatworld.world.Actor; import dan.dit.whatsthat.util.flatworld.world.FlatRectWorld; import dan.dit.whatsthat.util.flatworld.world.FlatWorldCallback; import dan.dit.whatsthat.util.image.BitmapUtil; import dan.dit.whatsthat.util.image.ImageUtil; /** * Created by daniel on 15.04.15. */ public class RiddleSnow extends RiddleGame implements FlatWorldCallback { private static final float STATE_DELTA_ON_IDEA_CHILD_COLLECT_WITH_ACTIVE_DEVIL = 0.1f; private static final float STATE_DELTA_ON_IDEA_CHILD_COLLECT = 1.f/3.f; private static final int STATE_DELTA_ON_WALL_EXPLOSION = 2; private static final float SNOWBALL_BASE_START_FRACTION = 1.f/4.f; private static final float GRAVITY = 400.f; //dp private static final float SNOWBALL_SCREENSIZE_MAX_FRACTION = 1.f / 3.f; private static final float BORDER_WIDTH = 3f; private static final float IDEA_COLLECTION_RADIUS_BASE = 21; public static final int IDEAS_REQUIRED_FOR_MAX_SIZE = 10; private static final long RELOAD_RIDDLE_BLOCK_DURATION = 2000L; //ms private static final long EXPLOSION_DELAY = 3000; //ms private static final long TOUCH_GRAVITY_FULL_EFFECT_DELAY = 500; //ms private static final double EXPLOSION_HIT_WALL_FRACTION = 0.6; private static final double SNOW_EXPLOSION_SIZE_MULTIPLIER = 1.7; private static final float CRASHED_WALL_BIGGER_SPEED_MULTIPLIER = 1.5f; private static final float CRASHED_WALL_SMALL_SPEED_MULTIPLIER = 0.5f; private static final float EXPLOSION_SPEED_MULTIPLIER = 10.f; private static final float DEVIL_RADIUS_FRACTION_OF_CELL_MAX_RADIUS = 0.25f; public static final boolean DEFAULT_DEVIL_IS_VISIBLE = true; private static final int MAX_WALL_COLLISONS_FOR_SCORE_BONUS = 0; private static final String CACHE_FULL_EXPLOSION0 = TypesHolder.Snow.NAME + "FullExplosion0"; private static final String CACHE_FULL_EXPLOSION1 = TypesHolder.Snow.NAME + "FullExplosion1"; private static final String CACHE_FULL_EXPLOSION2 = TypesHolder.Snow.NAME + "FullExplosion2"; private static final String CACHE_FULL_EXPLOSION3 = TypesHolder.Snow.NAME + "FullExplosion3"; private static final String CACHE_FULL_EXPLOSION4 = TypesHolder.Snow.NAME + "FullExplosion4"; private long mReloadRiddleMoveBlockDuration; private Bitmap mBackgroundSnow; private Bitmap mFogLayer; private Canvas mFogLayerCanvas; private Bitmap[] mFullExplosion; private Paint mExplosionPaint; private FlatRectWorld mWorld; private float mGravity; private float mRiddleOffsetX; private float mRiddleOffsetY; private Paint mBorderPaint; private Random mRand; private boolean mFeatureTouchGravity; private long mTouchGravityStartPressTime; private float mTouchGravityPressX; private float mTouchGravityPressY; private Canvas mBackgroundSnowCanvas; private boolean mTouchGravityIsPressed; private List<Float> mExplosionHistoryX; private List<Float> mExplosionHistoryY; private List<Integer> mExplosionHistoryType; private List<Integer> mExplosionHistorySize; private Paint mClearPaint; private Cell mCell; private Idea mIdea; private Devil mDevil; private long mIdleTimeCounter; public RiddleSnow(Riddle riddle, Image image, Bitmap bitmap, Resources res, RiddleConfig config, PercentProgressListener listener) { super(riddle, image, bitmap, res, config, listener); } @Override public void onClose() { super.onClose(); mWorld = null; mBackgroundSnow = null; mBorderPaint = null; mRand = null; mBackgroundSnowCanvas = null; mFogLayer = null; mFogLayerCanvas = null; ImageUtil.CACHE.freeImage(CACHE_FULL_EXPLOSION0, mFullExplosion[0]); ImageUtil.CACHE.freeImage(CACHE_FULL_EXPLOSION1, mFullExplosion[1]); ImageUtil.CACHE.freeImage(CACHE_FULL_EXPLOSION2, mFullExplosion[2]); ImageUtil.CACHE.freeImage(CACHE_FULL_EXPLOSION3, mFullExplosion[3]); ImageUtil.CACHE.freeImage(CACHE_FULL_EXPLOSION4, mFullExplosion[4]); mFullExplosion = null; mExplosionPaint = null; mExplosionHistoryX = null; mExplosionHistoryY = null; mExplosionHistorySize = null; mExplosionHistoryType = null; mClearPaint = null; } @Override public void draw(Canvas canvas) { if (!isNotClosed()) { return; } canvas.drawBitmap(mBackgroundSnow, 0, 0, null); mWorld.draw(canvas, null); canvas.drawRect(BORDER_WIDTH / 2.f, BORDER_WIDTH / 2.f, mConfig.mWidth - BORDER_WIDTH / 2.f, mConfig.mHeight - BORDER_WIDTH / 2.f, mBorderPaint); } @Override protected void initBitmap(Resources res, PercentProgressListener listener) { setTouchControl(!TestSubject.getInstance().hasToggleableFeature(SortimentHolder.ARTICLE_KEY_SNOW_FEATURE_ORIENTATION_SENSOR)); mWorld = new FlatRectWorld(new RectF(0, 0, mConfig.mWidth, mConfig.mHeight), new GeneralHitboxCollider(), this); mRand = new Random(); mClearPaint = new Paint(); mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mBorderPaint = new Paint(); mBorderPaint.setStyle(Paint.Style.STROKE); mBorderPaint.setStrokeWidth(BORDER_WIDTH); mBorderPaint.setColor(Color.BLACK); mRiddleOffsetX = (mConfig.mWidth - mBitmap.getWidth()) / 2.f; mRiddleOffsetY = (mConfig.mHeight - mBitmap.getHeight()) / 2.f; listener.onProgressUpdate(30); mGravity = ImageUtil.convertDpToPixel(GRAVITY, mConfig.mScreenDensity); // fog layer bitmap will be changed in progress so caching it makes no sense mFogLayer = ImageUtil.loadBitmap(res, R.drawable.nebel, mConfig.mWidth, mConfig.mHeight, BitmapUtil.MODE_FIT_EXACT); mFogLayer.setHasAlpha(true); mFogLayerCanvas = new Canvas(mFogLayer); mBackgroundSnow = Bitmap.createBitmap(mConfig.mWidth, mConfig.mHeight, mBitmap.getConfig()); mBackgroundSnow.setHasAlpha(true); mBackgroundSnowCanvas = new Canvas(mBackgroundSnow); drawBackground(); listener.onProgressUpdate(50); mIdleTimeCounter = AchievementSnow.Achievement7.IDLE_TIME_PASSED; Compacter currentStateData = getCurrentState(); boolean devilVisible = DEFAULT_DEVIL_IS_VISIBLE; int devilState = Devil.STATE_PROTECT; if (currentStateData != null) { mReloadRiddleMoveBlockDuration = RELOAD_RIDDLE_BLOCK_DURATION; if (currentStateData.getSize() >= 6) { try { if (mConfig.mWidth == currentStateData.getInt(0) && mConfig.mHeight == currentStateData.getInt(1)) { initCell(res, currentStateData.getFloat(2), currentStateData.getFloat(3), currentStateData.getFloat(4)); } devilState = currentStateData.getInt(5); devilVisible = currentStateData.getBoolean(6); } catch (CompactedDataCorruptException e) { currentStateData = null; } } } listener.onProgressUpdate(60); if (currentStateData == null) { mReloadRiddleMoveBlockDuration = 0L; initCell(res, mConfig.mWidth / 2.f, mConfig.mHeight / 2.f, 0.f); listener.onProgressUpdate(70); } initIdea(res); initDevil(res, devilState, devilVisible); nextIdea(); int explosionSize = (int) (2 * mCell.mMaxRadius * SNOW_EXPLOSION_SIZE_MULTIPLIER); mFullExplosion = new Bitmap[5]; mFullExplosion[0] = ImageUtil.CACHE.obtainImage(CACHE_FULL_EXPLOSION0, res, R.drawable.explosion1, explosionSize, explosionSize, false); mFullExplosion[1] = ImageUtil.CACHE.obtainImage(CACHE_FULL_EXPLOSION1, res, R.drawable.explosion2, explosionSize, explosionSize, false); mFullExplosion[2] = ImageUtil.CACHE.obtainImage(CACHE_FULL_EXPLOSION2, res, R.drawable.explosion3, explosionSize, explosionSize, false); mFullExplosion[3] = ImageUtil.CACHE.obtainImage(CACHE_FULL_EXPLOSION3, res, R.drawable.explosion4, explosionSize, explosionSize, false); mFullExplosion[4] = ImageUtil.CACHE.obtainImage(CACHE_FULL_EXPLOSION4, res, R.drawable.explosion5, explosionSize, explosionSize, false); mExplosionPaint = new Paint(); mExplosionPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); listener.onProgressUpdate(80); mExplosionHistoryX = new ArrayList<>(); mExplosionHistoryY = new ArrayList<>(); mExplosionHistorySize = new ArrayList<>(); mExplosionHistoryType = new ArrayList<>(); if (currentStateData != null) { for (int i = 8; i + 3 < currentStateData.getSize(); i+=4) { try { drawExplosion(currentStateData.getFloat(i), currentStateData.getFloat(i + 1), currentStateData.getInt(i + 2), currentStateData.getInt(i + 3)); } catch (CompactedDataCorruptException e) { Log.e("Riddle", "Corrupt data when reconstructing snow: " + e); } } } listener.onProgressUpdate(100); } private void initCell(Resources res, float x, float y, float radius) { float maxRadius = (Math.min(mConfig.mWidth, mConfig.mHeight) / 2.f * SNOWBALL_SCREENSIZE_MAX_FRACTION); int dim = (int) (maxRadius * 2); Bitmap[] explosive = new Bitmap[] { ImageUtil.loadBitmap(res, R.drawable.zelle_explosion_1, dim, dim, BitmapUtil.MODE_FIT_EXACT), ImageUtil.loadBitmap(res, R.drawable.zelle_explosion_2, dim, dim, BitmapUtil.MODE_FIT_EXACT)}; float startRadius = maxRadius * SNOWBALL_BASE_START_FRACTION; float currRadius = startRadius; if (radius >= startRadius) { currRadius = radius; } Bitmap maxCell = ImageUtil.loadBitmap(res, R.drawable.zelle, dim, dim, BitmapUtil.MODE_FIT_EXACT); mCell = Cell.make(x, y, currRadius, maxCell, explosive, startRadius, maxRadius); mWorld.addActor(mCell); } private void initIdea(Resources res) { float radius = ImageUtil.convertDpToPixel(IDEA_COLLECTION_RADIUS_BASE, mConfig.mScreenDensity); int size = 2 * (int) radius; Bitmap imageCandy = ImageUtil.loadBitmap(res, R.drawable.idea_candy, size, size, true); Bitmap imageToxic = ImageUtil.loadBitmap(res, R.drawable.idea_toxic, size, size, true); mIdea = makeIdea(0, 0, radius, imageCandy, imageToxic); mWorld.addActor(mIdea); } private void initDevil(Resources res, int state, boolean devilVisible) { float radius = mCell.mMaxRadius * DEVIL_RADIUS_FRACTION_OF_CELL_MAX_RADIUS; int size = (int) (2 * radius); Bitmap imageProtect = ImageUtil.loadBitmap(res, R.drawable.angel, size, size, true); Bitmap imageDamaged = ImageUtil.loadBitmap(res, R.drawable.angel_damaged, size, size, true); Bitmap imageRecovering = ImageUtil.loadBitmap(res, R.drawable.angel_recovering, size, size, true); Bitmap[] stateImages = new Bitmap[Devil.STATES_COUNT]; stateImages[Devil.STATE_PROTECT] = imageProtect; stateImages[Devil.STATE_DAMAGED] = imageDamaged; stateImages[Devil.STATE_RECOVERING] = imageRecovering; mDevil = makeDevil(mCell, 0, 0, radius, stateImages, state, res); boolean wasSilent = mDevil.mSilent; mDevil.mSilent = true; if (devilVisible) { mDevil.onAppear(); } else { mDevil.onLeaveWorld(); } mWorld.addActor(mDevil); mDevil.mSilent = wasSilent; } private void nextIdea() { final int tries = 5; int count = 0; do { mWorld.setRandomPositionInside(mIdea, mRand); count++; } while (count < tries && mWorld.getCollider().checkCollision(mIdea.getHitbox(), mCell.getHitbox())); // try to get it in some distance, not too important } private void needForSpeed() { float cellX = mCell.getHitbox().getCenterX(); float cellY = mCell.getHitbox().getCenterY(); if (mFeatureTouchGravity && mTouchGravityIsPressed && (mTouchGravityPressX != cellX || mTouchGravityPressY != cellY)) { float gravityFraction = Math.min(1.0f, (System.currentTimeMillis() - mTouchGravityStartPressTime) / ((float) TOUCH_GRAVITY_FULL_EFFECT_DELAY)); double angleBetweenTouchAndSnow = Math.atan2(cellY - mTouchGravityPressY, cellX - mTouchGravityPressX); float forceX = - gravityFraction * mGravity * (float) Math.cos(angleBetweenTouchAndSnow); float forceY = - gravityFraction * mGravity * (float) Math.sin(angleBetweenTouchAndSnow); mCell.updateFrictionAndAccel(forceX, forceY, 0.f); } updateSpeedAchievementData(); } private void updateSpeedAchievementData() { mConfig.mAchievementTypeData.putValue(AchievementSnow.KEY_TYPE_MAX_SPEED, (long) mCell.getSpeed(), AchievementSnow.CELL_SPEED_REQUIRED_DELTA); } private void drawExplosion(float explosionCenterX, float explosionCenterY, int explosionSize, int explosionType) { mExplosionHistoryX.add(explosionCenterX); mExplosionHistoryY.add(explosionCenterY); mExplosionHistorySize.add(explosionSize); mExplosionHistoryType.add(explosionType); Bitmap explosionImage; if (explosionSize >= 2 * mCell.mMaxRadius * SNOW_EXPLOSION_SIZE_MULTIPLIER) { // full explosion explosionImage = mFullExplosion[explosionType]; } else { explosionImage = BitmapUtil.resize(mFullExplosion[explosionType], explosionSize, explosionSize); } mFogLayerCanvas.drawBitmap(explosionImage, explosionCenterX - explosionImage.getWidth() / 2.f, explosionCenterY - explosionImage.getHeight() / 2.f, mExplosionPaint); drawBackground(); } private void drawBackground() { mBackgroundSnowCanvas.drawPaint(mClearPaint); mBackgroundSnowCanvas.drawBitmap(mBitmap, mRiddleOffsetX, mRiddleOffsetY, null); mBackgroundSnowCanvas.drawBitmap(mFogLayer, 0, 0, null); } @Override public boolean requiresPeriodicEvent() { return true; } @Override public void onPeriodicEvent(long updateTime) { mReloadRiddleMoveBlockDuration -= updateTime; if (mIdleTimeCounter > 0L) { mIdleTimeCounter -= updateTime; } if (mReloadRiddleMoveBlockDuration > 0L) { return; // wait some time after loading existing riddle so we don't crash immediately } if (mIdleTimeCounter <= 0L && mConfig.mAchievementGameData != null && mIdleTimeCounter != Long.MIN_VALUE) { mIdleTimeCounter = Long.MIN_VALUE; mConfig.mAchievementGameData.increment(AchievementSnow.Achievement7.KEY_IDLE_TIME_PASSED, 1L, 0L); } mWorld.update(updateTime); needForSpeed(); if (mCell.updateAndCheckExplosionTimer(updateTime)) { onExplosion(false); } } @Override public boolean onOrientationEvent(float azimuth, float pitch, float roll) { if (mFeatureTouchGravity) { return false; } // forceX/Y in screen coordinate space float forceX = mGravity * (float) Math.sin(roll); float forceY = -mGravity * (float) Math.sin(pitch); mCell.updateFrictionAndAccel(forceX, forceY, 3.f / 4.f); return false; // we draw periodically and not on orientation event } @Override public boolean onMotionEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { mDevil.checkIfMocked(event.getX(), event.getY()); } if (mFeatureTouchGravity && event.getActionMasked() == MotionEvent.ACTION_DOWN) { mTouchGravityStartPressTime = System.currentTimeMillis(); mTouchGravityPressX = event.getX(); mTouchGravityPressY = event.getY(); mTouchGravityIsPressed = true; mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_CLICKS_DOWN_DURING_NO_SENSOR, 1L, 0L); } else if (mFeatureTouchGravity && event.getActionMasked() == MotionEvent.ACTION_MOVE) { mTouchGravityPressX = event.getX(); mTouchGravityPressY = event.getY(); } else if (mFeatureTouchGravity && event.getActionMasked() == MotionEvent.ACTION_UP) { mTouchGravityIsPressed = false; mCell.updateFrictionAndAccel(0, 0, 0); } return false; } private void setTouchControl(boolean enable) { mFeatureTouchGravity = enable; } public boolean requiresOrientationSensor() { return !mFeatureTouchGravity; } @Override public void enableNoOrientationSensorAlternative() { setTouchControl(true); } @NonNull @Override protected String compactCurrentState() { Compacter cmp = new Compacter(); cmp.appendData(mConfig.mWidth); cmp.appendData(mConfig.mHeight); cmp.appendData(mCell.getHitbox().getCenterX()); cmp.appendData(mCell.getHitbox().getCenterY()); cmp.appendData(mCell.mHitboxCircle.getRadius()); cmp.appendData(mDevil.mState); cmp.appendData(mDevil.isActive()); cmp.appendData(""); // in case we need the slot Iterator<Float> xIt = mExplosionHistoryX.iterator(); Iterator<Float> yIt = mExplosionHistoryY.iterator(); Iterator<Integer> sizeIt = mExplosionHistorySize.iterator(); Iterator<Integer> typeIt = mExplosionHistoryType.iterator(); while (xIt.hasNext()) { cmp.appendData(xIt.next()); cmp.appendData(yIt.next()); cmp.appendData(sizeIt.next()); cmp.appendData(typeIt.next()); } return cmp.compact(); } @Override protected void addBonusReward(@NonNull RiddleScore.Rewardable rewardable) { int wallCollisions = mConfig.mAchievementGameData != null ? mConfig.mAchievementGameData.getValue(AchievementSnow.KEY_GAME_COLLISION_COUNT, 0L).intValue() : 0; int bonus = (wallCollisions <= MAX_WALL_COLLISONS_FOR_SCORE_BONUS ? TypesHolder.SCORE_MEDIUM : 0); rewardable.addBonus(bonus); } @Override protected Bitmap makeSnapshot() { int width = SNAPSHOT_DIMENSION.getWidthForDensity(mConfig.mScreenDensity); int height = SNAPSHOT_DIMENSION.getHeightForDensity(mConfig.mScreenDensity); Bitmap snapshot = Bitmap.createScaledBitmap(mBitmap, width, height, false); Canvas canvas = new Canvas(snapshot); Bitmap overlay = Bitmap.createScaledBitmap(mBackgroundSnow, width, height, false); canvas.drawBitmap(overlay, 0, 0, null); float fractionX = width / (float) mConfig.mWidth; float fractionY = height / (float) mConfig.mHeight; RectF cellHitbox = mCell.getHitbox().getBoundingRect(); Bitmap snow = Bitmap.createScaledBitmap(mCell.mMaxCell, (int) (fractionX * cellHitbox.width()), (int) (fractionY * cellHitbox.height()), false); canvas.drawBitmap(snow, (int) (cellHitbox.left * fractionX), (int) (cellHitbox.top * fractionY), null); return snapshot; } @Override protected void initAchievementData() { long wasEnabled = mConfig.mAchievementGameData.getValue(AchievementSnow.KEY_GAME_FEATURE_ORIENTATION_SENSOR_ENABLED, -1L); Log.d("Riddle", "Was feature enabled: " + wasEnabled); mConfig.mAchievementGameData.putValue(AchievementSnow .KEY_GAME_FEATURE_ORIENTATION_SENSOR_ENABLED, mFeatureTouchGravity ? 1L : 0L, AchievementProperties.UPDATE_POLICY_ALWAYS); if (wasEnabled != -1L && ((wasEnabled == 0L) == mFeatureTouchGravity)) { Log.d("Riddle", "Feature changed."); mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_FEATURE_ORIENTATION_SENSOR_CHANGED, 1L, 0L); } } @Override public void onReachedEndOfWorld(Actor columbus, float x, float y, int borderFlags) { boolean collisionLeft = (borderFlags & FlatRectWorld.BORDER_FLAG_LEFT) != 0; boolean collisionRight = (borderFlags & FlatRectWorld.BORDER_FLAG_RIGHT) != 0; boolean collisionTop = (borderFlags & FlatRectWorld.BORDER_FLAG_TOP) != 0; boolean collisionBottom = (borderFlags & FlatRectWorld.BORDER_FLAG_BOTTOM) != 0; if (columbus == mDevil) { mDevil.onTouchedOutside(); return; } if (columbus != mCell) { return; } mConfig.mAchievementGameData.putValues(AchievementSnow.KEY_GAME_COLLISION_SPEED, (long) mCell.getSpeed(), AchievementProperties.UPDATE_POLICY_ALWAYS, AchievementSnow.KEY_GAME_PRE_COLLISION_CELL_STATE, (long) mCell.getState(), AchievementProperties.UPDATE_POLICY_ALWAYS, null, 0L, 0L); mCell.applyWallPhysics(mWorld, collisionLeft, collisionTop, collisionRight, collisionBottom, this); mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_COLLISION_COUNT, 1L, AchievementProperties.UPDATE_POLICY_ALWAYS); onExplosion(true); } private void onExplosion(boolean hitWall) { float radius = mCell.mHitboxCircle.getRadius(); float explosionX = mCell.getHitbox().getCenterX(); float explosionY = mCell.getHitbox().getCenterY(); if (mCell.onExplosion(hitWall, mDevil)) { updateCellStateAchievementData(); int explosionSize = (int) (1 + SNOW_EXPLOSION_SIZE_MULTIPLIER * 2 * radius * (hitWall ? EXPLOSION_HIT_WALL_FRACTION : 1.f)); int explosionIndex = mRand.nextInt(mFullExplosion.length); if (hitWall) { mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_WALL_EXPLOSION, 1L, 0L); } else { mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_BIG_EXPLOSION, 1L, 0L); } drawExplosion(explosionX, explosionY, explosionSize, explosionIndex); updateSpeedAchievementData(); } } @Override public void onLeftWorld(Actor jesus, int borderFlags) { jesus.onLeaveWorld(); } @Override public void onCollision(Actor colliding1, Actor colliding2) { if (checkCollisionPair(colliding1, colliding2, mCell, mIdea)) { mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_IDEAS_COLLECTED, 1, 0); mCell.onCollectIdea(); mDevil.onCellCollectIdea(); updateCellStateAchievementData(); nextIdea(); } else if (checkCollisionPair(colliding1, colliding2, mDevil, mIdea)) { if (mDevil.attemptCollectIdea()) { mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_ANGEL_COLLECTED_IDEA, 1L, 0L); } } else if (((colliding1 == mCell && colliding2.onCollision(mCell))) || (colliding2 == mCell && colliding1.onCollision(mCell))) { mConfig.mAchievementGameData.increment(AchievementSnow.KEY_GAME_CELL_COLLECTED_SPORE, 1L, 0L); updateCellStateAchievementData(); } else if (((colliding1 == mDevil && colliding2.onCollision(mDevil))) || (colliding2 == mDevil && colliding1.onCollision(mDevil))) { // on collision for devil required, statement can be empty! } } private static boolean checkCollisionPair(Actor colliding1, Actor colliding2, Actor toCheck1, Actor toCheck2) { return (colliding1 == toCheck1 && colliding2 == toCheck2) || (colliding1 == toCheck2 && colliding2 == toCheck1); } private void updateCellStateAchievementData() { mConfig.mAchievementGameData.putValue(AchievementSnow.KEY_GAME_CELL_STATE, (long) mCell.getState(), AchievementProperties.UPDATE_POLICY_ALWAYS); } @Override public void onMoverStateChange(Actor actor) { } public static class Cell extends Actor { public static final int STATE_NORMAL = -1; public static final int STATE_EXPLOSIVE = -2; private static final long FRAME_DURATION = 250L; private final HitboxNewtonFrictionMover mCellMover; private HitboxCircle mHitboxCircle; private final float mStartRadius; private final float mMaxRadius; private final float mStateRadiusDelta; private final Bitmap mMaxCell; private final Frames mCellLook; private long mExplosionCountDown; public Cell(HitboxCircle hitbox, HitboxNewtonFrictionMover mover, Frames cellLook, float startRadius, float maxRadius, Bitmap defaultMaxCell) { super(hitbox, mover, cellLook); mHitboxCircle = hitbox; mCellMover = mover; mStartRadius = startRadius; mMaxRadius = maxRadius; mStateRadiusDelta = (mMaxRadius - mStartRadius) / (float) IDEAS_REQUIRED_FOR_MAX_SIZE; mMaxCell = defaultMaxCell; mCellLook = cellLook; setActive(true); } public static Cell make(float x, float y, float radius, Bitmap defaultMaxCell, Bitmap[] explosionFrames, float startRadius, float maxRadius) { HitboxCircle hitbox = new HitboxCircle(x,y, radius); HitboxNewtonFrictionMover mover = new HitboxNewtonFrictionMover(); Frames cellLook = Cell.makeCellFrames(null, radius, defaultMaxCell); Look explosionCellLook = new Frames(explosionFrames, FRAME_DURATION); Cell cell = new Cell(hitbox, mover, cellLook, startRadius, maxRadius, defaultMaxCell); cell.putStateFrames(STATE_NORMAL, cellLook); cell.putStateFrames(STATE_EXPLOSIVE, explosionCellLook); return cell; } private static Frames makeCellFrames(Frames base, float radius, Bitmap defaultMaxCell) { int size = (int) (radius * 2); Bitmap[] frames = base == null ? new Bitmap[1] : base.getFrames(); if (frames[0] == null || frames[0].getWidth() != size) { frames[0] = BitmapUtil.resize(defaultMaxCell, size, size); } return base == null ? new Frames(frames, FRAME_DURATION) : base; } private float calculateFriction() { double fractionOfMaxSize = mHitboxCircle.getRadius() / mMaxRadius; return (float) (0.55 * Math.exp(-1.5 * fractionOfMaxSize)); } public void onCollectIdea() { boolean explosion = false; float radius = mHitboxCircle.getRadius(); if (radius >= mMaxRadius) { radius = mMaxRadius; explosion = true; } else { radius += mStateRadiusDelta; if (radius >= mMaxRadius) { radius = mMaxRadius; explosion = true; } Cell.makeCellFrames(mCellLook, radius, mMaxCell); } mHitboxCircle.setRadius(radius); if (explosion && mExplosionCountDown == 0L) { setStateFrames(STATE_EXPLOSIVE); mExplosionCountDown = EXPLOSION_DELAY; } } private int getState() { if (mHitboxCircle.getRadius() >= mMaxRadius) { return IDEAS_REQUIRED_FOR_MAX_SIZE; } int state = Math.round((mHitboxCircle.getRadius() - mStartRadius) / mStateRadiusDelta); return Math.min(state, IDEAS_REQUIRED_FOR_MAX_SIZE - 1); } private double getSpeed() { return mCellMover.getSpeed(); } public void updateFrictionAndAccel(float forceX, float forceY, float prevAccelFraction) { float friction = calculateFriction(); mCellMover.setFriction(friction); float accelX = (1-friction) * forceX; float accelY = (1-friction) * forceY; mCellMover.setAcceleration(accelX * (1.f - prevAccelFraction) + mCellMover.getAccelerationX() * prevAccelFraction, accelY * (1.f - prevAccelFraction) + mCellMover.getAccelerationY() * prevAccelFraction); } public void applyWallPhysics(FlatRectWorld world, boolean collisionLeft, boolean collisionTop, boolean collisionRight, boolean collisionBottom, RiddleGame game) { float speedMultiplier = mHitboxCircle.getRadius() > mStartRadius ? -CRASHED_WALL_BIGGER_SPEED_MULTIPLIER : -CRASHED_WALL_SMALL_SPEED_MULTIPLIER; if (collisionLeft) { mHitboxCircle.setLeft(world.getLeft() + 1); mCellMover.multiplySpeed(speedMultiplier, 1.f); } if (collisionTop) { mHitboxCircle.setTop(world.getTop() + 1); mCellMover.multiplySpeed(1.f, speedMultiplier); } if (collisionRight) { mHitboxCircle.setRight(world.getRight() - 1); mCellMover.multiplySpeed(speedMultiplier, 1.f); } if (collisionBottom) { mHitboxCircle.setBottom(world.getBottom() - 1); mCellMover.multiplySpeed(1.f, speedMultiplier); } if (Math.abs(speedMultiplier) > 1.f) { float speed = Math.abs(mCellMover.getSpeed() * speedMultiplier); final float minSpeed = 250.f; final float maxSpeed = 2000.f; speed += getState() * 100; if (speed > minSpeed) { Log.d("Riddle", "Speed: " + speed + " of state " + getState()); speed = Math.min(maxSpeed, speed); final long time = 50L; final int repeatCount = 8; final float minRotateDegrees = 1f; final float maxRotateDegrees = 3f; final float minTranslate = 5f; final float maxTranslate = 35f; final float translate = MathFunction.QuadraticInterpolation.evaluate(minSpeed, minTranslate, maxSpeed, maxTranslate, speed); final float rotateDegrees = MathFunction.QuadraticInterpolation.evaluate (minSpeed, minRotateDegrees, maxSpeed, maxRotateDegrees, speed); float dx = collisionLeft ? -translate : collisionRight ? translate : 0f; float dy = collisionTop ? -translate : collisionBottom ? translate : 0f; game.addAnimation(new RiddleCanvasAnimation.Builder() .setLives(repeatCount) .setInterpolator(RiddleCanvasAnimation.CanvasAnimation.INTERPOLATOR_LINEAR) .setNextLifeMode(RiddleCanvasAnimation.CanvasAnimation .NEXT_LIFE_MODE_REVERSE_INVERT) .addRotate(rotateDegrees, 0.5f, 0.5f, time) .addTranslate(dx, dy, time) .build()); } } } private boolean updateAndCheckExplosionTimer(long updateTime) { if (mExplosionCountDown > 0) { mExplosionCountDown -= updateTime; return mHitboxCircle.getRadius() >= mMaxRadius && mExplosionCountDown <= 0; } return false; } private boolean onExplosion(boolean hitWall, Devil devil) { final float radius = mHitboxCircle.getRadius(); if (radius > mStartRadius) { float delta; if (hitWall) { if (devil.isActive()) { delta = STATE_DELTA_ON_WALL_EXPLOSION * mStateRadiusDelta; } else { delta = radius - mStartRadius; } } else { devil.onAppear(); delta = radius - mStartRadius; mCellMover.multiplySpeed(EXPLOSION_SPEED_MULTIPLIER, EXPLOSION_SPEED_MULTIPLIER); } return shrinkCell(delta); } return false; } private boolean shrinkCell(double shrinkDelta) { float radius = mHitboxCircle.getRadius(); if (radius > mStartRadius) { radius -= shrinkDelta; if (radius < mStartRadius) { radius = mStartRadius; } makeCellFrames(mCellLook, radius, mMaxCell); mHitboxCircle.setRadius(radius); mExplosionCountDown = 0L; setStateFrames(STATE_NORMAL); return true; } return false; } public boolean onCollectIdeaChild(Devil devil) { return !(devil.isActive() && devil.mState == Devil.STATE_PROTECT) && shrinkCell(mStateRadiusDelta * (devil.isActive() ? STATE_DELTA_ON_IDEA_CHILD_COLLECT_WITH_ACTIVE_DEVIL : STATE_DELTA_ON_IDEA_CHILD_COLLECT)); } } private Idea makeIdea(float x, float y, float radius, Bitmap candy, Bitmap toxic) { HitboxCircle hitbox = new HitboxCircle(x, y, radius); Look candyLook = new BitmapLook(candy); Look toxicLook = new BitmapLook(toxic); return new Idea(hitbox, candyLook, toxicLook); } private class Idea extends Actor { private static final int STATE_CANDY = 0; private static final int STATE_TOXIC = 1; private static final int MAX_CHILDREN = 40; private static final double SPAWNS_PER_SECOND = 2.; private List<IdeaChild> mChildren = new LinkedList<>(); private Random mRand = new Random(); public Idea(HitboxCircle hitbox, Look candyLook, Look toxicLook) { super(hitbox, HitboxNoMover.INSTANCE, candyLook); putStateFrames(STATE_CANDY, candyLook); putStateFrames(STATE_TOXIC, toxicLook); for (int i = 0; i < MAX_CHILDREN; i++) { IdeaChild child = makeIdeaChild(this); mChildren.add(child); mWorld.addActor(child); } setActive(true); } @Override public boolean update(long updatePeriod) { boolean result = super.update(updatePeriod); if (mRand.nextDouble() < updatePeriod / 1000. * SPAWNS_PER_SECOND) { for (IdeaChild child : mChildren) { if (!child.isActive()) { child.prepare(mRand); child.setActive(true); break; } } } return result; } } private IdeaChild makeIdeaChild(Idea parent) { RectF parentBounds = parent.getHitbox().getBoundingRect(); float parentHalfWidth = parentBounds.width() / 2.f; float radius = parentHalfWidth * IdeaChild.PARENT_RADIUS_FRACTION; Hitbox hitbox = new HitboxCircle(parentBounds.centerX(), parentBounds.centerY(), radius); HitboxNewtonFrictionMover mover = new HitboxNewtonFrictionMover(); return new IdeaChild(hitbox, mover, new CircleLook(radius, CHILD_COLORS[mRand.nextInt(CHILD_COLORS.length)])); } private static final int[] CHILD_COLORS = new int[] {0xfff10000, 0xff06a928, 0xffff9600, 0xff7b00f9, 0xffd6f400, 0xff009071}; private class IdeaChild extends Actor { private static final float FRICTION = 0.5f; private static final float PARENT_RADIUS_FRACTION = 0.20f; private final HitboxNewtonFrictionMover mMover; public IdeaChild(Hitbox hitbox, HitboxNewtonFrictionMover mover, Look defaultLook) { super(hitbox, mover, defaultLook); mMover = mover; } @Override public void onLeaveWorld() { setActive(false); } @Override public boolean onCollision(Actor with) { if (with == mCell) { setActive(false); return mCell.onCollectIdeaChild(mDevil); } else if (with == mDevil) { mDevil.attemptCollectIdeaChild(this); return true; } return false; } private void prepare(Random rand) { RectF parentBounds = mIdea.getHitbox().getBoundingRect(); float parentHalfWidth = parentBounds.width() / 2.f; float angle = rand.nextFloat() * (float) Math.PI * 2; float cosAngle = (float) Math.cos(angle); float sinAngle = (float) Math.sin(angle); getHitbox().setCenter(parentBounds.centerX() + parentHalfWidth * cosAngle, parentBounds.centerY() + parentHalfWidth * sinAngle); float outspeed = Math.min(mWorld.getWidth(), mWorld.getHeight()) * 0.3f; mMover.setSpeed(outspeed * cosAngle, outspeed* sinAngle); mMover.setFriction(FRICTION * rand.nextFloat()); } } private Devil makeDevil(Cell cell, float x, float y, float radius, Bitmap[] stateImages, int state, Resources res) { HitboxCircle hitbox = new HitboxCircle(x, y, radius); Look[] looks = new Look[stateImages.length]; for (int i = 0; i < stateImages.length; i++) { looks[i] = new BitmapLook(stateImages[i]); } HitboxMoonMover moonMover = new HitboxMoonMover(cell.getHitbox(), 1, ImageUtil.convertDpToPixel(7.f, mConfig.mScreenDensity)); return new Devil(hitbox, moonMover, state, looks, res); } private static final long[] SURROUND_DURATION = new long[] {1900L, 2000L, 3000L}; private class Devil extends Actor { private static final long TOUCH_OUTSIDE_LOCK_DURATION = 250L; private static final long RECOVER_DURATION = 20000L; private static final int STATE_PROTECT = 0; private static final int STATE_DAMAGED = 1; private static final int STATE_RECOVERING = 2; private static final int STATES_COUNT = 3; private final Resources mRes; private int mState; private long mLastOutsideTouch; private HitboxMoonMover mMoonMover; private NinePatchLook[] mTalkingBackground; private WorldEffect mTalkingEffect; private boolean mSilent; private long mTimeToRecover; public Devil(HitboxCircle hitbox, HitboxMoonMover moonMover, int state, Look[] stateLooks, Resources res) { super(hitbox, moonMover, stateLooks[state]); for (int i = 0; i < STATES_COUNT; i++) { putStateFrames(i, stateLooks[i]); } mMoonMover = moonMover; mMoonMover.setMoonYear(SURROUND_DURATION[state]); mState = state; mRes = res; mTalkingBackground = new NinePatchLook[] { new NinePatchLook(NinePatchLook.loadNinePatch(res, R.drawable.say_tl), mConfig.mScreenDensity), new NinePatchLook(NinePatchLook.loadNinePatch(res, R.drawable.say_tr), mConfig.mScreenDensity), new NinePatchLook(NinePatchLook.loadNinePatch(res, R.drawable.say_br), mConfig.mScreenDensity), new NinePatchLook(NinePatchLook.loadNinePatch(res, R.drawable.say_bl), mConfig.mScreenDensity)}; setActive(true); updateCellCandyVision(); moonMover.update(getHitbox(), 0L); } private void updateCellCandyVision() { if (isActive()) { mIdea.setStateFrames(Idea.STATE_TOXIC); } else { mIdea.setStateFrames(Idea.STATE_CANDY); } } private void recover() { if (mState == STATE_PROTECT) { return; } mState = STATE_PROTECT; setStateFrames(mState); mMoonMover.setMoonYear(SURROUND_DURATION[mState]); talk(R.array.devil_talk_recovered, 0.7); } @Override public boolean update(long updateTime) { boolean result = super.update(updateTime); if (mState == STATE_RECOVERING) { mTimeToRecover -= updateTime; if (mTimeToRecover <= 0) { recover(); } } return result; } public void onTouchedOutside() { if (mState == STATE_RECOVERING) { return; // ignore } if (System.currentTimeMillis() - mLastOutsideTouch >= TOUCH_OUTSIDE_LOCK_DURATION) { mLastOutsideTouch = 0L; } if (mLastOutsideTouch == 0L) { mLastOutsideTouch = System.currentTimeMillis(); mMoonMover.invertDirection(); } } public boolean talk(int textId, double probability) { if (!mSilent && (mTalkingEffect == null || mTalkingEffect.getState() == WorldEffect.STATE_TIMEOUT) && mRand.nextDouble() < probability) { String[] texts = mRes.getStringArray(textId); mTalkingEffect = mWorld.attachTimedMessage(this, mTalkingBackground, texts[mRand.nextInt(texts.length)], 5000L); mTalkingEffect.startFade(0xFFFFFFFF, 0x00FFFFFF, 2000L, 3000L, false); return true; } return false; } public boolean attemptCollectIdea() { if (mState < STATE_RECOVERING) { mState++; setStateFrames(mState); mMoonMover.setMoonYear(SURROUND_DURATION[mState]); nextIdea(); if (mState == STATE_RECOVERING) { mTimeToRecover = RECOVER_DURATION; talk(R.array.devil_talk_recovering_start, 0.5); } return true; } return false; } public void attemptCollectIdeaChild(IdeaChild toCollect) { if (mState < STATE_RECOVERING) { toCollect.setActive(false); } } @Override public void onLeaveWorld() { setActive(false); talk(R.array.devil_talk_killed, 1.0); mConfig.mAchievementGameData.putValue(AchievementSnow.KEY_GAME_DEVIL_VISIBLE_STATE, 0L, AchievementProperties.UPDATE_POLICY_ALWAYS); updateCellCandyVision(); } public void onAppear() { setActive(true); recover(); talk(R.array.devil_talk_return, 1.0); mConfig.mAchievementGameData.putValue(AchievementSnow.KEY_GAME_DEVIL_VISIBLE_STATE, 1L, AchievementProperties.UPDATE_POLICY_ALWAYS); updateCellCandyVision(); } public void checkIfMocked(float x, float y) { if (isActive() && mDevil.getHitbox().isInside(x, y)) { if (mDevil.talk(R.array.devil_talk_mock, 0.05)) { mConfig.mAchievementGameData.increment(AchievementSnow .KEY_GAME_DEVIL_TALK_ANNOYED_COUNT, 1L, 0L); } } } public void onCellCollectIdea() { if (mDevil.isActive()) { if (mCell.getState() == IDEAS_REQUIRED_FOR_MAX_SIZE) { mDevil.talk(R.array.devil_talk_cell_eat_candy_explosive, 1.); } else { mDevil.talk(R.array.devil_talk_cell_eat_candy, 1.0 / IDEAS_REQUIRED_FOR_MAX_SIZE); } } } } }