License text

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER 
 * Copyright  2008, 2010 Oracle and/or its affiliates.  All rights reserved. 
 * Use is subject to license terms.
 * 
 * This file is available and licensed under the following license:
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met: 
 * 
 *   * Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer. 
 *
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 *   * Neither the name of Oracle Corporation nor the names of its contributors 
 *     may be used to endorse or promote products derived from this software 
 *     without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

    

package brickbreaker;

import javafx.animation.*;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.text.*;
import java.util.Vector;
import brickbreaker.Config;

/**
 * @author Pavel Porvatov
 */

public class Level extends CustomNode {
    var bricks = new Vector();
    var brickCount: Integer;
    var fadeBricks: Brick[];
    var bonuses: Bonus[];
    var group: Group;
    var lifes: Bonus[];

    var catchedBonus = 0;

    // States
    // 0 - starting level
    // 1 - ball is catched
    // 2 - playing
    // 3 - game over
    var state = 0;

    var batDirection = 0;

    var ballDirX: Number;
    var ballDirY: Number;

    public function start() {
        startingTimeline.play();
        timeline.play();
        group.content[0].requestFocus();

        updateScore(0);
        updateLifes();
    }

    public function stop() {
        startingTimeline.stop();
        timeline.stop();
    }

    public var levelNumber: Integer on replace {
        var level = LevelData.getLevelData(levelNumber);

        for (row in [0..<sizeof level], col in [0..<Config.FIELD_BRICK_IN_ROW]) {
            var rowString = level[row];

            var brick: Brick;

            if (rowString != null and col < rowString.length()) {
                var type = rowString.substring(col, col + 1);

                if (type != " ") {
                    brick = Brick {
                        type: Brick.getBrickType(type)
                        translateX: col * Config.brickWidth
                        translateY: Config.fieldY + row * Config.brickHeight
                    }

                    if (brick.type != Brick.TYPE_GREY) {
                        brickCount++
                    }
                }
            }

            bricks.addElement(brick)
        }
    }

    function getBrick(row: Integer, col: Integer): Brick {
        var i = row * Config.FIELD_BRICK_IN_ROW + col;

        if (col < 0 or col >= Config.FIELD_BRICK_IN_ROW or row < 0 or
                i >= bricks.size()) {
            null
        } else {
            bricks.elementAt(i) as Brick
        }
    }

    function updateScore(inc: Integer) {
        Main.mainFrame.score += inc;
        score.content = "{Main.mainFrame.score}";
    }

    function moveBat(newX: Integer) {
        var x = newX;

        if (x < 0) {
            x = 0
        }

        if (x + bat.width > Config.fieldWidth) {
            x = Config.fieldWidth - bat.width
        }

        if (state == 1) {
            var ballX = ball.translateX + x - bat.translateX;

            if (ballX < 0) {
                ballX = 0
            }

            def BALL_MAX_X = Config.fieldWidth - ball.diameter;

            if (ballX > BALL_MAX_X) {
                ballX = BALL_MAX_X
            }

            ball.translateX = ballX;
        }

        bat.translateX = x;
    }

    function kickBrick(row: Integer, col: Integer) {
        var brick = getBrick(row, col);

        if (brick == null or
                (catchedBonus != Bonus.TYPE_STRIKE and not brick.kick())) {
            return;
        }

        updateScore(10);

        if (brick.type != Brick.TYPE_GREY) {
            brickCount--;

            if (brickCount == 0) {
                Main.mainFrame.state++
            }
        }

        bricks.setElementAt(null, row * Config.FIELD_BRICK_IN_ROW + col);

        if (Config.isMobile or Config.isTV) {
            // Mobile platform may not support the opacity property
            // Therefore brick is removed immediately
            brick.visible = false;

            delete brick from fadeBricks;
        } else {
            insert brick into fadeBricks;
        }

        if (Utils.random(8) == 0 and sizeof bonuses < 5) {
            // Generate bonus
            var bonus = Bonus {
                type: Utils.random(Bonus.COUNT) + 1
                translateY: brick.translateY
                visible: true
            }

            bonus.translateX = brick.translateX + (Config.brickWidth - bonus.width) / 2;

            insert bonus into group.content;
            insert bonus into bonuses;
        }
    }

