Game.java

/*
 * Copyright © 2012 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */
package com.nokia.example.battletank.game;

import com.nokia.example.battletank.Main;
import com.nokia.example.battletank.game.dialog.GameOverDialog;
import com.nokia.example.battletank.game.dialog.LevelCompleteDialog;
import com.nokia.example.battletank.game.entities.Entity;
import com.nokia.example.battletank.game.entities.Enemy;
import com.nokia.example.battletank.game.entities.Bonus;
import com.nokia.example.battletank.game.entities.ExplosionsManager;
import com.nokia.example.battletank.game.entities.BulletManager;
import com.nokia.example.battletank.game.entities.TreeManager;
import com.nokia.example.battletank.game.entities.Bullet;
import com.nokia.example.battletank.game.entities.Base;
import com.nokia.example.battletank.game.entities.BonusManager;
import com.nokia.example.battletank.game.entities.EnemyManager;
import com.nokia.example.battletank.game.entities.Tank;
import com.nokia.example.battletank.game.entities.Player;
import com.nokia.example.battletank.game.audio.AudioManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.LayerManager;

public class Game {

    private static final int POINTS_PER_LIFE = 100; // hit points of tank
    private Resources resources; // contains game images
    private final int soundPadding;
    public boolean soundsEnabled = true;
    private int levelNumber = 0;
    private int lives;
    private int score = 0;
    private int highScore = 0;
    private int enemiesInReserve;
    private Level level;
    private Player player;
    private EnemyManager enemyManager;
    private Base base;
    private Hud hud;
    private GameOverDialog gameOverDialog;
    private LevelCompleteDialog levelCompleteDialog;
    private LayerManager layerManager;
    private int viewportLeft;
    private int viewportTop;
    private int viewportWidth;
    private int viewportHeight;
    private BulletManager bulletManager;
    private ExplosionsManager explosionManager;
    private BonusManager bonusManager;
    private TreeManager treeManager;

    public Game(int w, int h) {
        resources = new Resources(w, h);
        soundPadding = resources.gridSizeInPixels * 20;
        setViewportSize(w, h);
        hud = new Hud(resources, w, h);
        gameOverDialog = new GameOverDialog(resources);
        levelCompleteDialog = new LevelCompleteDialog(resources);
    }

    /**
     * Renders the game state.
     *
     * @param g Graphics object
     */
    public void render(Graphics g) {
        level.refresh();
        bulletManager.refresh();
        explosionManager.refresh();
        bonusManager.refresh();
        base.refresh();
        player.refresh();
        enemyManager.refresh();
        refreshViewport();
        clearScreen(g);
        layerManager.paint(g, 0, 0);
        gameOverDialog.paint(g, viewportWidth, viewportHeight);
        levelCompleteDialog.paint(g, viewportWidth, viewportHeight);
        hud.paint(g, viewportWidth, viewportHeight);
    }

    /**
     * Updates objects according to the game logic.
     *
     * @param keyStates States of device keys
     */
    public void update(int keyStates) {
        // update 2 steps
        for (int i = 0; i < 2; i++) {
            bulletManager.update();
            bonusManager.update();
            base.update();
            player.update(keyStates);
            enemyManager.update();
        }
        AudioManager.playEffects();
    }

    /**
     * Continues to next level or starts a new game depending on win/lose
     *
     * @throws ProtectedContentException
     * @throws IOException
     */
    public void leftSoftkeyPressed()
        throws ProtectedContentException, IOException {
        if (isLevelComplete()) {
            nextLevel();
        }
        else if (isGameOver()) {
            newGame();
        }
    }

    /**
     * Resets the game state and starts a new game.
     *
     * @throws ProtectedContentException
     * @throws IOException
     */
    public void newGame()
        throws ProtectedContentException, IOException {
        gameOverDialog.hide();
        levelCompleteDialog.hide();
        levelNumber = 1;
        lives = 3;
        score = 0;
        loadLevel();
    }

    /**
     * Proceeds player to next level.
     *
     * @throws ProtectedContentException
     * @throws IOException
     */
    public void nextLevel()
        throws ProtectedContentException, IOException {
        levelCompleteDialog.hide();
        levelNumber++;
        lives++;
        loadLevel();
    }

    public boolean isGameOver() {
        return gameOverDialog.isVisible();
    }

    public boolean isLevelComplete() {
        return levelCompleteDialog.isVisible();
    }

    public final void setViewportSize(int w, int h) {
        viewportWidth = w;
        viewportHeight = h;
    }

    /**
     * Returns volume of sound produced by entity depending on distance.
     *
     * @param e Game entity producing sound
     * @return volume of the sound
     */
    public int getVolume(Entity e) {
        int x = e.getCenterX();
        int dx = viewportLeft - x;
        if (dx < 0) {
            dx = x - viewportLeft - viewportWidth;
            if (dx < 0) {
                dx = 0;
            }
        }
        int y = e.getCenterY();
        int dy = viewportTop - y;
        if (dy < 0) {
            dy = y - viewportTop - viewportHeight;
            if (dy < 0) {
                dy = 0;
            }
        }
        if (dx > soundPadding || dy > soundPadding) {
            return 0;
        }
        return 100 - 100 * (dx ^ 2 + dy ^ 2) / (2 * soundPadding ^ 2);
    }

