From a7b41552152c734331f751e5e0f0d7c3746651cf Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 00:22:30 +0000 Subject: [PATCH 01/21] Put some simple instructions in --- js/main.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/main.js b/js/main.js index 2622e85..1713b08 100644 --- a/js/main.js +++ b/js/main.js @@ -98,6 +98,7 @@ function drawScene (images) { infoBox = new InfoBox({ initialLines : [ 'SkiFree.js', + 'Use the mouse to control the skier', 'Travelled 0m', 'Skiers left: ' + livesLeft, 'Created by Dan Hough (@basicallydan)' @@ -187,6 +188,7 @@ function drawScene (images) { infoBox.setLines([ 'SkiFree.js', + 'Use the mouse to control the skier', 'Travelled ' + distanceTravelledInMetres + 'm', 'Skiers left: ' + livesLeft, 'Created by Dan Hough (@basicallydan)' From 0be5f2bb32d6b4cfecfde968e7c4b6bd88de6043 Mon Sep 17 00:00:00 2001 From: Daniel Hough Date: Fri, 1 Feb 2013 00:18:36 +0000 Subject: [PATCH 02/21] Updated features --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9759742..4907e42 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ I just started work this Christmas 2012 so I've not done a whole lot yet. * Collision detection with trees - and the appropriate reaction * Turning left and right * Stopping -* MONSTAHS! GRAAAAHHH! Although, all they will do at the moment is whack into you and slow you down for a minute. +* MONSTAHS! GRAAAAHHH! They even eat you and then run away because they're full +* Distance tracking so you can see how far you've gone you absolute badass ## So, what's left to do? This is what I'm gonna do, probably in this order. Who the hell knows. There are other features to the original game but I'm not going to add them to the list until I've gotten through this one. -* Distance tracking * Rainbow jump platforms & jumping * Big trees & crashing into them both whilst skiing and jumping * Rocks From b9a3ad310b3f09ac56d71f42fe5d091549b9a0e3 Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 07:06:29 +0000 Subject: [PATCH 03/21] Using touch events to move so that it'll work on phones --- index.html | 2 + js/hammer.js | 859 ++++++++++++++++++++++++++++++++++++++++++++ js/jquery.hammer.js | 26 ++ js/main.js | 16 +- 4 files changed, 900 insertions(+), 3 deletions(-) create mode 100755 js/hammer.js create mode 100755 js/jquery.hammer.js diff --git a/index.html b/index.html index 25d9aa6..aa5a12d 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,8 @@ + + diff --git a/js/hammer.js b/js/hammer.js new file mode 100755 index 0000000..b2a738d --- /dev/null +++ b/js/hammer.js @@ -0,0 +1,859 @@ +/* + * Hammer.JS + * version 0.6.4 + * author: Eight Media + * https://github.com/EightMedia/hammer.js + * Licensed under the MIT license. + */ +function Hammer(element, options, undefined) +{ + var self = this; + + var defaults = mergeObject({ + // prevent the default event or not... might be buggy when false + prevent_default : false, + css_hacks : true, + + swipe : true, + swipe_time : 500, // ms + swipe_min_distance : 20, // pixels + + drag : true, + drag_vertical : true, + drag_horizontal : true, + // minimum distance before the drag event starts + drag_min_distance : 20, // pixels + + // pinch zoom and rotation + transform : true, + scale_treshold : 0.1, + rotation_treshold : 15, // degrees + + tap : true, + tap_double : true, + tap_max_interval : 300, + tap_max_distance : 10, + tap_double_distance: 20, + + hold : true, + hold_timeout : 500, + + allow_touch_and_mouse : false + }, Hammer.defaults || {}); + options = mergeObject(defaults, options); + + // some css hacks + (function() { + if(!options.css_hacks) { + return false; + } + + var vendors = ['webkit','moz','ms','o','']; + var css_props = { + "userSelect": "none", + "touchCallout": "none", + "touchAction": "none", + "userDrag": "none", + "tapHighlightColor": "rgba(0,0,0,0)" + }; + + var prop = ''; + for(var i = 0; i < vendors.length; i++) { + for(var p in css_props) { + prop = p; + if(vendors[i]) { + prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); + } + element.style[ prop ] = css_props[p]; + } + } + })(); + + // holds the distance that has been moved + var _distance = 0; + + // holds the exact angle that has been moved + var _angle = 0; + + // holds the direction that has been moved + var _direction = 0; + + // holds position movement for sliding + var _pos = { }; + + // how many fingers are on the screen + var _fingers = 0; + + var _first = false; + + var _gesture = null; + var _prev_gesture = null; + + var _touch_start_time = null; + var _prev_tap_pos = {x: 0, y: 0}; + var _prev_tap_end_time = null; + + var _hold_timer = null; + + var _offset = {}; + + // keep track of the mouse status + var _mousedown = false; + + var _event_start; + var _event_move; + var _event_end; + + var _has_touch = ('ontouchstart' in window); + + var _can_tap = false; + + + /** + * option setter/getter + * @param string key + * @param mixed value + * @return mixed value + */ + this.option = function(key, val) { + if(val !== undefined) { + options[key] = val; + } + + return options[key]; + }; + + + /** + * angle to direction define + * @param float angle + * @return string direction + */ + this.getDirectionFromAngle = function( angle ) { + var directions = { + down: angle >= 45 && angle < 135, //90 + left: angle >= 135 || angle <= -135, //180 + up: angle < -45 && angle > -135, //270 + right: angle >= -45 && angle <= 45 //0 + }; + + var direction, key; + for(key in directions){ + if(directions[key]){ + direction = key; + break; + } + } + return direction; + }; + + + /** + * destroy events + * @return void + */ + this.destroy = function() { + if(_has_touch || options.allow_touch_and_mouse) { + removeEvent(element, "touchstart touchmove touchend touchcancel", handleEvents); + } + + // for non-touch + if (!_has_touch || options.allow_touch_and_mouse) { + removeEvent(element, "mouseup mousedown mousemove", handleEvents); + removeEvent(element, "mouseout", handleMouseOut); + } + }; + + + /** + * count the number of fingers in the event + * when no fingers are detected, one finger is returned (mouse pointer) + * @param event + * @return int fingers + */ + function countFingers( event ) + { + // there is a bug on android (until v4?) that touches is always 1, + // so no multitouch is supported, e.g. no, zoom and rotation... + return event.touches ? event.touches.length : 1; + } + + /** + * Gets the event xy positions from a mouse event. + * @param event + * @return {Array} + */ + function getXYMouse(event) { + var doc = document, + body = doc.body; + + return [{ + x: event.pageX || event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && doc.clientLeft || 0 ), + y: event.pageY || event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && doc.clientTop || 0 ) + }]; + } + + /** + * gets the event xy positions from touch event. + * @param event + * @return {Array} + */ + function getXYTouch(event) { + var pos = [], src; + for(var t=0, len = options.two_touch_max ? Math.min(2, event.touches.length) : event.touches.length; t 0) { + + _fn = getXYTouch; + } + } else { + _fn = getXYTouch; + } + + return _fn(event); + } + + + /** + * calculate the angle between two points + * @param object pos1 { x: int, y: int } + * @param object pos2 { x: int, y: int } + */ + function getAngle( pos1, pos2 ) + { + return Math.atan2(pos2.y - pos1.y, pos2.x - pos1.x) * 180 / Math.PI; + } + + /** + * calculate the distance between two points + * @param object pos1 { x: int, y: int } + * @param object pos2 { x: int, y: int } + */ + function getDistance( pos1, pos2 ) + { + var x = pos2.x - pos1.x, y = pos2.y - pos1.y; + return Math.sqrt((x * x) + (y * y)); + } + + + /** + * calculate the scale size between two fingers + * @param object pos_start + * @param object pos_move + * @return float scale + */ + function calculateScale(pos_start, pos_move) + { + if(pos_start.length == 2 && pos_move.length == 2) { + var start_distance = getDistance(pos_start[0], pos_start[1]); + var end_distance = getDistance(pos_move[0], pos_move[1]); + return end_distance / start_distance; + } + + return 0; + } + + + /** + * calculate the rotation degrees between two fingers + * @param object pos_start + * @param object pos_move + * @return float rotation + */ + function calculateRotation(pos_start, pos_move) + { + if(pos_start.length == 2 && pos_move.length == 2) { + var start_rotation = getAngle(pos_start[1], pos_start[0]); + var end_rotation = getAngle(pos_move[1], pos_move[0]); + return end_rotation - start_rotation; + } + + return 0; + } + + + /** + * trigger an event/callback by name with params + * @param string name + * @param array params + */ + function triggerEvent( eventName, params ) + { + // return touches object + params.touches = getXYfromEvent(params.originalEvent); + params.type = eventName; + + // trigger callback + if(isFunction(self["on"+ eventName])) { + self["on"+ eventName].call(self, params); + } + } + + + /** + * cancel event + * @param object event + * @return void + */ + + function cancelEvent(event) + { + event = event || window.event; + if(event.preventDefault){ + event.preventDefault(); + event.stopPropagation(); + }else{ + event.returnValue = false; + event.cancelBubble = true; + } + } + + + /** + * reset the internal vars to the start values + */ + function reset() + { + _pos = {}; + _first = false; + _fingers = 0; + _distance = 0; + _angle = 0; + _gesture = null; + } + + + var gestures = { + // hold gesture + // fired on touchstart + hold : function(event) + { + // only when one finger is on the screen + if(options.hold) { + _gesture = 'hold'; + clearTimeout(_hold_timer); + + _hold_timer = setTimeout(function() { + if(_gesture == 'hold') { + triggerEvent("hold", { + originalEvent : event, + position : _pos.start + }); + } + }, options.hold_timeout); + } + }, + + // swipe gesture + // fired on touchend + swipe : function(event) + { + if (!_pos.move || _gesture === "transform") { + return; + } + + // get the distance we moved + var _distance_x = _pos.move[0].x - _pos.start[0].x; + var _distance_y = _pos.move[0].y - _pos.start[0].y; + _distance = Math.sqrt(_distance_x*_distance_x + _distance_y*_distance_y); + + // compare the kind of gesture by time + var now = new Date().getTime(); + var touch_time = now - _touch_start_time; + + if(options.swipe && (options.swipe_time >= touch_time) && (_distance >= options.swipe_min_distance)) { + // calculate the angle + _angle = getAngle(_pos.start[0], _pos.move[0]); + _direction = self.getDirectionFromAngle(_angle); + + _gesture = 'swipe'; + + var position = { x: _pos.move[0].x - _offset.left, + y: _pos.move[0].y - _offset.top }; + + var event_obj = { + originalEvent : event, + position : position, + direction : _direction, + distance : _distance, + distanceX : _distance_x, + distanceY : _distance_y, + angle : _angle + }; + + // normal slide event + triggerEvent("swipe", event_obj); + } + }, + + + // drag gesture + // fired on mousemove + drag : function(event) + { + // get the distance we moved + var _distance_x = _pos.move[0].x - _pos.start[0].x; + var _distance_y = _pos.move[0].y - _pos.start[0].y; + _distance = Math.sqrt(_distance_x * _distance_x + _distance_y * _distance_y); + + // drag + // minimal movement required + if(options.drag && (_distance > options.drag_min_distance) || _gesture == 'drag') { + // calculate the angle + _angle = getAngle(_pos.start[0], _pos.move[0]); + _direction = self.getDirectionFromAngle(_angle); + + // check the movement and stop if we go in the wrong direction + var is_vertical = (_direction == 'up' || _direction == 'down'); + + if(((is_vertical && !options.drag_vertical) || (!is_vertical && !options.drag_horizontal)) && (_distance > options.drag_min_distance)) { + return; + } + + _gesture = 'drag'; + + var position = { x: _pos.move[0].x - _offset.left, + y: _pos.move[0].y - _offset.top }; + + var event_obj = { + originalEvent : event, + position : position, + direction : _direction, + distance : _distance, + distanceX : _distance_x, + distanceY : _distance_y, + angle : _angle + }; + + // on the first time trigger the start event + if(_first) { + triggerEvent("dragstart", event_obj); + + _first = false; + } + + // normal slide event + triggerEvent("drag", event_obj); + + cancelEvent(event); + } + }, + + + // transform gesture + // fired on touchmove + transform : function(event) + { + if(options.transform) { + var count = countFingers(event); + if (count !== 2) { + return false; + } + + var rotation = calculateRotation(_pos.start, _pos.move); + var scale = calculateScale(_pos.start, _pos.move); + + if (_gesture === 'transform' || + Math.abs(1 - scale) > options.scale_treshold || + Math.abs(rotation) > options.rotation_treshold) { + + _gesture = 'transform'; + _pos.center = { + x: ((_pos.move[0].x + _pos.move[1].x) / 2) - _offset.left, + y: ((_pos.move[0].y + _pos.move[1].y) / 2) - _offset.top + }; + + if(_first) + _pos.startCenter = _pos.center; + + var _distance_x = _pos.center.x - _pos.startCenter.x; + var _distance_y = _pos.center.y - _pos.startCenter.y; + _distance = Math.sqrt(_distance_x*_distance_x + _distance_y*_distance_y); + + var event_obj = { + originalEvent : event, + position : _pos.center, + scale : scale, + rotation : rotation, + distance : _distance, + distanceX : _distance_x, + distanceY : _distance_y + }; + + // on the first time trigger the start event + if (_first) { + triggerEvent("transformstart", event_obj); + _first = false; + } + + triggerEvent("transform", event_obj); + + cancelEvent(event); + + return true; + } + } + + return false; + }, + + + // tap and double tap gesture + // fired on touchend + tap : function(event) + { + // compare the kind of gesture by time + var now = new Date().getTime(); + var touch_time = now - _touch_start_time; + + // dont fire when hold is fired + if(options.hold && !(options.hold && options.hold_timeout > touch_time)) { + return; + } + + // when previous event was tap and the tap was max_interval ms ago + var is_double_tap = (function(){ + if (_prev_tap_pos && + options.tap_double && + _prev_gesture == 'tap' && + _pos.start && + (_touch_start_time - _prev_tap_end_time) < options.tap_max_interval) + { + var x_distance = Math.abs(_prev_tap_pos[0].x - _pos.start[0].x); + var y_distance = Math.abs(_prev_tap_pos[0].y - _pos.start[0].y); + return (_prev_tap_pos && _pos.start && Math.max(x_distance, y_distance) < options.tap_double_distance); + } + return false; + })(); + + if(is_double_tap) { + _gesture = 'double_tap'; + _prev_tap_end_time = null; + + triggerEvent("doubletap", { + originalEvent : event, + position : _pos.start + }); + cancelEvent(event); + } + + // single tap is single touch + else { + var x_distance = (_pos.move) ? Math.abs(_pos.move[0].x - _pos.start[0].x) : 0; + var y_distance = (_pos.move) ? Math.abs(_pos.move[0].y - _pos.start[0].y) : 0; + _distance = Math.max(x_distance, y_distance); + + if(_distance < options.tap_max_distance) { + _gesture = 'tap'; + _prev_tap_end_time = now; + _prev_tap_pos = _pos.start; + + if(options.tap) { + triggerEvent("tap", { + originalEvent : event, + position : _pos.start + }); + cancelEvent(event); + } + } + } + } + }; + + + function handleEvents(event) + { + var count; + switch(event.type) + { + case 'mousedown': + case 'touchstart': + count = countFingers(event); + _can_tap = count === 1; + + //We were dragging and now we are zooming. + if (count === 2 && _gesture === "drag") { + + //The user needs to have the dragend to be fired to ensure that + //there is proper cleanup from the drag and move onto transforming. + triggerEvent("dragend", { + originalEvent : event, + direction : _direction, + distance : _distance, + angle : _angle + }); + } + _setup(); + + if(options.prevent_default) { + cancelEvent(event); + } + break; + + case 'mousemove': + case 'touchmove': + count = countFingers(event); + + //The user has gone from transforming to dragging. The + //user needs to have the proper cleanup of the state and + //setup with the new "start" points. + if (!_mousedown && count === 1) { + return false; + } else if (!_mousedown && count === 2) { + _can_tap = false; + + reset(); + _setup(); + } + + _event_move = event; + _pos.move = getXYfromEvent(event); + + if(!gestures.transform(event)) { + gestures.drag(event); + } + break; + + case 'mouseup': + case 'mouseout': + case 'touchcancel': + case 'touchend': + var callReset = true; + + _mousedown = false; + _event_end = event; + + // swipe gesture + gestures.swipe(event); + + // drag gesture + // dragstart is triggered, so dragend is possible + if(_gesture == 'drag') { + triggerEvent("dragend", { + originalEvent : event, + direction : _direction, + distance : _distance, + angle : _angle + }); + } + + // transform + // transformstart is triggered, so transformed is possible + else if(_gesture == 'transform') { + // define the transform distance + var _distance_x = _pos.center.x - _pos.startCenter.x; + var _distance_y = _pos.center.y - _pos.startCenter.y; + + triggerEvent("transformend", { + originalEvent : event, + position : _pos.center, + scale : calculateScale(_pos.start, _pos.move), + rotation : calculateRotation(_pos.start, _pos.move), + distance : _distance, + distanceX : _distance_x, + distanceY : _distance_y + }); + + //If the user goes from transformation to drag there needs to be a + //state reset so that way a dragstart/drag/dragend will be properly + //fired. + if (countFingers(event) === 1) { + reset(); + _setup(); + callReset = false; + } + } else if (_can_tap && event.type != 'mouseout') { + gestures.tap(_event_start); + } + + if (_gesture !== null) { + _prev_gesture = _gesture; + + // trigger release event + // "release" by default doesn't return the co-ords where your + // finger was released. "position" will return "the last touched co-ords" + + triggerEvent("release", { + originalEvent : event, + gesture : _gesture, + position : _pos.move || _pos.start + }); + } + + // reset vars if this was not a transform->drag touch end operation. + if (callReset) { + reset(); + } + break; + } // end switch + + /** + * Performs a blank setup. + * @private + */ + function _setup() { + _pos.start = getXYfromEvent(event); + _touch_start_time = new Date().getTime(); + _fingers = countFingers(event); + _first = true; + _event_start = event; + + // borrowed from jquery offset https://github.com/jquery/jquery/blob/master/src/offset.js + var box = element.getBoundingClientRect(); + var clientTop = element.clientTop || document.body.clientTop || 0; + var clientLeft = element.clientLeft || document.body.clientLeft || 0; + var scrollTop = window.pageYOffset || element.scrollTop || document.body.scrollTop; + var scrollLeft = window.pageXOffset || element.scrollLeft || document.body.scrollLeft; + + _offset = { + top: box.top + scrollTop - clientTop, + left: box.left + scrollLeft - clientLeft + }; + + _mousedown = true; + + // hold gesture + gestures.hold(event); + } + } + + + function handleMouseOut(event) { + if(!isInsideHammer(element, event.relatedTarget)) { + handleEvents(event); + } + } + + + // bind events for touch devices + // except for windows phone 7.5, it doesnt support touch events..! + if(_has_touch || options.allow_touch_and_mouse) { + addEvent(element, "touchstart touchmove touchend touchcancel", handleEvents); + } + + // for non-touch + if (!_has_touch || options.allow_touch_and_mouse) { + addEvent(element, "mouseup mousedown mousemove", handleEvents); + addEvent(element, "mouseout", handleMouseOut); + } + + + /** + * find if element is (inside) given parent element + * @param object element + * @param object parent + * @return bool inside + */ + function isInsideHammer(parent, child) { + // get related target for IE + if(!child && window.event && window.event.toElement){ + child = window.event.toElement; + } + + if(parent === child){ + return true; + } + + // loop over parentNodes of child until we find hammer element + if(child){ + var node = child.parentNode; + while(node !== null){ + if(node === parent){ + return true; + } + node = node.parentNode; + } + } + return false; + } + + + /** + * merge 2 objects into a new object + * @param object obj1 + * @param object obj2 + * @return object merged object + */ + function mergeObject(obj1, obj2) { + var output = {}; + + if(!obj2) { + return obj1; + } + + for (var prop in obj1) { + if (prop in obj2) { + output[prop] = obj2[prop]; + } else { + output[prop] = obj1[prop]; + } + } + return output; + } + + + /** + * check if object is a function + * @param object obj + * @return bool is function + */ + function isFunction( obj ){ + return Object.prototype.toString.call( obj ) == "[object Function]"; + } + + + /** + * attach event + * @param node element + * @param string types + * @param object callback + */ + function addEvent(element, types, callback) { + types = types.split(" "); + for(var t= 0,len=types.length; t= 20) { + if (!monstersComeOut && distanceTravelledInMetres >= 200) { monstersComeOut = true; } @@ -208,9 +208,19 @@ function drawScene (images) { } }, 10); - $(mainCanvas).mousemove(function (e) { + $(mainCanvas) +/* .mousemove(function (e) { mouseX = e.pageX; mouseY = e.pageY; + })*/ + .hammer({}) + .bind('hold', function (e) { + mouseX = e.position[0].x; + mouseY = e.position[0].y; + }) + .bind('drag', function (e) { + mouseX = e.position.x; + mouseY = e.position.y; }); } From 9c8112f048fc0fdd3f3f69a8b01fcf90ac9a7667 Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 07:12:05 +0000 Subject: [PATCH 04/21] Tap events too. --- js/main.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/js/main.js b/js/main.js index 90f1bd7..2e6fcb7 100644 --- a/js/main.js +++ b/js/main.js @@ -209,15 +209,19 @@ function drawScene (images) { }, 10); $(mainCanvas) -/* .mousemove(function (e) { + .mousemove(function (e) { mouseX = e.pageX; mouseY = e.pageY; - })*/ + }) .hammer({}) .bind('hold', function (e) { mouseX = e.position[0].x; mouseY = e.position[0].y; }) + .bind('tap', function (e) { + mouseX = e.position[0].x; + mouseY = e.position[0].y; + }) .bind('drag', function (e) { mouseX = e.position.x; mouseY = e.position.y; From 0057ab0e9130cdd71e3db9d210c267449013d230 Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 07:13:46 +0000 Subject: [PATCH 05/21] Click events too for good measure --- js/main.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/main.js b/js/main.js index 2e6fcb7..a6661e1 100644 --- a/js/main.js +++ b/js/main.js @@ -213,6 +213,10 @@ function drawScene (images) { mouseX = e.pageX; mouseY = e.pageY; }) + .bind('click', function (e) { + mouseX = e.pageX; + mouseY = e.pageY; + }) .hammer({}) .bind('hold', function (e) { mouseX = e.position[0].x; From eeabc282ead2916cec8d93b7576b483311bcf691 Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 07:38:50 +0000 Subject: [PATCH 06/21] Listening for some more events to move, and also for F and double-tap to speed up --- index.html | 2 +- js/main.js | 42 ++++++++++++++++++++++++++++++++++++------ js/skier.js | 16 ++++++++++++++++ js/sprite.js | 7 +++++++ 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index aa5a12d..85e6d8d 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@ - + Fork me on GitHub diff --git a/js/main.js b/js/main.js index a6661e1..5382dff 100644 --- a/js/main.js +++ b/js/main.js @@ -2,6 +2,8 @@ var mainCanvas = document.getElementById('skifree-canvas'); var dContext = mainCanvas.getContext('2d'); var imageSources = [ 'sprite-characters.png', 'skifree-objects.png' ]; var global = this; +var infoBoxControls = 'Use the mouse to control the skier'; +if (isMobileDevice()) infoBoxControls = 'Tap on the piste to control the skier'; var sprites = { 'skier' : { @@ -98,7 +100,7 @@ function drawScene (images) { infoBox = new InfoBox({ initialLines : [ 'SkiFree.js', - 'Use the mouse to control the skier', + infoBoxControls, 'Travelled 0m', 'Skiers left: ' + livesLeft, 'Created by Dan Hough (@basicallydan)' @@ -109,7 +111,7 @@ function drawScene (images) { } }); - skier.setPosition(mouseX, getMiddleOfViewport()); + skier.setPosition(mouseX, getMiddleOfViewport() - skier.getMaxHeight()); setInterval(function () { var skierOpposite = skier.getMovingTowardOpposite(); @@ -129,7 +131,7 @@ function drawScene (images) { if (monster.isFull) { monster.move(); } else if (!skier.isBeingEaten) { - monster.setSpeed(1); + monster.setSpeed(4 - skier.getSpeed()); monster.moveToward(skier.getXPosition(), skier.getYPosition(), true); } else if (skier.isBeingEaten && !monster.isEating) { monster.setSpeed(skier.getSpeed()); @@ -156,6 +158,7 @@ function drawScene (images) { console.log('Deleting tree'); return (delete trees[i]); } + tree.setSpeed(skier.getSpeed()); var moveTreeTowardX = tree.getXPosition() + skierOpposite[0]; var moveTreeTowardY = tree.getYPosition() + skierOpposite[1]; @@ -188,10 +191,11 @@ function drawScene (images) { infoBox.setLines([ 'SkiFree.js', - 'Use the mouse to control the skier', + infoBoxControls, 'Travelled ' + distanceTravelledInMetres + 'm', 'Skiers left: ' + livesLeft, - 'Created by Dan Hough (@basicallydan)' + 'Created by Dan Hough (@basicallydan)', + 'Current Speed: ' + skier.getSpeed() ]); if (!monstersComeOut && distanceTravelledInMetres >= 200) { @@ -217,6 +221,11 @@ function drawScene (images) { mouseX = e.pageX; mouseY = e.pageY; }) + .bind('keypress', function (e) { + if (e.keyCode === 102) { + skier.speedBoost(); + } + }) .hammer({}) .bind('hold', function (e) { mouseX = e.position[0].x; @@ -229,7 +238,12 @@ function drawScene (images) { .bind('drag', function (e) { mouseX = e.position.x; mouseY = e.position.y; - }); + }) + .bind('doubletap', function (e) { + skier.speedBoost(); + }) + // Focus on the canvas so we can listen for key events immediately + .focus(); } function resizeCanvas() { @@ -267,6 +281,22 @@ function getAboveViewport() { return 0 - (mainCanvas.height / 4).floor(); } +function isMobileDevice() { + if(navigator.userAgent.match(/Android/i) || + navigator.userAgent.match(/webOS/i) || + navigator.userAgent.match(/iPhone/i) || + navigator.userAgent.match(/iPad/i) || + navigator.userAgent.match(/iPod/i) || + navigator.userAgent.match(/BlackBerry/i) || + navigator.userAgent.match(/Windows Phone/i) + ) { + return true; + } + else { + return false; + } +} + window.addEventListener('resize', resizeCanvas, false); resizeCanvas(); diff --git a/js/skier.js b/js/skier.js index 122acfd..ebf4559 100644 --- a/js/skier.js +++ b/js/skier.js @@ -8,6 +8,8 @@ var Sprite = require('./Sprite'); hits: that.superior('hits') }; + var canSpeedBoost = true; + var obstaclesHit = []; var pixelsTravelled = 0; @@ -96,6 +98,20 @@ var Sprite = require('./Sprite'); return false; }; + that.speedBoost = function () { + var originalSpeed = that.speed; + if (canSpeedBoost) { + canSpeedBoost = false; + that.setSpeed(that.speed * 2); + setTimeout(function () { + that.setSpeed(originalSpeed); + setTimeout(function () { + canSpeedBoost = true; + }, 10000); + }, 2000); + } + }; + that.hasHitObstacle = function () { that.isMoving = false; that.hasBeenHit = true; diff --git a/js/sprite.js b/js/sprite.js index 96bb7a3..a7ca484 100644 --- a/js/sprite.js +++ b/js/sprite.js @@ -9,6 +9,9 @@ that.movingToward = [ 0, 0 ]; that.metresDownTheMountain = 0; that.movingWithConviction = false; + that.maxHeight = (function () { + return Object.values(that.data.parts).map(function (p) { return p[3]; }).max(); + }()); function incrementX(amount) { that.x += amount.toNumber(); @@ -77,6 +80,10 @@ that.width = w; }; + this.getMaxHeight = function getMaxHeight() { + return that.maxHeight; + }; + this.move = function move () { if (typeof that.movingToward[0] !== 'undefined') { if (that.x > that.movingToward[0]) { From b714582eb7b2710da1f80db0645d3df7160d009f Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 07:54:03 +0000 Subject: [PATCH 07/21] Added some touch icons because. --- apple-touch-icon-114x114-precomposed.png | Bin 0 -> 5642 bytes apple-touch-icon-57x57-precomposed.png | Bin 0 -> 2961 bytes apple-touch-icon-72x72-precomposed.png | Bin 0 -> 3784 bytes apple-touch-icon.png | Bin 0 -> 2961 bytes index.html | 15 ++++++++++++++- yeti-big.png | Bin 0 -> 59015 bytes 6 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 apple-touch-icon-114x114-precomposed.png create mode 100644 apple-touch-icon-57x57-precomposed.png create mode 100644 apple-touch-icon-72x72-precomposed.png create mode 100644 apple-touch-icon.png create mode 100644 yeti-big.png diff --git a/apple-touch-icon-114x114-precomposed.png b/apple-touch-icon-114x114-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..27cf2a488176981ea8bda20e3cd67ce517675699 GIT binary patch literal 5642 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 k8&22INpr(07*qoM6N<$f>?dTCjbBd literal 0 HcmV?d00001 diff --git a/apple-touch-icon-57x57-precomposed.png b/apple-touch-icon-57x57-precomposed.png new file mode 100644 index 0000000000000000000000000000000000000000..2bcad51a6cfeabfb2e9addcf8279cd166dac9d5c GIT binary patch literal 2961 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 Skifree.js - + + + + + + + + + + + + + + diff --git a/yeti-big.png b/yeti-big.png new file mode 100644 index 0000000000000000000000000000000000000000..02ec9b1d5a6c8d2012a6a59cb116550b69c6f6db GIT binary patch literal 59015 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¨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 Date: Fri, 1 Feb 2013 08:28:00 +0000 Subject: [PATCH 08/21] Updated feature list --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4907e42..2bf532f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ I just started work this Christmas 2012 so I've not done a whole lot yet. * Stopping * MONSTAHS! GRAAAAHHH! They even eat you and then run away because they're full * Distance tracking so you can see how far you've gone you absolute badass +* Speed boost (this was a little-known feature to get away from monsters) using the F key +* **MOBILE SUPPORT** - This is cool - try loading the [**the demo page**](http://basicallydan.github.com/skifree.js) on a mobile device and then use your finger to direct the skier around the pistle. Also try double-tap ;) ## So, what's left to do? @@ -28,10 +30,12 @@ Some features which weren't in the original which I'd like to give a go: * Being a snowboarder instead of a boring old skier * Tricks, or something? +* LocalStorage high-score +* Multiplayer (ooooo wouldn't that be fun?!) ## F*ck this, let me play the game goddammit -* Open up index.html in Chrome, or maybe even Firefox - I haven't tested it in anything but Chrome 23.0 yet, and I probably won't I'm afraid +* Open up index.html in Chrome, or maybe even Firefox - I haven't tested it in anything but Chrome and Mobile Safari yet, and I probably won't I'm afraid * Go. ## I like to run Unit tests before I do ANYTHING. From c036837e9137190fbb805c6ee815ff00472beef8 Mon Sep 17 00:00:00 2001 From: Dan Hough Date: Fri, 1 Feb 2013 08:27:38 +0000 Subject: [PATCH 09/21] Totally fixed the tests. Woops. --- js/sprite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/sprite.js b/js/sprite.js index a7ca484..747e87f 100644 --- a/js/sprite.js +++ b/js/sprite.js @@ -5,7 +5,7 @@ that.y = 0; that.height = 0; that.speed = 3; - that.data = data; + that.data = data || { parts : {} }; that.movingToward = [ 0, 0 ]; that.metresDownTheMountain = 0; that.movingWithConviction = false; From 8d6e158f3ab40b376dde01316588a14672476eea Mon Sep 17 00:00:00 2001 From: tomgrim1 Date: Fri, 1 Feb 2013 19:01:18 +0000 Subject: [PATCH 10/21] Added WASD controls. --- js/main.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/js/main.js b/js/main.js index 5382dff..683f1e6 100644 --- a/js/main.js +++ b/js/main.js @@ -222,9 +222,30 @@ function drawScene (images) { mouseY = e.pageY; }) .bind('keypress', function (e) { + // F Key if (e.keyCode === 102) { skier.speedBoost(); } + //W Key + if (e.Keycode === 87 || e.keyCode === 119) { + mouseX = 0; + mouseY = 0; + } + // A Key + if (e.keyCode === 65 || e.keyCode === 97) { + mouseX = 0; + mouseY = mainCanvas.height; + } + // S Key + if (e.keyCode === 83 || e.keyCode === 115) { + mouseX = mainCanvas.width / 2; + mouseY = mainCanvas.height; + } + // D Key + if (e.keyCode === 68 || e.keyCode === 100) { + mouseX = mainCanvas.width; + mouseY = mainCanvas.height; + } }) .hammer({}) .bind('hold', function (e) { From 0eed62a95cfbdb70582a88164e0ccdd40347f025 Mon Sep 17 00:00:00 2001 From: Devin Doolin Date: Sat, 2 Feb 2013 16:30:46 +0900 Subject: [PATCH 11/21] Added basic localStorage-based high scoring --- js/main.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/js/main.js b/js/main.js index 5382dff..5db5330 100644 --- a/js/main.js +++ b/js/main.js @@ -45,6 +45,8 @@ var pixelsPerMetre = 18; var monstersComeOut = false; var distanceTravelledInMetres = 0; var livesLeft = 5; +var highScore = 0; +if (localStorage.getItem('highScore')) highScore = localStorage.getItem('highScore'); function loadImages (sources, next) { var loaded = 0; @@ -85,14 +87,15 @@ function drawScene (images) { monstersComeOut = false; distanceTravelledInMetres = 0; livesLeft = 5; + highScore = localStorage.getItem('highScore'); monsters = []; trees = []; } function detectEnd () { - if (livesLeft === 0) { - paused = true; - } + paused = true; + highScore = localStorage.setItem('highScore', distanceTravelledInMetres); + console.log('Game over!'); } skier = new Skier(sprites.skier); @@ -102,6 +105,7 @@ function drawScene (images) { 'SkiFree.js', infoBoxControls, 'Travelled 0m', + 'High Score: ' + highScore, 'Skiers left: ' + livesLeft, 'Created by Dan Hough (@basicallydan)' ], @@ -194,6 +198,7 @@ function drawScene (images) { infoBoxControls, 'Travelled ' + distanceTravelledInMetres + 'm', 'Skiers left: ' + livesLeft, + 'High Score: ' + highScore, 'Created by Dan Hough (@basicallydan)', 'Current Speed: ' + skier.getSpeed() ]); @@ -210,6 +215,11 @@ function drawScene (images) { newMonster.setSpeed(1); monsters.push(newMonster); } + + if (livesLeft === 0) { + detectEnd(); + resetGame(); + } }, 10); $(mainCanvas) From b5acafd5e7111ee7f78b780b273aaad3ad9d2e34 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 3 Feb 2013 09:02:47 +0000 Subject: [PATCH 12/21] Skier's internal distance tracker is reset on game reset --- js/main.js | 3 ++- js/skier.js | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/js/main.js b/js/main.js index 359a7f4..f269ec6 100644 --- a/js/main.js +++ b/js/main.js @@ -44,7 +44,7 @@ var sprites = { var pixelsPerMetre = 18; var monstersComeOut = false; var distanceTravelledInMetres = 0; -var livesLeft = 5; +var livesLeft = 1; var highScore = 0; if (localStorage.getItem('highScore')) highScore = localStorage.getItem('highScore'); @@ -90,6 +90,7 @@ function drawScene (images) { highScore = localStorage.getItem('highScore'); monsters = []; trees = []; + skier.reset(); } function detectEnd () { diff --git a/js/skier.js b/js/skier.js index ebf4559..b6485b0 100644 --- a/js/skier.js +++ b/js/skier.js @@ -1,7 +1,7 @@ var Sprite = require('./Sprite'); (function(global) { - function Skier(data, heightData) { + function Skier(data) { var that = new Sprite(data); var sup = { draw: that.superior('draw'), @@ -16,6 +16,14 @@ var Sprite = require('./Sprite'); that.isMoving = true; that.hasBeenHit = false; + that.reset = function () { + obstaclesHit = []; + pixelsTravelled = 0; + that.isMoving = true; + that.hasBeenHit = false; + canSpeedBoost = true; + }; + function getBeingEatenSprite() { return 'blank'; } @@ -127,6 +135,14 @@ var Sprite = require('./Sprite'); that.isBeingEaten = true; }; + that.reset = function () { + obstaclesHit = []; + pixelsTravelled = 0; + that.isMoving = true; + that.hasBeenHit = false; + canSpeedBoost = true; + }; + return that; } From d72791bd92ffbbc5c290cbc503bc663e8760f431 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 3 Feb 2013 09:05:00 +0000 Subject: [PATCH 13/21] Lives Left set to 5 --- js/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/main.js b/js/main.js index f269ec6..40cc5ce 100644 --- a/js/main.js +++ b/js/main.js @@ -44,7 +44,7 @@ var sprites = { var pixelsPerMetre = 18; var monstersComeOut = false; var distanceTravelledInMetres = 0; -var livesLeft = 1; +var livesLeft = 5; var highScore = 0; if (localStorage.getItem('highScore')) highScore = localStorage.getItem('highScore'); From 6f878cff34321bde5eebb2e136a6b252f77cc4bc Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 3 Feb 2013 09:08:18 +0000 Subject: [PATCH 14/21] Added stuff about contributors to readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 2bf532f..4e2a01e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,15 @@ Some features which weren't in the original which I'd like to give a go: * Make a pull request with your awesome additions. * Maybe raise an issue? +## Contributors + +Here's some lovely people who were kind enough to have opinions and spirit enough to make a pull request. + +* [@tomgrim1](https://github.com/tomgrim1) +* [@ddoolin](https://github.com/ddoolin) + +Thanks! + ## Third-party credits * [HTML5 Boilerplate](http://html5boilerplate.com) provided a bunch of useful markup and stuff From 9dbad661f71ebd0980d0bf561c54d2a1b2ed5ae9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 3 Feb 2013 10:56:06 +0000 Subject: [PATCH 15/21] License, also Some short refactoring in skier.js --- README.md | 4 ++++ js/skier.js | 14 ++++++++++---- license.txt | 8 ++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 license.txt diff --git a/README.md b/README.md index 4e2a01e..75dedf3 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,7 @@ Thanks! * [HTML5 Boilerplate](http://html5boilerplate.com) provided a bunch of useful markup and stuff * [Wing Wang Wao](http://spriters-resource.com/submitter/Wing%20Wang%20Wao) of the [Spriters Resource Forum](http://spriters-resource.com) did an amazing job of providing the sprites which I have extended slightly. Thanks! + +## License + +* See [license.txt](blob/master/license.txt) \ No newline at end of file diff --git a/js/skier.js b/js/skier.js index b6485b0..9c2f5cd 100644 --- a/js/skier.js +++ b/js/skier.js @@ -7,6 +7,12 @@ var Sprite = require('./Sprite'); draw: that.superior('draw'), hits: that.superior('hits') }; + var directions = { + esEast: function(xDiff) { return xDiff > 300; }, + sEast: function(xDiff) { return xDiff > 75; }, + wsWest: function(xDiff) { return xDiff < -300; }, + sWest: function(xDiff) { return xDiff < -75; } + }; var canSpeedBoost = true; @@ -78,13 +84,13 @@ var Sprite = require('./Sprite'); } } - if (xDiff > 300) { + if (directions.esEast(xDiff)) { return 'esEast'; - } else if (xDiff > 75) { + } else if (directions.sEast(xDiff)) { return 'sEast'; - } else if (xDiff < -300) { + } else if (directions.wsWest(xDiff)) { return 'wsWest'; - } else if (xDiff < -76) { + } else if (directions.sWest(xDiff)) { return 'sWest'; } return 'south'; diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..7d8f48c --- /dev/null +++ b/license.txt @@ -0,0 +1,8 @@ + +Copyright (C) 2013 Daniel Hough + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file From fe1129dd3787f39f803a6058b6a97673c8212283 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 3 Feb 2013 10:59:20 +0000 Subject: [PATCH 16/21] Added should as a devDependency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index add7f86..e5fa2ec 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "sugar":"1.3.x", - "mocha":"1.7.x" + "mocha":"1.8.x", + "should": "1.2.x" } } \ No newline at end of file From 2231b8252866c73889d2d03d6759bae49dd8388a Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 3 Feb 2013 12:46:28 +0000 Subject: [PATCH 17/21] New hit method. Unit tests for it. --- index.html | 11 ++++---- js/{ => lib}/commonjs-require.js | 5 ++-- js/{ => lib}/extenders.js | 0 js/lib/guid.js | 24 +++++++++++++++++ js/{ => lib}/plugins.js | 0 js/main.js | 21 ++++++++++----- js/sprite.js | 34 ++++++++++++++++++++++++ js/{ => vendor}/hammer.js | 0 js/{ => vendor}/jquery.hammer.js | 0 test/skier.test.js | 2 +- test/sprite.test.js | 45 ++++++++++++++++++++++++++++++++ 11 files changed, 126 insertions(+), 16 deletions(-) rename js/{ => lib}/commonjs-require.js (55%) rename js/{ => lib}/extenders.js (100%) create mode 100644 js/lib/guid.js rename js/{ => lib}/plugins.js (100%) rename js/{ => vendor}/hammer.js (100%) mode change 100755 => 100644 rename js/{ => vendor}/jquery.hammer.js (100%) mode change 100755 => 100644 diff --git a/index.html b/index.html index 1849a96..6792224 100644 --- a/index.html +++ b/index.html @@ -41,12 +41,13 @@ - - + + - - - + + + + diff --git a/js/commonjs-require.js b/js/lib/commonjs-require.js similarity index 55% rename from js/commonjs-require.js rename to js/lib/commonjs-require.js index 344f418..1f28c66 100644 --- a/js/commonjs-require.js +++ b/js/lib/commonjs-require.js @@ -7,8 +7,7 @@ var global = this; function require (name) { - if (name.substring(0, 2) == './') { - name = name.substring(2); - } + var indexToGoFrom = name.lastIndexOf('/'); + name = name.substring(indexToGoFrom + 1); return global[name]; } \ No newline at end of file diff --git a/js/extenders.js b/js/lib/extenders.js similarity index 100% rename from js/extenders.js rename to js/lib/extenders.js diff --git a/js/lib/guid.js b/js/lib/guid.js new file mode 100644 index 0000000..167d6e2 --- /dev/null +++ b/js/lib/guid.js @@ -0,0 +1,24 @@ +(function(global) { + function guid () + { + var S4 = function () + { + return Math.floor( + Math.random() * 0x10000 /* 65536 */ + ).toString(16); + }; + + return ( + S4() + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + S4() + S4() + ); + } + global.guid = guid; +})(this); + +if (typeof module !== 'undefined') { + module.exports = this.guid; +} \ No newline at end of file diff --git a/js/plugins.js b/js/lib/plugins.js similarity index 100% rename from js/plugins.js rename to js/lib/plugins.js diff --git a/js/main.js b/js/main.js index 40cc5ce..586606d 100644 --- a/js/main.js +++ b/js/main.js @@ -67,6 +67,10 @@ function loadImages (sources, next) { }); } +function treeMonsterHitBehaviour(monster, tree) { + monster.deleteOnNextCycle(); +} + function drawScene (images) { var skier; var infoBox; @@ -128,7 +132,7 @@ function drawScene (images) { skier.draw(dContext); monsters.each(function (monster, i) { - if (monster.isAbove(getAboveViewport() - 100)) { + if (monster.isAbove(getAboveViewport() - 100) || monster.deleted) { console.log('Deleting monster'); return (delete monsters[i]); } @@ -156,10 +160,13 @@ function drawScene (images) { monster.moveTowardWithConviction(getRandomlyInTheCentre(), getAboveViewport()); }); } + + monster.cycle(); }); trees.each(function (tree, i) { if (tree.isAbove(0)) { + tree.deleteOnNextCycle(); console.log('Deleting tree'); return (delete trees[i]); } @@ -174,12 +181,6 @@ function drawScene (images) { skier.hasHitObstacle(tree); } - monsters.each(function (monster, i) { - if (monster.hits(tree)) { - return (delete monsters[i]); - } - }); - tree.draw(dContext, 'main'); }); @@ -188,6 +189,9 @@ function drawScene (images) { var newTree = new Sprite(sprites.smallTree); newTree.setSpeed(skier.getSpeed()); newTree.setPosition(getRandomlyInTheCentre(200), getBelowViewport()); + monsters.each(function (monster) { + monster.onHitting(newTree, treeMonsterHitBehaviour); + }); trees.push(newTree); }); } @@ -214,6 +218,9 @@ function drawScene (images) { var newMonster = new Monster(sprites.monster); newMonster.setPosition(getRandomlyInTheCentre(), getAboveViewport()); newMonster.setSpeed(1); + trees.each(function (tree) { + newMonster.onHitting(tree, treeMonsterHitBehaviour); + }); monsters.push(newMonster); } diff --git a/js/sprite.js b/js/sprite.js index 747e87f..27be20f 100644 --- a/js/sprite.js +++ b/js/sprite.js @@ -1,6 +1,9 @@ (function (global) { + var GUID = require('./lib/guid'); function Sprite (data) { + var hittableObjects = {}; var that = this; + that.id = GUID(); that.x = 0; that.y = 0; that.height = 0; @@ -9,6 +12,7 @@ that.movingToward = [ 0, 0 ]; that.metresDownTheMountain = 0; that.movingWithConviction = false; + that.deleted = false; that.maxHeight = (function () { return Object.values(that.data.parts).map(function (p) { return p[3]; }).max(); }()); @@ -84,6 +88,21 @@ return that.maxHeight; }; + this.cycle = function () { + Object.keys(hittableObjects, function (k, objectData) { + if (objectData.object.deleted) { + console.log('Deleting reference'); + delete hittableObjects[k]; + } else { + if (that.hits(objectData.object)) { + objectData.callbacks.each(function (callback) { + callback(that, objectData.object); + }); + } + } + }); + }; + this.move = function move () { if (typeof that.movingToward[0] !== 'undefined') { if (that.x > that.movingToward[0]) { @@ -121,6 +140,21 @@ that.movingWithConviction = true; }; + this.onHitting = function (objectToHit, callback) { + if (hittableObjects[objectToHit.id]) { + return hittableObjects[objectToHit.id].callbacks.push(callback); + } + + hittableObjects[objectToHit.id] = { + object: objectToHit, + callbacks: [ callback ] + }; + }; + + this.deleteOnNextCycle = function () { + that.deleted = true; + }; + this.hits = function hits (other) { var verticalIntersect = false; var horizontalIntersect = false; diff --git a/js/hammer.js b/js/vendor/hammer.js old mode 100755 new mode 100644 similarity index 100% rename from js/hammer.js rename to js/vendor/hammer.js diff --git a/js/jquery.hammer.js b/js/vendor/jquery.hammer.js old mode 100755 new mode 100644 similarity index 100% rename from js/jquery.hammer.js rename to js/vendor/jquery.hammer.js diff --git a/test/skier.test.js b/test/skier.test.js index 2e4760c..47c8a59 100644 --- a/test/skier.test.js +++ b/test/skier.test.js @@ -1,4 +1,4 @@ -var extenders = require(__dirname + '/../js/extenders'); +var extenders = require(__dirname + '/../js/lib/extenders'); var Sprite = require(__dirname + '/../js/sprite'); var Skier = require(__dirname + '/../js/skier'); var should = require('should'); diff --git a/test/sprite.test.js b/test/sprite.test.js index 5becc8b..208133b 100644 --- a/test/sprite.test.js +++ b/test/sprite.test.js @@ -155,5 +155,50 @@ describe('Sprite', function() { object1.hits(object2).should.not.equal(false); object2.hits(object1).should.not.equal(false); }); + + it('should run the appropriate callback when two things hit in a cycle', function() { + var object1 = new Sprite(); + var object2 = new Sprite(); + var testString = 'not hit'; + + object1.setPosition(1, 1); + object1.setHeight(10); + object1.setWidth(10); + + object2.setPosition(3, 3); + object2.setHeight(10); + object2.setWidth(10); + + object1.onHitting(object2, function () { + testString = 'hit'; + }); + + object1.cycle(); + + testString.should.equal('hit'); + }); + + it('should not register the hit if a hittable object has been deleted', function() { + var object1 = new Sprite(); + var object2 = new Sprite(); + var testString = 'not hit'; + + object1.setPosition(1, 1); + object1.setHeight(10); + object1.setWidth(10); + + object2.setPosition(3, 3); + object2.setHeight(10); + object2.setWidth(10); + + object1.onHitting(object2, function () { + testString = 'hit'; + }); + + object2.deleteOnNextCycle(); + object1.cycle(); + + testString.should.equal('not hit'); + }); }); }); \ No newline at end of file From 3cd0e91691f54f528af4c1a0e6b979e354c65d55 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Feb 2013 00:37:49 +0000 Subject: [PATCH 18/21] Fixed bug causing mid-eat skier to move after a tree hit, switched to reactive hitting --- js/main.js | 96 +++++++++++++++++++++++++++------------------------- js/skier.js | 25 +++++--------- js/sprite.js | 26 +++++++++++++- 3 files changed, 83 insertions(+), 64 deletions(-) diff --git a/js/main.js b/js/main.js index 586606d..6f024a9 100644 --- a/js/main.js +++ b/js/main.js @@ -18,7 +18,8 @@ var sprites = { wsWest : [ 24, 37, 24, 34 ], west : [ 0, 37, 24, 34 ], hit : [ 0, 78, 31, 31 ] - } + }, + id : 'player' }, 'smallTree' : { $imageFile : 'skifree-objects.png', @@ -67,10 +68,26 @@ function loadImages (sources, next) { }); } -function treeMonsterHitBehaviour(monster, tree) { +function monsterHitsTreeBehaviour(monster) { monster.deleteOnNextCycle(); } +function skierHitsTreeBehaviour(skier, tree) { + skier.hasHitObstacle(tree); +} + +function skierHitsMonsterBehaviour(skier, monster) { + skier.hasHitObstacle(monster); + skier.isEatenBy(monster, function () { + livesLeft -= 1; + monster.isFull = true; + monster.isEating = false; + skier.isBeingEaten = false; + monster.setSpeed(skier.getSpeed()); + monster.moveTowardWithConviction(getRandomlyInTheCentre(), getAboveViewport()); + }); +} + function drawScene (images) { var skier; var infoBox; @@ -148,19 +165,6 @@ function drawScene (images) { } monster.draw(dContext); - - if (skier.hits(monster)) { - skier.hasHitObstacle(monster); - skier.isEatenBy(monster, function () { - livesLeft -= 1; - monster.isFull = true; - monster.isEating = false; - skier.isBeingEaten = false; - monster.setSpeed(skier.getSpeed()); - monster.moveTowardWithConviction(getRandomlyInTheCentre(), getAboveViewport()); - }); - } - monster.cycle(); }); @@ -170,60 +174,60 @@ function drawScene (images) { console.log('Deleting tree'); return (delete trees[i]); } - tree.setSpeed(skier.getSpeed()); - - var moveTreeTowardX = tree.getXPosition() + skierOpposite[0]; - var moveTreeTowardY = tree.getYPosition() + skierOpposite[1]; - - tree.moveToward(moveTreeTowardX, moveTreeTowardY); - if (skier.hits(tree)) { - skier.hasHitObstacle(tree); - } + tree.moveAwayFromSprite(skier); tree.draw(dContext, 'main'); }); - if (Number.random(8) === 1 && skier.isMoving) { - (Number.random(1)).times(function () { - var newTree = new Sprite(sprites.smallTree); - newTree.setSpeed(skier.getSpeed()); - newTree.setPosition(getRandomlyInTheCentre(200), getBelowViewport()); - monsters.each(function (monster) { - monster.onHitting(newTree, treeMonsterHitBehaviour); - }); - trees.push(newTree); + if (Number.random(16) === 1 && skier.isMoving) { + var newTree = new Sprite(sprites.smallTree); + newTree.setSpeed(skier.getSpeed()); + newTree.setPosition(getRandomlyInTheCentre(200), getBelowViewport()); + + monsters.each(function (monster) { + monster.onHitting(newTree, monsterHitsTreeBehaviour); }); + + skier.onHitting(newTree, skierHitsTreeBehaviour); + + trees.push(newTree); } distanceTravelledInMetres = parseFloat(skier.getPixelsTravelledDownMountain() / pixelsPerMetre).toFixed(1); - infoBox.setLines([ - 'SkiFree.js', - infoBoxControls, - 'Travelled ' + distanceTravelledInMetres + 'm', - 'Skiers left: ' + livesLeft, - 'High Score: ' + highScore, - 'Created by Dan Hough (@basicallydan)', - 'Current Speed: ' + skier.getSpeed() - ]); - if (!monstersComeOut && distanceTravelledInMetres >= 200) { monstersComeOut = true; } - infoBox.draw(dContext); - if (monstersComeOut && Number.random(300) === 1) { var newMonster = new Monster(sprites.monster); newMonster.setPosition(getRandomlyInTheCentre(), getAboveViewport()); newMonster.setSpeed(1); + trees.each(function (tree) { - newMonster.onHitting(tree, treeMonsterHitBehaviour); + newMonster.onHitting(tree, monsterHitsTreeBehaviour); }); + + skier.onHitting(newMonster, skierHitsMonsterBehaviour); + monsters.push(newMonster); } + skier.cycle(); + + infoBox.setLines([ + 'SkiFree.js', + infoBoxControls, + 'Travelled ' + distanceTravelledInMetres + 'm', + 'Skiers left: ' + livesLeft, + 'High Score: ' + highScore, + 'Created by Dan Hough (@basicallydan)', + 'Current Speed: ' + skier.getSpeed() + ]); + + infoBox.draw(dContext); + if (livesLeft === 0) { detectEnd(); resetGame(); diff --git a/js/skier.js b/js/skier.js index 9c2f5cd..c2aa1a0 100644 --- a/js/skier.js +++ b/js/skier.js @@ -14,6 +14,8 @@ var Sprite = require('./Sprite'); sWest: function(xDiff) { return xDiff < -75; } }; + var recoveryTimeout; + var canSpeedBoost = true; var obstaclesHit = []; @@ -46,20 +48,6 @@ var Sprite = require('./Sprite'); that.movingToward = [ cx, cy ]; }; - that.getMovingTowardOpposite = function () { - if (!that.isMoving || that.hasBeenHit) { - return [0, 0]; - } - - var skierMouseX = (that.movingToward[0] - that.getXPosition()); - var skierMouseY = (that.movingToward[1] - that.getYPosition()); - - var oppositeX = (Math.abs(skierMouseX) > 75 ? 0 - skierMouseX : 0); - var oppositeY = -skierMouseY; - - return [ oppositeX, oppositeY ]; - }; - that.getPixelsTravelledDownMountain = function () { return pixelsTravelled; }; @@ -100,12 +88,12 @@ var Sprite = require('./Sprite'); }; that.hits = function (obs) { - if (obstaclesHit.indexOf(obs) !== -1) { + if (obstaclesHit.indexOf(obs.id) !== -1) { return false; } if (sup.hits(obs)) { - obstaclesHit.push(obs); + obstaclesHit.push(obs.id); return true; } @@ -129,7 +117,10 @@ var Sprite = require('./Sprite'); that.hasHitObstacle = function () { that.isMoving = false; that.hasBeenHit = true; - setTimeout (function() { + if (recoveryTimeout) { + clearTimeout(recoveryTimeout); + } + recoveryTimeout = setTimeout(function() { that.isMoving = true; that.hasBeenHit = false; }, 1500); diff --git a/js/sprite.js b/js/sprite.js index 27be20f..b2a448d 100644 --- a/js/sprite.js +++ b/js/sprite.js @@ -3,7 +3,7 @@ function Sprite (data) { var hittableObjects = {}; var that = this; - that.id = GUID(); + that.id = data.id || GUID(); that.x = 0; that.y = 0; that.height = 0; @@ -88,6 +88,20 @@ return that.maxHeight; }; + that.getMovingTowardOpposite = function () { + if (!that.isMoving) { + return [0, 0]; + } + + var dx = (that.movingToward[0] - that.getXPosition()); + var dy = (that.movingToward[1] - that.getYPosition()); + + var oppositeX = (Math.abs(dx) > 75 ? 0 - dx : 0); + var oppositeY = -dy; + + return [ oppositeX, oppositeY ]; + }; + this.cycle = function () { Object.keys(hittableObjects, function (k, objectData) { if (objectData.object.deleted) { @@ -135,6 +149,16 @@ that.move(); }; + this.moveAwayFromSprite = function (otherSprite) { + var opposite = otherSprite.getMovingTowardOpposite(); + that.setSpeed(otherSprite.getSpeed()); + + var moveTowardX = that.getXPosition() + opposite[0]; + var moveTowardY = that.getYPosition() + opposite[1]; + + that.moveToward(moveTowardX, moveTowardY); + }; + this.moveTowardWithConviction = function moveToward (cx, cy) { that.moveToward(cx, cy); that.movingWithConviction = true; From 7b5e7da4daeebb794682eb347440c94055286782 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Feb 2013 00:40:15 +0000 Subject: [PATCH 19/21] Fixed test which required id --- js/main.js | 2 -- js/sprite.js | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/js/main.js b/js/main.js index 6f024a9..4e28d45 100644 --- a/js/main.js +++ b/js/main.js @@ -150,7 +150,6 @@ function drawScene (images) { monsters.each(function (monster, i) { if (monster.isAbove(getAboveViewport() - 100) || monster.deleted) { - console.log('Deleting monster'); return (delete monsters[i]); } @@ -171,7 +170,6 @@ function drawScene (images) { trees.each(function (tree, i) { if (tree.isAbove(0)) { tree.deleteOnNextCycle(); - console.log('Deleting tree'); return (delete trees[i]); } diff --git a/js/sprite.js b/js/sprite.js index b2a448d..937d757 100644 --- a/js/sprite.js +++ b/js/sprite.js @@ -3,7 +3,8 @@ function Sprite (data) { var hittableObjects = {}; var that = this; - that.id = data.id || GUID(); + if (data && data.id) that.id = data.id; + else that.id = GUID(); that.x = 0; that.y = 0; that.height = 0; @@ -105,7 +106,6 @@ this.cycle = function () { Object.keys(hittableObjects, function (k, objectData) { if (objectData.object.deleted) { - console.log('Deleting reference'); delete hittableObjects[k]; } else { if (that.hits(objectData.object)) { From 232365c36d160b4bde2bc2d2161a75ec6a41cdb9 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Feb 2013 01:03:31 +0000 Subject: [PATCH 20/21] Basic jumping support --- js/main.js | 39 +++++++++++++++++++++++++++++++++++++-- js/skier.js | 39 +++++++++++++++++++++++++++++++++++---- js/sprite.js | 22 +++++++++++++++++++--- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/js/main.js b/js/main.js index 4e28d45..f1adc23 100644 --- a/js/main.js +++ b/js/main.js @@ -17,7 +17,8 @@ var sprites = { sWest : [ 49, 37, 17, 34 ], wsWest : [ 24, 37, 24, 34 ], west : [ 0, 37, 24, 34 ], - hit : [ 0, 78, 31, 31 ] + hit : [ 0, 78, 31, 31 ], + jumping : [ 84, 0, 32, 34 ] }, id : 'player' }, @@ -40,6 +41,12 @@ var sprites = { eating4 : [ 219, 112, 25, 43 ], eating5 : [ 243, 112, 26, 43 ] } + }, + 'jump' : { + $imageFile : 'skifree-objects.png', + parts : { + main : [ 109, 55, 32, 8 ] + } } }; var pixelsPerMetre = 18; @@ -76,6 +83,10 @@ function skierHitsTreeBehaviour(skier, tree) { skier.hasHitObstacle(tree); } +function skierHitsJumpBehaviour(skier, jump) { + skier.hasHitJump(jump); +} + function skierHitsMonsterBehaviour(skier, monster) { skier.hasHitObstacle(monster); skier.isEatenBy(monster, function () { @@ -93,6 +104,7 @@ function drawScene (images) { var infoBox; var trees = []; var monsters = []; + var jumps = []; var mouseX = getCentreOfViewport(); var mouseY = mainCanvas.height; var paused = false; @@ -144,7 +156,9 @@ function drawScene (images) { mainCanvas.width = mainCanvas.width; - skier.moveToward(mouseX, mouseY); + if (!skier.isJumping) { + skier.moveToward(mouseX, mouseY); + } skier.draw(dContext); @@ -178,6 +192,17 @@ function drawScene (images) { tree.draw(dContext, 'main'); }); + jumps.each(function (jump, i) { + if (jump.isAbove(0)) { + jump.deleteOnNextCycle(); + return (delete jumps[i]); + } + + jump.moveAwayFromSprite(skier); + + jump.draw(dContext, 'main'); + }); + if (Number.random(16) === 1 && skier.isMoving) { var newTree = new Sprite(sprites.smallTree); newTree.setSpeed(skier.getSpeed()); @@ -192,6 +217,16 @@ function drawScene (images) { trees.push(newTree); } + if (Number.random(32) === 1 && skier.isMoving) { + var newJump = new Sprite(sprites.jump); + newJump.setSpeed(skier.getSpeed()); + newJump.setPosition(getRandomlyInTheCentre(200), getBelowViewport()); + + skier.onHitting(newJump, skierHitsJumpBehaviour); + + jumps.push(newJump); + } + distanceTravelledInMetres = parseFloat(skier.getPixelsTravelledDownMountain() / pixelsPerMetre).toFixed(1); if (!monstersComeOut && distanceTravelledInMetres >= 200) { diff --git a/js/skier.js b/js/skier.js index c2aa1a0..4f45bd5 100644 --- a/js/skier.js +++ b/js/skier.js @@ -14,15 +14,18 @@ var Sprite = require('./Sprite'); sWest: function(xDiff) { return xDiff < -75; } }; - var recoveryTimeout; + var cancelableStateTimeout; var canSpeedBoost = true; var obstaclesHit = []; var pixelsTravelled = 0; + var z = 0; + that.isMoving = true; that.hasBeenHit = false; + that.isJumping = false; that.reset = function () { obstaclesHit = []; @@ -36,6 +39,10 @@ var Sprite = require('./Sprite'); return 'blank'; } + function getJumpingSprite() { + return 'jumping'; + } + that.moveToward = function (cx, cy) { if (that.hasBeenHit) return; @@ -58,6 +65,10 @@ var Sprite = require('./Sprite'); return getBeingEatenSprite(); } + if (that.isJumping) { + return getJumpingSprite(); + } + if (that.hasBeenHit) { return 'hit'; } @@ -92,6 +103,10 @@ var Sprite = require('./Sprite'); return false; } + if (!obs.occupiesZIndex(z)) { + return false; + } + if (sup.hits(obs)) { obstaclesHit.push(obs.id); return true; @@ -117,15 +132,31 @@ var Sprite = require('./Sprite'); that.hasHitObstacle = function () { that.isMoving = false; that.hasBeenHit = true; - if (recoveryTimeout) { - clearTimeout(recoveryTimeout); + if (cancelableStateTimeout) { + clearTimeout(cancelableStateTimeout); } - recoveryTimeout = setTimeout(function() { + cancelableStateTimeout = setTimeout(function() { that.isMoving = true; that.hasBeenHit = false; }, 1500); }; + that.hasHitJump = function () { + that.isMoving = true; + that.hasBeenHit = false; + that.isJumping = true; + z = 1; + that.incrementSpeedBy(1); + if (cancelableStateTimeout) { + clearTimeout(cancelableStateTimeout); + } + cancelableStateTimeout = setTimeout(function() { + z = 0; + that.isJumping = false; + that.incrementSpeedBy(-1); + }, 2500); + }; + that.isEatenBy = function (monster, whenEaten) { monster.startEating(whenEaten); that.isMoving = false; diff --git a/js/sprite.js b/js/sprite.js index 937d757..622e13b 100644 --- a/js/sprite.js +++ b/js/sprite.js @@ -2,9 +2,9 @@ var GUID = require('./lib/guid'); function Sprite (data) { var hittableObjects = {}; + var zIndexesOccupied = [ 0 ]; var that = this; - if (data && data.id) that.id = data.id; - else that.id = GUID(); + that.id = GUID(); that.x = 0; that.y = 0; that.height = 0; @@ -18,6 +18,14 @@ return Object.values(that.data.parts).map(function (p) { return p[3]; }).max(); }()); + if (data && data.id){ + that.id = data.id; + } + + if (data && data.zIndexesOccupied) { + zIndexesOccupied = data.zIndexesOccupied; + } + function incrementX(amount) { that.x += amount.toNumber(); } @@ -69,10 +77,14 @@ return [that.x, that.y + that.height]; }; - this.setSpeed = function setSpeed (s) { + this.setSpeed = function (s) { that.speed = s; }; + this.incrementSpeedBy = function (s) { + that.speed += s; + }; + that.getSpeed = function getSpeed () { return that.speed; }; @@ -179,6 +191,10 @@ that.deleted = true; }; + this.occupiesZIndex = function (z) { + return zIndexesOccupied.indexOf(z) >= 0; + }; + this.hits = function hits (other) { var verticalIntersect = false; var horizontalIntersect = false; From 79a187d6c18992b046784b29b79564e538473555 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Feb 2013 01:04:09 +0000 Subject: [PATCH 21/21] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75dedf3..c854a0a 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ I just started work this Christmas 2012 so I've not done a whole lot yet. * Distance tracking so you can see how far you've gone you absolute badass * Speed boost (this was a little-known feature to get away from monsters) using the F key * **MOBILE SUPPORT** - This is cool - try loading the [**the demo page**](http://basicallydan.github.com/skifree.js) on a mobile device and then use your finger to direct the skier around the pistle. Also try double-tap ;) +* Rainbow jump platforms & jumping - though a couple of improvements could be made ## So, what's left to do? This is what I'm gonna do, probably in this order. Who the hell knows. There are other features to the original game but I'm not going to add them to the list until I've gotten through this one. -* Rainbow jump platforms & jumping * Big trees & crashing into them both whilst skiing and jumping * Rocks * Snowboarders