    function updateLifes() {
        // Remove bats
        while (sizeof lifes > Main.mainFrame.lifeCount) {
            var lifeBat = lifes[sizeof lifes - 1];

            delete lifeBat from lifes;
            delete lifeBat from group.content;
        }

        // Add lifes (but no more than 9 for desktop and 6 for mobile)
        def maxVisibleLifes = if (Config.isMobile) 6 else 9;
        def scale = if (Config.isMobile) 1 else if (Config.isTV) 1.2 else 0.8;

        for (life in [sizeof lifes..<java.lang.Math.min(Main.mainFrame.lifeCount,
                    maxVisibleLifes)]) {
            var lifeBonus = Bonus {
                type: Bonus.TYPE_LIFE
                scaleX: scale
                scaleY: scale
            }
            def lifeTranslate = if(Config.screenWidth > 250 and Config.isMobile) 80 else 1;
            lifeBonus.translateX = lifesCaption.translateX + lifeTranslate +
                lifesCaption.boundsInLocal.width + (life mod 3) * lifeBonus.width;
            lifeBonus.translateY = lifesCaption.translateY + lifeTranslate/4 +
                (life / 3) * lifeBonus.height*mobScaling;

            insert lifeBonus into lifes;
            insert lifeBonus into group.content;
        }
    }

    function correctBallSpeed() {
        // Correct ballDirX and ballDirY
        var speed = java.lang.Math.sqrt(ballDirX * ballDirX + ballDirY * ballDirY);

        if (speed > Config.ballMaxSpeed) {
            ballDirX *= Config.ballMaxSpeed / speed;
            ballDirY *= Config.ballMaxSpeed / speed;

            speed = Config.ballMaxSpeed;
        }

        if (speed < Config.ballMinSpeed) {
            ballDirX *= Config.ballMinSpeed / speed;
            ballDirY *= Config.ballMinSpeed / speed;

            speed = Config.ballMinSpeed;
        }

        if (java.lang.Math.abs(ballDirX) < Config.ballMinCoordSpeed) {
            ballDirX = Utils.sign(ballDirX) * Config.ballMinCoordSpeed;

            ballDirY = Utils.sign(ballDirY) *
                java.lang.Math.sqrt(speed * speed - ballDirX * ballDirX);
        } else if (java.lang.Math.abs(ballDirY) < Config.ballMinCoordSpeed) {
            ballDirY = Utils.sign(ballDirY) * Config.ballMinCoordSpeed;

            ballDirX = Utils.sign(ballDirX) *
                java.lang.Math.sqrt(speed * speed - ballDirY * ballDirY);

        }
    }

    def bat = Bat {
        translateY: Config.batY
        visible: false
    }

    def ball = Ball {
        visible: false
    }

    def roundCaption = Text {
        content: "ROUND"
        textOrigin: TextOrigin.TOP
    }

    def round = Text {
        translateX: bind roundCaption.translateX +
            roundCaption.boundsInLocal.width + Config.infoTextSpace
        translateY: bind roundCaption.translateY
        content: "{levelNumber}"
        textOrigin: TextOrigin.TOP
        font: bind roundCaption.font
    }

    def scoreCaption = Text {
        content: "SCORE"
        fill: bind roundCaption.fill
        textOrigin: TextOrigin.TOP
        font: bind roundCaption.font
    }

    def score = Text {
        translateX: bind scoreCaption.translateX +
            scoreCaption.boundsInLocal.width + Config.infoTextSpace
        translateY: bind scoreCaption.translateY
        fill: bind round.fill
        textOrigin: TextOrigin.TOP
        font: bind roundCaption.font
    }

    def lifesCaption = Text {
        content: "LIFE"
        fill: bind roundCaption.fill
        textOrigin: TextOrigin.TOP
        font: bind roundCaption.font
    }

