Last active
August 29, 2015 14:01
-
-
Save TheXenocide/78ce5b2291dc2dc819fb to your computer and use it in GitHub Desktop.
A remake of one of the earliest games I ever ported to JavaScript. I decided to remake it this past weekend while showing the old one to my roommate. Complete re-implementation using AngularJS took about 4 hours.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head><title>Lunar Lockout</title> | |
<script src="https://code.angularjs.org/1.2.9/angular.js"></script> | |
<script> | |
// note that ng-keydown is available since angular 1.1.5 | |
var app = angular.module('lunar', []); | |
app.filter('range', function() { | |
return function(input, total) { | |
total = parseInt(total); | |
for (var i = 0; i < total; i++) { | |
input.push(i); | |
} | |
return input; | |
}; | |
}); | |
(function() { | |
var currentLevel = 0; // TODO: allow continuing from last session | |
// "allLevels" is an array of levels | |
// each level is an array of characters | |
// characters are simple objects with x and y properties on a 5x5 grid, | |
// 0,0 being the top left and 4,4 being the bottom right. | |
// the first character is always the red "objective" character | |
var allLevels = [ | |
[{x:4,y:4},{x:4,y:0},{x:2,y:1},{x:1,y:2},{x:3,y:3}] | |
,[{x:1,y:4},{x:2,y:0},{x:4,y:1},{x:1,y:2},{x:3,y:3}] | |
,[{x:4,y:4},{x:3,y:0},{x:1,y:1},{x:3,y:3},{x:1,y:4}] | |
,[{x:1,y:4},{x:0,y:0},{x:4,y:0},{x:3,y:1},{x:2,y:3}] | |
,[{x:1,y:4},{x:3,y:0},{x:1,y:1},{x:3,y:4}] | |
,[{x:2,y:0},{x:0,y:0},{x:4,y:3},{x:1,y:4}] | |
,[{x:1,y:4},{x:2,y:0},{x:0,y:1},{x:3,y:2},{x:3,y:4}] | |
,[{x:3,y:3},{x:0,y:0},{x:3,y:0},{x:1,y:1},{x:0,y:4}] | |
,[{x:2,y:0},{x:4,y:0},{x:2,y:2},{x:1,y:3},{x:3,y:4}] | |
,[{x:4,y:4},{x:1,y:1},{x:4,y:2},{x:0,y:4},{x:2,y:4}] | |
,[{x:2,y:1},{x:2,y:0},{x:0,y:1},{x:4,y:1},{x:2,y:2},{x:2,y:3}] | |
,[{x:4,y:4},{x:1,y:0},{x:4,y:1},{x:1,y:4}] | |
,[{x:3,y:3},{x:1,y:0},{x:3,y:1},{x:0,y:3},{x:2,y:4}] | |
,[{x:1,y:4},{x:2,y:0},{x:4,y:1},{x:0,y:2},{x:3,y:3},{x:0,y:4}] | |
,[{x:4,y:4},{x:4,y:0},{x:1,y:1},{x:4,y:2},{x:2,y:3}] | |
,[{x:2,y:0},{x:0,y:0},{x:1,y:2},{x:4,y:2},{x:1,y:4}] | |
,[{x:1,y:4},{x:2,y:0},{x:4,y:0},{x:1,y:2},{x:3,y:3}] | |
,[{x:2,y:0},{x:0,y:4},{x:2,y:4},{x:4,y:4}] | |
,[{x:4,y:4},{x:1,y:0},{x:2,y:1},{x:3,y:2},{x:0,y:3},{x:2,y:4}] | |
,[{x:3,y:3},{x:1,y:0},{x:4,y:1},{x:0,y:2},{x:1,y:4}] | |
,[{x:2,y:0},{x:2,y:1},{x:2,y:2},{x:4,y:2},{x:0,y:3},{x:3,y:4}] | |
,[{x:4,y:4},{x:0,y:1},{x:4,y:1},{x:3,y:3},{x:0,y:4}] | |
,[{x:1,y:4},{x:1,y:0},{x:0,y:1},{x:3,y:1},{x:4,y:3}] | |
,[{x:0,y:4},{x:3,y:1},{x:1,y:2},{x:3,y:4},{x:4,y:4}] | |
,[{x:1,y:4},{x:0,y:1},{x:2,y:1},{x:3,y:1},{x:4,y:4}] | |
,[{x:2,y:0},{x:0,y:0},{x:4,y:1},{x:0,y:2},{x:3,y:3}] | |
,[{x:4,y:4},{x:1,y:0},{x:4,y:0},{x:0,y:2},{x:3,y:2},{x:0,y:4}] | |
,[{x:4,y:4},{x:1,y:0},{x:4,y:0},{x:4,y:2},{x:0,y:3}] | |
,[{x:2,y:0},{x:0,y:0},{x:4,y:0},{x:0,y:4},{x:4,y:4}] | |
,[{x:2,y:0},{x:0,y:1},{x:4,y:1},{x:0,y:2},{x:1,y:4},{x:3,y:4}] | |
,[{x:3,y:3},{x:0,y:0},{x:2,y:0},{x:4,y:1},{x:0,y:2},{x:1,y:4}] | |
,[{x:1,y:4},{x:1,y:0},{x:4,y:1},{x:0,y:4},{x:2,y:4}] | |
,[{x:4,y:4},{x:0,y:0},{x:1,y:0},{x:4,y:1},{x:0,y:3},{x:3,y:4}] | |
,[{x:4,y:4},{x:2,y:0},{x:4,y:0},{x:0,y:2},{x:3,y:3},{x:0,y:4}] | |
,[{x:2,y:0},{x:0,y:0},{x:4,y:0},{x:2,y:1},{x:0,y:4},{x:4,y:4}] | |
,[{x:2,y:0},{x:0,y:1},{x:4,y:1},{x:0,y:3},{x:4,y:3},{x:2,y:4}] | |
,[{x:3,y:3},{x:0,y:0},{x:3,y:0},{x:4,y:0},{x:0,y:3}] | |
,[{x:1,y:4},{x:2,y:0},{x:4,y:0},{x:0,y:2},{x:1,y:2},{x:4,y:4}] | |
,[{x:4,y:4},{x:0,y:0},{x:2,y:0},{x:4,y:0},{x:0,y:2},{x:0,y:4}] | |
,[{x:1,y:4},{x:0,y:0},{x:2,y:0},{x:4,y:0},{x:4,y:3}] | |
]; | |
var currentCharacters; | |
var selectedCharacter; | |
var setLevelImpl = function(level) { | |
currentLevel = level % allLevels.length; | |
currentCharacters = []; | |
// need to perform a deep clone to prevent altering source data from allLevels | |
var levelToClone = allLevels[currentLevel]; | |
for (var charIdx in levelToClone) { | |
var origChar = levelToClone[charIdx]; | |
currentCharacters.push({x: origChar.x, y: origChar.y}); | |
} | |
selectedCharacter = 0; | |
} | |
setLevelImpl(currentLevel); | |
app.factory('board', function() { | |
return { | |
setLevel: function(level) { | |
setLevelImpl(level); | |
}, | |
nextLevel: function() { | |
setLevelImpl(currentLevel + 1); | |
}, | |
resetLevel: function() { | |
setLevelImpl(currentLevel); | |
}, | |
getCurrentLevel: function() { | |
return currentLevel; | |
}, | |
setSelectedCharacter: function(index) { | |
selectedCharacter = index; | |
}, | |
getSelectedCharacter: function() { | |
return selectedCharacter; | |
}, | |
selectNextCharacter: function() { | |
selectedCharacter = (selectedCharacter + 1) % currentCharacters.length; | |
}, | |
// returns a true if the level is completed after the move, otherwise returns false | |
// allowed direction parameter values are: 'left', 'right', 'up', 'down' | |
moveSelectedCharacter: function(direction) { | |
// TODO: algorithm for movement | |
var selXY = currentCharacters[selectedCharacter]; | |
var nearestXY; | |
// -1 indicates that no collision was found and there is no "nearest character" to | |
// run into. If this value is any value other than -1 it represents the index of | |
// the character nearest to the selected character on the plane it is moving in | |
var nearestCharIdx = -1; | |
// we only need to worry about that character closest to the selected character so | |
// so we'll set up an initial "nearest" character that is farther away than any real | |
// character could be | |
switch (direction) { | |
case 'left': | |
case 'up': | |
nearestXY = {x: -1, y: -1}; | |
break; | |
case 'right': | |
case 'down': | |
nearestXY = {x: 5, y: 5}; | |
break; | |
} | |
for (var idx in currentCharacters) { | |
if (idx == selectedCharacter) continue; // skip selected character, can't collide with yourself | |
var $chr = currentCharacters[idx]; | |
if (direction == 'left' || direction == 'right') { | |
// find only characters with the same vertical position for left/right collisions | |
if ($chr.y == selXY.y) { | |
// if the character we're currently testing is nearer to the selected (moving) character | |
// than the previous closest, we'll mark the current character as the closest. | |
if ( | |
(direction == 'left' && $chr.x > nearestXY.x && $chr.x < selXY.x) || | |
(direction == 'right' && $chr.x < nearestXY.x && $chr.x > selXY.x) | |
) { | |
nearestXY = $chr; | |
nearestCharIdx = idx; | |
} | |
} | |
} else if (direction == 'up' || direction == 'down') { | |
// find only characters with the same horizontal position for up/down collisions | |
if ($chr.x == selXY.x) { | |
// if the character we're currently testing is nearer to the selected (moving) character | |
// than the previous closest, we'll mark the current character as the closest. | |
if ( | |
(direction == 'up' && $chr.y > nearestXY.y && $chr.y < selXY.y) || | |
(direction == 'down' && $chr.y < nearestXY.y && $chr.y > selXY.y) | |
) { | |
nearestXY = $chr; | |
nearestCharIdx = idx; | |
} | |
} | |
} | |
} | |
// if no characters are in the way | |
if (nearestCharIdx == -1) return false; | |
switch (direction) { | |
case 'left': | |
selXY.x = nearestXY.x + 1; | |
break; | |
case 'right': | |
selXY.x = nearestXY.x - 1; | |
break; | |
case 'up': | |
selXY.y = nearestXY.y + 1; | |
break; | |
case 'down': | |
selXY.y = nearestXY.y - 1; | |
break; | |
} | |
// character 0 is always the objective character; if he is located at 2,2 | |
// he has completed the puzzle so we return true. | |
if (selectedCharacter == 0 && selXY.x == 2 && selXY.y == 2) return true; | |
// otherwise we return false | |
return false; | |
}, | |
getCurrentCharacters: function() { | |
return currentCharacters; | |
} | |
}; | |
}); | |
})(); | |
app.controller('lockout', ['$scope','$timeout','board', function($scope, $timeout, board) { | |
//$scope.characters = board.getCurrentCharacters(); | |
//$scope.selected = board.getSelectedCharacter(); | |
$scope.getTileType = function(x, y) { | |
var sel = board.getSelectedCharacter(); | |
var chars = board.getCurrentCharacters(); | |
for (var idx in chars) { | |
var $chr = chars[idx]; | |
if ($chr.x == x && $chr.y == y) { | |
var ret = 'character idx' + idx; | |
if (idx == sel) ret += ' selected'; | |
return ret; | |
} | |
} | |
return (x == 2 && y == 2) ? 'target' : 'none'; | |
} | |
$scope.onTileClicked = function(x, y) { | |
var chars = board.getCurrentCharacters(); | |
for (var idx in chars) { | |
var $chr = chars[idx]; | |
if ($chr.x == x && $chr.y == y) { | |
board.setSelectedCharacter(idx); | |
return; | |
} | |
} | |
// TODO: click based movement | |
} | |
$scope.onKeyDown = function(ev) { | |
var lvlComplete = false; | |
var handled = false; | |
switch (ev.which) { | |
case 37: | |
lvlComplete = board.moveSelectedCharacter('left'); | |
handled = true; | |
break; | |
case 38: | |
lvlComplete = board.moveSelectedCharacter('up'); | |
handled = true; | |
break; | |
case 39: | |
lvlComplete = board.moveSelectedCharacter('right'); | |
handled = true; | |
break; | |
case 40: | |
lvlComplete = board.moveSelectedCharacter('down'); | |
handled = true; | |
break; | |
case 9: | |
board.selectNextCharacter(); | |
handled = true; | |
break; | |
case 32: // space key | |
case 82: // r key | |
board.resetLevel(); | |
handled = true; | |
break; | |
} | |
if (lvlComplete) { | |
$timeout(function() { | |
alert("Congratulations, you've cleared the level"); | |
board.nextLevel(); | |
}, 50); | |
} | |
if (handled) ev.preventDefault(); | |
//$scope.pressed = ev.which; | |
// 37 == left | |
// 38 == up | |
// 39 == right | |
// 40 == down | |
// 82 == "r" | |
// 32 == " " | |
}; | |
}]); | |
</script> | |
<style type="text/css"> | |
td { | |
border: solid 2px black; | |
width: 50; | |
height: 50; | |
} | |
td.target { | |
border: double 2px red; | |
} | |
td.character { | |
border-radius:25px; | |
} | |
td.idx0 { | |
background-color: red; | |
} | |
td.idx1 { | |
background-color: orange; | |
} | |
td.idx2 { | |
background-color: green; | |
} | |
td.idx3 { | |
background-color: purple; | |
} | |
td.idx4 { | |
background-color: yellow; | |
} | |
td.idx5 { | |
background-color: cyan; | |
} | |
td.selected { | |
border: dashed 2px black; | |
} | |
</style> | |
</head> | |
<body ng-app="lunar" ng-controller="lockout" ng-keydown="onKeyDown($event)"> | |
<table> | |
<tr ng-repeat="y in [] | range: 5"> | |
<td ng-repeat="x in [] | range: 5" id="tile{{x}}-{{y}}" class="{{getTileType(x,y)}}" ng-click="onTileClicked(x,y)"> | |
| |
</td> | |
</tr> | |
</table> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment