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