    def mobScaling = if(Config.screenWidth > 250) 1.5 else 1;
    // Panel with game information. It looks with
    // some differences on mobile and desktop platforms
    def infoPanel = if (Config.isMobile) {
        // Mobile version of info panel
        roundCaption.fill = Color.rgb(51, 153, 51);
        roundCaption.font = Font {
            name: "Bitstream Vera Sans Bold"
            size: 10
        }

        roundCaption.translateX = 130;
        roundCaption.translateY = 15;

        round.fill = Color.rgb(0, 255, 0);

        scoreCaption.translateX = 130;
        scoreCaption.translateY = 30;

        lifesCaption.translateX = 130;
        lifesCaption.translateY = 45;

        Group {
            scaleX: mobScaling
            scaleY: mobScaling
            translateX: if(Config.screenWidth > 250) 80 else 0
            translateY: if(Config.screenWidth > 250) 25 else 0
            content: [
                // Black rectangle
                Rectangle {
                    width: Config.screenWidth/mobScaling
                    height: Config.fieldY/mobScaling
                    fill: Color.BLACK
                },

                // Logo
                ImageView {
                    var image = Config.images[Config.IMAGE_LOGO];

                    image: image
                    translateX: 10
                    translateY: 10
                },

                roundCaption,
                round,
                scoreCaption,
                score,
                lifesCaption
            ]
        }
    } else {
        // Desktop/TV version of info panel
        var TV_RELOACATE_FACTOR = 1.0;
	if(Config.isTV) TV_RELOACATE_FACTOR = 1.1;
        roundCaption.fill = Color.rgb(51, 102, 51);
        roundCaption.font = Font {
            name: "Impact"
            size: 18
        }

        roundCaption.translateX = 750 * TV_RELOACATE_FACTOR;
        roundCaption.translateY = 128;

        round.fill = Color.rgb(0, 204, 102);

        scoreCaption.translateX = 750 * TV_RELOACATE_FACTOR;
        scoreCaption.translateY = 164;

        lifesCaption.translateX = 750 * TV_RELOACATE_FACTOR;
        lifesCaption.translateY = 200;

        def INFO_LEGEND_COLOR = Color.rgb(0, 114, 188);

        Group {
            var infoWidth = Config.screenWidth - Config.fieldWidth;

            content: [
                // Black rectangle
                Rectangle {
                    translateX: Config.fieldWidth
                    width: infoWidth
                    height: Config.screenHeight
                    fill: Color.BLACK
                },

                // Vertical line
                ImageView {
                    image: Image {
                        url: "{Config.IMAGE_DIR}/vline.png"
                    }
                    translateX: Config.fieldWidth + 3
                },

                // Logo
                ImageView {
                    var image = Config.images[Config.IMAGE_LOGO];

                    image: image
                    translateX: 750 * TV_RELOACATE_FACTOR
                    translateY: 30
                },

                roundCaption,
                round,
                scoreCaption,
                score,
                lifesCaption,

                // Paint the "legend"
                Text {
                    translateX: 750 * TV_RELOACATE_FACTOR
                    translateY: 310
                    content: "LEGEND"
                    fill: INFO_LEGEND_COLOR
                    textOrigin: TextOrigin.TOP
                    font: Font {
                        name: "Impact"
                        size: 18
                    }
                },

                for (i in [0..<Bonus.COUNT]) {
                    def bonus = Bonus {
                        type: i
                    }

                    def text = Text {
                        translateX: 820 * TV_RELOACATE_FACTOR
                        translateY: 350 + i * 40
                        content: Bonus.NAMES[i]
                        fill: INFO_LEGEND_COLOR
                        textOrigin: TextOrigin.TOP
                        font: Font {
                            name: "Arial"
                            size: 12
                        }
                    }

                    bonus.translateX = (750 + (820 - 750 - bonus.width) / 2) * TV_RELOACATE_FACTOR;
                    bonus.translateY = text.translateY -
                        (bonus.height - text.boundsInLocal.height) / 2;

                    // Workaround JFXC-2379
                    [bonus as Node, text as Node]
                }
            ]
        }
    }

