├── Controller └── Index │ └── Index.php ├── README.md ├── composer.json ├── etc ├── frontend │ └── routes.xml └── module.xml ├── registration.php └── view └── frontend ├── layout └── vinaikopp_breakout_index_index.xml ├── requirejs-config.js ├── templates └── breakout.phtml └── web ├── css └── breakout.css ├── js ├── dom-node-binding.js ├── game.js ├── game │ ├── ball.js │ ├── brick.js │ ├── frame.js │ ├── game-component.js │ ├── paddle.js │ ├── score-board.js │ ├── state │ │ ├── ball-state.js │ │ ├── brick-state-factory.js │ │ ├── frame-state.js │ │ ├── game-state.js │ │ ├── paddle-state.js │ │ ├── score-board-state.js │ │ └── wall-state.js │ └── wall.js └── writable-computed.js └── template ├── game-component.html └── score-board.html /Controller/Index/Index.php: -------------------------------------------------------------------------------- 1 | pageFactory = $pageFactory; 21 | } 22 | 23 | public function execute() 24 | { 25 | return $this->pageFactory->create(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UiComponents Breakout 2 | 3 | This is a small example module showing how to manage slightly more complex interdependent shared view state with Magento UiComponents. 4 | 5 | It uses a simple version of the 80s arcade game Breakout as a scenario. 6 | 7 | ![Breakout with UiComponents](https://media.giphy.com/media/wHApe1yNITxvO75Yw5/giphy.gif ) 8 | 9 | I think UiComponents where not designed to be used in this manner, but it works as a demo for external state management. 10 | 11 | This module was created written as an example for my presentation at MageTitansIT 2018 in Milano. 12 | 13 | (c) Vinai Kopp 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vinaikopp/module-breakout", 3 | "description": "n.a.", 4 | "require": { 5 | "php": "^7.0.0" 6 | }, 7 | "type": "magento2-module", 8 | "license": [ 9 | "BSD-3-Clause" 10 | ], 11 | "autoload": { 12 | "files": [ 13 | "registration.php" 14 | ], 15 | "psr-4": { 16 | "VinaiKopp\\Breakout\\": "" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /etc/frontend/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | Breakout 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /view/frontend/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | map: { 3 | '*': { 4 | gameComponent: 'VinaiKopp_Breakout/js/game/game-component', 5 | writableComputed: 'VinaiKopp_Breakout/js/writable-computed', 6 | gameState: 'VinaiKopp_Breakout/js/game/state/game-state', 7 | frameState: 'VinaiKopp_Breakout/js/game/state/frame-state', 8 | scoreBoardState: 'VinaiKopp_Breakout/js/game/state/score-board-state', 9 | ballState: 'VinaiKopp_Breakout/js/game/state/ball-state', 10 | paddleState: 'VinaiKopp_Breakout/js/game/state/paddle-state', 11 | wallState: 'VinaiKopp_Breakout/js/game/state/wall-state', 12 | brickComponent: 'VinaiKopp_Breakout/js/game/brick', 13 | brickFactory: 'VinaiKopp_Breakout/js/game/state/brick-state-factory' 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /view/frontend/templates/breakout.phtml: -------------------------------------------------------------------------------- 1 | 6 | 38 | 39 |
40 |
41 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /view/frontend/web/css/breakout.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('https://fonts.googleapis.com/css?family=Press+Start+2P'); 3 | 4 | #breakout-wrapper { 5 | height: 600px; 6 | font-family: 'Press Start 2P', cursive; 7 | } 8 | 9 | .breakout { 10 | position: relative; 11 | } 12 | 13 | .breakout div { 14 | position: absolute; 15 | } 16 | 17 | .score-board { 18 | padding-top: .4em; 19 | padding-left: .5em; 20 | } 21 | 22 | .score-board h1 { 23 | margin-bottom: 0.2em; 24 | } 25 | -------------------------------------------------------------------------------- /view/frontend/web/js/dom-node-binding.js: -------------------------------------------------------------------------------- 1 | define(['ko'], function(ko) { 2 | 'use strict'; 3 | 4 | ko.bindingHandlers.bindDomNode = { 5 | init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { 6 | if (valueAccessor()) { 7 | bindingContext.$data.domNode = element; 8 | } 9 | } 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /view/frontend/web/js/game.js: -------------------------------------------------------------------------------- 1 | define(['gameComponent', 'ko', 'gameState'], function (GameComponent, ko, game) { 2 | 'use strict'; 3 | 4 | return GameComponent.extend({ 5 | initialize: function () { 6 | this._super(); 7 | this.initDimensions(game); 8 | ko.defineProperty(this, 'running', function () { 9 | return game.running; 10 | }); 11 | this.setStartState(); 12 | }, 13 | setStartState: function () { 14 | game.width = 600; 15 | game.height = 500; 16 | game.running = false; 17 | game.status = 'pending'; 18 | this._super(); 19 | }, 20 | run: function () { 21 | this._super(); 22 | if (game.status !== 'started' && game.running) { 23 | game.running = false; 24 | } 25 | if (game.running) { 26 | window.requestAnimationFrame(this.run.bind(this)); 27 | } 28 | }, 29 | start: function () { 30 | if (game.status === 'win' || game.status === 'fail') { 31 | this.reset(); 32 | } else { 33 | if (game.status === 'pending') { 34 | game.status = 'started'; 35 | } 36 | if (!game.running) { 37 | game.running = true; 38 | this.run(); 39 | } 40 | } 41 | }, 42 | stop: function () { 43 | game.running = false; 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/ball.js: -------------------------------------------------------------------------------- 1 | define(['gameComponent', 'ballState', 'frameState'], function (GameComponent, ball, frame) { 2 | 'use strict'; 3 | 4 | return GameComponent.extend({ 5 | defaults: { 6 | backgroundColor: 'red', 7 | initialVelocity: 5 8 | }, 9 | initialize: function () { 10 | this._super(); 11 | this.initDimensions(ball); 12 | this.setStartState(); 13 | }, 14 | setStartState: function () { 15 | ball.width = 20; 16 | ball.height = 20; 17 | ball.left = (frame.width / 2) - (ball.width / 2); 18 | ball.top = (frame.height * .60) - (ball.height / 2); 19 | ball.xV = ball.yV = this.initialVelocity; 20 | }, 21 | run: function () { 22 | this._super(); 23 | ball.left += ball.xV; 24 | ball.top += ball.yV; 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/brick.js: -------------------------------------------------------------------------------- 1 | define( 2 | ['gameComponent', 'wallState', 'ballState', 'scoreBoardState'], 3 | function (GameComponent, wall, ball, scoreBoard) { 4 | 'use strict'; 5 | 6 | function transposeBallFromFrameToWallCoordinates(ballInFrameCoords, wall) { 7 | const ballInWallCoords = {}; 8 | 9 | ['left', 'top'].forEach(function (p) { 10 | Object.defineProperty(ballInWallCoords, p, { 11 | get: function () { 12 | return ballInFrameCoords[p] - wall[p]; 13 | }, 14 | set: function (v) { 15 | ballInFrameCoords[p] = v + wall[p]; 16 | } 17 | }); 18 | }); 19 | ['width', 'height'].forEach(function (p) { 20 | Object.defineProperty(ballInWallCoords, p, { 21 | get: function () { 22 | return ballInFrameCoords[p]; 23 | } 24 | }); 25 | }); 26 | ['xV', 'yV'].forEach(function (p) { 27 | Object.defineProperty(ballInWallCoords, p, { 28 | get: function () { 29 | return ballInFrameCoords[p]; 30 | }, 31 | set: function (v) { 32 | ballInFrameCoords[p] = v; 33 | } 34 | }); 35 | }); 36 | return ballInWallCoords; 37 | } 38 | 39 | function bottomSide(thing) { 40 | return thing.top + thing.height; 41 | } 42 | 43 | function rightSide(thing) { 44 | return thing.left + thing.width; 45 | } 46 | 47 | function horizontallyIntersects(ball, brick) { 48 | return rightSide(ball) > brick.left && ball.left < rightSide(brick); 49 | } 50 | 51 | function verticallyIntersects(ball, brick) { 52 | return bottomSide(ball) > brick.top && ball.top < bottomSide(brick); 53 | } 54 | 55 | function goingUp(ball) { 56 | return ball.yV < 0; 57 | } 58 | 59 | function goingDown(ball) { 60 | return ball.yV > 0; 61 | } 62 | 63 | function goingRight(ball) { 64 | return ball.xV > 0; 65 | } 66 | 67 | function goingLeft(ball) { 68 | return ball.xV < 0; 69 | } 70 | 71 | function ballBelow(ball, brick) { 72 | return ball.top > bottomSide(brick); 73 | } 74 | 75 | function ballAbove(ball, brick) { 76 | return bottomSide(ball) < brick.top; 77 | } 78 | 79 | function ballRightOf(ball, brick) { 80 | return ball.left > brick.left + brick.width; 81 | } 82 | 83 | function ballLeftOf(ball, brick) { 84 | return rightSide(ball) < brick.left; 85 | } 86 | 87 | function hitFromBelow(ball, brick) { 88 | const touch = ball.top < bottomSide(brick); 89 | return goingUp(ball) && horizontallyIntersects(ball, brick) && !ballAbove(ball, brick) && touch; 90 | } 91 | 92 | function hitFromAbove(ball, brick) { 93 | const touch = bottomSide(ball) > brick.top; 94 | return goingDown(ball) && horizontallyIntersects(ball, brick) && !ballBelow(ball, brick) && touch; 95 | } 96 | 97 | function hitFromLeft(ball, brick) { 98 | const touch = rightSide(ball) > brick.left; 99 | return goingRight(ball) && verticallyIntersects(ball, brick) && touch && !ballRightOf(ball, brick); 100 | } 101 | 102 | function hitFromRight(ball, brick) { 103 | const touch = ball.left < rightSide(brick); 104 | return goingLeft(ball) && verticallyIntersects(ball, brick) && touch && !ballLeftOf(ball, brick); 105 | } 106 | 107 | function speedUp(ball) { 108 | ball.yV += goingDown(ball) ? 0.3 : -0.3; 109 | ball.xV += goingRight(ball) ? 0.3 : -0.3; 110 | } 111 | 112 | 113 | ball = transposeBallFromFrameToWallCoordinates(ball, wall); 114 | 115 | return GameComponent.extend({ 116 | initialize: function (options) { 117 | const brick = options.brick; 118 | this.index = 'brick-' + brick.row + '-' + brick.col; 119 | this.initDimensions(brick); 120 | brick.width = function () { 121 | return wall.width / wall.cols; 122 | }; 123 | brick.height = function () { 124 | return wall.height / wall.rows; 125 | }; 126 | brick.left = function () { 127 | return brick.col * brick.width; 128 | }; 129 | brick.top = function () { 130 | return brick.row * brick.height; 131 | }; 132 | this._super(); 133 | }, 134 | run: function () { 135 | const brick = this.brick; 136 | 137 | if (hitFromBelow(ball, brick) || hitFromAbove(ball, brick)) { 138 | this.verticalHit(brick); 139 | } 140 | else if (hitFromLeft(ball, brick) || hitFromRight(ball, brick)) { 141 | this.horizontalHit(brick); 142 | } 143 | this._super(); 144 | }, 145 | verticalHit: function (brick) { 146 | ball.top = goingDown(ball) ? brick.top - ball.height - 1 : bottomSide(brick) + 1; 147 | ball.yV *= -1; 148 | this.hit(); 149 | }, 150 | horizontalHit: function (brick) { 151 | ball.left = goingRight(ball) ? brick.left - ball.width - 1 : rightSide(brick) + 1; 152 | ball.xV *= -1; 153 | this.hit(); 154 | }, 155 | hit: function () { 156 | scoreBoard.score++; 157 | wall.brickCount--; 158 | speedUp(ball); 159 | this.destroy(); 160 | } 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/frame.js: -------------------------------------------------------------------------------- 1 | define(['gameComponent', 'ballState', 'frameState', 'gameState'], function (GameComponent, ball, frame, game) { 2 | 'use strict'; 3 | 4 | return GameComponent.extend({ 5 | initialize: function () { 6 | this._super(); 7 | this.initDimensions(frame); 8 | frame.width = function () { 9 | return game.width * .98; 10 | }; 11 | frame.height = function () { 12 | return game.height * .80; 13 | }; 14 | frame.left = function () { 15 | return game.width * .01; 16 | }; 17 | frame.top = function () { 18 | return game.height * .19; 19 | }; 20 | }, 21 | run: function () { 22 | this._super(); 23 | this.checkBallCollision(); 24 | }, 25 | checkBallCollision: function () { 26 | if (ball.left < 0) { 27 | ball.xV *= -1; 28 | ball.left = 0; 29 | } else if (ball.left + ball.width > frame.width) { 30 | ball.xV *= -1; 31 | ball.left = frame.width - ball.width; 32 | } 33 | if (ball.top < 0) { 34 | ball.yV *= -1; 35 | ball.top = 0; 36 | } else if (ball.top + ball.height > frame.height) { 37 | game.status = 'fail'; 38 | ball.yV *= -1; 39 | ball.top = frame.height - ball.height; 40 | } 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/game-component.js: -------------------------------------------------------------------------------- 1 | define(['uiComponent', 'ko', 'VinaiKopp_Breakout/js/dom-node-binding', 'es6-collections'], function (Component, ko) { 2 | 'use strict'; 3 | 4 | return Component.extend({ 5 | defaults: { 6 | template: 'VinaiKopp_Breakout/game-component', 7 | border: '1px solid black', 8 | backgroundColor: 'white', 9 | bindDomNode: false 10 | }, 11 | initDimensions: function (state) { 12 | ['width', 'height', 'top', 'left'].forEach(function (property) { 13 | ko.defineProperty(this, property, function () { 14 | return state[property] && state[property] + 'px'; 15 | }); 16 | }.bind(this)); 17 | }, 18 | run: function () { 19 | this.elems.each(function (elem) { 20 | elem.run(); 21 | }); 22 | }, 23 | setStartState: function () { 24 | 25 | }, 26 | reset: function () { 27 | this.setStartState(); 28 | this.elems.each(function (elem) { 29 | elem.reset(); 30 | }); 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/paddle.js: -------------------------------------------------------------------------------- 1 | define( 2 | ['gameComponent', 'ko', 'ballState', 'frameState', 'paddleState'], 3 | function (GameComponent, ko, ball, frame, paddle) { 4 | 'use strict'; 5 | 6 | var targetPageX = ko.observable(); 7 | 8 | var inFrameXPos; 9 | 10 | const trackMouse = function (e) { 11 | targetPageX(e.pageX); 12 | }; 13 | 14 | function calculatePaddlePosition (paddleDomNode) { 15 | const elementPageX = paddleDomNode ? paddleDomNode.getBoundingClientRect().left : 0; 16 | const delta = targetPageX() - elementPageX; 17 | const targetFrameX = inFrameXPos + delta - Math.floor(paddle.width / 2); 18 | return inFrameXPos = Math.min(Math.max(0, targetFrameX), frame.width - paddle.width); 19 | } 20 | 21 | return GameComponent.extend({ 22 | defaults: { 23 | bindDomNode: true, 24 | pauseWithGame: true 25 | }, 26 | initialize: function () { 27 | this._super(); 28 | this.initDimensions(paddle); 29 | paddle.width = function () { 30 | return frame.width * .21; 31 | }; 32 | paddle.height = function () { 33 | return frame.height * .05; 34 | }; 35 | inFrameXPos = (frame.width / 2) - (paddle.width / 2); 36 | this.setStartState(); 37 | if (! this.pauseWithGame) { 38 | paddle.left = function () { 39 | return calculatePaddlePosition(this.domNode); 40 | }.bind(this); 41 | } 42 | document.addEventListener('mousemove', trackMouse); 43 | }, 44 | destroy: function () { 45 | document.removeEventListener('mousemove', trackMouse); 46 | this._super(); 47 | }, 48 | setStartState: function () { 49 | paddle.top = frame.height - paddle.height - 2; 50 | const middlePosition = (frame.width / 2) - (paddle.width / 2); 51 | targetPageX(middlePosition); 52 | if (this.pauseWithGame) { 53 | paddle.left = middlePosition; 54 | } 55 | }, 56 | run: function () { 57 | if (this.pauseWithGame) { 58 | paddle.left = calculatePaddlePosition(this.domNode); 59 | } 60 | this.checkBallCollision(); 61 | this._super(); 62 | }, 63 | checkBallCollision: function () { 64 | const ballRight = ball.left + ball.width; 65 | const ballBottom = ball.top + ball.height; 66 | const paddleRight = paddle.left + paddle.width; 67 | 68 | if (ball.left < paddleRight && 69 | ballRight > paddle.left && 70 | ballBottom > paddle.top && 71 | ball.top < paddle.top 72 | ) { 73 | ball.yV *= -1; 74 | } 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/score-board.js: -------------------------------------------------------------------------------- 1 | define(['gameComponent', 'ko', 'scoreBoardState', 'gameState'], function (GameComponent, ko, scoreBoard, game) { 2 | 'use strict'; 3 | 4 | const statusLabels = { 5 | pending: 'Click to start Game', 6 | started: 'Running', 7 | paused: 'Paused', 8 | win: 'Congrats, you win! Click to reset.', 9 | fail: 'Game Over! Click to reset.' 10 | }; 11 | 12 | return GameComponent.extend({ 13 | defaults: { 14 | template: "VinaiKopp_Breakout/score-board" 15 | }, 16 | initialize: function () { 17 | this._super(); 18 | this.initDimensions(scoreBoard); 19 | scoreBoard.width = function () { 20 | return game.width * .97; 21 | }; 22 | scoreBoard.height = function () { 23 | return game.height * .15; 24 | }; 25 | scoreBoard.left = function () { 26 | return game.width * .01; 27 | }; 28 | scoreBoard.top = function () { 29 | return game.height * .01; 30 | }; 31 | ko.defineProperty(this, 'score', function () { 32 | return scoreBoard.score; 33 | }); 34 | this.setStartState(); 35 | }, 36 | statusLabel: ko.pureComputed(function () { 37 | if (game.status === 'started') { 38 | return game.running ? statusLabels['started'] : statusLabels['paused']; 39 | } 40 | return statusLabels[game.status] || 'Error: Unknown Status "' + game.status + '"'; 41 | }), 42 | setStartState: function () { 43 | scoreBoard.score = 0; 44 | this._super(); 45 | } 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/ball-state.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return ko.track({ 5 | width: writableComputed(), 6 | height: writableComputed(), 7 | left: writableComputed(), 8 | top: writableComputed(), 9 | xV: 0, 10 | yV: 0 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/brick-state-factory.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return function (row, col) { 5 | return ko.track({ 6 | width: writableComputed(), 7 | height: writableComputed(), 8 | top: writableComputed(), 9 | left: writableComputed(), 10 | row: row, 11 | col: col 12 | }); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/frame-state.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return ko.track({ 5 | width: writableComputed(), 6 | height: writableComputed(), 7 | top: writableComputed(), 8 | left: writableComputed() 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/game-state.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return ko.track({ 5 | width: writableComputed(), 6 | height: writableComputed(), 7 | left: writableComputed(), 8 | top: writableComputed(), 9 | running: false, 10 | status: null 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/paddle-state.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return ko.track({ 5 | width: writableComputed(), 6 | height: writableComputed(), 7 | left: writableComputed(), 8 | top: writableComputed() 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/score-board-state.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return ko.track({ 5 | width: writableComputed(), 6 | height: writableComputed(), 7 | top: writableComputed(), 8 | left: writableComputed(), 9 | score: 0 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/state/wall-state.js: -------------------------------------------------------------------------------- 1 | define(['ko', 'writableComputed'], function (ko, writableComputed) { 2 | 'use strict'; 3 | 4 | return ko.track({ 5 | width: writableComputed(), 6 | height: writableComputed(), 7 | top: writableComputed(), 8 | left: writableComputed(), 9 | rows: 0, 10 | cols: 0, 11 | brickCount: 0 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /view/frontend/web/js/game/wall.js: -------------------------------------------------------------------------------- 1 | define( 2 | ['gameComponent', 'ko', 'frameState', 'wallState', 'brickComponent', 'brickFactory', 'gameState'], 3 | function (GameComponent, ko, frame, wall, BrickComponent, createBrick, game) { 4 | 'use strict'; 5 | 6 | return GameComponent.extend({ 7 | defaults: { 8 | border: 'none', 9 | backgroundColor: 'transparent' 10 | }, 11 | initialize: function () { 12 | this._super(); 13 | this.initDimensions(wall); 14 | wall.cols = 5; 15 | wall.rows = 4; 16 | wall.left = 0; 17 | wall.top = function () { 18 | return frame.height * .1; 19 | }; 20 | wall.height = function () { 21 | return frame.height * .4; 22 | }; 23 | wall.width = function () { 24 | return frame.width; 25 | }; 26 | this.setStartState(); 27 | }, 28 | setStartState: function () { 29 | this.destroyChildren(); 30 | for (var row = 0; row < wall.rows; row++) { 31 | for (var col = 0; col < wall.cols; col++) { 32 | this.insertChild(new BrickComponent({brick: createBrick(row, col)})); 33 | } 34 | } 35 | wall.brickCount = wall.rows * wall.cols; 36 | }, 37 | run: function () { 38 | this._super(); 39 | if (wall.brickCount === 0) { 40 | game.status = 'win'; 41 | } 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /view/frontend/web/js/writable-computed.js: -------------------------------------------------------------------------------- 1 | define(['ko'], function (ko) { 2 | 'use strict'; 3 | 4 | return function writableObservable(initialValue) { 5 | 6 | const observable = ko.observable(initialValue); 7 | 8 | return ko.pureComputed({ 9 | read: function () { 10 | const v = observable(); 11 | return v instanceof Function ? v() : v; 12 | }, 13 | write: function (newValue) { 14 | observable(newValue); 15 | } 16 | }) 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /view/frontend/web/template/game-component.html: -------------------------------------------------------------------------------- 1 |
6 | 7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /view/frontend/web/template/score-board.html: -------------------------------------------------------------------------------- 1 |
5 |

Score:

6 |
7 | 8 |
9 | --------------------------------------------------------------------------------