ExplonoidCanvas.java

/**
* Copyright (c) 2012-2013 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.explonoid;

import com.nokia.example.explonoid.audio.AudioManager;
import com.nokia.example.explonoid.effects.ElectricArc;
import com.nokia.example.explonoid.effects.LightManager;
import com.nokia.example.explonoid.effects.Slideable;
import com.nokia.example.explonoid.game.Game;
import com.nokia.example.explonoid.game.Resources;
import com.nokia.example.explonoid.menu.InfoScreen;
import com.nokia.example.explonoid.menu.MainMenu;
import com.nokia.example.explonoid.menu.Menu;
import java.util.Random;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;

/**
 * Main canvas for game.
 */
public class ExplonoidCanvas
        extends GameCanvas
        implements CommandListener {

    public static final int INTERVAL = 30;
    private static final int LEFT_SOFTKEY = -6;
    private static final int RIGHT_SOFTKEY = -7;
    private static final double DEFAULT_WIDTH = 240;
    private static final double DEFAULT_HEIGHT = 320;
    private boolean pressed = false;
    private float scaling;
    private int gameState;
    private int nextState;
    private int cornerX;
    private int cornerY;
    private int displayWidth;
    private int displayHeight;
    private int gameWidth;
    private int gameHeight;
    private int x = 0;
    private int y = 0;
    private GameThread gameThread;
    private Main main;
    private MainMenu menu;
    private InfoScreen info;
    private Game game;
    private Slideable currentView;
    private Slideable targetView;
    private boolean electricity;
    private Resources r;
    private Random rnd = new Random();
    private AudioManager audioManager;
    private Command backCommand;

    public ExplonoidCanvas(Main main) {
        super(false);
        this.setFullScreenMode(true);
        this.main = main;

        audioManager = AudioManager.getInstance();
        
        final String keyboard = System.getProperty("com.nokia.keyboard.type");
        
        if (Main.isFullTouchDevice()) {
            backCommand = new Command("Back", Command.BACK, 0);
            addCommand(backCommand);
            setCommandListener(this);
        }
    }

    /**
     * Creates a new game and loads the previous status of the game, if it exists
     */
    private void loadOrCreateGame() {
        game = new Game(cornerX, cornerY, gameWidth, gameHeight, r, new Game.Listener() {

            public void changeState(int state) {
                switch (state) {
                    case Game.STATE_MENU:
                        showMenu();
                        break;
                    case Game.STATE_LEVEL:
                        nextLevel();
                        break;
                }
            }

            public void handleEvent(int event) {
                switch (event) {
                    case Game.EVENT_BALL_OUT:
                        audioManager.playSample(r.SAMPLE_DEATH);
                        break;
                    case Game.EVENT_BONUS:
                        audioManager.playSample(r.SAMPLE_BONUS);
                        break;
                    case Game.EVENT_BRICK_EXPLOSION:
                        audioManager.playSample(r.SAMPLE_EXPLOSION);
                        break;
                    case Game.EVENT_BRICK_COLLISION:
                        audioManager.playSample(getRndCollisionSample());
                        break;
                    case Game.EVENT_BUTTON_PRESSED:
                        audioManager.playSample(r.SAMPLE_BUTTON);
                        break;
                    case Game.EVENT_GAME_OVER:
                        // No sound at the moment
                        break;
                    case Game.EVENT_LEVEL_CHANGE:
                        audioManager.playSample(r.SAMPLE_TELEPORT);
                        break;
                    case Game.EVENT_LEVEL_STARTED:
                        menu.showResume();
                        break;
                    case Game.EVENT_PLATE_COLLISION:
                        audioManager.playSample(getRndCollisionSample());
                        break;
                    case Game.EVENT_WALL_COLLISION:
                        audioManager.playSample(r.SAMPLE_WALL);
                        break;
                }
            }
        });
        try {
            //RecordStore.deleteRecordStore("GameSnapshot"); // Clear state data for testing purposes
            RecordStore snapshot = RecordStore.openRecordStore("GameSnapshot", true);
            if (snapshot.getNumRecords() == 0 || !game.load(snapshot.getRecord(getRecordId(snapshot)))) {
                game.newGame();
            } else {
                menu.showResume();
            }
            snapshot.closeRecordStore();
        }
        catch (RecordStoreException e) {
            game.newGame();
        }
        catch (NumberFormatException nfe) {
            nfe.printStackTrace();
        }
        showMenu();
    }

    public void commandAction(Command c, Displayable d) {
        if (c == backCommand) {
            if (gameState == Game.STATE_MENU) {
                main.exit();
            } else {
                showMenu();
            }
        }
    }

    /**
     * Returns the name of a randomly selected collision sample
     * @return The sample name in a String
     */
    public String getRndCollisionSample() {
        switch (rnd.nextInt(2)) {
            case 0:
                return r.SAMPLE_COLLISION;
            case 1:
                return r.SAMPLE_COLLISION2;
            case 2:
                return r.SAMPLE_COLLISION3;
            default:
                return r.SAMPLE_COLLISION;
        }
    }

    /**
     * Save the state of the game to RecordStore
     */
    public void saveGame() {
        if (game == null) {
            return;
        }
        try {
            RecordStore snapshot = RecordStore.openRecordStore("GameSnapshot", true);
            if (snapshot.getNumRecords() == 0) {
                snapshot.addRecord(null, 0, 0);
            }
            byte[] data = game.getSnapshot();
            snapshot.setRecord(getRecordId(snapshot), data, 0, data.length);
            snapshot.closeRecordStore();
            System.out.println("state saved succesfully");
        }
        catch (RecordStoreException e) {
            System.out.println("exception occured while saving");
        }
    }

    /**
     * Get the first Record ID for a given record store
     */
    private int getRecordId(RecordStore store) throws RecordStoreException {
        RecordEnumeration e = store.enumerateRecords(null, null, false);
        try {
            return e.nextRecordId();
        }
        finally {
            e.destroy();
        }
    }

    /**
     * Check the state of keys
     */
    private void checkKeys() {
        int keyState = getKeyStates();
        if (gameState == Game.STATE_LEVEL) {
            if ((keyState & LEFT_PRESSED) != 0) {
                game.movePlateLeft();
            }
            else if ((keyState & RIGHT_PRESSED) != 0) {
                game.movePlateRight();
            }
        }
    }

    /**
     * Handles the key pressed events
     * @param key
     */
    protected void keyPressed(int key) {
        switch (gameState) {
            case Game.STATE_MENU:
                switch (getGameAction(key)) {
                    case UP:
                        menu.selectPrev();
                        break;
                    case DOWN:
                        menu.selectNext();
                        break;
                    case FIRE:
                        menu.clickSelected();
                }
                break;
            case Game.STATE_INFO:
                switch (getGameAction(key)) {
                    case UP:
                        info.selectPrev();
                        break;
                    case DOWN:
                        info.selectNext();
                        break;
                    case FIRE:
                        info.clickSelected();
                }
                break;
            case Game.STATE_LEVEL:
                switch (key) {
                    case LEFT_SOFTKEY:
                        game.leftButtonPressed();
                        break;
                    case RIGHT_SOFTKEY:
                        game.rightButtonPressed();
                        break;
                }
                if (getGameAction(key) == FIRE) {
                    game.fire();
                }
                break;
        }
    }

    /**
     * Updates the game logic and transitions
     */
    public void update() {
        switch (gameState) {
            case Game.STATE_TRANSITION:
                if (!(currentView != null && currentView.slideOut())) {
                    currentView = null;
                    if (!(targetView != null && targetView.slideIn())) {
                        currentView = targetView;
                        targetView = null;
                        gameState = nextState;
                    }
                }
                break;
            case Game.STATE_LEVEL:
                game.update();
                break;
        }
    }

    /**
     * Render a frame
     */
    public void render() {
        final Graphics g = getGraphics();
        paint(g);
        flushGraphics();
    }

    /**
     * Renders the view
     * @param g
     */
    public void paint(Graphics g) {
        int anchor = Graphics.LEFT | Graphics.TOP;
        g.setColor(0xFF000000);
        g.fillRect(0, 0, displayWidth, displayHeight);
        g.drawImage(r.background, cornerX, displayHeight, Graphics.LEFT | Graphics.BOTTOM);
        g.drawImage((gameState == Game.STATE_MENU || gameState == Game.STATE_INFO) ? r.signOn : r.signOff,
                    cornerX, cornerY + (int) (scaling * 86 + 0.5), anchor);
        if(gameState == Game.STATE_TRANSITION) {
            g.setClip(cornerX, cornerY, gameWidth, gameHeight);
        } else {
            g.setClip(0, 0, displayWidth, displayHeight);
        }
        try {
            if (currentView != null) {
                currentView.paint(g);
            }
            if (targetView != null) {
                targetView.paint(g);
            }
        }
        catch (NullPointerException npe) {
            // just no painting then
        }
        if ((gameState == Game.STATE_MENU || gameState == Game.STATE_INFO) && pressed) {
            ElectricArc arc = new ElectricArc();
            for (int a = 2 + rnd.nextInt(4); a > 0; a--) {
                g.setColor(rnd.nextInt(255), 255, 255);
                arc.paint(g, x, y, (int) (scaling * 10), (int) (scaling * 10));
            }
        }
    }

    protected void showNotify() {
        r.loadResources();
        if (menu == null) {
            createMenu();
            menu.hideResume();
        }
        if (info == null) {
            createInfoScreen();
        }
        if (game == null) {
            loadOrCreateGame();
        }
        startApp();
    }

    protected void hideNotify() {
        pauseApp();
        r.freeResources();
        audioManager.stopAll();
    }

    protected void sizeChanged(int w, int h) {
        // do only stuff when the size actually changes
        if ((h == displayHeight) && (w == displayWidth)) {
            return;
        }
        
        calculateScalingFactor();
        r.freeResources();
        LightManager.allowDimming();

        menu = null;
        info = null;
        game = null;
        
        showNotify();
    }
    
    /**
     * Stop the game loop
     */
    public void stopApp() {
        gameState = Game.STATE_MENU;
        this.gameThread.requestStop();
        LightManager.allowDimming();
    }

    /**
     * Start the game loop
     */
    public void startApp() {
        if (gameThread == null) {
            gameThread = new GameThread();
        }
        gameThread.requestStart();
        LightManager.avoidDimming();
    }

    /**
     * Pause the game loop
     */
    public void pauseApp() {
        gameThread.requestPause();
        LightManager.allowDimming();
    }

    private void calculateScalingFactor() {
        // Figure out the scaling factor
        displayWidth = getWidth();
        displayHeight = getHeight();

        scaling = 1f;
        if (displayWidth != DEFAULT_WIDTH) {
            scaling = (float) (DEFAULT_WIDTH / displayWidth);
        }
        this.r = new Resources(scaling);
        gameWidth = (int) (scaling * DEFAULT_WIDTH + 0.5);
        gameHeight = (int) (scaling * DEFAULT_HEIGHT + 0.5);

        cornerX = (int) (displayWidth / 2 - gameWidth / 2 + 0.5);
        cornerY = displayHeight - gameHeight;
    }

    /**
     * 
     * Game Thread
     */
    class GameThread
            extends Thread {

        private boolean pause = true;
        private boolean stop = false;
        private boolean started = false;

        public void requestStart() {
            this.pause = false;
            if (!started) {
                this.start();
                this.started = true;
            }
            else {
                synchronized (this) {
                    notify();
                }
            }
        }

        public void requestPause() {
            this.pause = true;
        }

        public void requestStop() {
            this.stop = true;
        }

        /**
         * The game loop. This example uses only one thread for updating
         * game logic and for rendering with constant frequency.
         */
        public void run() {
            long time = 0;
            while (!stop) {
                try {
                    if (pause) {
                        synchronized (this) {
                            wait();
                        }
                    }
                    else {
                        time = System.currentTimeMillis();
                        checkKeys();
                        update();
                        render();

                        // Sleep the rest of the time
                        time = INTERVAL - (System.currentTimeMillis() - time);
                        Thread.sleep((time < 0 ? 0 : time));
                    }
                }
                catch (Exception e) {
                }
            }
        }
    }

    /**
     * Handle pointer pressed
     */
    public void pointerPressed(int x, int y) {
        this.x = x;
        this.y = y;
        pressed = true;

        switch (gameState) {
            case Game.STATE_LEVEL:
                game.pointerPressed(x - cornerX, y - cornerY);
                break;
            case Game.STATE_MENU:
                menu.pointerEvent(Menu.POINTER_PRESSED, x, y);
                audioManager.playSample(r.SAMPLE_BUTTON);
                break;
            case Game.STATE_INFO:
                info.pointerEvent(Menu.POINTER_PRESSED, x, y);
                audioManager.playSample(r.SAMPLE_BUTTON);
                break;
        }
        if (gameState == Game.STATE_MENU) {
            electricity = audioManager.loopSample(r.SAMPLE_ELECTRICITY);
        }
    }

    /**
     * Handle pointer released
     */
    public void pointerReleased(int x, int y) {
        pressed = false;
        if (electricity) {
            audioManager.stopPlayer(r.SAMPLE_ELECTRICITY);
            electricity = false;
        }
        
        switch (gameState) {
            case Game.STATE_LEVEL:
                game.pointerReleased(x - cornerX, y - cornerY);
                break;
            case Game.STATE_MENU:
                menu.pointerEvent(Menu.POINTER_RELEASED, x, y);
                break;
            case Game.STATE_INFO:
                info.pointerEvent(Menu.POINTER_RELEASED, x, y);
                break;
        }
    }

    /**
     * Handle pointer dragged
     */
    public void pointerDragged(int x, int y) {
        this.x = x;
        this.y = y;

        switch (gameState) {
            case Game.STATE_LEVEL:
                game.pointerDragged(x - cornerX, y - cornerY);
                break;
            case Game.STATE_MENU:
                menu.pointerEvent(Menu.POINTER_DRAGGED, x, y);
                break;
            case Game.STATE_INFO:
                info.pointerEvent(Menu.POINTER_PRESSED, x, y);
                break;
        }
    }

    /**
     * Create and initialize game menu
     */
    private void createMenu() {
        double menuScaling = (float) (displayHeight / DEFAULT_HEIGHT);
        menu = new MainMenu(cornerX, cornerY, gameWidth, gameHeight, new Menu.Listener() {

            public void itemClicked(int item) {
                switch (item) {
                    case MainMenu.RESUME:
                        resumeGame();
                        break;
                    case MainMenu.NEWGAME:
                        newGame();                        
                        break;
                    case MainMenu.SENSORS:
                        game.enableSensors(menu.toggleSensors());
                        break;
                    case MainMenu.SOUNDS:
                        AudioManager.setAudioEnabled(menu.toggleSounds());
                        break;
                    case MainMenu.INFO:
                        showInfo();
                        break;
                }
            }
        }, menuScaling);
    }

    /**
     * Create and initialize information screen
     */
    private void createInfoScreen() {
        info = new InfoScreen(cornerX, cornerY, gameWidth, gameHeight, new Menu.Listener() {

            public void itemClicked(int item) {
                switch (item) {
                    case InfoScreen.LINK:
                        Main midlet = Main.getInstance();
                        try {
                            if (midlet.platformRequest("http://projects.developer.nokia.com/JMEExplonoid")) {
                                midlet.exit();
                            }
                        }
                        catch (ConnectionNotFoundException cnfe) {
                        }
                        break;
                    case InfoScreen.BACK:
                        showMenu();
                        break;
                }
            }
        }, scaling);
    }

    /**
     * Change view
     * @param targetView The view to change to
     */
    public void changeView(Slideable targetView) {
        this.targetView = targetView;
        gameState = Game.STATE_TRANSITION;
        audioManager.playSample(r.SAMPLE_TRANSITION);
    }

    /**
     * Resume to game preserving the game state
     */
    private void resumeGame() {
        nextState = Game.STATE_LEVEL;
        changeView(game);
    }

    /**
     * Start new game
     */
    private void newGame() {
        game.newGame();
        nextState = Game.STATE_LEVEL;
        changeView(game);
    }

    /**
     * Show menu screen
     */
    public void showMenu() {
        nextState = Game.STATE_MENU;
        changeView(menu);
    }

    /**
     * Show info screen
     */
    public void showInfo() {
        nextState = Game.STATE_INFO;
        changeView(info);
    }

    /**
     * Change level
     */
    public void nextLevel() {
        nextState = Game.STATE_LEVEL;
        changeView(game);
    }
}