    def message: ImageView = ImageView {
        translateX: bind (Config.fieldWidth - message.image.width) / 2
        translateY: bind Config.fieldY +
            (Config.fieldHeight - message.image.height) / 2
        image: Config.images[Config.IMAGE_READY]
        visible: false
    }

    override public function create(): Node {
        group = Group {
            content: [
                // Background
                ImageView {
                    focusTraversable: true
                    image: Config.images[Config.IMAGE_BACKGROUND]
                    fitWidth: Config.screenWidth
                    fitHeight: Config.screenHeight
                    onMouseMoved: function( e: MouseEvent ):Void {
                        moveBat(e.x - bat.width / 2);
                    }

                    onMouseDragged: function( e: MouseEvent ):Void {
                        // Support touch-only devices like some mobile phones
                        moveBat(e.x - bat.width / 2);
                    }

                    onMousePressed: function( e: MouseEvent ):Void {
                        if (state == 2) {
                            // Support touch-only devices like some mobile phones
                            moveBat(e.x - bat.width / 2);
                        }

                        if (state == 1) {
                            state = 2;
                        }

                        if (state == 3) {
                            Main.mainFrame.state = 0;
                        }
                    }

                    onKeyPressed: function( e: KeyEvent ):Void {
                        if ((e.code == KeyCode.VK_POWER) or (e.code == KeyCode.VK_X)) {
                            FX.exit();
                        }

                        if (state == 1 and (e.code == KeyCode.VK_SPACE or
                                e.code == KeyCode.VK_ENTER or e.code == KeyCode.VK_PLAY)) {
                            state = 2;
                        }

                        if (state == 3) {
                            Main.mainFrame.state = 0;
                        }

                        if (state == 2 and e.code == KeyCode.VK_Q) {
                            // Lost live
                            lostLife();

                            return;
                        }

                        if ((e.code == KeyCode.VK_LEFT or e.code == KeyCode.VK_TRACK_PREV)) {
                            batDirection = -Config.batSpeed;
                        }

                        if ((e.code == KeyCode.VK_RIGHT or e.code == KeyCode.VK_TRACK_NEXT)) {
                            batDirection = Config.batSpeed;
                        }
                    }

                    onKeyReleased: function( e: KeyEvent ):Void {
                        if (e.code == KeyCode.VK_LEFT or e.code == KeyCode.VK_RIGHT
                            or e.code == KeyCode.VK_TRACK_PREV or e.code == KeyCode.VK_TRACK_NEXT) {
                            batDirection = 0;
                        }
                    }
                },

                // Bricks
                for (row in [0..<bricks.size() / Config.FIELD_BRICK_IN_ROW],
                        col in [0..<Config.FIELD_BRICK_IN_ROW]) {
                    getBrick(row, col);
                },

                message,

                ball,
                bat,

                infoPanel
            ]
        }
    }

