1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.egg;
18
19import android.animation.TimeAnimator;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.Matrix;
25import android.graphics.Outline;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.PorterDuff;
29import android.graphics.Rect;
30import android.graphics.drawable.Drawable;
31import android.graphics.drawable.GradientDrawable;
32import android.media.AudioAttributes;
33import android.media.AudioManager;
34import android.os.Vibrator;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.Gravity;
38import android.view.KeyEvent;
39import android.view.MotionEvent;
40import android.util.Slog;
41import android.view.View;
42import android.view.ViewOutlineProvider;
43import android.view.animation.DecelerateInterpolator;
44import android.widget.FrameLayout;
45import android.widget.ImageView;
46import android.widget.TextView;
47
48import com.android.systemui.R;
49
50import java.util.ArrayList;
51
52public class LLand extends FrameLayout {
53    public static final String TAG = "LLand";
54
55    public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
56    public static final boolean DEBUG_DRAW = false; // DEBUG
57
58    public static void L(String s, Object ... objects) {
59        if (DEBUG) {
60            Slog.d(TAG, objects.length == 0 ? s : String.format(s, objects));
61        }
62    }
63
64    public static final boolean AUTOSTART = true;
65    public static final boolean HAVE_STARS = true;
66
67    public static final float DEBUG_SPEED_MULTIPLIER = 1f; // 0.1f;
68    public static final boolean DEBUG_IDDQD = Log.isLoggable(TAG + ".iddqd", Log.DEBUG);
69
70    final static int[] POPS = {
71            // resid                // spinny!  // alpha
72            R.drawable.pop_belt,    0,          255,
73            R.drawable.pop_droid,   0,          255,
74            R.drawable.pop_pizza,   1,          255,
75            R.drawable.pop_stripes, 0,          255,
76            R.drawable.pop_swirl,   1,          255,
77            R.drawable.pop_vortex,  1,          255,
78            R.drawable.pop_vortex2, 1,          255,
79            R.drawable.pop_ball,    0,          190,
80    };
81
82    private static class Params {
83        public float TRANSLATION_PER_SEC;
84        public int OBSTACLE_SPACING, OBSTACLE_PERIOD;
85        public int BOOST_DV;
86        public int PLAYER_HIT_SIZE;
87        public int PLAYER_SIZE;
88        public int OBSTACLE_WIDTH, OBSTACLE_STEM_WIDTH;
89        public int OBSTACLE_GAP;
90        public int OBSTACLE_MIN;
91        public int BUILDING_WIDTH_MIN, BUILDING_WIDTH_MAX;
92        public int BUILDING_HEIGHT_MIN;
93        public int CLOUD_SIZE_MIN, CLOUD_SIZE_MAX;
94        public int STAR_SIZE_MIN, STAR_SIZE_MAX;
95        public int G;
96        public int MAX_V;
97            public float SCENERY_Z, OBSTACLE_Z, PLAYER_Z, PLAYER_Z_BOOST, HUD_Z;
98        public Params(Resources res) {
99            TRANSLATION_PER_SEC = res.getDimension(R.dimen.translation_per_sec);
100            OBSTACLE_SPACING = res.getDimensionPixelSize(R.dimen.obstacle_spacing);
101            OBSTACLE_PERIOD = (int) (OBSTACLE_SPACING / TRANSLATION_PER_SEC);
102            BOOST_DV = res.getDimensionPixelSize(R.dimen.boost_dv);
103            PLAYER_HIT_SIZE = res.getDimensionPixelSize(R.dimen.player_hit_size);
104            PLAYER_SIZE = res.getDimensionPixelSize(R.dimen.player_size);
105            OBSTACLE_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_width);
106            OBSTACLE_STEM_WIDTH = res.getDimensionPixelSize(R.dimen.obstacle_stem_width);
107            OBSTACLE_GAP = res.getDimensionPixelSize(R.dimen.obstacle_gap);
108            OBSTACLE_MIN = res.getDimensionPixelSize(R.dimen.obstacle_height_min);
109            BUILDING_HEIGHT_MIN = res.getDimensionPixelSize(R.dimen.building_height_min);
110            BUILDING_WIDTH_MIN = res.getDimensionPixelSize(R.dimen.building_width_min);
111            BUILDING_WIDTH_MAX = res.getDimensionPixelSize(R.dimen.building_width_max);
112            CLOUD_SIZE_MIN = res.getDimensionPixelSize(R.dimen.cloud_size_min);
113            CLOUD_SIZE_MAX = res.getDimensionPixelSize(R.dimen.cloud_size_max);
114            STAR_SIZE_MIN = res.getDimensionPixelSize(R.dimen.star_size_min);
115            STAR_SIZE_MAX = res.getDimensionPixelSize(R.dimen.star_size_max);
116
117            G = res.getDimensionPixelSize(R.dimen.G);
118            MAX_V = res.getDimensionPixelSize(R.dimen.max_v);
119
120            SCENERY_Z = res.getDimensionPixelSize(R.dimen.scenery_z);
121            OBSTACLE_Z = res.getDimensionPixelSize(R.dimen.obstacle_z);
122            PLAYER_Z = res.getDimensionPixelSize(R.dimen.player_z);
123            PLAYER_Z_BOOST = res.getDimensionPixelSize(R.dimen.player_z_boost);
124            HUD_Z = res.getDimensionPixelSize(R.dimen.hud_z);
125
126            // Sanity checking
127            if (OBSTACLE_MIN <= OBSTACLE_WIDTH / 2) {
128                Slog.e(TAG, "error: obstacles might be too short, adjusting");
129                OBSTACLE_MIN = OBSTACLE_WIDTH / 2 + 1;
130            }
131        }
132    }
133
134    private TimeAnimator mAnim;
135    private Vibrator mVibrator;
136    private AudioManager mAudioManager;
137    private final AudioAttributes mAudioAttrs = new AudioAttributes.Builder()
138            .setUsage(AudioAttributes.USAGE_GAME).build();
139
140    private TextView mScoreField;
141    private View mSplash;
142
143    private Player mDroid;
144    private ArrayList<Obstacle> mObstaclesInPlay = new ArrayList<Obstacle>();
145
146    private float t, dt;
147
148    private int mScore;
149    private float mLastPipeTime; // in sec
150    private int mWidth, mHeight;
151    private boolean mAnimating, mPlaying;
152    private boolean mFrozen; // after death, a short backoff
153    private boolean mFlipped;
154
155    private int mTimeOfDay;
156    private static final int DAY = 0, NIGHT = 1, TWILIGHT = 2, SUNSET = 3;
157    private static final int[][] SKIES = {
158            { 0xFFc0c0FF, 0xFFa0a0FF }, // DAY
159            { 0xFF000010, 0xFF000000 }, // NIGHT
160            { 0xFF000040, 0xFF000010 }, // TWILIGHT
161            { 0xFFa08020, 0xFF204080 }, // SUNSET
162    };
163
164    private static Params PARAMS;
165
166    public LLand(Context context) {
167        this(context, null);
168    }
169
170    public LLand(Context context, AttributeSet attrs) {
171        this(context, attrs, 0);
172    }
173
174    public LLand(Context context, AttributeSet attrs, int defStyle) {
175        super(context, attrs, defStyle);
176        mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
177        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
178        setFocusable(true);
179        PARAMS = new Params(getResources());
180        mTimeOfDay = irand(0, SKIES.length);
181
182        // we assume everything will be laid out left|top
183        setLayoutDirection(LAYOUT_DIRECTION_LTR);
184    }
185
186    @Override
187    public boolean willNotDraw() {
188        return !DEBUG;
189    }
190
191    public int getGameWidth() { return mWidth; }
192    public int getGameHeight() { return mHeight; }
193    public float getGameTime() { return t; }
194    public float getLastTimeStep() { return dt; }
195
196    public void setScoreField(TextView tv) {
197        mScoreField = tv;
198        if (tv != null) {
199            tv.setTranslationZ(PARAMS.HUD_Z);
200            if (!(mAnimating && mPlaying)) {
201                tv.setTranslationY(-500);
202            }
203        }
204    }
205
206    public void setSplash(View v) {
207        mSplash = v;
208    }
209
210    @Override
211    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
212        stop();
213        reset();
214        if (AUTOSTART) {
215            start(false);
216        }
217    }
218
219    final float hsv[] = {0, 0, 0};
220
221    private void thump() {
222        if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) {
223            // No interruptions. Not even game haptics.
224            return;
225        }
226        mVibrator.vibrate(80, mAudioAttrs);
227    }
228
229    public void reset() {
230        L("reset");
231        final Drawable sky = new GradientDrawable(
232                GradientDrawable.Orientation.BOTTOM_TOP,
233                SKIES[mTimeOfDay]
234        );
235        sky.setDither(true);
236        setBackground(sky);
237
238        mFlipped = frand() > 0.5f;
239        setScaleX(mFlipped ? -1 : 1);
240
241        setScore(0);
242
243        int i = getChildCount();
244        while (i-->0) {
245            final View v = getChildAt(i);
246            if (v instanceof GameView) {
247                removeViewAt(i);
248            }
249        }
250
251        mObstaclesInPlay.clear();
252
253        mWidth = getWidth();
254        mHeight = getHeight();
255
256        boolean showingSun = (mTimeOfDay == DAY || mTimeOfDay == SUNSET) && frand() > 0.25;
257        if (showingSun) {
258            final Star sun = new Star(getContext());
259            sun.setBackgroundResource(R.drawable.sun);
260            final int w = getResources().getDimensionPixelSize(R.dimen.sun_size);
261            sun.setTranslationX(frand(w, mWidth-w));
262            if (mTimeOfDay == DAY) {
263                sun.setTranslationY(frand(w, (mHeight * 0.66f)));
264                sun.getBackground().setTint(0);
265            } else {
266                sun.setTranslationY(frand(mHeight * 0.66f, mHeight - w));
267                sun.getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP);
268                sun.getBackground().setTint(0xC0FF8000);
269
270            }
271            addView(sun, new LayoutParams(w, w));
272        }
273        if (!showingSun) {
274            final boolean dark = mTimeOfDay == NIGHT || mTimeOfDay == TWILIGHT;
275            final float ff = frand();
276            if ((dark && ff < 0.75f) || ff < 0.5f) {
277                final Star moon = new Star(getContext());
278                moon.setBackgroundResource(R.drawable.moon);
279                moon.getBackground().setAlpha(dark ? 255 : 128);
280                moon.setScaleX(frand() > 0.5 ? -1 : 1);
281                moon.setRotation(moon.getScaleX() * frand(5, 30));
282                final int w = getResources().getDimensionPixelSize(R.dimen.sun_size);
283                moon.setTranslationX(frand(w, mWidth - w));
284                moon.setTranslationY(frand(w, mHeight - w));
285                addView(moon, new LayoutParams(w, w));
286            }
287        }
288
289        final int mh = mHeight / 6;
290        final boolean cloudless = frand() < 0.25;
291        final int N = 20;
292        for (i=0; i<N; i++) {
293            final float r1 = frand();
294            final Scenery s;
295            if (HAVE_STARS && r1 < 0.3 && mTimeOfDay != DAY) {
296                s = new Star(getContext());
297            } else if (r1 < 0.6 && !cloudless) {
298                s = new Cloud(getContext());
299            } else {
300                s = new Building(getContext());
301
302                s.z = (float)i/N;
303                s.setTranslationZ(PARAMS.SCENERY_Z * (1+s.z));
304                s.v = 0.85f * s.z; // buildings move proportional to their distance
305                hsv[0] = 175;
306                hsv[1] = 0.25f;
307                hsv[2] = 1 * s.z;
308                s.setBackgroundColor(Color.HSVToColor(hsv));
309                s.h = irand(PARAMS.BUILDING_HEIGHT_MIN, mh);
310            }
311            final LayoutParams lp = new LayoutParams(s.w, s.h);
312            if (s instanceof Building) {
313                lp.gravity = Gravity.BOTTOM;
314            } else {
315                lp.gravity = Gravity.TOP;
316                final float r = frand();
317                if (s instanceof Star) {
318                    lp.topMargin = (int) (r * r * mHeight);
319                } else {
320                    lp.topMargin = (int) (1 - r*r * mHeight/2) + mHeight/2;
321                }
322            }
323
324            addView(s, lp);
325            s.setTranslationX(frand(-lp.width, mWidth + lp.width));
326        }
327
328        mDroid = new Player(getContext());
329        mDroid.setX(mWidth / 2);
330        mDroid.setY(mHeight / 2);
331        addView(mDroid, new LayoutParams(PARAMS.PLAYER_SIZE, PARAMS.PLAYER_SIZE));
332
333        mAnim = new TimeAnimator();
334        mAnim.setTimeListener(new TimeAnimator.TimeListener() {
335            @Override
336            public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) {
337                step(t, dt);
338            }
339        });
340    }
341
342    private void setScore(int score) {
343        mScore = score;
344        if (mScoreField != null) {
345            mScoreField.setText(DEBUG_IDDQD ? "??" : String.valueOf(score));
346        }
347    }
348
349    private void addScore(int incr) {
350        setScore(mScore + incr);
351    }
352
353    public void start(boolean startPlaying) {
354        L("start(startPlaying=%s)", startPlaying?"true":"false");
355        if (startPlaying) {
356            mPlaying = true;
357
358            t = 0;
359            // there's a sucker born every OBSTACLE_PERIOD
360            mLastPipeTime = getGameTime() - PARAMS.OBSTACLE_PERIOD;
361
362            if (mSplash != null && mSplash.getAlpha() > 0f) {
363                mSplash.setTranslationZ(PARAMS.HUD_Z);
364                mSplash.animate().alpha(0).translationZ(0).setDuration(400);
365
366                mScoreField.animate().translationY(0)
367                        .setInterpolator(new DecelerateInterpolator())
368                        .setDuration(1500);
369            }
370
371            mScoreField.setTextColor(0xFFAAAAAA);
372            mScoreField.setBackgroundResource(R.drawable.scorecard);
373            mDroid.setVisibility(View.VISIBLE);
374            mDroid.setX(mWidth / 2);
375            mDroid.setY(mHeight / 2);
376        } else {
377            mDroid.setVisibility(View.GONE);
378        }
379        if (!mAnimating) {
380            mAnim.start();
381            mAnimating = true;
382        }
383    }
384
385    public void stop() {
386        if (mAnimating) {
387            mAnim.cancel();
388            mAnim = null;
389            mAnimating = false;
390            mScoreField.setTextColor(0xFFFFFFFF);
391            mScoreField.setBackgroundResource(R.drawable.scorecard_gameover);
392            mTimeOfDay = irand(0, SKIES.length); // for next reset
393            mFrozen = true;
394            postDelayed(new Runnable() {
395                    @Override
396                    public void run() {
397                        mFrozen = false;
398                    }
399                }, 250);
400        }
401    }
402
403    public static final float lerp(float x, float a, float b) {
404        return (b - a) * x + a;
405    }
406
407    public static final float rlerp(float v, float a, float b) {
408        return (v - a) / (b - a);
409    }
410
411    public static final float clamp(float f) {
412        return f < 0f ? 0f : f > 1f ? 1f : f;
413    }
414
415    public static final float frand() {
416        return (float) Math.random();
417    }
418
419    public static final float frand(float a, float b) {
420        return lerp(frand(), a, b);
421    }
422
423    public static final int irand(int a, int b) {
424        return (int) lerp(frand(), (float) a, (float) b);
425    }
426
427    private void step(long t_ms, long dt_ms) {
428        t = t_ms / 1000f; // seconds
429        dt = dt_ms / 1000f;
430
431        if (DEBUG) {
432            t *= DEBUG_SPEED_MULTIPLIER;
433            dt *= DEBUG_SPEED_MULTIPLIER;
434        }
435
436        // 1. Move all objects and update bounds
437        final int N = getChildCount();
438        int i = 0;
439        for (; i<N; i++) {
440            final View v = getChildAt(i);
441            if (v instanceof GameView) {
442                ((GameView) v).step(t_ms, dt_ms, t, dt);
443            }
444        }
445
446        // 2. Check for altitude
447        if (mPlaying && mDroid.below(mHeight)) {
448            if (DEBUG_IDDQD) {
449                poke();
450                unpoke();
451            } else {
452                L("player hit the floor");
453                thump();
454                stop();
455            }
456        }
457
458        // 3. Check for obstacles
459        boolean passedBarrier = false;
460        for (int j = mObstaclesInPlay.size(); j-->0;) {
461            final Obstacle ob = mObstaclesInPlay.get(j);
462            if (mPlaying && ob.intersects(mDroid) && !DEBUG_IDDQD) {
463                L("player hit an obstacle");
464                thump();
465                stop();
466            } else if (ob.cleared(mDroid)) {
467                if (ob instanceof Stem) passedBarrier = true;
468                mObstaclesInPlay.remove(j);
469            }
470        }
471
472        if (mPlaying && passedBarrier) {
473            addScore(1);
474        }
475
476        // 4. Handle edge of screen
477        // Walk backwards to make sure removal is safe
478        while (i-->0) {
479            final View v = getChildAt(i);
480            if (v instanceof Obstacle) {
481                if (v.getTranslationX() + v.getWidth() < 0) {
482                    removeViewAt(i);
483                }
484            } else if (v instanceof Scenery) {
485                final Scenery s = (Scenery) v;
486                if (v.getTranslationX() + s.w < 0) {
487                    v.setTranslationX(getWidth());
488                }
489            }
490        }
491
492        // 3. Time for more obstacles!
493        if (mPlaying && (t - mLastPipeTime) > PARAMS.OBSTACLE_PERIOD) {
494            mLastPipeTime = t;
495            final int obstacley =
496                    (int)(frand() * (mHeight - 2*PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) +
497                    PARAMS.OBSTACLE_MIN;
498
499            final int inset = (PARAMS.OBSTACLE_WIDTH - PARAMS.OBSTACLE_STEM_WIDTH) / 2;
500            final int yinset = PARAMS.OBSTACLE_WIDTH/2;
501
502            final int d1 = irand(0,250);
503            final Obstacle s1 = new Stem(getContext(), obstacley - yinset, false);
504            addView(s1, new LayoutParams(
505                    PARAMS.OBSTACLE_STEM_WIDTH,
506                    (int) s1.h,
507                    Gravity.TOP|Gravity.LEFT));
508            s1.setTranslationX(mWidth+inset);
509            s1.setTranslationY(-s1.h-yinset);
510            s1.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f);
511            s1.animate()
512                    .translationY(0)
513                    .setStartDelay(d1)
514                    .setDuration(250);
515            mObstaclesInPlay.add(s1);
516
517            final Obstacle p1 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH);
518            addView(p1, new LayoutParams(
519                    PARAMS.OBSTACLE_WIDTH,
520                    PARAMS.OBSTACLE_WIDTH,
521                    Gravity.TOP|Gravity.LEFT));
522            p1.setTranslationX(mWidth);
523            p1.setTranslationY(-PARAMS.OBSTACLE_WIDTH);
524            p1.setTranslationZ(PARAMS.OBSTACLE_Z);
525            p1.setScaleX(0.25f);
526            p1.setScaleY(0.25f);
527            p1.animate()
528                    .translationY(s1.h-inset)
529                    .scaleX(1f)
530                    .scaleY(1f)
531                    .setStartDelay(d1)
532                    .setDuration(250);
533            mObstaclesInPlay.add(p1);
534
535            final int d2 = irand(0,250);
536            final Obstacle s2 = new Stem(getContext(),
537                    mHeight - obstacley - PARAMS.OBSTACLE_GAP - yinset,
538                    true);
539            addView(s2, new LayoutParams(
540                    PARAMS.OBSTACLE_STEM_WIDTH,
541                    (int) s2.h,
542                    Gravity.TOP|Gravity.LEFT));
543            s2.setTranslationX(mWidth+inset);
544            s2.setTranslationY(mHeight+yinset);
545            s2.setTranslationZ(PARAMS.OBSTACLE_Z*0.75f);
546            s2.animate()
547                    .translationY(mHeight-s2.h)
548                    .setStartDelay(d2)
549                    .setDuration(400);
550            mObstaclesInPlay.add(s2);
551
552            final Obstacle p2 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH);
553            addView(p2, new LayoutParams(
554                    PARAMS.OBSTACLE_WIDTH,
555                    PARAMS.OBSTACLE_WIDTH,
556                    Gravity.TOP|Gravity.LEFT));
557            p2.setTranslationX(mWidth);
558            p2.setTranslationY(mHeight);
559            p2.setTranslationZ(PARAMS.OBSTACLE_Z);
560            p2.setScaleX(0.25f);
561            p2.setScaleY(0.25f);
562            p2.animate()
563                    .translationY(mHeight-s2.h-yinset)
564                    .scaleX(1f)
565                    .scaleY(1f)
566                    .setStartDelay(d2)
567                    .setDuration(400);
568            mObstaclesInPlay.add(p2);
569        }
570
571        if (DEBUG_DRAW) invalidate();
572    }
573
574    @Override
575    public boolean onTouchEvent(MotionEvent ev) {
576        L("touch: %s", ev);
577        switch (ev.getAction()) {
578            case MotionEvent.ACTION_DOWN:
579                poke();
580                return true;
581            case MotionEvent.ACTION_UP:
582                unpoke();
583                return true;
584        }
585        return false;
586    }
587
588    @Override
589    public boolean onTrackballEvent(MotionEvent ev) {
590        L("trackball: %s", ev);
591        switch (ev.getAction()) {
592            case MotionEvent.ACTION_DOWN:
593                poke();
594                return true;
595            case MotionEvent.ACTION_UP:
596                unpoke();
597                return true;
598        }
599        return false;
600    }
601
602    @Override
603    public boolean onKeyDown(int keyCode, KeyEvent ev) {
604        L("keyDown: %d", keyCode);
605        switch (keyCode) {
606            case KeyEvent.KEYCODE_DPAD_CENTER:
607            case KeyEvent.KEYCODE_DPAD_UP:
608            case KeyEvent.KEYCODE_SPACE:
609            case KeyEvent.KEYCODE_ENTER:
610            case KeyEvent.KEYCODE_BUTTON_A:
611                poke();
612                return true;
613        }
614        return false;
615    }
616
617    @Override
618    public boolean onKeyUp(int keyCode, KeyEvent ev) {
619        L("keyDown: %d", keyCode);
620        switch (keyCode) {
621            case KeyEvent.KEYCODE_DPAD_CENTER:
622            case KeyEvent.KEYCODE_DPAD_UP:
623            case KeyEvent.KEYCODE_SPACE:
624            case KeyEvent.KEYCODE_ENTER:
625            case KeyEvent.KEYCODE_BUTTON_A:
626                unpoke();
627                return true;
628        }
629        return false;
630    }
631
632    @Override
633    public boolean onGenericMotionEvent (MotionEvent ev) {
634        L("generic: %s", ev);
635        return false;
636    }
637
638    private void poke() {
639        L("poke");
640        if (mFrozen) return;
641        if (!mAnimating) {
642            reset();
643            start(true);
644        } else if (!mPlaying) {
645            start(true);
646        }
647        mDroid.boost();
648        if (DEBUG) {
649            mDroid.dv *= DEBUG_SPEED_MULTIPLIER;
650            mDroid.animate().setDuration((long) (200/DEBUG_SPEED_MULTIPLIER));
651        }
652    }
653
654    private void unpoke() {
655        L("unboost");
656        if (mFrozen) return;
657        if (!mAnimating) return;
658        mDroid.unboost();
659    }
660
661    @Override
662    public void onDraw(Canvas c) {
663        super.onDraw(c);
664
665        if (!DEBUG_DRAW) return;
666
667        final Paint pt = new Paint();
668        pt.setColor(0xFFFFFFFF);
669        final int L = mDroid.corners.length;
670        final int N = L/2;
671        for (int i=0; i<N; i++) {
672            final int x = (int) mDroid.corners[i*2];
673            final int y = (int) mDroid.corners[i*2+1];
674            c.drawCircle(x, y, 4, pt);
675            c.drawLine(x, y,
676                    mDroid.corners[(i*2+2)%L],
677                    mDroid.corners[(i*2+3)%L],
678                    pt);
679        }
680
681        pt.setStyle(Paint.Style.STROKE);
682        pt.setStrokeWidth(getResources().getDisplayMetrics().density);
683
684        final int M = getChildCount();
685        pt.setColor(0x8000FF00);
686        for (int i=0; i<M; i++) {
687            final View v = getChildAt(i);
688            if (v == mDroid) continue;
689            if (!(v instanceof GameView)) continue;
690            if (v instanceof Pop) {
691                final Pop p = (Pop) v;
692                c.drawCircle(p.cx, p.cy, p.r, pt);
693            } else {
694                final Rect r = new Rect();
695                v.getHitRect(r);
696                c.drawRect(r, pt);
697            }
698        }
699
700        pt.setColor(Color.BLACK);
701        final StringBuilder sb = new StringBuilder("obstacles: ");
702        for (Obstacle ob : mObstaclesInPlay) {
703            sb.append(ob.hitRect.toShortString());
704            sb.append(" ");
705        }
706        pt.setTextSize(20f);
707        c.drawText(sb.toString(), 20, 100, pt);
708    }
709
710    static final Rect sTmpRect = new Rect();
711
712    private interface GameView {
713        public void step(long t_ms, long dt_ms, float t, float dt);
714    }
715
716    private class Player extends ImageView implements GameView {
717        public float dv;
718
719        private boolean mBoosting;
720
721        private final int[] sColors = new int[] {
722                0xFF78C557,
723        };
724
725        private final float[] sHull = new float[] {
726                0.3f,  0f,    // left antenna
727                0.7f,  0f,    // right antenna
728                0.92f, 0.33f, // off the right shoulder of Orion
729                0.92f, 0.75f, // right hand (our right, not his right)
730                0.6f,  1f,    // right foot
731                0.4f,  1f,    // left foot BLUE!
732                0.08f, 0.75f, // sinistram
733                0.08f, 0.33f, // cold shoulder
734        };
735        public final float[] corners = new float[sHull.length];
736
737        public Player(Context context) {
738            super(context);
739
740            setBackgroundResource(R.drawable.android);
741            getBackground().setTintMode(PorterDuff.Mode.SRC_ATOP);
742            getBackground().setTint(sColors[0]);
743            setOutlineProvider(new ViewOutlineProvider() {
744                @Override
745                public void getOutline(View view, Outline outline) {
746                    final int w = view.getWidth();
747                    final int h = view.getHeight();
748                    final int ix = (int) (w * 0.3f);
749                    final int iy = (int) (h * 0.2f);
750                    outline.setRect(ix, iy, w - ix, h - iy);
751                }
752            });
753        }
754
755        public void prepareCheckIntersections() {
756            final int inset = (PARAMS.PLAYER_SIZE - PARAMS.PLAYER_HIT_SIZE)/2;
757            final int scale = PARAMS.PLAYER_HIT_SIZE;
758            final int N = sHull.length/2;
759            for (int i=0; i<N; i++) {
760                corners[i*2]   = scale * sHull[i*2]   + inset;
761                corners[i*2+1] = scale * sHull[i*2+1] + inset;
762            }
763            final Matrix m = getMatrix();
764            m.mapPoints(corners);
765        }
766
767        public boolean below(int h) {
768            final int N = corners.length/2;
769            for (int i=0; i<N; i++) {
770                final int y = (int) corners[i*2+1];
771                if (y >= h) return true;
772            }
773            return false;
774        }
775
776        public void step(long t_ms, long dt_ms, float t, float dt) {
777            if (getVisibility() != View.VISIBLE) return; // not playing yet
778
779            if (mBoosting) {
780                dv = -PARAMS.BOOST_DV;
781            } else {
782                dv += PARAMS.G;
783            }
784            if (dv < -PARAMS.MAX_V) dv = -PARAMS.MAX_V;
785            else if (dv > PARAMS.MAX_V) dv = PARAMS.MAX_V;
786
787            final float y = getTranslationY() + dv * dt;
788            setTranslationY(y < 0 ? 0 : y);
789            setRotation(
790                    90 + lerp(clamp(rlerp(dv, PARAMS.MAX_V, -1 * PARAMS.MAX_V)), 90, -90));
791
792            prepareCheckIntersections();
793        }
794
795        public void boost() {
796            mBoosting = true;
797            dv = -PARAMS.BOOST_DV;
798
799            animate().cancel();
800            animate()
801                    .scaleX(1.25f)
802                    .scaleY(1.25f)
803                    .translationZ(PARAMS.PLAYER_Z_BOOST)
804                    .setDuration(100);
805            setScaleX(1.25f);
806            setScaleY(1.25f);
807        }
808
809        public void unboost() {
810            mBoosting = false;
811
812            animate().cancel();
813            animate()
814                    .scaleX(1f)
815                    .scaleY(1f)
816                    .translationZ(PARAMS.PLAYER_Z)
817                    .setDuration(200);
818        }
819    }
820
821    private class Obstacle extends View implements GameView {
822        public float h;
823
824        public final Rect hitRect = new Rect();
825
826        public Obstacle(Context context, float h) {
827            super(context);
828            setBackgroundColor(0xFFFF0000);
829            this.h = h;
830        }
831
832        public boolean intersects(Player p) {
833            final int N = p.corners.length/2;
834            for (int i=0; i<N; i++) {
835                final int x = (int) p.corners[i*2];
836                final int y = (int) p.corners[i*2+1];
837                if (hitRect.contains(x, y)) return true;
838            }
839            return false;
840        }
841
842        public boolean cleared(Player p) {
843            final int N = p.corners.length/2;
844            for (int i=0; i<N; i++) {
845                final int x = (int) p.corners[i*2];
846                if (hitRect.right >= x) return false;
847            }
848            return true;
849        }
850
851        @Override
852        public void step(long t_ms, long dt_ms, float t, float dt) {
853            setTranslationX(getTranslationX()-PARAMS.TRANSLATION_PER_SEC*dt);
854            getHitRect(hitRect);
855        }
856    }
857
858    private class Pop extends Obstacle {
859        int mRotate;
860        int cx, cy, r;
861        public Pop(Context context, float h) {
862            super(context, h);
863            int idx = 3*irand(0, POPS.length/3);
864            setBackgroundResource(POPS[idx]);
865            setAlpha((float)(POPS[idx+2])/255);
866            setScaleX(frand() < 0.5f ? -1 : 1);
867            mRotate = POPS[idx+1] == 0 ? 0 : (frand() < 0.5f ? -1 : 1);
868            setOutlineProvider(new ViewOutlineProvider() {
869                @Override
870                public void getOutline(View view, Outline outline) {
871                    final int pad = (int) (getWidth() * 0.02f);
872                    outline.setOval(pad, pad, getWidth()-pad, getHeight()-pad);
873                }
874            });
875        }
876
877        public boolean intersects(Player p) {
878            final int N = p.corners.length/2;
879            for (int i=0; i<N; i++) {
880                final int x = (int) p.corners[i*2];
881                final int y = (int) p.corners[i*2+1];
882                if (Math.hypot(x-cx, y-cy) <= r) return true;
883            }
884            return false;
885        }
886
887        @Override
888        public void step(long t_ms, long dt_ms, float t, float dt) {
889            super.step(t_ms, dt_ms, t, dt);
890            if (mRotate != 0) {
891                setRotation(getRotation() + dt * 45 * mRotate);
892            }
893
894            cx = (hitRect.left + hitRect.right)/2;
895            cy = (hitRect.top + hitRect.bottom)/2;
896            r = getWidth()/2;
897        }
898    }
899
900    private class Stem extends Obstacle {
901        Paint mPaint = new Paint();
902        Path mShadow = new Path();
903        boolean mDrawShadow;
904
905        public Stem(Context context, float h, boolean drawShadow) {
906            super(context, h);
907            mDrawShadow = drawShadow;
908            mPaint.setColor(0xFFAAAAAA);
909            setBackground(null);
910        }
911
912        @Override
913        public void onAttachedToWindow() {
914            super.onAttachedToWindow();
915            setWillNotDraw(false);
916            setOutlineProvider(new ViewOutlineProvider() {
917                @Override
918                public void getOutline(View view, Outline outline) {
919                    outline.setRect(0, 0, getWidth(), getHeight());
920                }
921            });
922        }
923        @Override
924        public void onDraw(Canvas c) {
925            final int w = c.getWidth();
926            final int h = c.getHeight();
927            final GradientDrawable g = new GradientDrawable();
928            g.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
929            g.setGradientCenter(w * 0.75f, 0);
930            g.setColors(new int[] { 0xFFFFFFFF, 0xFFAAAAAA });
931            g.setBounds(0, 0, w, h);
932            g.draw(c);
933            if (!mDrawShadow) return;
934            mShadow.reset();
935            mShadow.moveTo(0,0);
936            mShadow.lineTo(w, 0);
937            mShadow.lineTo(w, PARAMS.OBSTACLE_WIDTH/2+w*1.5f);
938            mShadow.lineTo(0, PARAMS.OBSTACLE_WIDTH/2);
939            mShadow.close();
940            c.drawPath(mShadow, mPaint);
941        }
942    }
943
944    private class Scenery extends FrameLayout implements GameView {
945        public float z;
946        public float v;
947        public int h, w;
948        public Scenery(Context context) {
949            super(context);
950        }
951
952        @Override
953        public void step(long t_ms, long dt_ms, float t, float dt) {
954            setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt * v);
955        }
956    }
957
958    private class Building extends Scenery {
959        public Building(Context context) {
960            super(context);
961
962            w = irand(PARAMS.BUILDING_WIDTH_MIN, PARAMS.BUILDING_WIDTH_MAX);
963            h = 0; // will be setup later, along with z
964
965            setTranslationZ(PARAMS.SCENERY_Z);
966        }
967    }
968
969    private class Cloud extends Scenery {
970        public Cloud(Context context) {
971            super(context);
972            setBackgroundResource(frand() < 0.01f ? R.drawable.cloud_off : R.drawable.cloud);
973            getBackground().setAlpha(0x40);
974            w = h = irand(PARAMS.CLOUD_SIZE_MIN, PARAMS.CLOUD_SIZE_MAX);
975            z = 0;
976            v = frand(0.15f,0.5f);
977        }
978    }
979
980    private class Star extends Scenery {
981        public Star(Context context) {
982            super(context);
983            setBackgroundResource(R.drawable.star);
984            w = h = irand(PARAMS.STAR_SIZE_MIN, PARAMS.STAR_SIZE_MAX);
985            v = z = 0;
986        }
987    }
988}
989