    /**
     * Gets the state of the current game (to be saved).
     *
     * @return The game state as bytes
     */
    public byte[] getState() {
        ByteArrayOutputStream bout = null;
        try {
            bout = new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream(bout);
            dout.writeBoolean(Main.isTrial());
            dout.writeInt(levelNumber);
            dout.writeInt(lives);
            dout.writeInt(score);
            dout.writeInt(highScore);
            dout.writeInt(enemiesInReserve);
            level.writeTo(dout);
            player.writeTo(dout);
            enemyManager.writeTo(dout);
            base.writeTo(dout);
            bonusManager.writeTo(dout);
            treeManager.writeTo(dout);
            dout.writeBoolean(isGameOver());
            dout.writeBoolean(isLevelComplete());
            dout.writeBoolean(soundsEnabled);
            return bout.toByteArray();
        }
        catch (IOException e) {
        }
        finally {
            try {
                if (bout != null) {
                    bout.close();
                }
            }
            catch (IOException e) {
            }
        }
        return new byte[0];
    }

    /**
     * Loads a previously saved game state and updates the game according to it.
     *
     * @param record Previously saved game state as bytes
     * @return true if loading was successful, false otherwise
     */
    public boolean load(byte[] record) {
        if (record == null) {
            return false;
        }
        try {
            DataInputStream din = new DataInputStream(new ByteArrayInputStream(
                record));
            Main.setTrial(din.readBoolean());
            levelNumber = din.readInt();
            lives = din.readInt();
            score = din.readInt();
            highScore = din.readInt();
            enemiesInReserve = din.readInt();
            level = Level.readFrom(din, resources);
            createPlayer();
            player.readFrom(din);
            createEnemies();
            enemyManager.readFrom(din);
            createBase();
            base.readFrom(din);
            createManagers();
            bonusManager.readFrom(din);
            treeManager.readFrom(din);
            treeManager.refresh();
            if (din.readBoolean()) {
                showGameOverDialog();
            }
            if (din.readBoolean()) {
                showLevelCompleteDialog();
            }
            soundsEnabled = din.readBoolean();
            hud.updateLives(lives);
            hud.updateScore(score);
            hud.updateEnemies(getNumberOfEnemies());
            return true;
        }
        catch (IOException e) {
        }
        return false;
    }

    private void spawnTank(Tank tank) {
        if (tank == player) {
            if (lives == 0) {
                showGameOverDialog();
                return;
            }
            player.spawn();
            lives--;
            hud.updateLives(lives);
        }
        else {
            if (enemiesInReserve == 0) {
                return;
            }
            ((Enemy) tank).spawn(Levels.getEnemyType(levelNumber,
                enemiesInReserve));
            enemiesInReserve--;
        }
    }

    private void loadLevel()
        throws ProtectedContentException, IOException {
        this.enemiesInReserve = Levels.getTotalEnemies(levelNumber);
        level = Level.load(levelNumber, resources);
        createBase();
        createPlayer();
        createEnemies();
        createManagers();
        hud.updateScore(score);
        hud.updateEnemies(getNumberOfEnemies());
        hud.updateLeftButton(Hud.NONE);
        startPlaying();
    }

    private int getNumberOfEnemies() {
        return enemiesInReserve + enemyManager.numberOfAlive();
    }

    private void createBase() {
        base = new Base(level, resources);
    }

    private void createPlayer() {
        player = new Player(level, resources, createTankListener());
    }

    private void createEnemies() {
        enemyManager = new EnemyManager(level, Levels.getConcurrentEnemies(
            levelNumber), resources, createTankListener());
    }

    private Tank.Listener createTankListener() {
        return new Tank.Listener() {

            public void destroyed(Tank tank) {
                spawnTank(tank);
            }

            public boolean collidesWithAnything(Tank tank, int x, int y, int w,
                int h) {
                if (level.collides(x, y, w, h) || base.collidesWith(x, y, w, h)) {
                    return true;
                }
                if (tank != player && player.collidesWith(x, y, w, h)) {
                    return true;
                }
                if (enemyManager.collidesWith(tank, x, y, w, h)) {
                    return true;
                }
                return false;
            }

            public boolean isInWater(Tank tank) {
                return level.isInWater(tank);
            }

            public void fireBullet(Tank tank) {
                bulletManager.fire(tank);
                AudioManager.bufferEffect(AudioManager.CANNON, getVolume(tank)
                    / 2);
            }
        };
    }

    private void showGameOverDialog() {
        levelCompleteDialog.hide();
        hud.updateLeftButton(Hud.NEWGAME);
        bonusManager.stopSpawning();
        if (!gameOverDialog.isVisible()) {
            highScore = Math.max(score, highScore);
            gameOverDialog.highScore = highScore;
            gameOverDialog.show(score, levelNumber);
        }
    }