    def timeline = Timeline {
        repeatCount: Timeline.INDEFINITE

        keyFrames : [
            KeyFrame {
                time: Config.ANIMATION_TIME
                action: function () {
                    // Process fadeBricks
                    for (brick in fadeBricks) {
                        brick.opacity -= 0.1;

                        if (brick.opacity <= 0) {
                            brick.visible = false;

                            delete brick from fadeBricks;
                        }
                    }

                    // Move bat if needed
                    if (batDirection != 0 and state != 0) {
                        moveBat(bat.translateX + batDirection);
                    }

                    // Process bonuses
                    for (bonus in bonuses) {
                        if (bonus.translateY > Config.screenHeight) {
                            bonus.visible = false;

                            delete bonus from bonuses;
                            delete bonus from group.content;
                        } else {
                            bonus.translateY += Config.bonusSpeed;

                            if (bonus.translateX + bonus.width > bat.translateX and
                                    bonus.translateX < bat.translateX + bat.width and
                                    bonus.translateY + bonus.height > bat.translateY and
                                    bonus.translateY < bat.translateY + bat.height) {
                                // Bonus is catched
                                updateScore(100);

                                catchedBonus = bonus.type;

                                bonus.visible = false;

                                delete bonus from bonuses;
                                delete bonus from group.content;

                                if (bonus.type == Bonus.TYPE_SLOW) {
                                    ballDirX /= 1.5;
                                    ballDirY /= 1.5;

                                    correctBallSpeed();
                                }

                                if (bonus.type == Bonus.TYPE_FAST) {
                                    ballDirX *= 1.5;
                                    ballDirY *= 1.5;

                                    correctBallSpeed();
                                }

                                if (bonus.type == Bonus.TYPE_CATCH) {
                                    // Do nothing
                                }

                                if (bonus.type == Bonus.TYPE_GROW_BAT) {
                                    if (bat.size < Bat.MAX_SIZE) {
                                        bat.size += 1;

                                        if (bat.translateX + bat.width > Config.fieldWidth) {
                                            bat.translateX = Config.fieldWidth - bat.width;
                                        }
                                    }
                                }

                                if (bonus.type == Bonus.TYPE_REDUCE_BAT) {
                                    if (bat.size > 0) {
                                        var oldWidth = bat.width;

                                        bat.size -= 1;
                                        bat.translateX += (oldWidth - bat.width) / 2;
                                    }
                                }

                                if (bonus.type == Bonus.TYPE_GROW_BALL) {
                                    if (ball.size < Ball.MAX_SIZE) {
                                        ball.size += 1;

                                        if (state == 1) {
                                            ball.translateY = Config.batY - ball.diameter
                                        }
                                    }
                                }

                                if (bonus.type == Bonus.TYPE_REDUCE_BALL) {
                                    if (ball.size > 0) {
                                        ball.size -= 1;

                                        if (state == 1) {
                                            ball.translateY = Config.batY - ball.diameter
                                        }
                                    }
                                }

                                if (bonus.type == Bonus.TYPE_LIFE) {
                                    Main.mainFrame.lifeCount += 1;

                                    updateLifes();
                                }
                            }
                        }
                    }

                    if (state != 2) {
                        return;
                    }

                    var newX = ball.translateX + ballDirX;
                    var newY = ball.translateY + ballDirY;

                    var inverseX = false;
                    var inverseY = false;

                    if (newX < 0) {
                        newX = -newX;
                        inverseX = true;
                    }

                    def BALL_MAX_X = Config.fieldWidth - ball.diameter;

                    if (newX > BALL_MAX_X) {
                        newX = BALL_MAX_X - (newX - BALL_MAX_X);
                        inverseX = true;
                    }

                    if (newY < Config.fieldY) {
                        newY = 2 * Config.fieldY - newY;
                        inverseY = true;
                    }

                    // Determine hit bat and ball
                    if (ballDirY > 0 and
                            ball.translateY + ball.diameter < Config.batY and
                            newY + ball.diameter >= Config.batY and
                            newX >= bat.translateX - ball.diameter and
                            newX < bat.translateX + bat.width + ball.diameter) {
                        inverseY = true;

                        // Speed up ball
                        var speed = java.lang.Math.sqrt(ballDirX * ballDirX + ballDirY * ballDirY);

                        ballDirX *= (speed + Config.ballSpeedInc) / speed;
                        ballDirY *= (speed + Config.ballSpeedInc) / speed;

                        // Correct ballDirX and ballDirY
                        var offsetX = newX + ball.diameter / 2 - bat.translateX - bat.width / 2;

                        // Don't change direction if center of bat was used
                        if (java.lang.Math.abs(offsetX) > bat.width / 4) {
                            ballDirX += offsetX / 5;

                            def MAX_COORD_SPEED = java.lang.Math.sqrt(speed * speed -
                                Config.ballMinCoordSpeed * Config.ballMinCoordSpeed);

                            if (java.lang.Math.abs(ballDirX) > MAX_COORD_SPEED) {
                                ballDirX = Utils.sign(ballDirX) * MAX_COORD_SPEED;
                            }

                            ballDirY = Utils.sign(ballDirY) *
                                java.lang.Math.sqrt(speed * speed - ballDirX * ballDirX);
                        }

                        correctBallSpeed();

                        if (catchedBonus == Bonus.TYPE_CATCH) {
                            newY = Config.batY - ball.diameter;

                            state = 1;
                        }
                    }

                    // Determine hit ball and brick
                    var firstCol: Integer = newX / Config.brickWidth as Integer;
                    var secondCol: Integer = (newX + ball.diameter) / Config.brickWidth as Integer;
                    var firstRow: Integer = (newY - Config.fieldY) /
                        Config.brickHeight as Integer;
                    var secondRow: Integer = (newY - Config.fieldY + ball.diameter)
                        / Config.brickHeight as Integer;

                    if (ballDirX > 0) {
                        var temp = secondCol;
                        secondCol = firstCol;
                        firstCol = temp;
                    }

                    if (ballDirY > 0) {
                        var temp = secondRow;
                        secondRow = firstRow;
                        firstRow = temp;
                    }

                    var vertBrick = getBrick(firstRow, secondCol);
                    var horBrick = getBrick(secondRow, firstCol);

                    if (vertBrick != null) {
                        kickBrick(firstRow, secondCol);

                        if (catchedBonus != Bonus.TYPE_STRIKE) {
                            inverseY = true;
                        }
                    }

                    if (horBrick != null and
                            (firstCol != secondCol or firstRow != secondRow)) {
                        kickBrick(secondRow, firstCol);

                        if (catchedBonus != Bonus.TYPE_STRIKE) {
                            inverseX = true;
                        }
                    }

                    if (firstCol != secondCol or firstRow != secondRow) {
                        var diagBrick = getBrick(firstRow, firstCol);

                        if (diagBrick != null and diagBrick != vertBrick and
                                diagBrick != horBrick) {
                            kickBrick(firstRow, firstCol);

                            if (vertBrick == null and horBrick == null and
                                    catchedBonus != Bonus.TYPE_STRIKE) {
                                inverseX = true;
                                inverseY = true;
                            }
                        }
                    }

                    ball.translateX = newX;
                    ball.translateY = newY;

                    if (inverseX) {
                        ballDirX = -ballDirX;
                    }

                    if (inverseY) {
                        ballDirY = -ballDirY;
                    }

                    if (ball.translateY > Config.screenHeight) {
                        // Ball was lost
                        lostLife();
                    }
                }
            }
        ]
    }

