/* * 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.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.support.annotation.NonNull; import android.util.Log; import android.view.MotionEvent; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Random; import dan.dit.whatsthat.R; import dan.dit.whatsthat.achievement.AchievementDataEvent; 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.RiddleView; import dan.dit.whatsthat.riddle.achievement.holders.AchievementLazor; import dan.dit.whatsthat.riddle.control.RiddleGame; 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.ShopArticleMulti; import dan.dit.whatsthat.testsubject.shopping.sortiment.SortimentHolder; import dan.dit.whatsthat.util.general.PercentProgressListener; import dan.dit.whatsthat.util.general.MathFunction; 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.look.CircleLook; import dan.dit.whatsthat.util.flatworld.look.Frames; import dan.dit.whatsthat.util.flatworld.look.Look; import dan.dit.whatsthat.util.flatworld.mover.HitboxMover; import dan.dit.whatsthat.util.flatworld.mover.HitboxNewtonMover; 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.ColorAnalysisUtil; import dan.dit.whatsthat.util.image.ImageUtil; /** * * Created by daniel on 04.09.15. */ public class RiddleLazor extends RiddleGame implements FlatWorldCallback { private static final float METEOR_BEAM_WIDTH_FRACTION_OF_SCREEN_WIDTH = 0.03f; //meteor trail // width private static final float METEOR_RADIUS_FRACTION_OF_SCREEN_WIDTH = 0.031f; //meteor head radius private static final float METEOR_DIAGONAL_DURATION = 9000.f; //ms, time for a meteor that moves directly from top left to bottom right private static final float ONE_SECOND = 1000.f; // ms, one second, fixed private static final double CHANCE_FOR_BONUS_BEAM_BASE = 0.12; // basic chance for super beam in percent private static final double ADDITIONAL_CHANCE_FOR_BONUS_BEAM_ARTICLE = 0.03; // when the third article is purchased the chance increases private static final float CANNON_BEAM_FRACTION_OF_SCREEN_WIDTH = 0.005f; private static final float CANNONBALL_RADIUS_FRACTION_OF_SCREEN_WIDTH = 0.038f; private static final float CANNONBALL_DIAGONAL_DURATION = 9000.f; //ms private static final long CANNON_RELOAD_DURATION_START = Cannon.LOADING_STATES_COUNT * 1600L; //for each loading state (3atm) wait x ms private static final long CANNON_RELOAD_DURATION_DIFFICULTY_ULTRA = Cannon.LOADING_STATES_COUNT * 500L; public static final long CANNONBALL_EXPLOSION_DURATION = 2000L; private static final float CANNONBALL_EXPLOSION_MAX_GROWTH_FACTOR = 2.3f; private static final int CANNONBALL_EXPLOSION_COLOR_START = 0xFFfbdc2e; private static final int CANNONBALL_EXPLOSION_COLOR_END = 0x66ff6023; private static final long CANNON_LOADED_FRAME_DURATION = 150L;//ms private static final long CANNON_FRAME_DURATION = 100L;//ms private static final float CANNON_RADIUS_FRACTION_OF_WIDTH = 0.09f; private static final float METEOR_EXPLOSION_RADIUS_FACTOR = 1.5f; // the factor on the radius of the meteor ball when exploding in the city private static final int DIFFICULTY_POINTS_GAIN_ON_METEOR_KILL = 2; private static final int DIFFICULTY_POINTS_LOSS_ON_METEOR_MINIMAL = 1; private static final int DIFFICULTY_POINTS_LOSS_ON_METEOR_MAXIMAL = 9; private static final int DIFFICULTY_POINTS_LOSS_ON_SHOOTING = 1; private static final int DIFFICULTY_POINTS_LOSS_ON_BONUS_METEOR_CRASHED_IF_PROTECTED = 1; private static final int DIFFICULTY_AT_BEGINNING = 10; private static final int DIFFICULTY_FOR_PROTECTION_BASE = 50; public static final int COLOR_TYPE_RED = 0; private static final int COLOR_TYPE_GREEN = 1; private static final int COLOR_TYPE_BLUE = 2; public static final int COLOR_TYPE_BONUS = 3; // bonus always as last type index as it is treated differently in some cases private static final int COLOR_TYPES_COUNT = 4; private static final int[] COLOR_TYPES = new int[] {COLOR_TYPE_RED, COLOR_TYPE_GREEN, COLOR_TYPE_BLUE, COLOR_TYPE_BONUS}; private static final float DIFFICULTY_ULTRA_AT = 100; private static final float METEOR_SPAWN_TIME_START = 3800; private static final float METEOR_SPAWN_TIME_DIFFICULTY_ULTRA = 300; private static final int[] DIFFICULTY_FOR_PROTECTION_ARTICLE_VALUES = new int[]{40, 30, 25, 20}; // if updating this, update string resources!! public static final Long RIGHT_CANNON_ID = 1L; public static final Long LEFT_CANNON_ID = 0L; private FlatRectWorld mFlatWorld; private int mDifficulty; private float mDiagonal; private float mMeteorRadiusPixels; private float mMeteorSpeedPixels; private int mMeteorBeamWidthPixels; private float mCannonBallRadiusPixels; private int mCannonBallBeamWidthPixels; private float mCannonBallSpeed; private Bitmap[] mVisibleTypeLayers; private Canvas[] mLayersCanvas; private Paint[] mColorTypePaints; private Bitmap[] mMeteorBalls; private Canvas mWorldCanvas; private Bitmap mWorldBitmap; private Paint mWorldBackgroundPaint; private long mNextMeteorDuration; private Random mRand; private boolean mRefreshLayers; private Bitmap mVisibleLayer; private Canvas mVisibleLayerCanvas; private Paint mClearPaint; private Paint[] mVisibleLayerPaints; private float mCityOffsetY; private Bitmap mCityLayer; private Canvas mCityLayerCanvas; private Bitmap mCannonBall; private Cannon mCannonLeft; private Cannon mCannonRight; private Paint mClearColorTypePaint; private Bitmap mCityDestructionMask; private Canvas mCityDestructionCanvas; private Paint mCityDestructionOverlayPaint; private Paint mMeteorDestructionPaint; private Paint mDifficultyTextPaint; private MathFunction mMeteorSpawnTimeInterpolator; private MathFunction mReloadTimeInterpolator; private Bitmap mProtectedCity; private Bitmap mGenerator; private boolean mProtected; private String mDifficultyText; private MathFunction mMeteorPointLossInterpolator; private int mDifficultyForProtection; private List<Integer> mDrawLogPaintId; private List<Integer> mDrawLogGeometryId; private List<Float> mDrawLogMainX; private List<Float> mDrawLogMainY; private List<Float> mDrawLogSecondX; private List<Float> mDrawLogSecondY; private List<Meteor> mMeteors; private int mReactorImprovements; private double mAdditionalChanceForBonusMeteorIfProtected; public RiddleLazor(Riddle riddle, Image image, Bitmap bitmap, Resources res, RiddleConfig config, PercentProgressListener listener) { super(riddle, image, bitmap, res, config, listener); } @Override protected void initAchievementData() { } @Override protected void addBonusReward(@NonNull RiddleScore.Rewardable rewardable) { int bonus = 0; if (mConfig.mAchievementGameData.getValue(AchievementLazor.KEY_GAME_METEOR_CRASHED_IN_CITY_COUNT, 0L) == 0L) { bonus = TypesHolder.SCORE_HARD; } else if (mConfig.mAchievementGameData.getValue(AchievementLazor.KEY_GAME_IS_PROTECTED, 0L) == 1L && mConfig.mAchievementGameData.getValue(AchievementLazor .KEY_GAME_METEOR_CRASHED_IN_CITY_COUNT, 0L) <= 3) { bonus = TypesHolder.SCORE_SIMPLE; } rewardable.addBonus(bonus); } @Override public void draw(Canvas canvas) { canvas.drawBitmap(mVisibleLayer, 0, 0, null); canvas.drawBitmap(mCityLayer, 0, mCityOffsetY, null); drawTextCenteredX(canvas, mDifficultyText, mCityLayer.getWidth() / 2, mCityOffsetY + mCityLayer.getHeight() / 2, mDummyRect, mDifficultyTextPaint); canvas.drawBitmap(mCityDestructionMask, 0, mCityOffsetY, mCityDestructionOverlayPaint); if (mProtected) { canvas.drawBitmap(mProtectedCity, 0, mCityOffsetY, null); } canvas.drawBitmap(mWorldBitmap, 0, 0, null); } private Rect mDummyRect; private static void drawTextCenteredX(Canvas canvas, String text, float x, float y, Rect dummyRect, Paint paint) { paint.getTextBounds(text, 0, text.length(), dummyRect); canvas.drawText(text, x - dummyRect.exactCenterX(), y, paint); } @Override protected void initBitmap(Resources res, PercentProgressListener listener) { mReactorImprovements = TestSubject.getInstance().getShopValue(SortimentHolder.ARTICLE_KEY_LAZOR_PROTECTION_AT_DIFFICULTY); mDiagonal = (float) Math.sqrt(mConfig.mWidth * mConfig.mWidth + mConfig.mHeight * mConfig.mHeight); mCityOffsetY = mBitmap.getHeight(); mRand = new Random(); mFlatWorld = new FlatRectWorld(new RectF(0, 0, mConfig.mWidth, mConfig.mHeight), new GeneralHitboxCollider(), this); mWorldBackgroundPaint = new Paint(); mWorldBackgroundPaint.setColor(Color.BLACK); mWorldBackgroundPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mWorldBitmap = Bitmap.createBitmap(mConfig.mWidth, mConfig.mHeight, mBitmap.getConfig()); mWorldCanvas = new Canvas(mWorldBitmap); mVisibleLayer = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888); mVisibleLayerCanvas = new Canvas(mVisibleLayer); mVisibleLayerPaints = new Paint[COLOR_TYPES_COUNT]; Paint visibleLayerPaint = new Paint(); visibleLayerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); for (int i = 0; i < COLOR_TYPES_COUNT - 1; i++) { mVisibleLayerPaints[i] = visibleLayerPaint; } Paint visibleAlphaLayerPaint = new Paint(); visibleAlphaLayerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); mVisibleLayerPaints[COLOR_TYPE_BONUS] = visibleAlphaLayerPaint; mVisibleTypeLayers = new Bitmap[COLOR_TYPES_COUNT]; mLayersCanvas = new Canvas[COLOR_TYPES_COUNT]; mClearColorTypePaint = new Paint(); mClearColorTypePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mColorTypePaints = new Paint[COLOR_TYPES_COUNT]; for (int type : COLOR_TYPES) { Paint typePaint = new Paint(); typePaint.setShader(new BitmapShader(mBitmap, Shader.TileMode.MIRROR, Shader.TileMode.MIRROR)); if (type == COLOR_TYPE_BONUS) { typePaint.setColorFilter(new PorterDuffColorFilter(colorTypeToColor(type), PorterDuff.Mode.DST_IN)); } else { typePaint.setColorFilter(new PorterDuffColorFilter(colorTypeToColor(type), PorterDuff.Mode.MULTIPLY)); } mColorTypePaints[type] = typePaint; mVisibleTypeLayers[type] = Bitmap.createBitmap(mVisibleLayer.getWidth(), mVisibleLayer.getHeight(), Bitmap.Config.ARGB_8888); mLayersCanvas[type] = new Canvas(mVisibleTypeLayers[type]); } mClearPaint = new Paint(); mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); listener.onProgressUpdate(33); mCityLayer = Bitmap.createBitmap(mConfig.mWidth, mConfig.mHeight - mBitmap.getHeight(), Bitmap.Config.ARGB_8888); mCityLayerCanvas = new Canvas(mCityLayer); mProtectedCity = ImageUtil.loadBitmap(res, R.drawable.skylinecity_protected, mCityLayer.getWidth(), mCityLayer.getHeight(), BitmapUtil.MODE_FIT_EXACT); mGenerator = ImageUtil.loadBitmap(res, R.drawable.atomkraftwerk, mCityLayer.getWidth(), mCityLayer.getHeight(), BitmapUtil.MODE_FIT_INSIDE); Bitmap city = ImageUtil.loadBitmap(res, R.drawable.skylinecity, mCityLayer.getWidth(), mCityLayer.getHeight(), BitmapUtil.MODE_FIT_EXACT); makeCityLayer(city); mCityDestructionMask = Bitmap.createBitmap(mCityLayer.getWidth(), mCityLayer.getHeight(), mCityLayer.getConfig()); mCityDestructionCanvas = new Canvas(mCityDestructionMask); mCityDestructionCanvas.drawColor(Color.TRANSPARENT); mCityDestructionOverlayPaint = new Paint(); mCityDestructionOverlayPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); mMeteorDestructionPaint = new Paint(); mMeteorDestructionPaint.setColor(res.getColor(RiddleView.BACKGROUND_COLOR_RESOURCE_ID)); mDummyRect = new Rect(); mDifficultyTextPaint = new Paint(); mDifficultyTextPaint.setTextSize(ImageUtil.convertDpToPixel(20.f, mConfig.mScreenDensity)); mDifficultyTextPaint.setAntiAlias(true); mDifficultyTextPaint.setColor(Color.YELLOW); Compacter cmp = getCurrentState(); initDifficulty(cmp); initMeteorData(res); initCannonData(res); listener.onProgressUpdate(50); initDrawLog(cmp, listener); mMeteorPointLossInterpolator = new MathFunction.QuadraticInterpolation(0, DIFFICULTY_POINTS_LOSS_ON_METEOR_MINIMAL, DIFFICULTY_ULTRA_AT, DIFFICULTY_POINTS_LOSS_ON_METEOR_MAXIMAL); mMeteorSpawnTimeInterpolator = new MathFunction.LinearInterpolation(0.f, METEOR_SPAWN_TIME_START, DIFFICULTY_ULTRA_AT, METEOR_SPAWN_TIME_DIFFICULTY_ULTRA); mReloadTimeInterpolator = new MathFunction.LinearInterpolation(0, CANNON_RELOAD_DURATION_START, DIFFICULTY_ULTRA_AT, CANNON_RELOAD_DURATION_DIFFICULTY_ULTRA); } private void makeCityLayer(Bitmap cityBitmap) { if (cityBitmap != null) { mCityLayerCanvas.drawBitmap(cityBitmap, 0, 0, null); } if (mGenerator != null) { // only null if drawable deleted or serious bug happens (like R file corrupt) mCityLayerCanvas.drawBitmap(mGenerator, mCityLayer.getWidth() / 2 - mGenerator.getWidth() / 2, 0, null); } } private void initDrawLog(Compacter data, PercentProgressListener listener) { mDrawLogPaintId = new ArrayList<>(); mDrawLogGeometryId = new ArrayList<>(); mDrawLogMainX = new ArrayList<>(); mDrawLogMainY = new ArrayList<>(); mDrawLogSecondX = new ArrayList<>(); mDrawLogSecondY = new ArrayList<>(); if (data != null && data.getSize() > 1) { Compacter drawLogData = new Compacter(data.getData(1)); for (int i = 0; i + 5 < drawLogData.getSize(); i += 6) { try { addToDrawLog(drawLogData.getInt(i), drawLogData.getInt(i + 1), drawLogData.getFloat(i + 2), drawLogData.getFloat(i + 3), drawLogData.getFloat(i + 4), drawLogData.getFloat(i + 5)); } catch (CompactedDataCorruptException e) { Log.e("Riddle", "Error reading draw log: " + e); break; } listener.onProgressUpdate(50 + (int) (50 * i / (double) drawLogData.getSize())); } mRefreshLayers = true; } } private void initDifficulty(Compacter data) { if (data != null && data.getSize() > 0) { try { mDifficulty = data.getInt(0); } catch (CompactedDataCorruptException e) { Log.e("Riddle", "Error loading difficulty from saved state."); } } else { mDifficulty = DIFFICULTY_AT_BEGINNING; } mDifficultyForProtection = DIFFICULTY_FOR_PROTECTION_BASE; for (int i = 0; i < DIFFICULTY_FOR_PROTECTION_ARTICLE_VALUES.length; i++) { if (ShopArticleMulti.hasPurchased(mReactorImprovements, i)) { mDifficultyForProtection = DIFFICULTY_FOR_PROTECTION_ARTICLE_VALUES[i]; } } onDifficultyUpdated(); } private void initMeteorData(Resources res) { mAdditionalChanceForBonusMeteorIfProtected = ShopArticleMulti.hasPurchased(mReactorImprovements, 3) ? ADDITIONAL_CHANCE_FOR_BONUS_BEAM_ARTICLE : 0.; mMeteors = new LinkedList<>(); mMeteorBeamWidthPixels = (int) (mConfig.mWidth * METEOR_BEAM_WIDTH_FRACTION_OF_SCREEN_WIDTH); setMeteorBeamWidthPixels(mMeteorBeamWidthPixels); mMeteorSpeedPixels = mDiagonal / (METEOR_DIAGONAL_DURATION / ONE_SECOND); mMeteorRadiusPixels = mConfig.mWidth * METEOR_RADIUS_FRACTION_OF_SCREEN_WIDTH; mMeteorRadiusPixels = Math.max(mMeteorRadiusPixels, 1); int size = (int) (mMeteorRadiusPixels * 2); mMeteorBalls = new Bitmap[COLOR_TYPES_COUNT]; mMeteorBalls[COLOR_TYPE_RED] = ImageUtil.loadBitmap(res, R.drawable.laser_red, size, size, BitmapUtil.MODE_FIT_EXACT); mMeteorBalls[COLOR_TYPE_GREEN] = ImageUtil.loadBitmap(res, R.drawable.laser_green, size, size, BitmapUtil.MODE_FIT_EXACT); mMeteorBalls[COLOR_TYPE_BLUE] = ImageUtil.loadBitmap(res, R.drawable.laser_blue, size, size, BitmapUtil.MODE_FIT_EXACT); mMeteorBalls[COLOR_TYPE_BONUS] = ImageUtil.loadBitmap(res, R.drawable.lazor_white, size, size, BitmapUtil.MODE_FIT_EXACT); } private void initCannonData(Resources res) { mCannonBallRadiusPixels = (int) (mConfig.mWidth * CANNONBALL_RADIUS_FRACTION_OF_SCREEN_WIDTH); mCannonBallRadiusPixels = Math.max(1, mCannonBallRadiusPixels); mCannonBallBeamWidthPixels = (int) (mConfig.mWidth * CANNON_BEAM_FRACTION_OF_SCREEN_WIDTH); mCannonBallBeamWidthPixels = Math.max(1, mCannonBallBeamWidthPixels); mCannonBallSpeed = mDiagonal / (CANNONBALL_DIAGONAL_DURATION / ONE_SECOND); int size = (int) (mCannonBallRadiusPixels * 2); mCannonBall = ImageUtil.loadBitmap(res, R.drawable.laser_alpha, size, size, BitmapUtil.MODE_FIT_EXACT); float cannonWallOffsetX = ImageUtil.convertDpToPixel(2.f, mConfig.mScreenDensity); float cannonRadius = CANNON_RADIUS_FRACTION_OF_WIDTH * mConfig.mWidth; int cannonSize = (int) (cannonRadius * 2); Bitmap[] loadedFrames = new Bitmap[] {ImageUtil.loadBitmap(res, R.drawable.lazor_loaded_frame1, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT), ImageUtil.loadBitmap(res, R.drawable.lazor_loaded_frame2, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT), ImageUtil.loadBitmap(res, R.drawable.lazor_loaded_frame3, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT), null}; loadedFrames[3] = loadedFrames[1]; Bitmap[] loading1Frames = new Bitmap[] {ImageUtil.loadBitmap(res, R.drawable.lazor_level1_frame1, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT), ImageUtil.loadBitmap(res, R.drawable.lazor_level1_frame2, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT)}; Bitmap[] loading2Frames = new Bitmap[] {ImageUtil.loadBitmap(res, R.drawable.lazor_level2_frame1, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT), ImageUtil.loadBitmap(res, R.drawable.lazor_level2_frame2, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT)}; Bitmap[] firedFrames = new Bitmap[] {ImageUtil.loadBitmap(res, R.drawable.lazor_fired_frame1, cannonSize, cannonSize, BitmapUtil.MODE_FIT_EXACT)}; mCannonLeft = new Cannon(new HitboxCircle(cannonRadius + cannonWallOffsetX, mConfig.mHeight, cannonRadius), HitboxNoMover.INSTANCE, loadedFrames, 0, - (int) cannonRadius); mCannonLeft.initLoadingLooks(loading1Frames, loading2Frames, firedFrames); mFlatWorld.addActor(mCannonLeft); mCannonRight = new Cannon(new HitboxCircle(mConfig.mWidth - cannonRadius - cannonWallOffsetX, mConfig.mHeight, cannonRadius), HitboxNoMover.INSTANCE, loadedFrames, 0, - (int) cannonRadius); mCannonRight.initLoadingLooks(loading1Frames, loading2Frames, firedFrames); mFlatWorld.addActor(mCannonRight); } private void setMeteorBeamWidthPixels(int beamWidthPixels) { mMeteorBeamWidthPixels = Math.max(1, beamWidthPixels); //at least one pixel width for (Paint mColorTypePaint : mColorTypePaints) { mColorTypePaint.setStrokeWidth(mMeteorBeamWidthPixels); } mMeteorDestructionPaint.setStrokeWidth(mMeteorBeamWidthPixels); mClearColorTypePaint.setStrokeWidth(mMeteorBeamWidthPixels); } @Override public boolean onMotionEvent(MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { float targetX = event.getX(); float targetY = event.getY(); Cannon cannon = findAvailableCannon(targetX, targetY); if (cannon != null) { cannon.shoot(targetX, targetY); } } return false; } @NonNull @Override protected String compactCurrentState() { synchronized (this) { // first clear off every falling meteor to prevent cheating by closing before a meteors hits // synchronize access to mMeteors since periodic event can still be running when closing riddle for (Meteor meteor : new ArrayList<>(mMeteors)) { meteor.onLeaveWorld(); } } Compacter data = new Compacter(); data.appendData(mDifficulty); Compacter drawLogData = new Compacter(); int size = mDrawLogPaintId.size(); for (int i = 0; i < size; i++) { drawLogData.appendData(mDrawLogPaintId.get(i)) .appendData(mDrawLogGeometryId.get(i)) .appendData(mDrawLogMainX.get(i)) .appendData(mDrawLogMainY.get(i)) .appendData(mDrawLogSecondX.get(i)) .appendData(mDrawLogSecondY.get(i)); } data.appendData(drawLogData.compact()); return data.compact(); } @Override public boolean requiresPeriodicEvent() { return true; } @Override public void onPeriodicEvent(long updatePeriod) { mFlatWorld.update(updatePeriod); checkedRefreshLayers(); mWorldCanvas.drawPaint(mWorldBackgroundPaint); mFlatWorld.draw(mWorldCanvas, null); updateMeteorsController(updatePeriod); mRefreshLayers = false; } private void checkedRefreshLayers() { if (mRefreshLayers) { mVisibleLayerCanvas.drawPaint(mClearPaint); for (int type : COLOR_TYPES) { mVisibleLayerCanvas.drawBitmap(mVisibleTypeLayers[type], 0, 0, mVisibleLayerPaints[type]); } } } private void updateMeteorsController(long updatePeriod) { mNextMeteorDuration -= updatePeriod; if (mNextMeteorDuration <= 0L) { //spawn new meteor mNextMeteorDuration = (long) mMeteorSpawnTimeInterpolator.evaluate(Math.min(mDifficulty, DIFFICULTY_ULTRA_AT)); mFlatWorld.addActor(makeMeteor(mRand.nextInt(mConfig.mWidth), 0, mRand.nextInt(mConfig.mWidth), mConfig.mHeight, nextColorType())); } } private int nextColorType() { if (mRand.nextDouble() < CHANCE_FOR_BONUS_BEAM_BASE + (mProtected ? mAdditionalChanceForBonusMeteorIfProtected : 0)) { return COLOR_TYPE_BONUS; } else { return mRand.nextInt(COLOR_TYPES_COUNT - 1); } } @Override public void onReachedEndOfWorld(Actor columbus, float x, float y, int borderFlags) { if ((borderFlags & FlatRectWorld.BORDER_FLAG_BOTTOM) != 0) { columbus.onReachedEndOfWorld(); } } @Override public void onLeftWorld(Actor jesus, int borderFlags) { jesus.onLeaveWorld(); } @Override public void onMoverStateChange(Actor actor) { } @Override public void onCollision(Actor colliding1, Actor colliding2) { colliding1.onCollision(colliding2); colliding2.onCollision(colliding1); } private static class BeamBallLook extends Look { private final Bitmap mBall; private BeamBall mBeamBall; private final Paint mPaint; protected BeamBallLook(Bitmap ball, Paint paint) { mPaint = paint; mBall = ball; } public static Paint makePaint(int color, int widthPixels) { Paint paint = new Paint(); paint.setColor(color); paint.setAntiAlias(true); // or better not because PIXEL ART?! paint.setStrokeWidth(widthPixels); return paint; } @Override public int getWidth() { return 0; } @Override public int getHeight() { return 0; } @Override public boolean update(long updatePeriod) { return false; } @Override public void draw(Canvas canvas, float x, float y, Paint paint) { float ballCenterX = mBeamBall.mHitbox.getCenterX(); float ballCenterY = mBeamBall.mHitbox.getCenterY(); canvas.drawLine(mBeamBall.mStartX, mBeamBall.mStartY, ballCenterX, ballCenterY, mPaint); canvas.drawBitmap(mBall, ballCenterX - mBall.getWidth() / 2, ballCenterY - mBall.getHeight() / 2, paint); } @Override public void reset() { } } private abstract class BeamBall extends Actor { protected final HitboxCircle mHitbox; protected final int mStartX; protected final int mStartY; protected BeamBall(HitboxCircle hitbox, HitboxNewtonMover mover, BeamBallLook defaultLook, int startX, int startY) { super(hitbox, mover, defaultLook); mHitbox = hitbox; mStartX = startX; mStartY = startY; } } private static final int METEOR_DESTRUCTION_PAINT_ID = -2; private static final int CLEAR_ALL_TRAILS_PAINT_ID = -1; private static final int CLEAR_RED_TRAIL_PAINT_ID = COLOR_TYPE_RED; private static final int CLEAR_GREEN_TRAIL_PAINT_ID = COLOR_TYPE_GREEN; private static final int CLEAR_BLUE_TRAIL_PAINT_ID = COLOR_TYPE_BLUE; private static final int CLEAR_BONUS_TRAIL_PAINT_ID = COLOR_TYPE_BONUS; private static final int ADD_RED_TRAIL_PAINT_ID = COLOR_TYPE_RED + COLOR_TYPES_COUNT; private static final int ADD_GREEN_TRAIL_PAINT_ID = COLOR_TYPE_GREEN + COLOR_TYPES_COUNT; private static final int ADD_BLUE_TRAIL_PAINT_ID = COLOR_TYPE_BLUE + COLOR_TYPES_COUNT; private static final int ADD_BONUS_TRAIL_PAINT_ID = COLOR_TYPE_BONUS + COLOR_TYPES_COUNT; private static final int DRAW_LOG_LINE_ID = 1; private static final int DRAW_LOG_CIRCLE_ID = 0; private void addToDrawLog(int paintId, int paintGeometry, float mainX, float mainY, float secondX, float secondY) { if (paintId == METEOR_DESTRUCTION_PAINT_ID) { if (paintGeometry == DRAW_LOG_LINE_ID) { mCityDestructionCanvas.drawLine(mainX, mainY, secondX, secondY, mMeteorDestructionPaint); } else if (paintGeometry == DRAW_LOG_CIRCLE_ID) { mCityDestructionCanvas.drawCircle(mainX, mainY, secondX, mMeteorDestructionPaint); } } else if (paintId == CLEAR_ALL_TRAILS_PAINT_ID) { for (int type : COLOR_TYPES) { mLayersCanvas[type].drawLine(mainX, mainY, secondX, secondY, mClearColorTypePaint); } } else if (paintId >= CLEAR_RED_TRAIL_PAINT_ID && paintId <= CLEAR_BONUS_TRAIL_PAINT_ID) { mLayersCanvas[paintId].drawLine(mainX, mainY, secondX, secondY, mClearColorTypePaint); } else if (paintId >= ADD_RED_TRAIL_PAINT_ID && paintId <= ADD_BONUS_TRAIL_PAINT_ID) { mLayersCanvas[paintId - COLOR_TYPES_COUNT].drawLine(mainX, mainY, secondX, secondY, mColorTypePaints[paintId - COLOR_TYPES_COUNT]); } mDrawLogPaintId.add(paintId); mDrawLogGeometryId.add(paintGeometry); mDrawLogMainX.add(mainX); mDrawLogMainY.add(mainY); mDrawLogSecondX.add(secondX); mDrawLogSecondY.add(secondY); } private class Cannon extends Actor { private static final int STATE_LOADED = 0; private static final int STATE_LOADING_2 = 1; private static final int STATE_LOADING_1 = 2; private static final int STATE_JUST_SHOT = 3; private static final int LOADING_STATES_COUNT = 3; private int mLoadedState; private final int mCannonShootStartOffsetX; private final int mCannonShootStartOffsetY; private long mLoadingStateCounter; public Cannon(Hitbox hitbox, HitboxMover mover, Bitmap[] lookLoaded, int shootStartOffsetX, int shootStartOffsetY) { super(hitbox, mover, new Frames(lookLoaded, CANNON_LOADED_FRAME_DURATION)); setActive(true); putStateFrames(STATE_LOADED, mCurrentLook); mCannonShootStartOffsetX = shootStartOffsetX; mCannonShootStartOffsetY = shootStartOffsetY; offsetLook(mCurrentLook); } private Look offsetLook(Look look) { look.setOffset(0, -getHitbox().getBoundingRect().height() / 2); return look; } private void initLoadingLooks(Bitmap[] loading1, Bitmap[] loading2, Bitmap[] loadingJustShot) { putStateFrames(STATE_LOADING_1, offsetLook(new Frames(loading1, CANNON_FRAME_DURATION))); putStateFrames(STATE_LOADING_2, offsetLook(new Frames(loading2, CANNON_FRAME_DURATION))); putStateFrames(STATE_JUST_SHOT, offsetLook(new Frames(loadingJustShot, CANNON_FRAME_DURATION))); } @Override public boolean update(long updatePeriod) { boolean result = super.update(updatePeriod); if (mLoadedState > STATE_LOADED) { mLoadingStateCounter -= updatePeriod; if (mLoadingStateCounter <= 0L) { mLoadingStateCounter = (long) (mReloadTimeInterpolator.evaluate(Math.min(mDifficulty, DIFFICULTY_ULTRA_AT)) / LOADING_STATES_COUNT); mLoadedState--; setStateFrames(mLoadedState); } } return result; } private void shoot(float targetX, float targetY) { if (isLoaded()) { mLoadedState = STATE_JUST_SHOT; setStateFrames(mLoadedState); int startX = (int) (getHitbox().getCenterX() + mCannonShootStartOffsetX); int startY = (int) (getHitbox().getCenterY() + mCannonShootStartOffsetY); HitboxCircle ballHitbox = new HitboxCircle(startX, startY, mCannonBallRadiusPixels); HitboxNewtonMover mover = new HitboxNewtonMover(targetX - startX, targetY - startY, mCannonBallSpeed); long timeout = (long) (Math.sqrt(distSquared(startX, startY, targetX, targetY)) / mCannonBallSpeed * ONE_SECOND); BeamBallLook look = new BeamBallLook(mCannonBall, BeamBallLook.makePaint(Color.CYAN, mCannonBallBeamWidthPixels)); CannonBall ball = new CannonBall(this, ballHitbox, mover, look, startX, startY, timeout); mFlatWorld.addActor(ball); updateDifficulty(-DIFFICULTY_POINTS_LOSS_ON_SHOOTING); } } public boolean isLoaded() { return mLoadedState == STATE_LOADED; } } private float distSquared(float x1, float y1, float x2, float y2) { return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); } private Cannon findAvailableCannon(float atX, float atY) { float distToLeft2 = distSquared(atX, atY, mCannonLeft.getHitbox().getCenterX(), mCannonLeft.getHitbox().getCenterY()); float distToRight2 = distSquared(atX, atY, mCannonRight.getHitbox().getCenterX(), mCannonRight.getHitbox().getCenterY()); if (mCannonLeft.isLoaded() && mCannonRight.isLoaded()) { return distToLeft2 < distToRight2 ? mCannonLeft : mCannonRight; } else if (mCannonLeft.isLoaded()) { return mCannonLeft; } else if (mCannonRight.isLoaded()) { return mCannonRight; } return null; } private class CannonBall extends BeamBall { private final Cannon mCannon; private long mTimeout; private long mExplosionDuration; private CircleLook mExplosionLook; private final float mBaseRadius; //needed for achievement data private long mFlyDuration; private int mCollisionCount; private CannonBall(Cannon cannon, HitboxCircle hitbox, HitboxNewtonMover mover, BeamBallLook defaultLook, int startX, int startY, long timeout) { super(hitbox, mover, defaultLook, startX, startY); defaultLook.mBeamBall = this; mCannon = cannon; mTimeout = timeout; mFlyDuration = mTimeout; mBaseRadius = mHitbox.getRadius(); setActive(true); } @Override public boolean onCollision(Actor with) { if (with instanceof Meteor) { startExplode(true); return true; } return false; } @Override public boolean update(long updatePeriod) { boolean result = super.update(updatePeriod); if (mTimeout > 0L) { mTimeout -= updatePeriod; if (mTimeout <= 0L) { startExplode(false); } } if (mExplosionDuration > 0L) { mExplosionDuration -= updatePeriod; if (mExplosionDuration <= 0L) { onExplosionEnd(); } else { expandExplosion(); } } return result; } private void onExplosionEnd() { mFlatWorld.removeActor(this); mConfig.mAchievementGameData.enableSilentChanges(AchievementDataEvent.EVENT_TYPE_DATA_UPDATE); mConfig.mAchievementGameData.increment(AchievementLazor.KEY_GAME_CANNON_BALL_EXPLOSION_END_COUNT, 1L, 0L); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_CANNON_BALL_ENDED_DESTROY_COUNT, (long) mCollisionCount, AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_CANNON_BALL_ENDED_CANNON_ID, mCannon == mCannonLeft ? LEFT_CANNON_ID : RIGHT_CANNON_ID, AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.disableSilentChanges(); } private void startExplode(boolean collided) { if (mExplosionDuration == 0L) { mFlyDuration -= mTimeout; mTimeout = 0L; mExplosionDuration = CANNONBALL_EXPLOSION_DURATION; mExplosionLook = new CircleLook(mHitbox.getRadius(), CANNONBALL_EXPLOSION_COLOR_START); setMover(HitboxNoMover.INSTANCE); putStateFrames(0, mExplosionLook); setStateFrames(0); if (collided && mConfig.mAchievementGameData != null) { mConfig.mAchievementGameData.increment(AchievementLazor.KEY_GAME_LAZOR_CANNON_COLLIDED_EARLY, 1L, 0L); } } } private void expandExplosion() { float fraction = (1.f - mExplosionDuration / (float) CANNONBALL_EXPLOSION_DURATION); float currRadius = mBaseRadius * (1.f + (CANNONBALL_EXPLOSION_MAX_GROWTH_FACTOR - 1.f) * fraction); mExplosionLook.setRadius(currRadius); mExplosionLook.setColor(ColorAnalysisUtil.interpolateColorLinear(CANNONBALL_EXPLOSION_COLOR_START, CANNONBALL_EXPLOSION_COLOR_END, fraction)); mHitbox.setRadius(currRadius); } @Override public void onLeaveWorld() { mFlatWorld.removeActor(this); } } public static int colorTypeToColor(int colorType) { switch (colorType) { case COLOR_TYPE_RED: return 0xFFFF0000; case COLOR_TYPE_GREEN: return 0xFF00FF00; case COLOR_TYPE_BLUE: return 0xFF0000FF; case COLOR_TYPE_BONUS: return 0xFFFFFFFF; } return 0; } protected Meteor makeMeteor(int startX, int startY, int targetX, int targetY, int colorType) { HitboxCircle hitbox = new HitboxCircle(startX, startY, mMeteorRadiusPixels); HitboxNewtonMover mover = new HitboxNewtonMover(targetX - startX, targetY - startY, mMeteorSpeedPixels); BeamBallLook look = new BeamBallLook(mMeteorBalls[colorType], BeamBallLook.makePaint(colorTypeToColor(colorType), mMeteorBeamWidthPixels)); Meteor meteor = new Meteor(hitbox, mover, look, startX, startY, colorType); synchronized (this) { mMeteors.add(meteor); } return meteor; } private class Meteor extends BeamBall { private final int mColorType; private Meteor(HitboxCircle hitbox, HitboxNewtonMover mover, BeamBallLook defaultLook, int startX, int startY, int colorType) { super(hitbox, mover, defaultLook, startX, startY); setActive(true); defaultLook.mBeamBall = this; mColorType = colorType; } @Override public boolean onCollision(Actor with) { if (with instanceof CannonBall) { addToDrawLog(mColorType + COLOR_TYPES_COUNT, DRAW_LOG_LINE_ID, mStartX, mStartY, mHitbox.getCenterX(), mHitbox.getCenterY()); mRefreshLayers = true; cleanUp(); CannonBall ball = (CannonBall) with; ball.mCollisionCount++; // done here since order of onCollision is not fixed and onMeteorDestroyed needs that data onMeteorDestroyed(ball); return true; } return false; } private void onMeteorDestroyed(CannonBall ball) { mConfig.mAchievementGameData.enableSilentChanges(AchievementDataEvent.EVENT_TYPE_DATA_UPDATE); mConfig.mAchievementGameData.increment(AchievementLazor.KEY_GAME_METEOR_DESTROYED_COUNT, 1L, 0L); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_LAST_METEOR_DESTROYED_Y_PERCENT, (long) (100 * mHitbox.getCenterY() / (double) mFlatWorld.getHeight()), AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_LAST_METEOR_DESTROYED_CANNONBALL_Y_PERCENT, (long) (100 * ball.getHitbox().getCenterY() / (double) mFlatWorld.getHeight()), AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_LAST_METEOR_DESTROYED_COLOR_TYPE, (long) mColorType, AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_LAST_METEOR_DESTROYED_LAZOR_CANNON_FLY_DURATION, ball.mFlyDuration, AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_LAST_METEOR_DESTROYED_CANNONBALL_DESTROY_COUNT, (long) ball.mCollisionCount, AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_LAST_METEOR_DESTROYED_CANNONBALL_EXPAND_TIME, CANNONBALL_EXPLOSION_DURATION - ball.mExplosionDuration, AchievementProperties.UPDATE_POLICY_ALWAYS); mConfig.mAchievementGameData.disableSilentChanges(); updateDifficulty(DIFFICULTY_POINTS_GAIN_ON_METEOR_KILL); } @Override public void onReachedEndOfWorld() { if (!mProtected) { // exploding at bottom if not protected addToDrawLog(METEOR_DESTRUCTION_PAINT_ID, DRAW_LOG_LINE_ID, mHitbox.getCenterX(), mHitbox.getCenterY() - mCityOffsetY, mStartX, mStartY - mCityOffsetY); addToDrawLog(METEOR_DESTRUCTION_PAINT_ID, DRAW_LOG_CIRCLE_ID, mHitbox.getCenterX(), mHitbox.getCenterY() - mCityOffsetY, mHitbox.getRadius() * METEOR_EXPLOSION_RADIUS_FACTOR, 0.f); } cleanUp(); clearTrail(); onMeteorCrashed(true); } @Override public void onLeaveWorld() { cleanUp(); clearTrail(); onMeteorCrashed(false); } private void cleanUp() { synchronized (this) { mMeteors.remove(this); } mFlatWorld.removeActor(this); } private void clearTrail() { if (mColorType == COLOR_TYPE_BONUS && !mProtected) { //clear from all layers if not protected addToDrawLog(CLEAR_ALL_TRAILS_PAINT_ID, DRAW_LOG_LINE_ID, mStartX, mStartY, mHitbox.getCenterX(), mHitbox.getCenterY()); } else { addToDrawLog(mColorType, DRAW_LOG_LINE_ID, mStartX, mStartY, mHitbox.getCenterX(), mHitbox.getCenterY()); } mRefreshLayers = true; } private void onMeteorCrashed(boolean intoCity) { int loss = (int) -mMeteorPointLossInterpolator.evaluate(mDifficulty); if (mColorType == COLOR_TYPE_BONUS && mProtected) { loss -= DIFFICULTY_POINTS_LOSS_ON_BONUS_METEOR_CRASHED_IF_PROTECTED; // bonus meteors require more energy to protect from, as they are much worse if not in protected state } mConfig.mAchievementGameData.increment(intoCity ? AchievementLazor.KEY_GAME_METEOR_CRASHED_IN_CITY_COUNT : AchievementLazor.KEY_GAME_METEOR_CRASHED_NOT_CITY_COUNT , 1L, 0L); updateDifficulty(loss); } } private void updateDifficulty(int delta) { mDifficulty += delta; if (mDifficulty < 0) { mDifficulty = 0; } else if (mDifficulty > DIFFICULTY_ULTRA_AT) { mDifficulty = (int) DIFFICULTY_ULTRA_AT; } onDifficultyUpdated(); } private void onDifficultyUpdated() { mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_DIFFICULTY, (long) mDifficulty, AchievementProperties.UPDATE_POLICY_ALWAYS); mDifficultyText = String.valueOf(mDifficulty) + '%'; if (mDifficulty >= mDifficultyForProtection && !mProtected) { setCityProtected(true); } else if (mDifficulty < mDifficultyForProtection && mProtected) { setCityProtected(false); } } private void setCityProtected(boolean protect) { if (protect != mProtected) { mProtected = protect; mConfig.mAchievementGameData.putValue(AchievementLazor.KEY_GAME_IS_PROTECTED, mProtected ? 1L : 0L, AchievementProperties.UPDATE_POLICY_ALWAYS); } } @Override public Bitmap makeSnapshot() { int width = SNAPSHOT_DIMENSION.getWidthForDensity(mConfig.mScreenDensity); int height = SNAPSHOT_DIMENSION.getHeightForDensity(mConfig.mScreenDensity); Matrix matrix = new Matrix(); float yScale = height/ (float) mConfig.mHeight; matrix.preScale(width / (float) mVisibleLayer.getWidth(), yScale); Bitmap snapshot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(snapshot); canvas.drawBitmap(mVisibleLayer, matrix, null); matrix.preTranslate(0, mCityOffsetY); canvas.drawBitmap(mCityLayer, matrix, null); canvas.drawBitmap(mCityDestructionMask, matrix, mCityDestructionOverlayPaint); if (mProtected) { canvas.drawBitmap(mProtectedCity, matrix, null); } return snapshot; } }