PureMVC Shop Example

Overview

Basic implementation of an in-game shop using the PureMVC framework

Try It

Use the following Loom CLI commands to run this example:

loom new MyPureMVCExample --example PureMVCExample
cd MyPureMVCExample
loom run

Screenshot

Shop Screenshot

Code

src/Demo.ls

package
{
    import loom.Application;

    import loom2d.Loom2D;
    import loom2d.display.StageScaleMode;
    import loom2d.display.Quad;
    import loom2d.display.DisplayObject;
    import loom2d.display.Sprite;
    import loom2d.display.Image;
    import loom2d.textures.Texture;
    import loom2d.events.Event;
    import loom2d.text.BitmapFont;
    import loom2d.text.TextField;

    import feathers.core.FocusManager;
    import feathers.system.DeviceCapabilities;
    import feathers.themes.MetalWorksMobileTheme;
    import feathers.layout.VerticalLayout;
    import feathers.controls.PanelScreen;
    import feathers.controls.Button;
    import feathers.controls.Label;
    import feathers.events.FeathersEventType;
    import feathers.controls.ScreenNavigator;
    import feathers.controls.ScreenNavigatorItem;
    import feathers.motion.transitions.ScreenSlidingStackTransitionManager;

    import org.puremvc.loomsdk.interfaces.IFacade;
    import org.puremvc.loomsdk.interfaces.IMediator;
    import org.puremvc.loomsdk.interfaces.INotification;
    import org.puremvc.loomsdk.patterns.facade.Facade;
    import org.puremvc.loomsdk.patterns.command.SimpleCommand;
    import org.puremvc.loomsdk.patterns.mediator.Mediator;
    import org.puremvc.loomsdk.patterns.proxy.Proxy;

    /**
     * An example of an in-game shop. Demonstrates a use of the PureMVC framework
     * to control views and modify data.
     */

    public class Demo extends Application 
    {
        public static var theme:MetalWorksMobileTheme;

        override public function run():void
        {
            // Initialize the stage.
            stage.scaleMode = StageScaleMode.LETTERBOX;
            stage.stageWidth = 960;
            stage.stageHeight = 640;

            // Initialize PureMVC
            var facade = Facade.getInstance();
            facade.registerCommand( StartupCommand.NAME, StartupCommand );
            facade.sendNotification( StartupCommand.NAME );
        }
    }


    /** ***********************************************************
    //  CONTROLLER
    // ************************************************************


    /**
     * PureMVC uses commands to perform logic between the views and the data model.
     * We use our StartUpCommand to register the commands we intend to use in the 
     * application, initialize the data model, and finally create our core views.
     */

    public class StartupCommand extends SimpleCommand
    {
        public static const NAME:String = "StartupCommand";

        override public function execute( notification:INotification ):void
        {
            // Unregister this command on execution since we only need to run this once
            facade.removeCommand( StartupCommand.NAME );

            registerCommands();
            initializeModel();
            initializeView();
        }

        private function registerCommands():void
        {
            trace( "Registering Commands" );
            facade.registerCommand( ShowDialogCommand.NAME, ShowDialogCommand );
            facade.registerCommand( RequestPurchaseCommand.NAME, RequestPurchaseCommand );
            facade.registerCommand( ConfirmPurchaseCommand.NAME, ConfirmPurchaseCommand );
            facade.registerCommand( ShowNotificationCommand.NAME, ShowNotificationCommand );
        }

        private function initializeModel():void
        {
            trace( "Initializing Data Model" );
            facade.registerProxy( new ItemProxy() );
        }

        private function initializeView():void
        {
            trace( "Initializing Core Views" );

            // We do some Feathers boilerplate initialization here...
            DeviceCapabilities.dpi = Platform.getDPI();
            TextField.registerBitmapFont( BitmapFont.load( "assets/arialComplete.fnt" ), "SourceSansPro" );
            TextField.registerBitmapFont( BitmapFont.load( "assets/arialComplete.fnt" ), "SourceSansProSemibold" );
            Demo.theme = new MetalWorksMobileTheme();
            FocusManager.pushFocusManager();

            // Then create our main views
            facade.registerMediator( new MasterViewMediator() );
            facade.registerMediator( new InventoryScreenMediator() );
            facade.registerMediator( new ShopScreenMediator() );
            facade.registerMediator( new PurchaseConfirmationScreenMediator() );
            facade.registerMediator( new NotificationScreenMediator() );

            sendNotification( ShowDialogCommand.NAME, ShopScreenMediator.NAME );
        }
    }

    /**
     * The ShowDialogCommand shows the screen passed in by name through the body of the notification
     * by accessing our MasterViewMediator and forwarding the dialog name to its showDialog() method.
     */

    public class ShowDialogCommand extends SimpleCommand
    {
        public static const NAME:String = "ShowDialogCommand";

        override public function execute( notification:INotification ):void
        {
            var masterViewMediator = facade.retrieveMediator( MasterViewMediator.NAME ) as MasterViewMediator;
            masterViewMediator.showDialog( notification.getBody() as String );
        }
    }

    /**
     * Opens up the purchase confirmation page, setting the displayed item by the id passed in
     * the notification body.
     */

    public class RequestPurchaseCommand extends SimpleCommand
    {
        public static const NAME:String = "RequestPurchaseCommand";

        override public function execute( notification:INotification ):void
        {
            var itemProxy = facade.retrieveProxy( ItemProxy.NAME ) as ItemProxy;
            var item = itemProxy.getItemDataById( notification.getBody() as String );

            if ( itemProxy.playerCoins < item.cost )
            {
                // If we don't have have enough coins to purchase the item, show a notification
                sendNotification( ShowNotificationCommand.NAME, "You do not have enough coins." );
            }
            else
            {
                // Otherwise configure our purchase confirmation screen to show the item, then open the dialog
                var purchasePage = facade.retrieveMediator( PurchaseConfirmationScreenMediator.NAME ) as PurchaseConfirmationScreenMediator;
                purchasePage.configure( notification.getBody() as String );
                sendNotification( ShowDialogCommand.NAME, PurchaseConfirmationScreenMediator.NAME );
            }
        }
    }

    /**
     * This command tells the ItemProxy to purchase the item passed in by id via notification body,
     * then opens up the notification screen with a message confirming the purchase was successful.
     */

    public class ConfirmPurchaseCommand extends SimpleCommand
    {
        public static const NAME:String = "ConfirmPurchaseCommand";

        override public function execute( notification:INotification ):void
        {
            ItemProxy( facade.retrieveProxy( ItemProxy.NAME ) ).purchaseItem( notification.getBody() as String );
            sendNotification( ShowNotificationCommand.NAME, "Purchase successful!" );
        }
    }

    /**
     * This command opens up the notification dialog, setting the text to the passed in notification body.
     */

    public class ShowNotificationCommand extends SimpleCommand
    {
        public static const NAME:String = "ShowNotificationCommand";

        override public function execute( notification:INotification ):void
        {
            NotificationScreenMediator( facade.retrieveMediator( NotificationScreenMediator.NAME ) ).notificationMessage = notification.getBody() as String;
            sendNotification( ShowDialogCommand.NAME, NotificationScreenMediator.NAME );
        }
    }


    // ************************************************************
    //  MODEL
    // ************************************************************


    /**
     * PureMVC uses what is referred to as a Proxy to store and manage the data model. The
     * ItemProxy handles item manifest data, player coins, player inventory, and purchases.
     */

    public class ItemProxy extends Proxy
    {
        public static const NAME:String = "ItemProxy";

        public static const INVENTORY_UPDATED:String = "InventoryUpdated";

        // We'll initialize our item data here for this example. You could also read in
        // data like this from a JSON file or web request

        private static const ITEM_DATA:Dictionary.<String, ItemVO> = {
            "goodItem" : new ItemVO( "goodItem", "Good Item", "An item. Pretty good.", 25, "assets/ball-red.png" ),
            "betterItem" : new ItemVO( "betterItem", "Better Item", "Even better than the good item.", 40, "assets/ball-blue.png" ),
            "bestItem" : new ItemVO( "bestItem", "Best Item", "The best item in the store.", 55, "assets/ball-green.png" )
        };

        private var _playerInventory:Dictionary.<String, int> = {};
        private var _playerCoins:int = 300;

        public function ItemProxy()
        {
            super( NAME );
        }

        public function get itemList():Dictionary.<String, ItemVO> { return ITEM_DATA; }

        public function get playerCoins():int { return _playerCoins; }

        public function getItemDataById( id:String ):ItemVO
        {
            return ITEM_DATA[ id ];
        }

        public function getQuantityOwned( id:String ):int
        {
            return _playerInventory[ id ] ? _playerInventory[ id ] : 0; 
        }

        public function purchaseItem( id:String ):void
        {
            var item = getItemDataById( id );
            _playerCoins -= item.cost;
            if ( _playerInventory[ id ] == null ) _playerInventory[ id ] = 1;
            else _playerInventory[ id ]++;

            // Let the rest of the views know that something has changed
            sendNotification( INVENTORY_UPDATED );
        }
    }

    /**
     * PureMVC Proxies hold data in simple objects called Value Objects, or VOs. The ItemVO here
     * stores all the data needed for a single item.
     */

    public class ItemVO
    {
        public var id:String;
        public var name:String; 
        public var description:String;
        public var cost:int;
        public var imagePath:String;

        public function ItemVO( id:String, name:String, description:String, cost:int, imagePath:String )
        {
            this.id = id;
            this.name = name;
            this.description = description;
            this.cost = cost;
            this.imagePath = imagePath;
        }
    }


    // ************************************************************
    //  VIEW
    // ************************************************************


    /**
     * PureMVC uses what is referred to as a Mediator to connect view components to the rest of the application.
     * This MasterViewMediator will be used as our root view provider, showing and hiding the view components
     * from the other individual screens.
     */

    public class MasterViewMediator extends Mediator
    {
        public static const NAME:String = "MasterViewMediator";
        public static const DIALOG_CHANGED:String = "DialogChanged";

        private var _currentDialog:IMediator;
        private var _navigator:ScreenNavigator;
        private var _transitionManager:ScreenSlidingStackTransitionManager;

        public function MasterViewMediator()
        {
            super( NAME, Loom2D.stage );
        }

        override public function onRegister():void
        {
            _navigator = new ScreenNavigator();
            _navigator.autoDisposeScreens = false; // Retaining screens so we don't need to recreate them every time they are shown
            Loom2D.stage.addChild( _navigator );
            _transitionManager = new ScreenSlidingStackTransitionManager( _navigator );
        }

        public function showDialog( mediatorName:String ):void
        {
            var mediator:IMediator = facade.retrieveMediator( mediatorName );

            if ( !_navigator.hasScreen( mediatorName ) )
                _navigator.addScreen( mediatorName, new ScreenNavigatorItem( mediator.getViewComponent() as DisplayObject ) );

            _currentDialog = mediator;
            _navigator.showScreen( mediatorName );
        }
    }

    /**
     * A base view Mediator class that contains all the functionality shared by the rest of the application views.
     * Do not instantiate directly, but subclass this to create views.
     */

    public class BaseScreenMediator extends Mediator
    {
        protected var _view:PanelScreen;
        protected var _layout:VerticalLayout;
        protected var _coinLabel:Label;

        public function BaseScreenMediator( name:String ):void
        {
            super( name );
        }

        override public function onRegister():void
        {
            super.onRegister();
            _view = new PanelScreen();
            setViewComponent( _view );
            preInitializeView();
            initializeView();
        }

        override public function listNotificationInterests():Vector.<String>
        {
            return [ ItemProxy.INVENTORY_UPDATED ];
        }

        override public function handleNotification( notification:INotification ):void
        {
            switch( notification.getName() )
            {
                case ItemProxy.INVENTORY_UPDATED:
                    _coinLabel.text = "x" + getItemProxy().playerCoins;
                    break;
            }
        }

        protected function getItemProxy():ItemProxy
        {
            return facade.retrieveProxy( ItemProxy.NAME ) as ItemProxy;
        }

        protected function preInitializeView():void
        {
            _layout = new VerticalLayout();
            _layout.horizontalAlign = VerticalLayout.HORIZONTAL_ALIGN_CENTER;
            _view.layout = _layout;

            _coinLabel = new Label();
            _coinLabel.text = "x" + getItemProxy().playerCoins;

            var coinImage = new Image( Texture.fromAsset( "assets/coin.png" ) );
            coinImage.width = coinImage.height = 48 * Demo.theme.scale;

            _view.headerProperties[ "rightItems" ] = [ coinImage, _coinLabel ]; 
        }

        protected function initializeView():void
        {
            // Override in subclasses to set up views
        }
    }

    /**
     * Our inventory screen. Displays rows of item data and quantities.
     */

    public class InventoryScreenMediator extends BaseScreenMediator
    {
        public static const NAME:String = "InventoryScreenMediator";

        protected var _shopMode:Boolean = false;
        protected var _itemRows:Vector.<ItemRowView> = [];

        // To allow subclassing, allow an alternate mediator name to be passed in

        public function InventoryScreenMediator( name:String = NAME )
        {
            super( name );
        }

        override public function handleNotification( notification:INotification ):void
        {
            super.handleNotification( notification );

            // If we receieve a notification that the inventory data has changed,
            // we update our rows to reflect the change

            if ( notification.getName() == ItemProxy.INVENTORY_UPDATED ) updateItems();
        }

        override protected function initializeView():void
        {
            _view.headerProperties[ "title" ] = "INVENTORY";

            var shopButton = new Button();
            shopButton.label = "SHOP";
            shopButton.addEventListener( Event.TRIGGERED, onShopButtonHit );

            _view.headerProperties[ "leftItems" ] = [ shopButton ];
        }

        protected function updateItems():void
        {
            _view.removeChildren( 0, _view.numChildren, false );

            var proxy = getItemProxy();
            var itemList = proxy.itemList;
            var i = 0;

            for each ( var item in itemList )
            {
                // If not in shop mode, only show inventory rows for owned items
                var totalOwned = proxy.getQuantityOwned( item.id );
                if ( !_shopMode && totalOwned == 0 ) continue;

                // Create new rows as needed and cache them
                if ( _itemRows.length <= i ) _itemRows.push( new ItemRowView() ); 
                var itemRow = _itemRows[ i ];

                // Alternate background colors every other row
                var rowColor = ( i & 1 ) ? 0x333333 : 0x666666;

                itemRow.configure( item, totalOwned, _shopMode, rowColor );
                _view.addChild( itemRow );
                i++;
            }
        }

        private function onShopButtonHit( e:Event ):void
        {
            sendNotification( ShowDialogCommand.NAME, ShopScreenMediator.NAME );
        }
    }

    /**
     * Our shop screen. Almost identical to our inventory screen, except shows ALL item types with the option to purchase.
     */

    public class ShopScreenMediator extends InventoryScreenMediator
    {
        public static const NAME:String = "ShopScreenMediator";

        public function ShopScreenMediator()
        {
            super( NAME );
            _shopMode = true;
        }

        override protected function initializeView():void
        {
            _view.headerProperties[ "title" ] = "SHOP";

            var inventoryButton = new Button();
            inventoryButton.label = "INVENTORY";
            inventoryButton.addEventListener( Event.TRIGGERED, onInventoryButtonHit );

            _view.headerProperties[ "leftItems" ] = [ inventoryButton ];

            updateItems();
        }

        private function onInventoryButtonHit( e:Event ):void
        {
            sendNotification( ShowDialogCommand.NAME, InventoryScreenMediator.NAME );
        }
    }

    /**
     * A basic notification screen that shows a line of text and an OK button that brings you back to the shop.
     */

    public class NotificationScreenMediator extends BaseScreenMediator
    {
        public static const NAME:String = "NotificationScreenMediator";

        private var _notificationLabel:Label;
        private var _okButton:Button;

        public function NotificationScreenMediator()
        {
            super( NAME );
        }

        public function set notificationMessage( value:String ):void
        {
            _notificationLabel.text = value;
        }

        public function get notificationMessage():String
        {
            return _notificationLabel.text;
        }

        override protected function initializeView():void
        {
            _view.headerProperties[ "title" ] = "";
            _layout.padding = 100;
            _layout.gap = 50;
            _notificationLabel = _view.addChild( new Label() ) as Label;
            _okButton = _view.addChild( new Button() ) as Button;
            _okButton.label = "OK";
            _okButton.addEventListener( Event.TRIGGERED, onOKButtonHit );
        }

        private function onOKButtonHit( e:Event ):void
        {
            sendNotification( ShowDialogCommand.NAME, ShopScreenMediator.NAME );
        }
    }

    /**
     * A purchase confirmation screen. Makes sure you really want to buy an item.
     */

    public class PurchaseConfirmationScreenMediator extends BaseScreenMediator
    {
        public static const NAME:String = "PurchaseConfirmationScreenMediator";

        private var _confirmLabel:Label;
        private var _icon:Image;
        private var _confirmButton:Button;
        private var _cancelButton:Button;
        private var _itemId:String;

        public function PurchaseConfirmationScreenMediator()
        {
            super( NAME );
        }

        public function configure( itemId:String ):void
        {
            var item = ItemProxy( facade.retrieveProxy( ItemProxy.NAME ) ).getItemDataById( itemId );
            _confirmLabel.text = "Purchase a " + item.name + " for " + item.cost + " coins?";
            _icon.texture = Texture.fromAsset( item.imagePath );
            _icon.width = _icon.height = 128;
            _view.invalidate();
            _itemId = itemId;
        }

        override protected function initializeView():void
        {
            _layout.gap = 20;
            _layout.padding = 30;
            _view.headerProperties[ "title" ] = "CONFIRM PURCHASE";
            _confirmLabel = _view.addChild( new Label() ) as Label;
            _icon = _view.addChild( new Image() ) as Image;
            _confirmButton = _view.addChild( new Button() ) as Button;
            _confirmButton.label = "CONFIRM";
            _confirmButton.addEventListener( Event.TRIGGERED, onConfirm );
            _cancelButton = _view.addChild( new Button() ) as Button;
            _cancelButton.label = "CANCEL";
            _cancelButton.addEventListener( Event.TRIGGERED, onCancel );
        }

        private function onConfirm( e:Event ):void
        {
            sendNotification( ConfirmPurchaseCommand.NAME, _itemId );
        }

        private function onCancel( e:Event ):void
        {
            sendNotification( ShowDialogCommand.NAME, ShopScreenMediator.NAME );
        }
    }

    /**
     * A simple view component that shows item data and an optional purchase button
     */

    public class ItemRowView extends Sprite
    {
        private var _bgQuad:Quad;
        private var _icon:Image;
        private var _nameLabel:Label;
        private var _descriptionLabel:Label;
        private var _ownedLabel:Label;
        private var _costIcon:Image;
        private var _costLabel:Label;
        private var _purchaseButton:Button;
        private var _currentItem:ItemVO;

        public function ItemRowView()
        {
            _bgQuad = addChild( new Quad( Loom2D.stage.stageWidth, 100 ) ) as Quad;

            _icon = addChild( new Image() ) as Image;
            _icon.x = 20;
            _icon.y = 10;
            _nameLabel = addChild( new Label() ) as Label;
            _nameLabel.x = 100;
            _nameLabel.y = 30;
            _descriptionLabel = addChild( new Label() ) as Label;
            _descriptionLabel.x = 240;
            _descriptionLabel.y = 30;
            _ownedLabel = addChild( new Label() ) as Label;
            _ownedLabel.x = 550;
            _ownedLabel.y = 30;
            _costIcon = addChild( new Image( Texture.fromAsset( "assets/coin.png" ) ) ) as Image;
            _costIcon.width = _costIcon.height = 32;
            _costIcon.x = 700;
            _costIcon.y = 25;
            _costLabel = addChild( new Label() ) as Label;
            _costLabel.x = 734;
            _costLabel.y = 30;
            _purchaseButton = addChild( new Button() ) as Button;
            _purchaseButton.label = "PURCHASE";
            _purchaseButton.addEventListener( Event.TRIGGERED, onPurchase );
            _purchaseButton.x = 800;
            _purchaseButton.y = 20;
        }

        public function configure( item:ItemVO, quantityOwned:int, showPurchaseButton:Boolean, rowColor:uint ):void
        {
            _icon.texture = Texture.fromAsset( item.imagePath );
            _icon.width = _icon.height = 64;
            _nameLabel.text = item.name;
            _descriptionLabel.text = item.description;
            _ownedLabel.text = "Owned: " + quantityOwned;
            _costLabel.text = "x" + item.cost;
            _bgQuad.color = rowColor;
            _currentItem = item;

            _costIcon.visible = _costLabel.visible = _purchaseButton.visible = showPurchaseButton;
        }

        private function onPurchase( e:Event ):void
        {
            // Since this isn't a Mediator, we need to access the facade via its Singleton
            // to send a notification
            Facade.getInstance().sendNotification( RequestPurchaseCommand.NAME, _currentItem.id );
        }
    }
}