    function lostLife() {
        Main.mainFrame.lifeCount--;

        if (Main.mainFrame.lifeCount < 0) {
            state = 3;

            ball.visible = false;
            bat.visible = false;
            message.image = Config.images[Config.IMAGE_GAMEOVER];
            message.visible = true;
            message.opacity = 1;
        } else {
            updateLifes();

            bat.size = Bat.DEFAULT_SIZE;
            ball.size = Ball.DEFAULT_SIZE;
            bat.translateX = (Config.fieldWidth - bat.width) / 2;
            ball.translateX = Config.fieldWidth / 2 - ball.diameter / 2;
            ball.translateY = Config.batY - ball.diameter;
            state = 1;
            catchedBonus = 0;
            ballDirX = (Utils.random(2) * 2 - 1) * Config.ballMinCoordSpeed;
            ballDirY = -Config.ballMinSpeed;
        }
    }

    def startingTimeline: Timeline = Timeline {
        keyFrames : [
            at (0.5s) {
                state => 0;
                message.opacity => 0.0;
                message.visible => true;
                bat.visible => false;
                ball.visible => false;
            },

            at (1.5s) {
                message.opacity => 1.0 tween Interpolator.LINEAR;
            },

            at (3s) {
                message.opacity => 1.0;
            },

            at (4s) {
                message.opacity => 0.0 tween Interpolator.LINEAR;
                message.visible => false;
                bat.visible => true;
                bat.translateX => (Config.fieldWidth - bat.width) / 2;
                ball.visible => true;
                ball.translateX => Config.fieldWidth / 2 - ball.diameter / 2;
                ball.translateY => Config.batY - ball.diameter;
                state => 1;
                ballDirX => (Utils.random(2) * 2 - 1) * Config.ballMinCoordSpeed;
                ballDirY => -Config.ballMinSpeed;
            }
        ]
    }
}