    private Bullet.Listener createBulletListener() {
        return new Bullet.Listener() {

            public boolean collide(Bullet bullet) {
                boolean collided = false;
                if (level.collideAndDestroy(bullet)) {
                    collided = true;
                }
                else if (base.collidesWith(bullet)) {
                    destroyBase();
                    collided = true;
                }
                else if (player.collidesWith(bullet)) {
                    destroyPlayer();
                    collided = true;
                }
                else {
                    Enemy enemy = enemyManager.collidesWith(bullet);
                    if (enemy != null) {
                        collided = true;
                        if (bullet.firedBy == player) {
                            destroyEnemy(enemy);
                        }
                    }
                }
                if (collided) {
                    explosionManager.show(bullet);
                    AudioManager.bufferEffect(AudioManager.EXPLOSION_SMALL,
                        getVolume(bullet));
                }
                return collided;
            }
        };
    }

    private Bonus.Listener createBonusListener() {
        return new Bonus.Listener() {

            public boolean collidesWithPlayer(Bonus bonus) {
                if (player.isAlive() && player.collidesWith(bonus)) {
                    switch (bonus.getType()) {
                        case Bonus.AMMO:
                            player.superAmmo();
                            break;
                        case Bonus.CLOCK:
                            enemyManager.freezeEnemies();
                            break;
                        case Bonus.GRENADE:
                            Enemy[] enemies = enemyManager.all();
                            for (int i = 0; i < enemies.length; i++) {
                                destroyEnemy(enemies[i]);
                            }
                            break;
                        case Bonus.LIFE:
                            lives++;
                            hud.updateLives(lives);
                            break;
                        case Bonus.SHOVEL:
                            base.protect();
                            break;
                        case Bonus.STAR:
                            player.superTank();
                            break;
                    }
                    return true;
                }
                return false;
            }
        };
    }

    private void destroyBase() {
        if (base.destroy()) {
            explosionManager.show(base);
            AudioManager.bufferEffect(AudioManager.EXPLOSION_LARGE, getVolume(
                base));
            showGameOverDialog();
        }
    }

    private void destroyPlayer() {
        if (player.destroy()) {
            explosionManager.show(player);
            AudioManager.bufferEffect(AudioManager.EXPLOSION_LARGE, 0);
        }
    }

    private void destroyEnemy(Enemy enemy) {
        if (enemy.destroy()) {
            explosionManager.show(enemy);
            AudioManager.bufferEffect(AudioManager.EXPLOSION_LARGE, getVolume(
                enemy));
            hud.updateEnemies(getNumberOfEnemies());
            if (!isGameOver()) {
                score += enemy.getPoints();
                if (hud.getEnemies() == 0) {
                    score += lives * POINTS_PER_LIFE;
                    showLevelCompleteDialog();
                }
                hud.updateScore(score);
            }
        }
    }

    private void startPlaying() {
        spawnTank(player);
        Enemy[] enemies = enemyManager.all();
        for (int i = 0; i < enemies.length; i++) {
            spawnTank(enemies[i]);
        }
    }

    private void clearScreen(Graphics g) {
        g.setColor(0x00000000);
        g.fillRect(0, 0, viewportWidth, viewportHeight);
    }

    private void refreshViewport() {
        setViewportCenter(player.getCenterX(), player.getCenterY());
    }

    private void setViewportCenter(int x, int y) {
        final int maxLeft = level.widthInPixels - viewportWidth;
        viewportLeft = maxLeft > 0 ? Math.max(Math.min(x - viewportWidth / 2,
            maxLeft), 0) : maxLeft / 2;
        final int maxTop = level.heightInPixels - viewportHeight;
        viewportTop = maxTop > 0 ? Math.max(Math.min(y - viewportHeight / 2,
            maxTop), 0) : maxTop / 2;
        layerManager.setViewWindow(viewportLeft, viewportTop, maxLeft > 0
            ? viewportWidth : level.widthInPixels - viewportLeft, maxTop > 0
            ? viewportHeight : level.heightInPixels - viewportTop);
    }

    private void showLevelCompleteDialog() {
        bonusManager.stopSpawning();
        hud.updateLeftButton(Hud.CONTINUE);
        if (!levelCompleteDialog.isVisible()) {
            levelCompleteDialog.show(score, levelNumber);
        }
    }

    private void createManagers() {
        final int numberOfTanks = 1 + level.getEnemySpawnPointsLength();
        bulletManager = new BulletManager(2 * numberOfTanks, resources,
            createBulletListener());
        explosionManager = new ExplosionsManager(2 * numberOfTanks, resources);
        bonusManager = new BonusManager(level, resources, createBonusListener());
        treeManager = new TreeManager(level, resources);
        layerManager = new LayerManager();
        treeManager.appendTo(layerManager);
        treeManager.refresh();
        bonusManager.appendTo(layerManager);
        explosionManager.appendTo(layerManager);
        layerManager.append(base.getSprite());
        layerManager.append(player.getSprite());
        enemyManager.appendTo(layerManager);
        bulletManager.appendTo(layerManager);
        layerManager.append(level.getWallLayer());
        layerManager.append(level.getGroundLayer());
    }
}